// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:io';
import 'dart:isolate';

import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:test_process/test_process.dart';

/// The path to the root directory of the `test` package.
final Future<String> packageDir =
    Isolate.resolvePackageUri(Uri(scheme: 'package', path: 'test/'))
        .then((uri) {
  var dir = p.dirname(uri!.path);
  // If it starts with a `/C:` or other drive letter, remove the leading `/`.
  if (dir[0] == '/' && dir[2] == ':') dir = dir.substring(1);
  return dir;
});

/// The path to the `pub` executable in the current Dart SDK.
final _pubPath = p.absolute(p.join(p.dirname(Platform.resolvedExecutable),
    Platform.isWindows ? 'pub.bat' : 'pub'));

/// The platform-specific message emitted when a nonexistent file is loaded.
final String noSuchFileMessage = Platform.isWindows
    ? 'The system cannot find the file specified.'
    : 'No such file or directory';

/// An operating system name that's different than the current operating system.
final otherOS = Platform.isWindows ? 'mac-os' : 'windows';

/// Expects that the entire stdout stream of [test] equals [expected].
void expectStdoutEquals(TestProcess test, String expected) =>
    _expectStreamEquals(test.stdoutStream(), expected);

/// Expects that the entire stderr stream of [test] equals [expected].
void expectStderrEquals(TestProcess test, String expected) =>
    _expectStreamEquals(test.stderrStream(), expected);

/// Expects that the entirety of the line stream [stream] equals [expected].
void _expectStreamEquals(Stream<String> stream, String expected) {
  expect((() async {
    var lines = await stream.toList();
    expect(lines.join('\n').trim(), equals(expected.trim()));
  })(), completes);
}

/// Returns a [StreamMatcher] that asserts that the stream emits strings
/// containing each string in [strings] in order.
///
/// This expects each string in [strings] to match a different string in the
/// stream.
StreamMatcher containsInOrder(Iterable<String> strings) =>
    emitsInOrder(strings.map((string) => emitsThrough(contains(string))));

/// Lazily compile the test package to kernel and re-use that, initialized with
/// [precompileTestExecutable].
String? _testExecutablePath;

/// Must be invoked before any call to [runTests], should be invoked in a top
/// level `setUpAll` for best caching results.
Future<void> precompileTestExecutable() async {
  if (_testExecutablePath != null) {
    throw StateError('Test executable already precompiled');
  }
  var tmpDirectory = await Directory.systemTemp.createTemp('test');
  var precompiledPath = p.join(tmpDirectory.path, 'test_runner.dill');
  var result = await Process.run(Platform.executable, [
    'compile',
    'kernel',
    p.url.join(await packageDir, 'bin', 'test.dart'),
    '-o',
    precompiledPath,
  ]);
  if (result.exitCode != 0) {
    throw StateError(
        'Failed to compile test runner:\n${result.stdout}\n${result.stderr}');
  }

  addTearDown(() async {
    await tmpDirectory.delete(recursive: true);
  });
  _testExecutablePath = precompiledPath;
}

/// Runs the test executable with the package root set properly.
///
/// You must invoke [precompileTestExecutable] before invoking this function.
Future<TestProcess> runTest(Iterable<String> args,
    {String? reporter,
    String? fileReporter,
    int? concurrency,
    Map<String, String>? environment,
    bool forwardStdio = false,
    String? packageConfig,
    Iterable<String>? vmArgs}) async {
  concurrency ??= 1;
  var testExecutablePath = _testExecutablePath;
  if (testExecutablePath == null) {
    throw StateError(
        'You must call `precompileTestExecutable` before calling `runTest`');
  }

  var allArgs = [
    ...?vmArgs,
    testExecutablePath,
    '--concurrency=$concurrency',
    if (reporter != null) '--reporter=$reporter',
    if (fileReporter != null) '--file-reporter=$fileReporter',
    ...args,
  ];

  environment ??= {};
  environment.putIfAbsent('_DART_TEST_TESTING', () => 'true');

  return await runDart(allArgs,
      environment: environment,
      description: 'dart bin/test.dart',
      forwardStdio: forwardStdio,
      packageConfig: packageConfig);
}

/// Runs Dart.
///
/// If [packageConfig] is provided then that is passed for the `--packages`
/// arg, otherwise the current isolate config is passed.
Future<TestProcess> runDart(Iterable<String> args,
    {Map<String, String>? environment,
    String? description,
    bool forwardStdio = false,
    String? packageConfig}) async {
  var allArgs = <String>[
    ...Platform.executableArguments.where((arg) =>
        !arg.startsWith('--package-root=') && !arg.startsWith('--packages=')),
    '--packages=${packageConfig ?? await Isolate.packageConfig}',
    ...args
  ];

  return await TestProcess.start(
      p.absolute(Platform.resolvedExecutable), allArgs,
      workingDirectory: d.sandbox,
      environment: environment,
      description: description,
      forwardStdio: forwardStdio);
}

/// Runs Pub.
Future<TestProcess> runPub(Iterable<String> args,
    {Map<String, String>? environment}) {
  return TestProcess.start(_pubPath, args,
      workingDirectory: d.sandbox,
      environment: environment,
      description: 'pub ${args.first}');
}
