// 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.

// ignore_for_file: library_private_types_in_public_api

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

import 'common/test_helper.dart';

class _TestClass {
  _TestClass();
  // Make sure these fields are not removed by the tree shaker.
  @pragma('vm:entry-point') // Prevent obfuscation
  dynamic x;
  @pragma('vm:entry-point') // Prevent obfuscation
  dynamic y;
}

_TestClass? target1 = _TestClass();
_TestClass? target2 = _TestClass();
_TestClass? target3 = _TestClass();
_TestClass? target4 = _TestClass();
_TestClass? target5 = _TestClass();
_TestClass? target6 = _TestClass();
_TestClass? target7 = _TestClass();
_TestClass? target8 = _TestClass();

@pragma('vm:entry-point') // Prevent obfuscation
Expando<_TestClass> expando = Expando<_TestClass>();
@pragma('vm:entry-point') // Prevent obfuscation
_TestClass globalObject = _TestClass();
@pragma('vm:entry-point') // Prevent obfuscation
dynamic globalList = List<dynamic>.filled(100, null);
@pragma('vm:entry-point') // Prevent obfuscation
dynamic globalMap1 = {};
@pragma('vm:entry-point') // Prevent obfuscation
dynamic globalMap2 = {};
@pragma('vm:entry-point') // Prevent obfuscation
_TestClass weakReachable = _TestClass();
@pragma('vm:entry-point') // Prevent obfuscation
_TestClass weakUnreachable = _TestClass();

void warmup() {
  globalObject.x = target1;
  globalObject.y = target2;
  globalList[12] = target3;
  globalMap1['key'] = target4;
  globalMap2[target5] = 'value';

  // The weak reference will be traced first in DFS, but the retaining path
  // include the strong reference.
  weakReachable.x = WeakReference<_TestClass>(target7!);
  weakReachable.y = target7;

  weakUnreachable.x = WeakReference<_TestClass>(target8!);
  weakUnreachable.y = null;
}

@pragma('vm:entry-point') // Prevent obfuscation
_TestClass getGlobalObject() => globalObject;

@pragma('vm:entry-point') // Prevent obfuscation
_TestClass? takeTarget1() {
  final tmp = target1;
  target1 = null;
  return tmp;
}

@pragma('vm:entry-point') // Prevent obfuscation
_TestClass? takeTarget2() {
  final tmp = target2;
  target2 = null;
  return tmp;
}

@pragma('vm:entry-point') // Prevent obfuscation
_TestClass? takeTarget3() {
  final tmp = target3;
  target3 = null;
  return tmp;
}

@pragma('vm:entry-point') // Prevent obfuscation
_TestClass? takeTarget4() {
  final tmp = target4;
  target4 = null;
  return tmp;
}

@pragma('vm:entry-point') // Prevent obfuscation
_TestClass? takeTarget5() {
  final tmp = target5;
  target5 = null;
  return tmp;
}

@pragma('vm:entry-point') // Prevent obfuscation
_TestClass? takeExpandoTarget() {
  final tmp = target6;
  target6 = null;
  final tmp2 = _TestClass();
  expando[tmp!] = tmp2;
  return tmp2;
}

@pragma('vm:entry-point') // Prevent obfuscation
_TestClass? takeWeakReachableTarget() {
  final tmp = target7;
  target7 = null;
  return tmp;
}

@pragma('vm:entry-point') // Prevent obfuscation
_TestClass? takeWeakUnreachableTarget() {
  final tmp = target8;
  target8 = null;
  return tmp;
}

@pragma('vm:entry-point') // Prevent obfuscation
bool getTrue() => true;

Future<InstanceRef> invoke(String selector) async {
  return await rootService.invoke(
    isolateId,
    isolate.rootLib!.id!,
    selector,
    [],
  ) as InstanceRef;
}

late final VmService rootService;
late final Isolate isolate;
late final String isolateId;

final tests = <IsolateTest>[
  // Initialization
  (VmService service, IsolateRef isolateRef) async {
    isolateId = isolateRef.id!;
    rootService = service;
    isolate = await service.getIsolate(isolateId);
  },
  // simple path
  (VmService service, IsolateRef isolateRef) async {
    final obj = await invoke('getGlobalObject');
    final result = await service.getRetainingPath(isolateId, obj.id!, 100);
    expect(result.gcRootType, 'user global');
    expect(result.elements!.length, 2);
    expect((result.elements![1].value! as FieldRef).name, 'globalObject');
  },

  (VmService service, IsolateRef isolateRef) async {
    final target = await invoke('takeTarget1');
    final result = await service.getRetainingPath(isolateId, target.id!, 100);
    expect(result.gcRootType, 'user global');
    final elements = result.elements!;
    expect(elements.length, 3);
    expect(elements[1].parentField, 'x');
    expect((elements[2].value as FieldRef).name, 'globalObject');
  },

  (VmService service, IsolateRef isolateRef) async {
    final target = await invoke('takeTarget2');
    final result = await service.getRetainingPath(isolateId, target.id!, 100);
    expect(result.gcRootType, 'user global');
    final elements = result.elements!;
    expect(elements.length, 3);
    expect(elements[1].parentField, 'y');
    expect((elements[2].value as FieldRef).name, 'globalObject');
  },

  (VmService service, IsolateRef isolateRef) async {
    final target = await invoke('takeTarget3');
    final result = await service.getRetainingPath(isolateId, target.id!, 100);
    expect(result.gcRootType, 'user global');
    final elements = result.elements!;
    expect(elements.length, 3);
    expect(elements[1].parentListIndex, 12);
    expect((elements[2].value as FieldRef).name, 'globalList');
  },

  (VmService service, IsolateRef isolateRef) async {
    final target = await invoke('takeTarget4');
    final result = await service.getRetainingPath(isolateId, target.id!, 100);
    expect(result.gcRootType, 'user global');
    final elements = result.elements!;
    expect(elements.length, 3);
    expect((elements[1].parentMapKey as InstanceRef).valueAsString, 'key');
    expect((elements[2].value as FieldRef).name, 'globalMap1');
  },

  (VmService service, IsolateRef isolateRef) async {
    final target = await invoke('takeTarget5');
    final result = await service.getRetainingPath(isolateId, target.id!, 100);
    expect(result.gcRootType, 'user global');
    final elements = result.elements!;
    expect(elements.length, 3);
    expect(
      (elements[1].parentMapKey as InstanceRef).classRef!.name,
      '_TestClass',
    );
    expect((elements[2].value as FieldRef).name, 'globalMap2');
  },

  (VmService service, IsolateRef isolateRef) async {
    // Regression test for https://github.com/dart-lang/sdk/issues/44016
    final target = await invoke('takeExpandoTarget');
    final result = await service.getRetainingPath(isolateId, target.id!, 100);
    final elements = result.elements!;
    expect(elements.length, 5);
    expect(
      (elements[1].parentMapKey as InstanceRef).classRef!.name,
      '_TestClass',
    );
    expect(elements[2].parentListIndex, isNotNull);
    expect((elements[4].value as FieldRef).name, 'expando');
  },

  (VmService service, IsolateRef isolateRef) async {
    final target = await invoke('takeWeakReachableTarget');
    final result = await service.getRetainingPath(isolateId, target.id!, 100);
    expect(result.gcRootType, 'user global');
    final elements = result.elements!;
    expect(elements.length, 3);
    expect(elements[1].parentField, 'y');
    expect((elements[2].value as FieldRef).name, 'weakReachable');
  },

  (VmService service, IsolateRef isolateRef) async {
    final target = await invoke('takeWeakUnreachableTarget');
    final result = await service.getRetainingPath(isolateId, target.id!, 100);
    final elements = result.elements!;
    expect(elements.length, 0);
  },

  // object store
  (VmService service, IsolateRef isolateRef) async {
    final target = await invoke('getTrue');
    final result = await service.getRetainingPath(isolateId, target.id!, 100);
    expect(
      result.gcRootType == 'isolate_object store' ||
          result.gcRootType == 'class table',
      true,
    );
    final elements = result.elements!;
    expect(elements.length, 0);
  },
];

void main([args = const <String>[]]) async => runIsolateTests(
      args,
      tests,
      'get_retaining_path_rpc_test.dart',
      testeeBefore: warmup,
    );
