// Copyright (c) 2024, 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';

import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';

import 'common/service_test_common.dart';
import 'common/test_helper.dart';

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

class D {}

@pragma('vm:entry-point')
D getDLiteral() => D();

class C {
  final field = D();
}

void testeeMain() {
  // ignore: unused_local_variable
  final c = C();
  debugger(); // LINE_A
}

final tests = <IsolateTest>[
  hasStoppedAtBreakpoint,
  stoppedAtLine(LINE_A),
  // Test the behaviour of an ID Zone with a `backingBufferKind` of `Ring`, an
  // `idAssignmentPolicy` of `AlwaysAllocate`, and the default capacity.
  (VmService service, IsolateRef isolateRef) async {
    final isolateId = isolateRef.id!;
    final idZone1 = await service.createIdZone(
      isolateId,
      IdZoneBackingBufferKind.kRing,
      IdAssignmentPolicy.kAlwaysAllocate,
    );
    expect(idZone1.id, 'zones/1');
    expect(idZone1.backingBufferKind, IdZoneBackingBufferKind.kRing);
    expect(idZone1.idAssignmentPolicy, IdAssignmentPolicy.kAlwaysAllocate);

    final cInstanceRef1 = await service.evaluateInFrame(
      isolateId,
      0,
      'c',
      idZoneId: idZone1.id,
    ) as InstanceRef;
    final cObjectId1 = cInstanceRef1.id!;
    expect(cObjectId1, 'objects/0/1');

    final cInstance1 = await service.getObject(isolateId, cObjectId1);
    expect(cInstance1.type, 'Instance');

    final cInstanceRef2 = await service.evaluateInFrame(
      isolateId,
      0,
      'c',
      idZoneId: idZone1.id,
    ) as InstanceRef;
    final cObjectId2 = cInstanceRef2.id!;
    expect(cObjectId2, 'objects/1/1');

    final cInstance2 = await service.getObject(isolateId, cObjectId2);
    expect(cInstance2.type, 'Instance');

    await service.invalidateIdZone(
      isolateId,
      idZone1.id!,
    );

    try {
      await service.getObject(isolateId, cObjectId1);
      fail('successfully retrieved object using expired ID');
    } on SentinelException catch (e) {
      expect(e.sentinel.kind, startsWith('Expired'));
      expect(e.sentinel.valueAsString, equals('<expired>'));
    }

    // Ensure that the zone can be reused after it was invalidated.
    final cInstanceRef3 = await service.evaluateInFrame(
      isolateId,
      0,
      'c',
      idZoneId: idZone1.id,
    ) as InstanceRef;
    expect(cInstanceRef3.id, 'objects/0/1');
  },

  // Test the behaviour of an ID Zone with a `backingBufferKind` of `Ring`, an
  // `idAssignmentPolicy` of `AlwaysAllocate`, and a capacity of 1.
  (VmService service, IsolateRef isolateRef) async {
    final isolateId = isolateRef.id!;
    final idZone2 = await service.createIdZone(
      isolateId,
      IdZoneBackingBufferKind.kRing,
      IdAssignmentPolicy.kAlwaysAllocate,
      capacity: 1,
    );
    expect(idZone2.id, 'zones/2');
    expect(idZone2.backingBufferKind, IdZoneBackingBufferKind.kRing);
    expect(idZone2.idAssignmentPolicy, IdAssignmentPolicy.kAlwaysAllocate);

    final cInstanceRef1 = await service.evaluateInFrame(
      isolateId,
      0,
      'c',
      idZoneId: idZone2.id,
    ) as InstanceRef;
    final cObjectId1 = cInstanceRef1.id!;
    expect(cObjectId1, 'objects/0/2');

    final cInstance1 = await service.getObject(isolateId, cObjectId1);
    expect(cInstance1.type, 'Instance');

    final cInstanceRef2 = await service.evaluateInFrame(
      isolateId,
      0,
      'c',
      idZoneId: idZone2.id,
    ) as InstanceRef;
    final cObjectId2 = cInstanceRef2.id!;
    expect(cObjectId2, 'objects/1/2');

    final cInstance2 = await service.getObject(isolateId, cObjectId2);
    expect(cInstance2.type, 'Instance');

    // [idZone2] only has a capacity of 1, so [cObjectId1] should have been
    // evicted when [cObjectId2] was allocated.
    try {
      await service.getObject(isolateId, cObjectId1);
      fail('successfully retrieved object using expired ID');
    } on SentinelException catch (e) {
      expect(e.sentinel.kind, startsWith('Expired'));
      expect(e.sentinel.valueAsString, equals('<expired>'));
    }
  },

  // Test the behaviour of an ID Zone with a `backingBufferKind` of `Ring`, an
  // `idAssignmentPolicy` of `ReuseExisting`, and the default capacity.
  (VmService service, IsolateRef isolateRef) async {
    final isolateId = isolateRef.id!;
    final idZone3 = await service.createIdZone(
      isolateId,
      IdZoneBackingBufferKind.kRing,
      IdAssignmentPolicy.kReuseExisting,
    );
    expect(idZone3.id, 'zones/3');
    expect(idZone3.backingBufferKind, IdZoneBackingBufferKind.kRing);
    expect(idZone3.idAssignmentPolicy, IdAssignmentPolicy.kReuseExisting);

    final cInstanceRef1 = await service.evaluateInFrame(
      isolateId,
      0,
      'c',
      idZoneId: idZone3.id,
    ) as InstanceRef;
    final cObjectId1 = cInstanceRef1.id!;
    expect(cObjectId1, 'objects/0/3');

    final cInstance1 = await service.getObject(isolateId, cObjectId1);
    expect(cInstance1.type, 'Instance');

    final cInstanceRef2 = await service.evaluateInFrame(
      isolateId,
      0,
      'c',
      idZoneId: idZone3.id,
    ) as InstanceRef;
    final cObjectId2 = cInstanceRef2.id!;
    expect(cObjectId2, 'objects/0/3');

    final cInstance2 = await service.getObject(isolateId, cObjectId2);
    expect(cInstance2.type, 'Instance');

    await service.invalidateIdZone(
      isolateId,
      idZone3.id!,
    );

    try {
      await service.getObject(isolateId, cObjectId1);
      fail('successfully retrieved object using expired ID');
    } on SentinelException catch (e) {
      expect(e.sentinel.kind, startsWith('Expired'));
      expect(e.sentinel.valueAsString, equals('<expired>'));
    }

    // Ensure that the zone can be reused after it was invalidated.
    final cInstanceRef3 = await service.evaluateInFrame(
      isolateId,
      0,
      'c',
      idZoneId: idZone3.id,
    ) as InstanceRef;
    expect(cInstanceRef3.id, 'objects/0/3');
  },

  // Test deleting an ID Zone.
  (VmService service, IsolateRef isolateRef) async {
    final isolateId = isolateRef.id!;
    final idZone4 = await service.createIdZone(
      isolateId,
      IdZoneBackingBufferKind.kRing,
      IdAssignmentPolicy.kAlwaysAllocate,
    );
    expect(idZone4.id, 'zones/4');
    expect(idZone4.backingBufferKind, IdZoneBackingBufferKind.kRing);
    expect(idZone4.idAssignmentPolicy, IdAssignmentPolicy.kAlwaysAllocate);

    await service.deleteIdZone(
      isolateId,
      idZone4.id!,
    );

    try {
      await service.evaluateInFrame(
        isolateId,
        0,
        'c',
        idZoneId: idZone4.id,
      );
      fail('successfully used an ID zone that should have been deleted');
    } on RPCError catch (e) {
      expect(e.code, RPCErrorKind.kInvalidParams.code);
    }
  },

  // Test the `idZoneId` parameters of all RPCs that have them.
  (VmService service, IsolateRef isolateRef) async {
    final isolateId = isolateRef.id!;
    final idZone5 = await service.createIdZone(
      isolateId,
      IdZoneBackingBufferKind.kRing,
      IdAssignmentPolicy.kAlwaysAllocate,
      capacity: 30,
    );
    expect(idZone5.id, 'zones/5');
    expect(idZone5.backingBufferKind, IdZoneBackingBufferKind.kRing);
    expect(idZone5.idAssignmentPolicy, IdAssignmentPolicy.kAlwaysAllocate);

    // The `idZoneId` parameter of [VmService.evaluateInFrame] is already tested
    // by the tests above.

    // Test the `idZoneId` parameter of [VmService.evaluate].

    final isolate = await service.getIsolate(isolateId);
    final dLiteralInstanceRef = await service.evaluate(
      isolateId,
      isolate.rootLib!.id!,
      'getDLiteral()',
      idZoneId: idZone5.id,
    ) as InstanceRef;
    final dLiteralObjectId = dLiteralInstanceRef.id!;
    expect(dLiteralObjectId, 'objects/0/5');

    final dLiteralInstance =
        await service.getObject(isolateId, dLiteralObjectId);
    expect(dLiteralInstance.type, 'Instance');
    final dClassRef = dLiteralInstance.classRef!;

    // Test the `idZoneId` parameter of [VmService.getInstances].

    final dInstanceSet = await service.getInstances(
      isolateId,
      dClassRef.id!,
      1,
      idZoneId: idZone5.id,
    );
    expect(dInstanceSet.totalCount, 1);
    expect(dInstanceSet.instances!.first.id, 'objects/1/5');

    // Test the `idZoneId` parameters of [VmService.getInstancesAsList] and
    // [VmService.getObject].

    final listInstanceRef = await service.getInstancesAsList(
      isolateId,
      dClassRef.id!,
      idZoneId: idZone5.id,
    );
    expect(listInstanceRef.length, 1);
    final listId = listInstanceRef.id!;
    expect(listId, 'objects/3/5');

    final listInstance = await service.getObject(
      isolateId,
      listId,
      idZoneId: idZone5.id,
    ) as Instance;
    expect(listInstance.elements!.first.id, 'objects/6/5');

    // Test the `idZoneId` parameter of [VmService.getInboundReferences].

    final dWithinCInstanceRef = await service.evaluateInFrame(
      isolateId,
      0,
      'c.field',
      idZoneId: idZone5.id,
    ) as InstanceRef;
    final dWithinCObjectId = dWithinCInstanceRef.id!;

    final inboundReferences = await service.getInboundReferences(
      isolateId,
      dWithinCObjectId,
      1,
      idZoneId: idZone5.id,
    );
    expect(inboundReferences.references!.length, 1);
    expect(inboundReferences.references![0].source!.id, 'objects/8/5');

    // Test the `idZoneId` parameter of [VmService.getRetainingPath].

    final retainingPath = await service.getRetainingPath(
      isolateId,
      dWithinCObjectId,
      2,
      idZoneId: idZone5.id,
    );
    expect(retainingPath.length, 2);
    expect(retainingPath.elements!.first.value!.id, 'objects/9/5');

    // Test the `idZoneId` parameter of [VmService.getStack].

    final stack = await service.getStack(
      isolateId,
      limit: 1,
      idZoneId: idZone5.id,
    );
    expect(stack.frames!.length, 1);
    final boundVariables = stack.frames!.first.vars!;
    expect(boundVariables.length, 1);
    expect((boundVariables.first.value as InstanceRef).id, 'objects/11/5');

    // Test the `idZoneId` parameter of [VmService.invoke].

    final dLiteral2InstanceRef = await service.invoke(
      isolateId,
      isolate.rootLib!.id!,
      'getDLiteral',
      [],
      idZoneId: idZone5.id,
    ) as InstanceRef;
    expect(dLiteral2InstanceRef.id!, 'objects/12/5');
  },
  resumeIsolate,
];

Future<void> main([args = const <String>[]]) => runIsolateTests(
      args,
      tests,
      'id_zones_test.dart',
      testeeConcurrent: testeeMain,
    );
