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

import 'package:dap/dap.dart';
import 'package:dds/dds.dart';
import 'package:dds/src/dap/constants.dart';
import 'package:dds_service_extensions/dap.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';

import 'common/test_helper.dart';

Future<Isolate> waitForFirstPausedRunnableIsolate(VmService service) async {
  VM vm;
  do {
    vm = await service.getVM();
  } while (vm.isolates!.isEmpty);
  final isolateId = vm.isolates!.first.id!;
  Isolate isolate;
  do {
    isolate = await service.getIsolate(isolateId);
  } while (!isolate.runnable! || isolate.pauseEvent == null);
  return isolate;
}

void main() {
  Process? process;
  DartDevelopmentService? dds;
  late VmService service;

  Future<void> createProcess({bool pauseOnStart = true}) async {
    process = await spawnDartProcess(
      'dap_over_dds_script.dart',
      disableServiceAuthCodes: true,
      pauseOnStart: pauseOnStart,
    );
  }

  tearDown(() async {
    await dds?.shutdown();
    process?.kill();
    process = null;
  });

  var nextSeq = 1;
  Future<DapResponse> sendDapRequest(String request,
      [Object? arguments]) async {
    final result = await service.sendDapRequest(
      jsonEncode(
        Request(
          command: request,
          seq: nextSeq++,
          arguments: arguments,
        ),
      ),
    );

    expect(result.dapResponse, isNotNull);
    expect(result.dapResponse.type, 'response');
    expect(result.dapResponse.message, isNull);
    expect(result.dapResponse.success, true);
    expect(result.dapResponse.command, request);

    return result;
  }

  Future<String?> instanceToString(String isolateId, String instanceId) async {
    final result = await service.invoke(isolateId, instanceId, 'toString', []);
    return result is InstanceRef ? result.valueAsString : null;
  }

  Future<String> variableToString(int variablesReference) async {
    // Because variables requests are for _child_ variables, a converted
    // Instance->Variable will return an initial container that has a single
    // 'value' field, which contains the string representation of the variable
    // as its value, and a further 'variablesReference' that can be used to
    // fetch child variables.
    final variablesResult = await sendDapRequest(
      'variables',
      VariablesArguments(variablesReference: variablesReference),
    );
    final variablesBody = VariablesResponseBody.fromMap(
      variablesResult.dapResponse.body as Map<String, Object?>,
    );
    expect(variablesBody.variables, hasLength(1));
    final variable = variablesBody.variables.single;
    expect(variable.name, 'value');
    expect(variable.variablesReference, isPositive);
    return variable.value;
  }

  test('DDS responds to DAP message', () async {
    await createProcess();
    dds = await DartDevelopmentService.startDartDevelopmentService(
      remoteVmServiceUri,
    );
    expect(dds!.isRunning, true);
    final serviceUri = dds!.wsUri!;
    service = await vmServiceConnectUri(serviceUri.toString());

    final breakpointArguments = SetBreakpointsArguments(
      breakpoints: [
        SourceBreakpoint(line: 20),
        SourceBreakpoint(line: 30),
      ],
      source: Source(
        name: 'main.dart',
        path: '/file/to/main.dart',
      ),
    );

    final result = await sendDapRequest('setBreakpoints', breakpointArguments);
    final response = SetBreakpointsResponseBody.fromMap(
        result.dapResponse.body as Map<String, Object?>);
    expect(response.breakpoints, hasLength(2));
    expect(response.breakpoints[0].verified, isFalse);
    expect(response.breakpoints[1].verified, isFalse);
  });

  test('DAP can map between variableReferences and InstanceRefs', () async {
    await createProcess(pauseOnStart: false);
    dds = await DartDevelopmentService.startDartDevelopmentService(
      remoteVmServiceUri,
    );
    service = await vmServiceConnectUri(dds!.wsUri!.toString());

    // Wait for the isolate to become runnable as `evaluateInFrame` requires
    // the isolate to be runnable.
    final isolate = await waitForFirstPausedRunnableIsolate(service);
    final isolateId = isolate.id!;

    // Get the variable for 'myInstance'.
    final originalInstanceRef = (await service.evaluateInFrame(
        isolateId, 0, 'myInstance')) as InstanceRef;
    final originalInstanceId = originalInstanceRef.id!;

    // Ask DAP to make a variableReference for it.
    final createVariableResult = await sendDapRequest(
      Command.createVariableForInstance,
      {
        Parameters.isolateId: isolateId,
        Parameters.instanceId: originalInstanceId,
      },
    );
    final createVariablesBody =
        createVariableResult.dapResponse.body as Map<String, Object?>;
    final variablesReference =
        createVariablesBody[Parameters.variablesReference] as int;

    // And now ask DAP to convert it back to an instance ID.
    final getInstanceResult = await sendDapRequest(
      Command.getVariablesInstanceId,
      {
        Parameters.variablesReference: variablesReference,
      },
    );
    final getInstanceRefBody =
        getInstanceResult.dapResponse.body as Map<String, Object?>;
    final mappedInstanceId =
        getInstanceRefBody[Parameters.instanceId] as String;

    // Now verify that the string value of these are all the same.
    expect(await instanceToString(isolateId, originalInstanceId), 'MyClass');
    expect(await variableToString(variablesReference), 'MyClass');
    expect(await instanceToString(isolateId, mappedInstanceId), 'MyClass');
  });

  test('DAP includes Isolate IDs in Threads', () async {
    await createProcess(pauseOnStart: false);
    dds = await DartDevelopmentService.startDartDevelopmentService(
      remoteVmServiceUri,
    );
    service = await vmServiceConnectUri(dds!.wsUri!.toString());

    // Get the expected isolateId.
    final isolate = await waitForFirstPausedRunnableIsolate(service);
    final isolateId = isolate.id!;

    // Ask DAP for all threads.
    final threadsResult = await sendDapRequest(Command.threads);
    final threadsBody = threadsResult.dapResponse.body as Map<String, Object?>;

    final threads = threadsBody['threads'] as List;
    expect(threads, hasLength(1));
    final thread = threads[0] as Map<String, Object?>;
    expect(thread, {
      'id': 1,
      'name': 'main',
      'isolateId': isolateId,
    });
  });
}
