// 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/group.dart';
import 'package:test_api/src/backend/invoker.dart';
import 'package:test_api/src/backend/live_test.dart';
import 'package:test_api/src/backend/message.dart';
import 'package:test_api/src/backend/metadata.dart';
import 'package:test_api/src/backend/state.dart';
import 'package:test_api/src/backend/suite.dart';

import '../utils.dart';

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

  group('Invoker.current', () {
    var invoker = Invoker.current;
    test('returns null outside of a test body', () {
      expect(invoker, isNull);
    });

    test('returns the current invoker in a test body', () async {
      late Invoker invoker;
      var liveTest = _localTest(() {
        invoker = Invoker.current!;
      }).load(suite);
      liveTest.onError.listen(expectAsync1((_) {}, count: 0));

      await liveTest.run();
      expect(invoker.liveTest, equals(liveTest));
    });

    test(
      'returns the current invoker in a test body after the test completes',
      () async {
        Status? status;
        var completer = Completer<Invoker>();
        var liveTest = _localTest(() {
          // Use the event loop to wait longer than a microtask for the test to
          // complete.
          Future(() {
            status = Invoker.current!.liveTest.state.status;
            completer.complete(Invoker.current);
          });
        }).load(suite);
        liveTest.onError.listen(expectAsync1((_) {}, count: 0));

        expect(liveTest.run(), completes);
        var invoker = await completer.future;
        expect(invoker.liveTest, equals(liveTest));
        expect(status, equals(Status.complete));
      },
    );
  });

  group('in a successful test,', () {
    test('the state changes from pending to running to complete', () async {
      late State stateInTest;
      late LiveTest liveTest;
      liveTest = _localTest(() {
        stateInTest = liveTest.state;
      }).load(suite);
      liveTest.onError.listen(expectAsync1((_) {}, count: 0));

      expect(liveTest.state.status, equals(Status.pending));
      expect(liveTest.state.result, equals(Result.success));

      var future = liveTest.run();

      expect(liveTest.state.status, equals(Status.running));
      expect(liveTest.state.result, equals(Result.success));

      await future;

      expect(stateInTest.status, equals(Status.running));
      expect(stateInTest.result, equals(Result.success));

      expect(liveTest.state.status, equals(Status.complete));
      expect(liveTest.state.result, equals(Result.success));
    });

    test('onStateChange fires for each state change', () {
      var liveTest = _localTest(() {}).load(suite);
      liveTest.onError.listen(expectAsync1((_) {}, count: 0));

      var first = true;
      liveTest.onStateChange.listen(
        expectAsync1(
          (state) {
            if (first) {
              expect(state.status, equals(Status.running));
              first = false;
            } else {
              expect(state.status, equals(Status.complete));
            }
            expect(state.result, equals(Result.success));
          },
          count: 2,
          max: 2,
        ),
      );

      return liveTest.run();
    });

    test('onComplete completes once the test body is done', () {
      var testRun = false;
      var liveTest = _localTest(() {
        testRun = true;
      }).load(suite);

      expect(
        liveTest.onComplete.then((_) {
          expect(testRun, isTrue);
        }),
        completes,
      );

      return liveTest.run();
    });
  });

  group('in a test with failures,', () {
    test('a synchronous throw is reported and causes the test to fail', () {
      var liveTest = _localTest(() {
        throw TestFailure('oh no');
      }).load(suite);

      expectSingleFailure(liveTest);
      return liveTest.run();
    });

    test('a synchronous reported failure causes the test to fail', () {
      var liveTest = _localTest(() {
        registerException(TestFailure('oh no'));
      }).load(suite);

      expectSingleFailure(liveTest);
      return liveTest.run();
    });

    test(
      'a failure reported asynchronously during the test causes it to fail',
      () {
        var liveTest = _localTest(() {
          Invoker.current!.addOutstandingCallback();
          Future(
            () => registerException(TestFailure('oh no')),
          ).whenComplete(Invoker.current!.removeOutstandingCallback);
        }).load(suite);

        expectSingleFailure(liveTest);
        return liveTest.run();
      },
    );

    test(
      'a failure thrown asynchronously during the test causes it to fail',
      () {
        var liveTest = _localTest(() {
          Invoker.current!.addOutstandingCallback();
          Future(
            () => throw TestFailure('oh no'),
          ).whenComplete(Invoker.current!.removeOutstandingCallback);
        }).load(suite);

        expectSingleFailure(liveTest);
        return liveTest.run();
      },
    );

    test(
      'a failure reported asynchronously after the test causes it to error',
      () {
        var liveTest = _localTest(() {
          Future(() => registerException(TestFailure('oh no')));
        }).load(suite);

        expectStates(liveTest, [
          const State(Status.running, Result.success),
          const State(Status.complete, Result.success),
          const State(Status.complete, Result.failure),
          const State(Status.complete, Result.error),
        ]);

        expectErrors(liveTest, [
          (error) {
            expect(
              lastState,
              equals(const State(Status.complete, Result.failure)),
            );
            expect(error, isTestFailure('oh no'));
          },
          (error) {
            expect(
              lastState,
              equals(const State(Status.complete, Result.error)),
            );
            expect(
              error,
              equals(
                'This test failed after it had already completed.\n'
                'Make sure to use a matching library which informs the '
                'test runner\nof pending async work.',
              ),
            );
          },
        ]);

        return liveTest.run();
      },
    );

    test('multiple asynchronous failures are reported', () {
      var liveTest = _localTest(() {
        Invoker.current!.addOutstandingCallback();
        Future(() => throw TestFailure('one'));
        Future(() => throw TestFailure('two'));
        Future(() => throw TestFailure('three'));
        Future(
          () => throw TestFailure('four'),
        ).whenComplete(Invoker.current!.removeOutstandingCallback);
      }).load(suite);

      expectStates(liveTest, [
        const State(Status.running, Result.success),
        const State(Status.complete, Result.failure),
      ]);

      expectErrors(liveTest, [
        (error) {
          expect(lastState?.status, equals(Status.complete));
          expect(error, isTestFailure('one'));
        },
        (error) {
          expect(error, isTestFailure('two'));
        },
        (error) {
          expect(error, isTestFailure('three'));
        },
        (error) {
          expect(error, isTestFailure('four'));
        },
      ]);

      return liveTest.run();
    });

    test("a failure after an error doesn't change the state of the test", () {
      var liveTest = _localTest(() {
        Future(() => throw TestFailure('fail'));
        throw 'error';
      }).load(suite);

      expectStates(liveTest, [
        const State(Status.running, Result.success),
        const State(Status.complete, Result.error),
      ]);

      expectErrors(liveTest, [
        (error) {
          expect(lastState, equals(const State(Status.complete, Result.error)));
          expect(error, equals('error'));
        },
        (error) {
          expect(error, isTestFailure('fail'));
        },
      ]);

      return liveTest.run();
    });
  });

  group('in a test with errors,', () {
    test('a synchronous throw is reported and causes the test to error', () {
      var liveTest = _localTest(() {
        throw 'oh no';
      }).load(suite);

      expectSingleError(liveTest);
      return liveTest.run();
    });

    test('a synchronous reported error causes the test to error', () {
      var liveTest = _localTest(() {
        registerException('oh no');
      }).load(suite);

      expectSingleError(liveTest);
      return liveTest.run();
    });

    test(
      'an error reported asynchronously during the test causes it to error',
      () {
        var liveTest = _localTest(() {
          Invoker.current!.addOutstandingCallback();
          Future(
            () => registerException('oh no'),
          ).whenComplete(Invoker.current!.removeOutstandingCallback);
        }).load(suite);

        expectSingleError(liveTest);
        return liveTest.run();
      },
    );

    test(
      'an error thrown asynchronously during the test causes it to error',
      () {
        var liveTest = _localTest(() {
          Invoker.current!.addOutstandingCallback();
          Future(
            () => throw 'oh no',
          ).whenComplete(Invoker.current!.removeOutstandingCallback);
        }).load(suite);

        expectSingleError(liveTest);
        return liveTest.run();
      },
    );

    test(
      'an error reported asynchronously after the test causes it to error',
      () {
        var liveTest = _localTest(() {
          Future(() => registerException('oh no'));
        }).load(suite);

        expectStates(liveTest, [
          const State(Status.running, Result.success),
          const State(Status.complete, Result.success),
          const State(Status.complete, Result.error),
        ]);

        expectErrors(liveTest, [
          (error) {
            expect(
              lastState,
              equals(const State(Status.complete, Result.error)),
            );
            expect(error, equals('oh no'));
          },
          (error) {
            expect(
              error,
              equals(
                'This test failed after it had already completed.\n'
                'Make sure to use a matching library which informs the '
                'test runner\nof pending async work.',
              ),
            );
          },
        ]);

        return liveTest.run();
      },
    );

    test('multiple asynchronous errors are reported', () {
      var liveTest = _localTest(() {
        Invoker.current!.addOutstandingCallback();
        Future(() => throw 'one');
        Future(() => throw 'two');
        Future(() => throw 'three');
        Future(
          () => throw 'four',
        ).whenComplete(Invoker.current!.removeOutstandingCallback);
      }).load(suite);

      expectStates(liveTest, [
        const State(Status.running, Result.success),
        const State(Status.complete, Result.error),
      ]);

      expectErrors(liveTest, [
        (error) {
          expect(lastState?.status, equals(Status.complete));
          expect(error, equals('one'));
        },
        (error) {
          expect(error, equals('two'));
        },
        (error) {
          expect(error, equals('three'));
        },
        (error) {
          expect(error, equals('four'));
        },
      ]);

      return liveTest.run();
    });

    test('an error after a failure changes the state of the test', () {
      var liveTest = _localTest(() {
        Future(() => throw 'error');
        throw TestFailure('fail');
      }).load(suite);

      expectStates(liveTest, [
        const State(Status.running, Result.success),
        const State(Status.complete, Result.failure),
        const State(Status.complete, Result.error),
      ]);

      expectErrors(liveTest, [
        (error) {
          expect(
            lastState,
            equals(const State(Status.complete, Result.failure)),
          );
          expect(error, isTestFailure('fail'));
        },
        (error) {
          expect(lastState, equals(const State(Status.complete, Result.error)));
          expect(error, equals('error'));
        },
      ]);

      return liveTest.run();
    });
  });

  test(
    "a test doesn't complete until there are no outstanding callbacks",
    () async {
      var outstandingCallbackRemoved = false;
      var liveTest = _localTest(() {
        Invoker.current!.addOutstandingCallback();

        // Pump the event queue to make sure the test isn't coincidentally
        // completing after the outstanding callback is removed.
        pumpEventQueue().then((_) {
          outstandingCallbackRemoved = true;
          Invoker.current!.removeOutstandingCallback();
        });
      }).load(suite);

      liveTest.onError.listen(expectAsync1((_) {}, count: 0));

      await liveTest.run();
      expect(outstandingCallbackRemoved, isTrue);
    },
  );

  test("a test's prints are captured and reported", () {
    expect(() {
      var liveTest = _localTest(() {
        print('Hello,');
        return Future(() => print('world!'));
      }).load(suite);

      expect(
        liveTest.onMessage.take(2).toList().then((messages) {
          expect(messages[0].type, equals(MessageType.print));
          expect(messages[0].text, equals('Hello,'));
          expect(messages[1].type, equals(MessageType.print));
          expect(messages[1].text, equals('world!'));
        }),
        completes,
      );

      return liveTest.run();
    }, prints(isEmpty));
  });

  group('timeout:', () {
    test('A test can be timed out', () {
      var liveTest = _localTest(
        () {
          Invoker.current!.addOutstandingCallback();
        },
        metadata: Metadata(
          chainStackTraces: true,
          timeout: const Timeout(Duration.zero),
        ),
      ).load(suite);

      expectStates(liveTest, [
        const State(Status.running, Result.success),
        const State(Status.complete, Result.error),
      ]);

      expectErrors(liveTest, [
        (error) {
          expect(lastState!.status, equals(Status.complete));
          expect(error, const TypeMatcher<TimeoutException>());
        },
      ]);

      liveTest.run();
    });

    test('can be ignored', () {
      suite = Suite(Group.root([]), suitePlatform, ignoreTimeouts: true);
      var liveTest = _localTest(
        () async {
          await Future<void>.delayed(const Duration(milliseconds: 10));
        },
        metadata: Metadata(
          chainStackTraces: true,
          timeout: const Timeout(Duration.zero),
        ),
      ).load(suite);

      expectStates(liveTest, [
        const State(Status.running, Result.success),
        const State(Status.complete, Result.success),
      ]);

      liveTest.run();
    });
  });

  group('runTearDowns', () {
    test('runs multiple tear downs', () async {
      var firstTearDownStarted = false;
      var secondTearDownStarted = false;
      await Invoker.current!.runTearDowns([
        () {
          firstTearDownStarted = true;
        },
        () {
          secondTearDownStarted = true;
        },
      ]);
      expect(secondTearDownStarted, isTrue);
      expect(firstTearDownStarted, isTrue);
    });

    test('waits for the future returned tear downs to complete', () async {
      var firstTearDownWork = Completer<void>();
      var secondTearDownStarted = false;
      var result = Invoker.current!.runTearDowns([
        () {
          secondTearDownStarted = true;
        },
        () async {
          await firstTearDownWork.future;
        },
      ]);
      await pumpEventQueue();
      expect(secondTearDownStarted, isFalse);
      firstTearDownWork.complete();
      await result;
      expect(secondTearDownStarted, isTrue);
    });

    test(
      'allows next tear down to run while there are still prior callbacks',
      () async {
        var firstTearDownAsyncWork = Completer<void>();
        var secondTearDownStarted = false;
        unawaited(
          Invoker.current!.runTearDowns([
            () {
              secondTearDownStarted = true;
            },
            () {
              Invoker.current!.addOutstandingCallback();
              firstTearDownAsyncWork.future.whenComplete(
                Invoker.current!.removeOutstandingCallback,
              );
            },
          ]),
        );
        await pumpEventQueue();
        expect(secondTearDownStarted, isTrue);
        firstTearDownAsyncWork.complete();
      },
    );

    test('forwards errors to the enclosing test but does not end it', () async {
      var liveTest = _localTest(() async {
        Invoker.current!.addOutstandingCallback();
        await Invoker.current!.runTearDowns([
          () {
            throw 'oh no';
          },
        ]);
      }).load(suite);

      expectStates(liveTest, [
        const State(Status.running, Result.success),
        const State(Status.complete, Result.error),
      ]);

      var isComplete = false;
      unawaited(liveTest.run().then((_) => isComplete = true));
      await pumpEventQueue();
      expect(liveTest.state.status, equals(Status.complete));
      expect(isComplete, isFalse);
    });
  });

  group('printOnFailure:', () {
    test("doesn't print anything if the test succeeds", () async {
      var liveTest = _localTest(() {
        Invoker.current!.printOnFailure('only on failure');
      }).load(suite);
      liveTest.onError.listen(expectAsync1((_) {}, count: 0));

      liveTest.onMessage.listen(expectAsync1((_) {}, count: 0));

      await liveTest.run();
    });

    test('prints if the test fails', () async {
      var liveTest = _localTest(() {
        Invoker.current!.printOnFailure('only on failure');
        expect(true, isFalse);
      }).load(suite);
      liveTest.onError.listen(expectAsync1((_) {}, count: 1));

      liveTest.onMessage.listen(
        expectAsync1((message) {
          expect(message.type, equals(MessageType.print));
          expect(message.text, equals('only on failure'));
        }, count: 1),
      );

      await liveTest.run();
    });
  });
}

LocalTest _localTest(FutureOr<void> Function() body, {Metadata? metadata}) {
  metadata ??= Metadata(chainStackTraces: true);
  return LocalTest('test', metadata, body);
}
