// 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(dynamic Function() body, {Metadata? metadata}) {
  metadata ??= Metadata(chainStackTraces: true);
  return LocalTest('test', metadata, body);
}
