// Copyright (c) 2016, 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 'dart:io';

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/test.dart'; // ignore: implementation_imports

import '../configuration.dart';
import '../environment.dart';
import '../load_exception.dart';
import '../runner_suite.dart';
import '../runner_test.dart';
import '../suite.dart';

/// A helper method for creating a [RunnerSuiteController] containing tests
/// that communicate over [channel].
///
/// This returns a controller so that the caller has a chance to control the
/// runner suite's debugging state based on plugin-specific logic.
///
/// If the suite is closed, this will close [channel].
///
/// The [message] parameter is an opaque object passed from the runner to
/// [PlatformPlugin.load]. Plugins shouldn't interact with it other than to pass
/// it on to [deserializeSuite].
///
/// If [mapper] is passed, it will be used to adjust stack traces for any errors
/// emitted by tests.
///
/// [gatherCoverage] is a callback which returns a hit-map containing merged
/// coverage report suitable for use with `package:coverage`.
RunnerSuiteController deserializeSuite(
    String path,
    SuitePlatform platform,
    SuiteConfiguration suiteConfig,
    Environment environment,
    StreamChannel<Object?> channel,
    Object /*Map<String, Object?>*/ message,
    {Future<Map<String, dynamic>> Function()? gatherCoverage}) {
  var disconnector = Disconnector<Object?>();
  var suiteChannel = MultiChannel<Object?>(channel.transform(disconnector));

  suiteChannel.sink.add(<String, Object?>{
    'type': 'initial',
    'platform': platform.serialize(),
    'metadata': suiteConfig.metadata.serialize(),
    'asciiGlyphs': Platform.isWindows,
    'path': path,
    'collectTraces': Configuration.current.reporter == 'json' ||
        Configuration.current.fileReporters.containsKey('json') ||
        suiteConfig.testSelections.any(
            (selection) => selection.line != null || selection.col != null),
    'noRetry': Configuration.current.noRetry,
    'foldTraceExcept': Configuration.current.foldTraceExcept.toList(),
    'foldTraceOnly': Configuration.current.foldTraceOnly.toList(),
    'allowDuplicateTestNames': suiteConfig.allowDuplicateTestNames,
    'ignoreTimeouts': suiteConfig.ignoreTimeouts,
    ...message as Map<String, dynamic>,
  });

  var completer = Completer<Group>();

  var loadSuiteZone = Zone.current;
  void handleError(Object error, StackTrace stackTrace) {
    disconnector.disconnect();

    if (completer.isCompleted) {
      // If we've already provided a controller, send the error to the
      // LoadSuite. This will cause the virtual load test to fail, which will
      // notify the user of the error.
      loadSuiteZone.handleUncaughtError(error, stackTrace);
    } else {
      completer.completeError(error, stackTrace);
    }
  }

  suiteChannel.stream.cast<Map<String, Object?>>().listen(
      (response) {
        switch (response['type'] as String) {
          case 'print':
            print(response['line']);
            break;

          case 'loadException':
            handleError(LoadException(path, response['message'] as Object),
                Trace.current());
            break;

          case 'error':
            var asyncError = RemoteException.deserialize(
                response['error'] as Map<String, dynamic>);
            handleError(
                LoadException(path, asyncError.error), asyncError.stackTrace);
            break;

          case 'success':
            var deserializer = _Deserializer(suiteChannel);
            completer.complete(
                deserializer.deserializeGroup(response['root'] as Map));
            break;
        }
      },
      onError: handleError,
      onDone: () {
        if (completer.isCompleted) return;
        completer.completeError(
            LoadException(path, 'Connection closed before test suite loaded.'),
            Trace.current());
      });

  return RunnerSuiteController(
      environment, suiteConfig, suiteChannel, completer.future, platform,
      path: path,
      onClose: () => disconnector.disconnect().onError(handleError),
      gatherCoverage: gatherCoverage);
}

/// A utility class for storing state while deserializing tests.
class _Deserializer {
  /// The channel over which tests communicate.
  final MultiChannel _channel;

  _Deserializer(this._channel);

  /// Deserializes [group] into a concrete [Group].
  Group deserializeGroup(Map group) {
    var metadata = Metadata.deserialize(group['metadata'] as Map);
    var trace =
        group['trace'] == null ? null : Trace.parse(group['trace'] as String);
    var location = switch (group['location']) {
      Map map => TestLocation.deserialize(map),
      _ => null,
    };
    var entries = (group['entries'] as List).map((entry) {
      var map = entry as Map;
      if (map['type'] == 'group') return deserializeGroup(map);
      return _deserializeTest(map)!;
    }).toList();

    return Group(group['name'] as String, entries,
        metadata: metadata,
        trace: trace,
        location: location,
        setUpAll: _deserializeTest(group['setUpAll'] as Map?),
        tearDownAll: _deserializeTest(group['tearDownAll'] as Map?));
  }

  /// Deserializes [test] into a concrete [Test] class.
  ///
  /// Returns `null` if [test] is `null`.
  Test? _deserializeTest(Map? test) {
    if (test == null) return null;

    var metadata = Metadata.deserialize(test['metadata'] as Map);
    var trace =
        test['trace'] == null ? null : Trace.parse(test['trace'] as String);
    var location = switch (test['location']) {
      Map map => TestLocation.deserialize(map),
      _ => null,
    };
    var testChannel = _channel.virtualChannel((test['channel'] as num).toInt());
    return RunnerTest(
        test['name'] as String, metadata, trace, location, testChannel);
  }
}
