// 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.

// TODO(nweiz): Remove this tag when we can get [packageDir] working without it
// (dart-lang/sdk#24022).
library test.test.io;

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

import 'package:package_resolver/package_resolver.dart';
import 'package:path/path.dart' as p;
import 'package:scheduled_test/descriptor.dart' as d;
import 'package:scheduled_test/scheduled_process.dart';
import 'package:scheduled_test/scheduled_stream.dart';
import 'package:scheduled_test/scheduled_test.dart';
import 'package:test/src/util/io.dart';

/// The path to the root directory of the `test` package.
final Future<String> packageDir = PackageResolver.current.packagePath('test');

/// 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";

/// A regular expression that matches the output of "pub serve".
final _servingRegExp =
    new RegExp(r'^Serving myapp [a-z]+ on http://localhost:(\d+)$');

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

/// A future that will return the port of a pub serve instance run via
/// [runPubServe].
///
/// This should only be called after [runPubServe].
Future<int> get pubServePort => _pubServePortCompleter.future;
Completer<int> _pubServePortCompleter;

/// The path to the sandbox directory.
///
/// This is only set in tests for which [useSandbox] is active.
String get sandbox => _sandbox;
String _sandbox;

/// Declares a [setUp] function that creates a sandbox diretory and sets it as
/// the default for scheduled_test's directory descriptors.
///
/// This should be called outside of any tests. If [additionalSetup] is passed,
/// it's run after the sandbox creation has been scheduled.
void useSandbox([void additionalSetup()]) {
  setUp(() {
    _sandbox = createTempDir();
    d.defaultRoot = _sandbox;

    currentSchedule.onComplete.schedule(() {
      try {
        new Directory(_sandbox).deleteSync(recursive: true);
      } on IOException catch (_) {
        // Silently swallow exceptions on Windows. If the test failed, there may
        // still be lingering processes that have files in the sandbox open,
        // which will cause this to fail on Windows.
        if (!Platform.isWindows) rethrow;
      }
    }, 'deleting the sandbox directory');

    if (additionalSetup != null) additionalSetup();
  });
}

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

/// Expects that the entire stderr stream of [test] equals [expected].
void expectStderrEquals(ScheduledProcess 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) =>
    inOrder(strings.map((string) => consumeThrough(contains(string))));

/// Runs the test executable with the package root set properly.
///
/// If [forwardStdio] is true, the standard output and error from the process
/// will be printed as part of the parent test. This is used for debugging.
ScheduledProcess runTest(List<String> args, {String reporter,
    int concurrency, Map<String, String> environment,
    bool forwardStdio: false}) {
  concurrency ??= 1;

  var allArgs = [
    packageDir.then((dir) => p.absolute(p.join(dir, 'bin/test.dart'))),
    "--concurrency=$concurrency"
  ];
  if (reporter != null) allArgs.add("--reporter=$reporter");
  allArgs.addAll(args);

  if (environment == null) environment = {};
  environment.putIfAbsent("_DART_TEST_TESTING", () => "true");

  var process = runDart(allArgs,
      environment: environment,
      description: "dart bin/test.dart");

  if (forwardStdio) {
    process.stdoutStream().listen(print);
    process.stderrStream().listen(print);
  }

  return process;
}

/// Runs Dart.
ScheduledProcess runDart(List<String> args, {Map<String, String> environment,
    String description}) {
  var allArgs = <Object>[]
    ..addAll(Platform.executableArguments.where((arg) =>
        !arg.startsWith("--package-root=") && !arg.startsWith("--packages=")))
    ..add(PackageResolver.current.processArgument)
    ..addAll(args);

  return new ScheduledProcess.start(
      p.absolute(Platform.resolvedExecutable), allArgs,
      workingDirectory: _sandbox,
      environment: environment,
      description: description);
}

/// Runs Pub.
ScheduledProcess runPub(List args, {Map<String, String> environment}) {
  return new ScheduledProcess.start(
      _pubPath, args,
      workingDirectory: _sandbox,
      environment: environment,
      description: "pub ${args.first}");
}

/// Runs "pub serve".
///
/// This returns assigns [_pubServePort] to a future that will complete to the
/// port of the "pub serve" instance.
ScheduledProcess runPubServe({List<String> args, String workingDirectory,
    Map<String, String> environment}) {
  _pubServePortCompleter = new Completer();
  currentSchedule.onComplete.schedule(() => _pubServePortCompleter = null);

  var allArgs = ['serve', '--port', '0'];
  if (args != null) allArgs.addAll(args);

  var pub = runPub(allArgs, environment: environment);

  schedule(() async {
    var match;
    while (match == null) {
      var line = await pub.stdout.next();
      match = _servingRegExp.firstMatch(line);
    }
    _pubServePortCompleter.complete(int.parse(match[1]));
  }, "waiting for pub serve to emit its port number");

  return pub;
}