// 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:stack_trace/stack_trace.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:test_api/backend.dart'
    show Metadata, RemoteException, SuitePlatform, TestLocation;
import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/live_test_controller.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports

import 'spawn_hybrid.dart';

/// A test running remotely, controlled by a stream channel.
class RunnerTest extends Test {
  @override
  final String name;
  @override
  final Metadata metadata;
  @override
  final Trace? trace;
  @override
  final TestLocation? location;

  /// The channel used to communicate with the test's `RemoteListener`.
  final MultiChannel _channel;

  RunnerTest(
    this.name,
    this.metadata,
    this.trace,
    this.location,
    this._channel,
  );

  @override
  LiveTest load(Suite suite, {Iterable<Group>? groups}) {
    late final LiveTestController controller;
    late final VirtualChannel testChannel;
    controller = LiveTestController(
      suite,
      this,
      () {
        controller.setState(const State(Status.running, Result.success));

        testChannel = _channel.virtualChannel();
        _channel.sink.add({'command': 'run', 'channel': testChannel.id});

        testChannel.stream.listen(
          (message) {
            final msg = message as Map;
            switch (msg['type'] as String) {
              case 'error':
                var asyncError = RemoteException.deserialize(
                  msg['error'] as Map<String, dynamic>,
                );
                var stackTrace = asyncError.stackTrace;
                controller.addError(asyncError.error, stackTrace);
                break;

              case 'state-change':
                controller.setState(
                  State(
                    Status.parse(msg['status'] as String),
                    Result.parse(msg['result'] as String),
                  ),
                );
                break;

              case 'message':
                controller.message(
                  Message(
                    MessageType.parse(msg['message-type'] as String),
                    msg['text'] as String,
                  ),
                );
                break;

              case 'complete':
                controller.completer.complete();
                break;

              case 'spawn-hybrid-uri':
                // When we kill the isolate that the test lives in, that will close
                // this virtual channel and cause the spawned isolate to close as
                // well.
                spawnHybridUri(
                  msg['url'] as String,
                  msg['message'],
                  suite,
                ).pipe(
                  testChannel.virtualChannel((msg['channel'] as num).toInt()),
                );
                break;
            }
          },
          onDone: () {
            // When the test channel closes—presumably because the browser
            // closed—mark the test as complete no matter what.
            if (controller.completer.isCompleted) return;
            controller.completer.complete();
          },
        );
      },
      () {
        // If the test has finished running, just disconnect the channel.
        if (controller.completer.isCompleted) {
          testChannel.sink.close();
          return;
        }

        unawaited(() async {
          // If the test is still running, send it a message telling it to shut
          // down ASAP. This causes the [Invoker] to eagerly throw exceptions
          // whenever the test touches it.
          testChannel.sink.add({'command': 'close'});
          await controller.completer.future;
          await testChannel.sink.close();
        }());
      },
      groups: groups,
    );
    return controller;
  }

  @override
  Test? forPlatform(SuitePlatform platform) {
    if (!metadata.testOn.evaluate(platform)) return null;
    return RunnerTest(
      name,
      metadata.forPlatform(platform),
      trace,
      location,
      _channel,
    );
  }

  @override
  Test? filter(bool Function(Test) callback) {
    if (callback(this)) {
      // filter() always returns new copies because they need to be attached
      // to their new parents.
      return clone();
    }

    return null;
  }

  @override
  Test clone() {
    return RunnerTest(name, metadata, trace, location, _channel);
  }
}
