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

@TestOn("vm")

import 'package:test_descriptor/test_descriptor.dart' as d;

import 'package:test/test.dart';

import '../io.dart';

void main() {
  setUp(() async {
    await d.file("test.dart", """
      import 'package:test/test.dart';

      void main() {
        test("no tags", () {});
        test("a", () {}, tags: "a");
        test("b", () {}, tags: "b");
        test("bc", () {}, tags: ["b", "c"]);
      }
    """).create();
  });

  group("--tags", () {
    test("runs all tests when no tags are specified", () async {
      var test = await runTest(["test.dart"]);
      expect(test.stdout, tagWarnings(['a', 'b', 'c']));
      expect(test.stdout, emitsThrough(contains(": no tags")));
      expect(test.stdout, emitsThrough(contains(": a")));
      expect(test.stdout, emitsThrough(contains(": b")));
      expect(test.stdout, emitsThrough(contains(": bc")));
      expect(test.stdout, emitsThrough(contains("+4: All tests passed!")));
      await test.shouldExit(0);
    });

    test("runs a test with only a specified tag", () async {
      var test = await runTest(["--tags=a", "test.dart"]);
      expect(test.stdout, tagWarnings(['b', 'c']));
      expect(test.stdout, emitsThrough(contains(": a")));
      expect(test.stdout, emitsThrough(contains("+1: All tests passed!")));
      await test.shouldExit(0);
    });

    test("runs a test with a specified tag among others", () async {
      var test = await runTest(["--tags=c", "test.dart"]);
      expect(test.stdout, tagWarnings(['a', 'b']));
      expect(test.stdout, emitsThrough(contains(": bc")));
      expect(test.stdout, emitsThrough(contains("+1: All tests passed!")));
      await test.shouldExit(0);
    });

    test("with multiple tags, runs only tests matching all of them", () async {
      var test = await runTest(["--tags=b,c", "test.dart"]);
      expect(test.stdout, tagWarnings(['a']));
      expect(test.stdout, emitsThrough(contains(": bc")));
      expect(test.stdout, emitsThrough(contains("+1: All tests passed!")));
      await test.shouldExit(0);
    });

    test("supports boolean selector syntax", () async {
      var test = await runTest(["--tags=b || c", "test.dart"]);
      expect(test.stdout, tagWarnings(['a']));
      expect(test.stdout, emitsThrough(contains(": b")));
      expect(test.stdout, emitsThrough(contains(": bc")));
      expect(test.stdout, emitsThrough(contains("+2: All tests passed!")));
      await test.shouldExit(0);
    });

    test("prints no warnings when all tags are specified", () async {
      var test = await runTest(["--tags=a,b,c", "test.dart"]);
      expect(test.stdout, emitsThrough(contains("No tests ran.")));
      await test.shouldExit(0);
    });
  });

  group("--exclude-tags", () {
    test("dosn't run a test with only an excluded tag", () async {
      var test = await runTest(["--exclude-tags=a", "test.dart"]);
      expect(test.stdout, tagWarnings(['b', 'c']));
      expect(test.stdout, emitsThrough(contains(": no tags")));
      expect(test.stdout, emitsThrough(contains(": b")));
      expect(test.stdout, emitsThrough(contains(": bc")));
      expect(test.stdout, emitsThrough(contains("+3: All tests passed!")));
      await test.shouldExit(0);
    });

    test("doesn't run a test with an exluded tag among others", () async {
      var test = await runTest(["--exclude-tags=c", "test.dart"]);
      expect(test.stdout, tagWarnings(['a', 'b']));
      expect(test.stdout, emitsThrough(contains(": no tags")));
      expect(test.stdout, emitsThrough(contains(": a")));
      expect(test.stdout, emitsThrough(contains(": b")));
      expect(test.stdout, emitsThrough(contains("+3: All tests passed!")));
      await test.shouldExit(0);
    });

    test("dosn't load a suite with an excluded tag", () async {
      await d.file("test.dart", """
        @Tags(const ["a"])

        import 'package:test/test.dart';

        void main() {
          throw "error";
        }
      """).create();

      var test = await runTest(["--exclude-tags=a", "test.dart"]);
      expect(test.stdout, emits("No tests ran."));
      await test.shouldExit(0);
    });

    test("allows unused tags", () async {
      var test = await runTest(["--exclude-tags=b,z", "test.dart"]);
      expect(test.stdout, tagWarnings(['a', 'c']));
      expect(test.stdout, emitsThrough(contains(": no tags")));
      expect(test.stdout, emitsThrough(contains(": a")));
      expect(test.stdout, emitsThrough(contains("+2: All tests passed!")));
      await test.shouldExit(0);
    });

    test("supports boolean selector syntax", () async {
      var test = await runTest(["--exclude-tags=b && c", "test.dart"]);
      expect(test.stdout, tagWarnings(['a']));
      expect(test.stdout, emitsThrough(contains(": no tags")));
      expect(test.stdout, emitsThrough(contains(": a")));
      expect(test.stdout, emitsThrough(contains(": b")));
      expect(test.stdout, emitsThrough(contains("+3: All tests passed!")));
      await test.shouldExit(0);
    });

    test("prints no warnings when all tags are specified", () async {
      var test = await runTest(["--exclude-tags=a,b,c", "test.dart"]);
      expect(test.stdout, emitsThrough(contains(": no tags")));
      expect(test.stdout, emitsThrough(contains("+1: All tests passed!")));
      await test.shouldExit(0);
    });
  });

  group("with a tagged group", () {
    setUp(() async {
      await d.file("test.dart", """
        import 'package:test/test.dart';

        void main() {
          group("a", () {
            test("in", () {});
          }, tags: "a");

          test("out", () {});
        }
      """).create();
    });

    test("includes tags specified on the group", () async {
      var test = await runTest(["-x", "a", "test.dart"]);
      expect(test.stdout, emitsThrough(contains(": out")));
      expect(test.stdout, emitsThrough(contains("+1: All tests passed!")));
      await test.shouldExit(0);
    });

    test("excludes tags specified on the group", () async {
      var test = await runTest(["-t", "a", "test.dart"]);
      expect(test.stdout, emitsThrough(contains(": a in")));
      expect(test.stdout, emitsThrough(contains("+1: All tests passed!")));
      await test.shouldExit(0);
    });
  });

  test('respects top-level @Tags annotations', () async {
    await d.file("test.dart", """
      @Tags(const ['a'])
      import 'package:test/test.dart';

      void main() {
        test("foo", () {});
      }
    """).create();

    var test = await runTest(["-x", "a", "test.dart"]);
    expect(test.stdout, emitsThrough(contains("No tests ran")));
    await test.shouldExit(0);
  });

  group("warning formatting", () {
    test("for multiple tags", () async {
      await d.file("test.dart", """
        import 'package:test/test.dart';

        void main() {
          test("foo", () {}, tags: ["a", "b"]);
        }
      """).create();

      var test = await runTest(["test.dart"]);
      expect(
          test.stdout,
          emitsThrough(lines(
              'Warning: Tags were used that weren\'t specified in dart_test.yaml.\n'
              '  a was used in the test "foo"\n'
              '  b was used in the test "foo"')));
      await test.shouldExit(0);
    });

    test("for multiple tests", () async {
      await d.file("test.dart", """
        import 'package:test/test.dart';

        void main() {
          test("foo", () {}, tags: "a");
          test("bar", () {}, tags: "a");
        }
      """).create();

      var test = await runTest(["test.dart"]);
      expect(
          test.stdout,
          emitsThrough(lines(
              'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n'
              '  a was used in:\n'
              '    the test "foo"\n'
              '    the test "bar"')));
      await test.shouldExit(0);
    });

    test("for groups", () async {
      await d.file("test.dart", """
        import 'package:test/test.dart';

        void main() {
          group("group", () {
            test("foo", () {});
            test("bar", () {});
          }, tags: "a");
        }
      """).create();

      var test = await runTest(["test.dart"]);
      expect(
          test.stdout,
          emitsThrough(lines(
              'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n'
              '  a was used in the group "group"')));
      await test.shouldExit(0);
    });

    test("for suites", () async {
      await d.file("test.dart", """
        @Tags(const ["a"])
        import 'package:test/test.dart';

        void main() {
          test("foo", () {});
          test("bar", () {});
        }
      """).create();

      var test = await runTest(["test.dart"]);
      expect(
          test.stdout,
          emitsThrough(lines(
              'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n'
              '  a was used in the suite itself')));
      await test.shouldExit(0);
    });

    test("doesn't double-print a tag warning", () async {
      await d.file("test.dart", """
        import 'package:test/test.dart';

        void main() {
          test("foo", () {}, tags: "a");
        }
      """).create();

      var test = await runTest(["-p", "vm,chrome", "test.dart"]);
      expect(
          test.stdout,
          emitsThrough(lines(
              'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n'
              '  a was used in the test "foo"')));
      expect(test.stdout, neverEmits(startsWith("Warning:")));
      await test.shouldExit(0);
    }, tags: "chrome");
  });

  group("invalid tags", () {
    test("are disallowed by test()", () async {
      await d.file("test.dart", """
        import 'package:test/test.dart';

        void main() {
          test("foo", () {}, tags: "a b");
        }
      """).create();

      var test = await runTest(["test.dart"]);
      expect(
          test.stdout,
          emitsThrough(
              '  Failed to load "test.dart": Invalid argument(s): Invalid tag "a '
              'b". Tags must be (optionally hyphenated) Dart identifiers.'));
      await test.shouldExit(1);
    });

    test("are disallowed by group()", () async {
      await d.file("test.dart", """
        import 'package:test/test.dart';

        void main() {
          group("group", () {
            test("foo", () {});
          }, tags: "a b");
        }
      """).create();

      var test = await runTest(["test.dart"]);
      expect(
          test.stdout,
          emitsThrough(
              '  Failed to load "test.dart": Invalid argument(s): Invalid tag "a '
              'b". Tags must be (optionally hyphenated) Dart identifiers.'));
      await test.shouldExit(1);
    });

    test("are disallowed by @Tags()", () async {
      await d.file("test.dart", """
        @Tags(const ["a b"])

        import 'package:test/test.dart';

        void main() {
          test("foo", () {});
        }
      """).create();

      var test = await runTest(["test.dart"]);
      expect(
          test.stdout,
          emitsThrough(lines('  Failed to load "test.dart":\n'
              '  Error on line 1, column 22: Invalid tag name. Tags must be '
              '(optionally hyphenated) Dart identifiers.')));
      await test.shouldExit(1);
    });
  });
}

/// Returns a [StreamMatcher] that asserts that a test emits warnings for [tags]
/// in order.
StreamMatcher tagWarnings(List<String> tags) => emitsInOrder(() sync* {
      yield emitsThrough(
          "Warning: ${tags.length == 1 ? 'A tag was' : 'Tags were'} used that "
          "${tags.length == 1 ? "wasn't" : "weren't"} specified in "
          "dart_test.yaml.");

      for (var tag in tags) {
        yield emitsThrough(startsWith("  $tag was used in"));
      }

      // Consume until the end of the warning block, and assert that it has no
      // further tags than the ones we specified.
      yield mayEmitMultiple(isNot(anyOf([contains(" was used in"), isEmpty])));
      yield isEmpty;
    }());

/// Returns a [StreamMatcher] that matches the lines of [string] in order.
StreamMatcher lines(String string) => emitsInOrder(string.split("\n"));
