// Copyright 2013 The Flutter Authors. 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 'dart:collection';
import 'dart:io' show stdout;
import 'dart:isolate';

import 'test.dart';

/// A suite of tests, added with the [test] method, which will be run in a
/// following event.
class TestSuite {
  /// Creates a new [TestSuite] with logs written to [logger] and callbacks
  /// given by [lifecycle].
  TestSuite({
    StringSink? logger,
    Lifecycle? lifecycle,
  }) :
    _logger = logger ?? stdout,
    _lifecycle = lifecycle ?? _DefaultLifecycle();

  final Lifecycle _lifecycle;
  final StringSink _logger;
  bool _testQueuePrimed = false;
  final Queue<Test> _testQueue = Queue<Test>();
  final Map<String, Test> _runningTests = <String, Test>{};

  /// Adds a test to the test suite.
  void test(
    String name,
    dynamic Function() body, {
    bool skip = false,
  }) {
    if (_runningTests.isNotEmpty) {
      throw StateError(
        'Test "$name" added after tests have started to run. '
        'Calls to test() must be synchronous with main().',
      );
    }
    if (skip) {
      _logger.writeln('Test $name: Skipped');
      return;
    }
    _pushTest(name, body);
  }

  void _pushTest(
    String name,
    dynamic Function() body,
  ) {
    final Test newTest = Test(name, body, logger: _logger);
    _testQueue.add(newTest);
    newTest.state = TestState.queued;
    if (!_testQueuePrimed) {
      // All tests() must be added synchronously with main, so we can enqueue an
      // event to start all tests to run after main() is done.
      Timer.run(_startAllTests);
      _testQueuePrimed = true;
    }
  }

  void _startAllTests() {
    for (final Test t in _testQueue) {
      _runningTests[t.name] = t;
      t.run(onDone: () {
        _runningTests.remove(t.name);
        if (_runningTests.isEmpty) {
          _lifecycle.onDone(_testQueue);
        }
      });
    }
    _lifecycle.onStart();
  }
}

/// Callbacks for the lifecycle of a [TestSuite].
abstract class Lifecycle {
  /// Called after a test suite has started.
  void onStart();

  /// Called after the last test in a test suite has completed.
  void onDone(Queue<Test> tests);
}

class _DefaultLifecycle implements Lifecycle {
  final ReceivePort _suitePort = ReceivePort('Suite port');
  late Queue<Test> _tests;

  @override
  void onStart() {
    _suitePort.listen((dynamic msg) {
      _suitePort.close();
      _processResults();
    });
  }

  @override
  void onDone(Queue<Test> tests) {
    _tests = tests;
    _suitePort.sendPort.send(null);
  }

  void _processResults() {
    bool testsSucceeded = true;
    for (final Test t in _tests) {
      testsSucceeded = testsSucceeded && (t.state == TestState.succeeded);
    }
    if (!testsSucceeded) {
      throw 'A test failed';
    }
  }
}
