// 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:dwds/expression_compiler.dart';
import 'package:dwds/src/config/tool_configuration.dart';
import 'package:dwds/src/debugging/inspector.dart';
import 'package:test/test.dart';
import 'package:test_common/logging.dart';
import 'package:test_common/test_sdk_configuration.dart';
import 'package:vm_service/vm_service.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';

import '../../fixtures/context.dart';
import '../../fixtures/project.dart';
import '../../fixtures/utilities.dart';
import 'test_inspector.dart';

void runTypeSystemVerificationTests({
  required TestSdkConfigurationProvider provider,
  required CompilationMode compilationMode,
  required bool canaryFeatures,
  required bool debug,
}) {
  final project = TestProject.testScopes;

  group('$compilationMode |', () {
    final context = TestContext(project, provider);
    late AppInspector inspector;

    setUpAll(() async {
      setCurrentLogWriter(debug: debug);
      await context.setUp(
        testSettings: TestSettings(
          compilationMode: compilationMode,
          canaryFeatures: canaryFeatures,
        ),
      );
      final chromeProxyService = context.service;
      inspector = chromeProxyService.inspector;
    });

    tearDownAll(() async {
      await context.tearDown();
    });

    final url = 'org-dartlang-app:///example/scopes/main.dart';

    String libraryName(CompilationMode compilationMode) =>
        compilationMode == CompilationMode.frontendServer
            ? 'example/scopes/main.dart'
            : 'example/scopes/main';

    String libraryVariableTypeExpression(
      String variable,
      CompilationMode compilationMode,
    ) => '''
            (function() {
              var dart = ${globalToolConfiguration.loadStrategy.loadModuleSnippet}('dart_sdk').dart;
              var libraryName = '${libraryName(compilationMode)}';
              var library = dart.getModuleLibraries(libraryName)['$url'];
              var x = library['$variable'];
              return dart.getReifiedType(x);
            })();
          ''';

    group('compiler', () {
      setUp(() => setCurrentLogWriter(debug: debug));

      test('uses correct type system', () async {
        final remoteObject = await inspector.jsEvaluate(
          libraryVariableTypeExpression('libraryPublicFinal', compilationMode),
        );
        expect(remoteObject.json['className'], 'dart_rti.Rti.new');
      });
    });
  });
}

void runTests({
  required TestSdkConfigurationProvider provider,
  required CompilationMode compilationMode,
  required bool canaryFeatures,
  required bool debug,
}) {
  final project = TestProject.testScopes;
  final context = TestContext(project, provider);

  late AppInspector inspector;

  group('$compilationMode |', () {
    setUpAll(() async {
      setCurrentLogWriter(debug: debug);
      await context.setUp(
        testSettings: TestSettings(
          compilationMode: compilationMode,
          canaryFeatures: canaryFeatures,
          moduleFormat: provider.ddcModuleFormat,
        ),
      );
      final chromeProxyService = context.service;
      inspector = chromeProxyService.inspector;
    });

    tearDownAll(() async {
      await context.tearDown();
    });

    final libraryUri = 'org-dartlang-app:///example/scopes/main.dart';

    String newInterceptorsExpression(String type) =>
        'new (require("dart_sdk")._interceptors.$type).new()';

    final newDartError = 'new (require("dart_sdk").dart).DartError';

    /// A reference to the the variable `libraryPublicFinal`, an instance of
    /// `MyTestClass`.
    Future<RemoteObject> getLibraryPublicFinalRef() =>
        inspector.invoke(libraryUri, 'getLibraryPublicFinal');

    /// A reference to the the variable `libraryPublic`, a List of Strings.
    Future<RemoteObject> getLibraryPublicRef() =>
        inspector.invoke(libraryUri, 'getLibraryPublic');

    /// A reference to the variable `map`.
    Future<RemoteObject> getMapRef() => inspector.invoke(libraryUri, 'getMap');

    /// A reference to the variable `identityMap`.
    Future<RemoteObject> getIdentityMapRef() =>
        inspector.invoke(libraryUri, 'getIdentityMap');

    /// A reference to the variable `stream`.
    Future<RemoteObject> getStreamRef() =>
        inspector.invoke(libraryUri, 'getStream');

    final unsupportedTestMsg =
        'This test is not supported with the DDC Library '
        "Bundle Format because the dartDevEmbedder doesn't let you access compiled "
        'constructors at runtime.';

    group('instanceRef', () {
      setUp(() => setCurrentLogWriter(debug: debug));

      test('for a null', () async {
        final remoteObject = await getLibraryPublicFinalRef();
        final nullVariable = await inspector.loadField(
          remoteObject,
          'notFinal',
        );
        final ref = await inspector.instanceRefFor(nullVariable);
        expect(ref!.valueAsString, 'null');
        expect(ref.kind, InstanceKind.kNull);
        final classRef = ref.classRef!;
        expect(classRef.name, 'Null');
        expect(classRef.id, 'classes|dart:core|Null');
        expect(inspector.isDisplayableObject(ref), isTrue);
      });

      test('for a double', () async {
        final remoteObject = await getLibraryPublicFinalRef();
        final count = await inspector.loadField(remoteObject, 'count');
        final ref = await inspector.instanceRefFor(count);
        expect(ref!.valueAsString, '0');
        expect(ref.kind, InstanceKind.kDouble);
        final classRef = ref.classRef!;
        expect(classRef.name, 'Double');
        expect(classRef.id, 'classes|dart:core|Double');
        expect(inspector.isDisplayableObject(ref), isTrue);
      });

      test('for an object', () async {
        final remoteObject = await getLibraryPublicFinalRef();
        final count = await inspector.loadField(remoteObject, 'myselfField');
        final ref = await inspector.instanceRefFor(count);
        expect(ref!.kind, InstanceKind.kPlainInstance);
        final classRef = ref.classRef!;
        expect(classRef.name, 'MyTestClass<dynamic>');
        expect(
          classRef.id,
          'classes|org-dartlang-app:///example/scopes/main.dart'
          '|MyTestClass<dynamic>',
        );
        expect(inspector.isDisplayableObject(ref), isTrue);
      });

      test('for a closure', () async {
        final remoteObject = await getLibraryPublicFinalRef();
        final properties = await inspector.getProperties(
          remoteObject.objectId!,
        );
        final closure = properties.firstWhere(
          (property) => property.name == 'closure',
        );
        final ref = await inspector.instanceRefFor(closure.value!);
        final functionName = ref!.closureFunction!.name;
        // Older SDKs do not contain function names
        if (functionName != 'Closure') {
          expect(functionName, 'someFunction');
        }
        expect(ref.kind, InstanceKind.kClosure);
        expect(inspector.isDisplayableObject(ref), isTrue);
      });

      test('for a list', () async {
        final remoteObject = await getLibraryPublicRef();
        final ref = await inspector.instanceRefFor(remoteObject);
        expect(ref!.length, greaterThan(0));
        expect(ref.kind, InstanceKind.kList);
        expect(ref.classRef!.name, matchListClassName('String'));
        expect(inspector.isDisplayableObject(ref), isTrue);
      });

      test('for map', () async {
        final remoteObject = await getMapRef();
        final ref = await inspector.instanceRefFor(remoteObject);
        expect(ref!.length, 2);
        expect(ref.kind, InstanceKind.kMap);
        expect(ref.classRef!.name, 'LinkedMap<Object, Object>');
        expect(inspector.isDisplayableObject(ref), isTrue);
      });

      test('for an IdentityMap', () async {
        final remoteObject = await getIdentityMapRef();
        final ref = await inspector.instanceRefFor(remoteObject);
        expect(ref!.length, 2);
        expect(ref.kind, InstanceKind.kMap);
        expect(ref.classRef!.name, 'IdentityMap<String, int>');
        expect(inspector.isDisplayableObject(ref), isTrue);
      });

      // Regression test for https://github.com/dart-lang/webdev/issues/2446.
      test('for a stream', () async {
        final remoteObject = await getStreamRef();
        final ref = await inspector.instanceRefFor(remoteObject);
        expect(ref!.kind, InstanceKind.kPlainInstance);
        final classRef = ref.classRef!;
        expect(classRef.name, '_ControllerStream<int>');
        expect(classRef.id, 'classes|dart:async|_ControllerStream<int>');
        expect(inspector.isDisplayableObject(ref), isTrue);
      });

      test(
        'for a Dart error',
        () async {
          final remoteObject = await inspector.jsEvaluate(newDartError);
          final ref = await inspector.instanceRefFor(remoteObject);
          expect(ref!.kind, InstanceKind.kPlainInstance);
          expect(ref.classRef!.name, 'NativeError');
          expect(inspector.isDisplayableObject(ref), isFalse);
          expect(inspector.isNativeJsError(ref), isTrue);
          expect(inspector.isNativeJsObject(ref), isFalse);
        },
        skip:
            provider.ddcModuleFormat == ModuleFormat.ddc &&
                    canaryFeatures == true
                ? unsupportedTestMsg
                : null,
      );

      test(
        'for a native JavaScript error',
        () async {
          final remoteObject = await inspector.jsEvaluate(
            newInterceptorsExpression('NativeError'),
          );
          final ref = await inspector.instanceRefFor(remoteObject);
          expect(ref!.kind, InstanceKind.kPlainInstance);
          expect(ref.classRef!.name, 'NativeError');
          expect(inspector.isDisplayableObject(ref), isFalse);
          expect(inspector.isNativeJsError(ref), isTrue);
          expect(inspector.isNativeJsObject(ref), isFalse);
        },
        skip:
            provider.ddcModuleFormat == ModuleFormat.ddc &&
                    canaryFeatures == true
                ? unsupportedTestMsg
                : null,
      );

      test(
        'for a native JavaScript type error',
        () async {
          final remoteObject = await inspector.jsEvaluate(
            newInterceptorsExpression('JSNoSuchMethodError'),
          );
          final ref = await inspector.instanceRefFor(remoteObject);
          expect(ref!.kind, InstanceKind.kPlainInstance);
          expect(ref.classRef!.name, 'JSNoSuchMethodError');
          expect(inspector.isDisplayableObject(ref), isFalse);
          expect(inspector.isNativeJsError(ref), isTrue);
          expect(inspector.isNativeJsObject(ref), isFalse);
        },
        skip:
            provider.ddcModuleFormat == ModuleFormat.ddc &&
                    canaryFeatures == true
                ? unsupportedTestMsg
                : null,
      );

      test(
        'for a native JavaScript object',
        () async {
          final remoteObject = await inspector.jsEvaluate(
            newInterceptorsExpression('LegacyJavaScriptObject'),
          );
          final ref = await inspector.instanceRefFor(remoteObject);
          expect(ref!.kind, InstanceKind.kPlainInstance);
          expect(ref.classRef!.name, 'LegacyJavaScriptObject');
          expect(inspector.isDisplayableObject(ref), isFalse);
          expect(inspector.isNativeJsError(ref), isFalse);
          expect(inspector.isNativeJsObject(ref), isTrue);
        },
        skip:
            provider.ddcModuleFormat == ModuleFormat.ddc &&
                    canaryFeatures == true
                ? unsupportedTestMsg
                : null,
      );
    });

    group('instance', () {
      setUp(() => setCurrentLogWriter(debug: debug));
      test('for an object', () async {
        final remoteObject = await getLibraryPublicFinalRef();
        final instance = await inspector.instanceFor(remoteObject);
        expect(instance!.kind, InstanceKind.kPlainInstance);
        final classRef = instance.classRef!;
        expect(classRef, isNotNull);
        expect(classRef.name, 'MyTestClass<dynamic>');
        final boundFieldNames =
            instance.fields!
                .map((boundField) => boundField.decl!.name)
                .toList();
        expect(boundFieldNames, [
          '_privateField',
          'abstractField',
          'closure',
          'count',
          'message',
          'myselfField',
          'notFinal',
          'tornOff',
        ]);
        final fieldNames =
            instance.fields!.map((boundField) => boundField.name).toList();
        expect(boundFieldNames, fieldNames);
        for (final field in instance.fields!) {
          expect(field.name, isNotNull);
          expect(field.decl!.declaredType, isNotNull);
        }
        expect(inspector.isDisplayableObject(instance), isTrue);
      });

      test('for closure', () async {
        final remoteObject = await getLibraryPublicFinalRef();
        final properties = await inspector.getProperties(
          remoteObject.objectId!,
        );
        final closure = properties.firstWhere(
          (property) => property.name == 'closure',
        );
        final instance = await inspector.instanceFor(closure.value!);
        expect(instance!.kind, InstanceKind.kClosure);
        expect(instance.classRef!.name, 'Closure');
        expect(inspector.isDisplayableObject(instance), isTrue);
      });

      test('for a nested object', () async {
        final libraryRemoteObject = await getLibraryPublicFinalRef();
        final fieldRemoteObject = await inspector.loadField(
          libraryRemoteObject,
          'myselfField',
        );
        final instance = await inspector.instanceFor(fieldRemoteObject);
        expect(instance!.kind, InstanceKind.kPlainInstance);
        final classRef = instance.classRef!;
        expect(classRef, isNotNull);
        expect(classRef.name, 'MyTestClass<dynamic>');
        expect(inspector.isDisplayableObject(instance), isTrue);
      });

      test('for a list', () async {
        final remote = await getLibraryPublicRef();
        final instance = await inspector.instanceFor(remote);
        expect(instance!.kind, InstanceKind.kList);
        final classRef = instance.classRef!;
        expect(classRef, isNotNull);
        expect(classRef.name, matchListClassName('String'));
        final first = instance.elements![0];
        expect(first.valueAsString, 'library');
        expect(inspector.isDisplayableObject(instance), isTrue);
      });

      test('for a map', () async {
        final remote = await getMapRef();
        final instance = await inspector.instanceFor(remote);
        expect(instance!.kind, InstanceKind.kMap);
        final classRef = instance.classRef!;
        expect(classRef.name, 'LinkedMap<Object, Object>');
        final first = instance.associations![0].value as InstanceRef;
        expect(first.kind, InstanceKind.kList);
        expect(first.length, 3);
        final second = instance.associations![1].value as InstanceRef;
        expect(second.kind, InstanceKind.kString);
        expect(second.valueAsString, 'something');
        expect(inspector.isDisplayableObject(instance), isTrue);
      });

      test('for an identityMap', () async {
        final remote = await getIdentityMapRef();
        final instance = await inspector.instanceFor(remote);
        expect(instance!.kind, InstanceKind.kMap);
        final classRef = instance.classRef!;
        expect(classRef.name, 'IdentityMap<String, int>');
        final first = instance.associations![0].value;
        expect(first.valueAsString, '1');
        expect(inspector.isDisplayableObject(instance), isTrue);
      });

      // Regression test for https://github.com/dart-lang/webdev/issues/2446.
      test('for a stream', () async {
        final remote = await getStreamRef();
        final instance = await inspector.instanceFor(remote);
        expect(instance!.kind, InstanceKind.kPlainInstance);
        final classRef = instance.classRef!;
        expect(classRef.name, '_ControllerStream<int>');
        expect(inspector.isDisplayableObject(instance), isTrue);
      });

      test(
        'for a Dart error',
        () async {
          final remoteObject = await inspector.jsEvaluate(newDartError);
          final instance = await inspector.instanceFor(remoteObject);
          expect(instance!.kind, InstanceKind.kPlainInstance);
          expect(instance.classRef!.name, 'NativeError');
          expect(inspector.isDisplayableObject(instance), isFalse);
          expect(inspector.isNativeJsError(instance), isTrue);
          expect(inspector.isNativeJsObject(instance), isFalse);
        },
        skip:
            provider.ddcModuleFormat == ModuleFormat.ddc &&
                    canaryFeatures == true
                ? unsupportedTestMsg
                : null,
      );

      test(
        'for a native JavaScript error',
        () async {
          final remoteObject = await inspector.jsEvaluate(
            newInterceptorsExpression('NativeError'),
          );
          final instance = await inspector.instanceFor(remoteObject);
          expect(instance!.kind, InstanceKind.kPlainInstance);
          expect(instance.classRef!.name, 'NativeError');
          expect(inspector.isDisplayableObject(instance), isFalse);
          expect(inspector.isNativeJsError(instance), isTrue);
          expect(inspector.isNativeJsObject(instance), isFalse);
        },
        skip:
            provider.ddcModuleFormat == ModuleFormat.ddc &&
                    canaryFeatures == true
                ? unsupportedTestMsg
                : null,
      );

      test(
        'for a native JavaScript type error',
        () async {
          final remoteObject = await inspector.jsEvaluate(
            newInterceptorsExpression('JSNoSuchMethodError'),
          );
          final instance = await inspector.instanceFor(remoteObject);
          expect(instance!.kind, InstanceKind.kPlainInstance);
          expect(instance.classRef!.name, 'JSNoSuchMethodError');
          expect(inspector.isDisplayableObject(instance), isFalse);
          expect(inspector.isNativeJsError(instance), isTrue);
          expect(inspector.isNativeJsObject(instance), isFalse);
        },
        skip:
            provider.ddcModuleFormat == ModuleFormat.ddc &&
                    canaryFeatures == true
                ? unsupportedTestMsg
                : null,
      );

      test(
        'for a native JavaScript object',
        () async {
          final remoteObject = await inspector.jsEvaluate(
            newInterceptorsExpression('LegacyJavaScriptObject'),
          );
          final instance = await inspector.instanceFor(remoteObject);
          expect(instance!.kind, InstanceKind.kPlainInstance);
          expect(instance.classRef!.name, 'LegacyJavaScriptObject');
          expect(inspector.isDisplayableObject(instance), isFalse);
          expect(inspector.isNativeJsError(instance), isFalse);
          expect(inspector.isNativeJsObject(instance), isTrue);
        },
        skip:
            provider.ddcModuleFormat == ModuleFormat.ddc &&
                    canaryFeatures == true
                ? unsupportedTestMsg
                : null,
      );
    });
  });
}
