// 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 'package:test/test.dart';
import 'package:test_api/src/backend/declarer.dart';
import 'package:test_api/src/backend/group.dart';
import 'package:test_api/src/backend/invoker.dart';
import 'package:test_api/src/backend/suite.dart';
import 'package:test_api/src/backend/test.dart';

import '../utils.dart';

late Suite _suite;

void main() {
  setUp(() {
    _suite = Suite(Group.root([]), suitePlatform, ignoreTimeouts: false);
  });

  group('.test()', () {
    test('declares a test with a description and body', () async {
      var bodyRun = false;
      var tests = declare(() {
        test('description', () {
          bodyRun = true;
        });
      });

      expect(tests, hasLength(1));
      expect(tests.single.name, equals('description'));

      await _runTest(tests[0] as Test);
      expect(bodyRun, isTrue);
    });

    test('declares a test with an object as the description', () async {
      var tests = declare(() {
        test(Object, () {});
      });

      expect(tests.single.name, equals('Object'));
    });

    test('declares multiple tests', () {
      var tests = declare(() {
        test('description 1', () {});
        test('description 2', () {});
        test('description 3', () {});
      });

      expect(tests, hasLength(3));
      expect(tests[0].name, equals('description 1'));
      expect(tests[1].name, equals('description 2'));
      expect(tests[2].name, equals('description 3'));
    });
  });

  group('.setUp()', () {
    test('is run before all tests', () async {
      var setUpRun = false;
      var tests = declare(() {
        setUp(() => setUpRun = true);

        test(
            'description 1',
            expectAsync0(() {
              expect(setUpRun, isTrue);
              setUpRun = false;
            }, max: 1));

        test(
            'description 2',
            expectAsync0(() {
              expect(setUpRun, isTrue);
              setUpRun = false;
            }, max: 1));
      });

      await _runTest(tests[0] as Test);
      await _runTest(tests[1] as Test);
    });

    test('can return a Future', () {
      var setUpRun = false;
      var tests = declare(() {
        setUp(() {
          return Future(() => setUpRun = true);
        });

        test(
            'description',
            expectAsync0(() {
              expect(setUpRun, isTrue);
            }, max: 1));
      });

      return _runTest(tests.single as Test);
    });

    test('runs in call order within a group', () async {
      var firstSetUpRun = false;
      var secondSetUpRun = false;
      var thirdSetUpRun = false;
      var tests = declare(() {
        setUp(expectAsync0(() async {
          expect(secondSetUpRun, isFalse);
          expect(thirdSetUpRun, isFalse);
          firstSetUpRun = true;
        }));

        setUp(expectAsync0(() async {
          expect(firstSetUpRun, isTrue);
          expect(thirdSetUpRun, isFalse);
          secondSetUpRun = true;
        }));

        setUp(expectAsync0(() async {
          expect(firstSetUpRun, isTrue);
          expect(secondSetUpRun, isTrue);
          thirdSetUpRun = true;
        }));

        test('description', expectAsync0(() {
          expect(firstSetUpRun, isTrue);
          expect(secondSetUpRun, isTrue);
          expect(thirdSetUpRun, isTrue);
        }));
      });

      await _runTest(tests.single as Test);
    });
  });

  group('.tearDown()', () {
    test('is run after all tests', () async {
      late bool tearDownRun;
      var tests = declare(() {
        setUp(() => tearDownRun = false);
        tearDown(() => tearDownRun = true);

        test(
            'description 1',
            expectAsync0(() {
              expect(tearDownRun, isFalse);
            }, max: 1));

        test(
            'description 2',
            expectAsync0(() {
              expect(tearDownRun, isFalse);
            }, max: 1));
      });

      await _runTest(tests[0] as Test);
      expect(tearDownRun, isTrue);
      await _runTest(tests[1] as Test);
      expect(tearDownRun, isTrue);
    });

    test('is run after an out-of-band failure', () async {
      late bool tearDownRun;
      var tests = declare(() {
        setUp(() => tearDownRun = false);
        tearDown(() => tearDownRun = true);

        test(
            'description 1',
            expectAsync0(() {
              Invoker.current!.addOutstandingCallback();
              Future(() => throw TestFailure('oh no'))
                  .whenComplete(Invoker.current!.removeOutstandingCallback);
            }, max: 1));
      });

      await _runTest(tests.single as Test, shouldFail: true);
      expect(tearDownRun, isTrue);
    });

    test('can return a Future', () async {
      var tearDownRun = false;
      var tests = declare(() {
        tearDown(() {
          return Future(() => tearDownRun = true);
        });

        test(
            'description',
            expectAsync0(() {
              expect(tearDownRun, isFalse);
            }, max: 1));
      });

      await _runTest(tests.single as Test);
      expect(tearDownRun, isTrue);
    });

    test("isn't run until there are no outstanding callbacks", () async {
      var outstandingCallbackRemoved = false;
      var outstandingCallbackRemovedBeforeTeardown = false;
      var tests = declare(() {
        tearDown(() {
          outstandingCallbackRemovedBeforeTeardown = outstandingCallbackRemoved;
        });

        test('description', () {
          Invoker.current!.addOutstandingCallback();
          pumpEventQueue().then((_) {
            outstandingCallbackRemoved = true;
            Invoker.current!.removeOutstandingCallback();
          });
        });
      });

      await _runTest(tests.single as Test);
      expect(outstandingCallbackRemovedBeforeTeardown, isTrue);
    });

    test("isn't run until test body completes after out-of-band error",
        () async {
      var hasTestFinished = false;
      var hasTestFinishedBeforeTeardown = false;
      var tests = declare(() {
        tearDown(() {
          hasTestFinishedBeforeTeardown = hasTestFinished;
        });

        test('description', () {
          Future<Never>.error('oh no');
          return pumpEventQueue().then((_) {
            hasTestFinished = true;
          });
        });
      });

      await _runTest(tests.single as Test, shouldFail: true);
      expect(hasTestFinishedBeforeTeardown, isTrue);
    });

    test("doesn't complete until there are no outstanding callbacks", () async {
      var outstandingCallbackRemoved = false;
      var tests = declare(() {
        tearDown(() {
          Invoker.current!.addOutstandingCallback();
          pumpEventQueue().then((_) {
            outstandingCallbackRemoved = true;
            Invoker.current!.removeOutstandingCallback();
          });
        });

        test('description', () {});
      });

      await _runTest(tests.single as Test);
      expect(outstandingCallbackRemoved, isTrue);
    });

    test('runs in reverse call order within a group', () async {
      var firstTearDownRun = false;
      var secondTearDownRun = false;
      var thirdTearDownRun = false;
      var tests = declare(() {
        tearDown(expectAsync0(() async {
          expect(secondTearDownRun, isTrue);
          expect(thirdTearDownRun, isTrue);
          firstTearDownRun = true;
        }));

        tearDown(expectAsync0(() async {
          expect(firstTearDownRun, isFalse);
          expect(thirdTearDownRun, isTrue);
          secondTearDownRun = true;
        }));

        tearDown(expectAsync0(() async {
          expect(firstTearDownRun, isFalse);
          expect(secondTearDownRun, isFalse);
          thirdTearDownRun = true;
        }));

        test(
            'description',
            expectAsync0(() {
              expect(firstTearDownRun, isFalse);
              expect(secondTearDownRun, isFalse);
              expect(thirdTearDownRun, isFalse);
            }, max: 1));
      });

      await _runTest(tests.single as Test);
    });

    test('runs further tearDowns in a group even if one fails', () async {
      var tests = declare(() {
        tearDown(expectAsync0(() {}));

        tearDown(() async {
          throw 'error';
        });

        test('description', expectAsync0(() {}));
      });

      await _runTest(tests.single as Test, shouldFail: true);
    });

    test('runs in the same error zone as the test', () {
      return expectTestsPass(() {
        late Zone testBodyZone;

        tearDown(() {
          final tearDownZone = Zone.current;
          expect(tearDownZone.inSameErrorZone(testBodyZone), isTrue,
              reason: 'The tear down callback is in a different error zone '
                  'than the test body.');
        });

        test('test', () {
          testBodyZone = Zone.current;
        });
      });
    });
  });

  group('in a group,', () {
    test("tests inherit the group's description", () {
      var entries = declare(() {
        group('group', () {
          test('description', () {});
        });
      });

      expect(entries, hasLength(1));
      var testGroup = entries.single as Group;
      expect(testGroup.name, equals('group'));
      expect(testGroup.entries, hasLength(1));
      expect(testGroup.entries.single, const TypeMatcher<Test>());
      expect(testGroup.entries.single.name, 'group description');
    });

    test("tests inherit the group's description when it's not a string", () {
      var entries = declare(() {
        group(Object, () {
          test('description', () {});
        });
      });

      expect(entries, hasLength(1));
      var testGroup = entries.single as Group;
      expect(testGroup.name, equals('Object'));
      expect(testGroup.entries, hasLength(1));
      expect(testGroup.entries.single, const TypeMatcher<Test>());
      expect(testGroup.entries.single.name, 'Object description');
    });

    test("a test's timeout factor is applied to the group's", () {
      var entries = declare(() {
        group('group', () {
          test('test', () {}, timeout: const Timeout.factor(3));
        }, timeout: const Timeout.factor(2));
      });

      expect(entries, hasLength(1));
      var testGroup = entries.single as Group;
      expect(testGroup.metadata.timeout.scaleFactor, equals(2));
      expect(testGroup.entries, hasLength(1));
      expect(testGroup.entries.single, const TypeMatcher<Test>());
      expect(testGroup.entries.single.metadata.timeout.scaleFactor, equals(6));
    });

    test("a test's timeout factor is applied to the group's duration", () {
      var entries = declare(() {
        group('group', () {
          test('test', () {}, timeout: const Timeout.factor(2));
        }, timeout: const Timeout(Duration(seconds: 10)));
      });

      expect(entries, hasLength(1));
      var testGroup = entries.single as Group;
      expect(testGroup.metadata.timeout.duration,
          equals(const Duration(seconds: 10)));
      expect(testGroup.entries, hasLength(1));
      expect(testGroup.entries.single, const TypeMatcher<Test>());
      expect(testGroup.entries.single.metadata.timeout.duration,
          equals(const Duration(seconds: 20)));
    });

    test("a test's timeout duration is applied over the group's", () {
      var entries = declare(() {
        group('group', () {
          test('test', () {}, timeout: const Timeout(Duration(seconds: 15)));
        }, timeout: const Timeout(Duration(seconds: 10)));
      });

      expect(entries, hasLength(1));
      var testGroup = entries.single as Group;
      expect(testGroup.metadata.timeout.duration,
          equals(const Duration(seconds: 10)));
      expect(testGroup.entries, hasLength(1));
      expect(testGroup.entries.single, const TypeMatcher<Test>());
      expect(testGroup.entries.single.metadata.timeout.duration,
          equals(const Duration(seconds: 15)));
    });

    test('disallows asynchronous groups', () async {
      declare(() {
        expect(() => group('group', () async {}), throwsArgumentError);
      });
    });

    group('.setUp()', () {
      test('is scoped to the group', () async {
        var setUpRun = false;
        var entries = declare(() {
          group('group', () {
            setUp(() => setUpRun = true);

            test(
                'description 1',
                expectAsync0(() {
                  expect(setUpRun, isTrue);
                  setUpRun = false;
                }, max: 1));
          });

          test(
              'description 2',
              expectAsync0(() {
                expect(setUpRun, isFalse);
                setUpRun = false;
              }, max: 1));
        });

        await _runTest((entries[0] as Group).entries.single as Test);
        await _runTest(entries[1] as Test);
      });

      test('runs from the outside in', () {
        var outerSetUpRun = false;
        var middleSetUpRun = false;
        var innerSetUpRun = false;
        var entries = declare(() {
          setUp(expectAsync0(() {
            expect(middleSetUpRun, isFalse);
            expect(innerSetUpRun, isFalse);
            outerSetUpRun = true;
          }, max: 1));

          group('middle', () {
            setUp(expectAsync0(() {
              expect(outerSetUpRun, isTrue);
              expect(innerSetUpRun, isFalse);
              middleSetUpRun = true;
            }, max: 1));

            group('inner', () {
              setUp(expectAsync0(() {
                expect(outerSetUpRun, isTrue);
                expect(middleSetUpRun, isTrue);
                innerSetUpRun = true;
              }, max: 1));

              test(
                  'description',
                  expectAsync0(() {
                    expect(outerSetUpRun, isTrue);
                    expect(middleSetUpRun, isTrue);
                    expect(innerSetUpRun, isTrue);
                  }, max: 1));
            });
          });
        });

        var middleGroup = entries.single as Group;
        var innerGroup = middleGroup.entries.single as Group;
        return _runTest(innerGroup.entries.single as Test);
      });

      test('handles Futures when chained', () {
        var outerSetUpRun = false;
        var innerSetUpRun = false;
        var entries = declare(() {
          setUp(expectAsync0(() {
            expect(innerSetUpRun, isFalse);
            return Future(() => outerSetUpRun = true);
          }, max: 1));

          group('inner', () {
            setUp(expectAsync0(() {
              expect(outerSetUpRun, isTrue);
              return Future(() => innerSetUpRun = true);
            }, max: 1));

            test(
                'description',
                expectAsync0(() {
                  expect(outerSetUpRun, isTrue);
                  expect(innerSetUpRun, isTrue);
                }, max: 1));
          });
        });

        var innerGroup = entries.single as Group;
        return _runTest(innerGroup.entries.single as Test);
      });

      test("inherits group's tags", () {
        var tests = declare(() {
          group('outer', () {
            group('inner', () {
              test('with tags', () {}, tags: 'd');
            }, tags: ['b', 'c']);
          }, tags: 'a');
        });

        var outerGroup = tests.single as Group;
        var innerGroup = outerGroup.entries.single as Group;
        var testWithTags = innerGroup.entries.single;
        expect(outerGroup.metadata.tags, unorderedEquals(['a']));
        expect(innerGroup.metadata.tags, unorderedEquals(['a', 'b', 'c']));
        expect(
            testWithTags.metadata.tags, unorderedEquals(['a', 'b', 'c', 'd']));
      });

      test('throws on invalid tags', () {
        expect(() {
          declare(() {
            group('a', () {}, tags: 1);
          });
        }, throwsArgumentError);
      });
    });

    group('.tearDown()', () {
      test('is scoped to the group', () async {
        late bool tearDownRun;
        var entries = declare(() {
          setUp(() => tearDownRun = false);

          group('group', () {
            tearDown(() => tearDownRun = true);

            test(
                'description 1',
                expectAsync0(() {
                  expect(tearDownRun, isFalse);
                }, max: 1));
          });

          test(
              'description 2',
              expectAsync0(() {
                expect(tearDownRun, isFalse);
              }, max: 1));
        });

        var testGroup = entries[0] as Group;
        await _runTest(testGroup.entries.single as Test);
        expect(tearDownRun, isTrue);
        await _runTest(entries[1] as Test);
        expect(tearDownRun, isFalse);
      });

      test('runs from the inside out', () async {
        var innerTearDownRun = false;
        var middleTearDownRun = false;
        var outerTearDownRun = false;
        var entries = declare(() {
          tearDown(expectAsync0(() {
            expect(innerTearDownRun, isTrue);
            expect(middleTearDownRun, isTrue);
            outerTearDownRun = true;
          }, max: 1));

          group('middle', () {
            tearDown(expectAsync0(() {
              expect(innerTearDownRun, isTrue);
              expect(outerTearDownRun, isFalse);
              middleTearDownRun = true;
            }, max: 1));

            group('inner', () {
              tearDown(expectAsync0(() {
                expect(outerTearDownRun, isFalse);
                expect(middleTearDownRun, isFalse);
                innerTearDownRun = true;
              }, max: 1));

              test(
                  'description',
                  expectAsync0(() {
                    expect(outerTearDownRun, isFalse);
                    expect(middleTearDownRun, isFalse);
                    expect(innerTearDownRun, isFalse);
                  }, max: 1));
            });
          });
        });

        var middleGroup = entries.single as Group;
        var innerGroup = middleGroup.entries.single as Group;
        await _runTest(innerGroup.entries.single as Test);
        expect(innerTearDownRun, isTrue);
        expect(middleTearDownRun, isTrue);
        expect(outerTearDownRun, isTrue);
      });

      test('handles Futures when chained', () async {
        var outerTearDownRun = false;
        var innerTearDownRun = false;
        var entries = declare(() {
          tearDown(expectAsync0(() {
            expect(innerTearDownRun, isTrue);
            return Future(() => outerTearDownRun = true);
          }, max: 1));

          group('inner', () {
            tearDown(expectAsync0(() {
              expect(outerTearDownRun, isFalse);
              return Future(() => innerTearDownRun = true);
            }, max: 1));

            test(
                'description',
                expectAsync0(() {
                  expect(outerTearDownRun, isFalse);
                  expect(innerTearDownRun, isFalse);
                }, max: 1));
          });
        });

        var innerGroup = entries.single as Group;
        await _runTest(innerGroup.entries.single as Test);
        expect(innerTearDownRun, isTrue);
        expect(outerTearDownRun, isTrue);
      });

      test('runs outer callbacks even when inner ones fail', () async {
        var outerTearDownRun = false;
        var entries = declare(() {
          tearDown(() {
            return Future(() => outerTearDownRun = true);
          });

          group('inner', () {
            tearDown(() {
              throw 'inner error';
            });

            test(
                'description',
                expectAsync0(() {
                  expect(outerTearDownRun, isFalse);
                }, max: 1));
          });
        });

        var innerGroup = entries.single as Group;
        await _runTest(innerGroup.entries.single as Test, shouldFail: true);
        expect(outerTearDownRun, isTrue);
      });
    });
  });

  group('duplicate names', () {
    test('can be enabled', () {
      expect(
          () => declare(() {
                test('a', expectAsync0(() {}, count: 0));
                test('a', expectAsync0(() {}, count: 0));
              }, allowDuplicateTestNames: false),
          throwsA(isA<DuplicateTestNameException>()
              .having((e) => e.name, 'name', 'a')));
    });

    test('are allowed by default', () {
      expect(
          declare(() {
            test('a', expectAsync0(() {}, count: 0));
            test('a', expectAsync0(() {}, count: 0));
          }).map((e) => e.name),
          equals(['a', 'a']));
    });
  });
}

/// Runs [test].
///
/// This automatically sets up an `onError` listener to ensure that the test
/// doesn't throw any invisible exceptions.
Future _runTest(Test test, {bool shouldFail = false}) {
  var liveTest = test.load(_suite);

  if (shouldFail) {
    liveTest.onError.listen(expectAsync1((_) {}));
  } else {
    liveTest.onError.listen((e) => registerException(e.error, e.stackTrace));
  }

  return liveTest.run();
}
