// Copyright (c) 2025, 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:developer' show debugger;
import 'dart:io' show Directory, File;
import 'dart:isolate' as i;

import 'package:path/path.dart' show join;
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';

import 'common/service_test_common.dart';

// AUTOGENERATED START
//
// Update these constants by running:
//
// dart pkg/vm_service/test/update_line_numbers.dart pkg/vm_service/test/breakpoint_resolution_after_reloading_test_common.dart
//
const LINE_A = 77;
// AUTOGENERATED END

const _v0Contents = '''
import 'dart:developer';

void f() {}

void main() {
  debugger();
  f();
  f();
}
''';

const _v1Contents = '''
import 'dart:developer';

void f() {
  (() {
    (() {
      print('v1');
    })();
  })();
}

void main() {
  f();
  f();
}
''';

const _v2Contents = '''
import 'dart:developer';

void f() {
  (() {
    print('v2.a');
    print('v2.b');
  })();
}

void main() {
  f();
  f();
}
''';

Future<void> testeeMain() async {
  // Spawn the child isolate.
  final tempDir = Directory.systemTemp.createTempSync();
  try {
    final rootLib = File(join(tempDir.path, 'main.dart'));
    rootLib.writeAsStringSync(_v0Contents);

    await i.Isolate.spawnUri(rootLib.uri, [], null);
    debugger(); // LINE_A
    tempDir.deleteSync(recursive: true);
  } catch (_) {
    tempDir.deleteSync(recursive: true);
    rethrow;
  }
}

final breakpointResolutionAfterReloadingTests = <IsolateTest>[
  // Ensure that the main isolate has stopped at the [debugger] statement at the
  // end of [testeeMain].
  hasStoppedAtBreakpoint,
  stoppedAtLine(LINE_A),
  (VmService service, IsolateRef isolateRef) async {
    final tempDir = Directory.systemTemp.createTempSync();
    try {
      // This test is a regression test against a bug caused by comparing script
      // URLs instead of script pointers in the debugger. To produce a situation
      // in which the bug used to occur, this test loads
      // [spawnedIsolateRootLib], modifies [spawnedIsolateRootLib], and then
      // reloads [spawnedIsolateRootLib].
      final spawnedIsolateRootLib = File(join(tempDir.path, 'main.dart'));

      // Find the spawned isolate.
      final vm = await service.getVM();
      final isolates = vm.isolates!;
      expect(isolates.length, 2);
      final spawnedIsolateRef = isolates.firstWhere(
        (i) => i != isolateRef,
      );
      final spawnedIsolateId = spawnedIsolateRef.id!;

      // Load [v1Contents] into the spawned isolate.
      spawnedIsolateRootLib.writeAsStringSync(_v1Contents);
      await service.reloadSources(
        spawnedIsolateId,
        rootLibUri: spawnedIsolateRootLib.uri.toString(),
        force: true,
      );

      Isolate spawnedIsolate = await service.getIsolate(spawnedIsolateId);
      Library rootLib = await service.getObject(
        spawnedIsolateId,
        spawnedIsolate.rootLib!.id!,
      ) as Library;
      String scriptId = rootLib.scripts![0].id!;

      // Add a breakpoint at `print('v1');`.
      await service.addBreakpoint(spawnedIsolateId, scriptId, 6);

      // Resuming the spawned isolate should let it run until it gets paused at
      // the breakpoint at `print('v1');`.
      await resumeIsolate(service, spawnedIsolateRef);
      await hasStoppedAtBreakpoint(service, spawnedIsolateRef);
      await stoppedAtLine(6)(service, spawnedIsolateRef);

      // Load [v2Contents] into the spawned isolate.
      spawnedIsolateRootLib.writeAsStringSync(_v2Contents);
      await service.reloadSources(
        spawnedIsolateId,
        rootLibUri: spawnedIsolateRootLib.uri.toString(),
        force: true,
      );

      spawnedIsolate = await service.getIsolate(spawnedIsolateId);
      rootLib = await service.getObject(
        spawnedIsolateId,
        spawnedIsolate.rootLib!.id!,
      ) as Library;
      scriptId = rootLib.scripts![0].id!;

      // Add a breakpoint at `print('v2.a');`.
      await service.addBreakpoint(spawnedIsolateId, scriptId, 5);

      // Resuming the spawned isolate should let it run until it gets paused at
      // the breakpoint at `print('v2.a');`.
      await resumeIsolate(service, spawnedIsolateRef);
      await hasStoppedAtBreakpoint(service, spawnedIsolateRef);
      await stoppedAtLine(5)(service, spawnedIsolateRef);

      // Add a breakpoint at `print('v2.b');`.
      final breakpoint3 =
          await service.addBreakpoint(spawnedIsolateId, scriptId, 6);
      expect(breakpoint3.breakpointNumber, 3);

      // We previously had a bug that would have made the breakpoint resolution
      // code get confused by the old closure that was defined in [v1Contents].
      // We prevent a reintroduction of that bug by ensuring that the newly set
      // breakpoint has been resolved immediately.
      expect(breakpoint3.resolved, true);

      await resumeIsolate(service, spawnedIsolateRef);
      tempDir.deleteSync(recursive: true);
    } catch (_) {
      tempDir.deleteSync(recursive: true);
      rethrow;
    }
  },
];
