// 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 'package:dds/src/dap/isolate_manager.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';

import 'mocks.dart';

main() {
  group('IsolateManager', () {
    late MockDartCliDebugAdapter adapter;
    late IsolateManager isolateManager;

    setUp(() async {
      adapter = MockDartCliDebugAdapter();
      isolateManager = adapter.isolateManager;

      isolateManager.debug = true;
      isolateManager.debugSdkLibraries = false;
      isolateManager.debugExternalPackageLibraries = true;
      await isolateManager.registerIsolate(
        adapter.mockService.isolate1,
        EventKind.kIsolateStart,
      );
      await isolateManager.registerIsolate(
        adapter.mockService.isolate2,
        EventKind.kIsolateStart,
      );
    });

    test('sends only changes to SDK libraries debuggable flag', () async {
      // Default is false, so should not send anything.
      isolateManager.debugSdkLibraries = false;
      await isolateManager.applyDebugOptions();
      expect(
        adapter.mockService.requests,
        isNot(
          contains(startsWith('setLibraryDebuggable(isolate1, libSdk,')),
        ),
      );

      // Changing to non-default should send a request.
      isolateManager.debugSdkLibraries = true;
      await isolateManager.applyDebugOptions();
      expect(
        adapter.mockService.requests,
        contains('setLibraryDebuggable(isolate1, libSdk, true)'),
      );

      // Setting back to default should now send.
      isolateManager.debugSdkLibraries = false;
      await isolateManager.applyDebugOptions();
      expect(
        adapter.mockService.requests,
        contains('setLibraryDebuggable(isolate1, libSdk, false)'),
      );
    });

    test('sends only changes to external package libraries debuggable flag',
        () async {
      // Default is true, so should not send anything.
      isolateManager.debugExternalPackageLibraries = true;
      await isolateManager.applyDebugOptions();
      expect(
        adapter.mockService.requests,
        isNot(
          contains(
              startsWith('setLibraryDebuggable(isolate1, libPkgExternal,')),
        ),
      );

      // Changing to non-default should send a request.
      isolateManager.debugExternalPackageLibraries = false;
      await isolateManager.applyDebugOptions();
      expect(
        adapter.mockService.requests,
        contains('setLibraryDebuggable(isolate1, libPkgExternal, false)'),
      );

      // Setting back to default should now send.
      isolateManager.debugExternalPackageLibraries = true;
      await isolateManager.applyDebugOptions();
      expect(
        adapter.mockService.requests,
        contains('setLibraryDebuggable(isolate1, libPkgExternal, true)'),
      );
    });

    /// Local packages are always debuggable, in the VM and because we have no
    /// settings. No changes should ever cause requests for them.
    test('never sends values for local package libraries debuggable flag',
        () async {
      isolateManager.debugSdkLibraries = true;
      isolateManager.debugExternalPackageLibraries = true;
      await isolateManager.applyDebugOptions();

      isolateManager.debugSdkLibraries = false;
      isolateManager.debugExternalPackageLibraries = false;
      await isolateManager.applyDebugOptions();

      expect(
        adapter.mockService.requests,
        isNot(
          contains(startsWith('setLibraryDebuggable(isolate1, libPkgLocal,')),
        ),
      );
    });

    test('clears thread data on resume', () async {
      final thread1 = isolateManager.threads[0];
      final thread2 = isolateManager.threads[1];

      // Store some data in both threads.
      final ref1 = thread1.storeData("test1");
      final ref2 = thread2.storeData("test2");

      // Resume thread1
      thread1.paused = true; // Fake pause to allow resume.
      await isolateManager.resumeThread(thread1.threadId);

      // Ensure thread1 had data cleared, but thread2 did not.
      expect(isolateManager.getStoredData(ref1), isNull);
      expect(isolateManager.getStoredData(ref2), isNotNull);
    });

    test('uses uri converter to convert resolved uri', () async {
      String? receivedUri;
      adapter.setUriConverter((uri) {
        receivedUri = uri;
        return '/returned/file/path.dart';
      });

      adapter.mockService.lookupResolvedPackageUrisResponse =
          UriList(uris: ['scheme://file/path.dart']);

      final thread = isolateManager.threads[0];
      final resolved =
          await thread.resolveUriToPath(Uri.parse('package:pkg/path.dart'));

      expect(adapter.mockService.receivedLookupResolvedPackageUris,
          ['package:pkg/path.dart']);
      expect(receivedUri, 'scheme://file/path.dart');
      expect(resolved, Uri.file('/returned/file/path.dart'));
    });

    test('sends resume after kPausePostRequest', () async {
      // Trigger initial startup.
      await adapter.isolateManager.handleEvent(Event(
          isolate: adapter.mockService.isolate1,
          kind: EventKind.kIsolateRunnable));
      isolateManager.threads[0].startupHandled = true;

      // Clear any previously called methods.
      adapter.mockService.calledMethods.clear();

      // Now simulate a hot reload causing the isolate to pause again.
      await adapter.isolateManager.handleEvent(Event(
          isolate: adapter.mockService.isolate1,
          kind: EventKind.kPausePostRequest));

      // Ensure we called readyToResume.
      expect(adapter.mockService.calledMethods.map((cm) => cm.method),
          contains('readyToResume'));

      // Expect it was resumed.
    });
  });
}
