// Copyright (c) 2021, 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:convert';
import 'dart:io';
import 'dart:isolate';

import 'package:dds/src/dap/logging.dart';
import 'package:dds/src/dap/server.dart';
import 'package:path/path.dart' as path;

/// Allow running from local source (useful in development).
final runFromSource =
    Platform.environment['DAP_TEST_INTEGRATION_FROM_SOURCE'] == 'true';

abstract class DapTestServer {
  Future<void> stop();
  StreamSink<List<int>> get sink;
  Stream<List<int>> get stream;
}

/// An instance of a DAP server running in-process (to aid debugging).
///
/// All communication still goes over the socket to ensure all messages are
/// serialized and deserialized but it's not quite the same running out of
/// process.
class InProcessDapTestServer extends DapTestServer {
  late final DapServer _server;
  final stdinController = StreamController<List<int>>();
  final stdoutController = StreamController<List<int>>();

  @override
  StreamSink<List<int>> get sink => stdinController.sink;
  @override
  Stream<List<int>> get stream => stdoutController.stream;

  InProcessDapTestServer._(
    List<String> args, {
    Function? onError,
  }) {
    _server = DapServer(
      stdinController.stream,
      stdoutController.sink,
      // Simulate flags based on the args to aid testing.
      ipv6: args.contains('--ipv6'),
      test: args.contains('--test'),
      onError: onError,
    );
  }

  @override
  Future<void> stop() async {
    _server.stop();
  }

  static Future<InProcessDapTestServer> create({
    Logger? logger,
    Function? onError,
    List<String>? additionalArgs,
  }) async {
    return InProcessDapTestServer._(
      [
        ...?additionalArgs,
      ],
      onError: onError,
    );
  }
}

/// An instance of a DAP server running out-of-process.
///
/// This is how an editor will usually consume DAP so is a more accurate test
/// but will be a little more difficult to debug tests as the debugger will not
/// be attached to the process.
class OutOfProcessDapTestServer extends DapTestServer {
  var _isShuttingDown = false;
  final Process _process;

  Future<int> get exitCode => _process.exitCode;
  @override
  StreamSink<List<int>> get sink => _process.stdin;
  @override
  Stream<List<int>> get stream => _process.stdout;

  OutOfProcessDapTestServer._(
    this._process,
    Logger? logger, {
    Function? onError,
  }) {
    // Handle any stderr from the process. If an error handler was provided by
    // the test, call it. Otherwise throw to fail the test as it's likely
    // unexpected.
    _process.stderr.transform(utf8.decoder).listen((error) {
      logger?.call(error);
      if (onError != null) {
        onError(error);
      } else {
        throw error;
      }
    });
    unawaited(_process.exitCode.then((code) {
      final message = 'Out-of-process DAP server terminated with code $code';
      logger?.call(message);
      if (!_isShuttingDown && code != 0) {
        throw message;
      }
    }));
  }

  @override
  Future<void> stop() async {
    _isShuttingDown = true;
    _process.kill();
    await _process.exitCode;
  }

  static Future<OutOfProcessDapTestServer> create({
    Logger? logger,
    Function? onError,
    List<String>? additionalArgs,
  }) async {
    final ddsEntryScript =
        await Isolate.resolvePackageUri(Uri.parse('package:dds/dds.dart'));
    final ddsLibFolder = path.dirname(ddsEntryScript!.toFilePath());
    final dartdevScript = path
        .normalize(path.join(ddsLibFolder, '../../dartdev/bin/dartdev.dart'));

    final args = [
      // When running from source, run the script instead of directly using
      // the "dart debug_adapter" command.
      if (runFromSource) dartdevScript,
      'debug_adapter',
      ...?additionalArgs,
    ];

    final process = await Process.start(Platform.resolvedExecutable, args);

    return OutOfProcessDapTestServer._(process, logger, onError: onError);
  }
}
