// Copyright (c) 2019, 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.

@TestOn('vm')
@Timeout(Duration(minutes: 2))
library;

import 'package:dwds/src/debugging/dart_scope.dart';
import 'package:dwds/src/services/chrome/chrome_proxy_service.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 'fixtures/context.dart';
import 'fixtures/project.dart';
import 'fixtures/utilities.dart';

void main() {
  // set to true for debug logging.
  const debug = false;

  final provider = TestSdkConfigurationProvider(verbose: debug);
  tearDownAll(provider.dispose);

  final context = TestContext(TestProject.testScopes, provider);

  setUpAll(() async {
    setCurrentLogWriter(debug: debug);
    await context.setUp(testSettings: TestSettings(verboseCompiler: debug));
  });

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

  group('temporary variable regular expression', () {
    setUpAll(() => setCurrentLogWriter(debug: debug));
    test('matches correctly for pre-patterns temporary variables', () {
      expect(previousDdcTemporaryVariableRegExp.hasMatch(r't4$'), isTrue);
      expect(previousDdcTemporaryVariableRegExp.hasMatch(r't4$0'), isTrue);
      expect(previousDdcTemporaryVariableRegExp.hasMatch(r't4$10'), isTrue);
      expect(previousDdcTemporaryVariableRegExp.hasMatch(r't4$0'), isTrue);
      expect(previousDdcTemporaryVariableRegExp.hasMatch(r't1'), isTrue);
      expect(previousDdcTemporaryVariableRegExp.hasMatch(r't10'), isTrue);
      expect(previousDdcTemporaryVariableRegExp.hasMatch(r'__t$TL'), isTrue);
      expect(
        previousDdcTemporaryVariableRegExp.hasMatch(r'__t$StringN'),
        isTrue,
      );
      expect(
        previousDdcTemporaryVariableRegExp.hasMatch(
          r'__t$IdentityMapOfString$T',
        ),
        isTrue,
      );

      expect(previousDdcTemporaryVariableRegExp.hasMatch(r't'), isFalse);
      expect(previousDdcTemporaryVariableRegExp.hasMatch(r't10foo'), isFalse);
      expect(previousDdcTemporaryVariableRegExp.hasMatch(r't$10foo'), isFalse);
    });

    test('matches correctly for post-patterns temporary variables', () {
      expect(ddcTemporaryVariableRegExp.hasMatch(r't$364$'), isTrue);
      expect(ddcTemporaryVariableRegExp.hasMatch(r't$364$0'), isTrue);
      expect(ddcTemporaryVariableRegExp.hasMatch(r't$364$10'), isTrue);
      expect(ddcTemporaryVariableRegExp.hasMatch(r't$364$0'), isTrue);
      expect(ddcTemporaryVariableRegExp.hasMatch(r't$361'), isTrue);
      expect(ddcTemporaryVariableRegExp.hasMatch(r't$36$350$350'), isTrue);
      expect(
        ddcTemporaryVariableRegExp.hasMatch(r't$36$350$354$35isSet'),
        isTrue,
      );
      expect(
        ddcTemporaryVariableRegExp.hasMatch(r't$36$35variable$35isSet'),
        isTrue,
      );
      expect(
        ddcTemporaryVariableRegExp.hasMatch(r'synthetic$35variable'),
        isTrue,
      );
      expect(ddcTemporaryTypeVariableRegExp.hasMatch(r'__t$TL'), isTrue);
      expect(ddcTemporaryTypeVariableRegExp.hasMatch(r'__t$StringN'), isTrue);
      expect(
        ddcTemporaryTypeVariableRegExp.hasMatch(r'__t$IdentityMapOfString$T'),
        isTrue,
      );

      expect(ddcTemporaryVariableRegExp.hasMatch(r't'), isFalse);
      expect(ddcTemporaryVariableRegExp.hasMatch(r'this'), isFalse);
      expect(ddcTemporaryVariableRegExp.hasMatch(r'\$this'), isFalse);
      expect(ddcTemporaryVariableRegExp.hasMatch(r't10'), isFalse);
      expect(ddcTemporaryVariableRegExp.hasMatch(r't10foo'), isFalse);
      expect(ddcTemporaryVariableRegExp.hasMatch(r'ten'), isFalse);
      expect(ddcTemporaryVariableRegExp.hasMatch(r'my$3635variable'), isFalse);
    });
  });

  group('variable scope', () {
    late ChromeProxyService service;
    VM vm;
    String? isolateId;
    late Stream<Event> stream;
    ScriptList scripts;
    late ScriptRef mainScript;
    Stack stack;

    // TODO: Be able to set breakpoints before start/reload so we can exercise
    // things that aren't in recurring loops.

    /// Support function for pausing and returning the stack at a line.
    Future<Stack> breakAt(String breakpointId, ScriptRef scriptRef) async {
      final lineNumber = await context.findBreakpointLine(
        breakpointId,
        isolateId!,
        scriptRef,
      );

      final bp = await service.addBreakpoint(
        isolateId!,
        scriptRef.id!,
        lineNumber,
      );
      // Wait for breakpoint to trigger.
      await stream.firstWhere(
        (event) => event.kind == EventKind.kPauseBreakpoint,
      );
      // Remove breakpoint so it doesn't impact other tests.
      await service.removeBreakpoint(isolateId!, bp.id!);
      final stack = await service.getStack(isolateId!);
      return stack;
    }

    Future<Instance> getInstance(InstanceRef ref) async {
      final result = await service.getObject(isolateId!, ref.id!);
      expect(result, isA<Instance>());
      return result as Instance;
    }

    void expectDartObject(String variableName, Instance instance) {
      expect(
        instance,
        isA<Instance>().having(
          (instance) => instance.classRef!.name,
          '$variableName: classRef.name',
          isNot(
            isIn(['NativeJavaScriptObject', 'JavaScriptObject', 'NativeError']),
          ),
        ),
      );
    }

    Future<void> expectDartVariables(
      Map<String?, InstanceRef?> variables,
    ) async {
      for (final name in variables.keys) {
        final instance = await getInstance(variables[name]!);
        expectDartObject(name!, instance);
      }
    }

    Map<String?, InstanceRef?> getFrameVariables(Frame frame) {
      return <String?, InstanceRef?>{
        for (final variable in frame.vars!)
          variable.name: variable.value as InstanceRef?,
      };
    }

    setUpAll(() => setCurrentLogWriter(debug: debug));

    setUp(() async {
      service = context.service;
      vm = await service.getVM();
      isolateId = vm.isolates!.first.id;
      scripts = await service.getScripts(isolateId!);
      await service.streamListen('Debug');
      stream = service.onEvent('Debug');
      mainScript = scripts.scripts!.firstWhere(
        (each) => each.uri!.contains('main.dart'),
      );
    });

    tearDown(() async {
      await service.resume(isolateId!);
    });

    test('variables in static function', () async {
      stack = await breakAt('staticFunction', mainScript);
      final variables = getFrameVariables(stack.frames!.first);
      await expectDartVariables(variables);

      final variableNames = variables.keys.toList()..sort();
      expect(variableNames, containsAll(['formal']));
    });

    test('variables in static async function', () async {
      stack = await breakAt('staticAsyncFunction', mainScript);
      final variables = getFrameVariables(stack.frames!.first);
      await expectDartVariables(variables);

      final variableNames = variables.keys.toList()..sort();
      final variableValues = variableNames
          .map((name) => variables[name]?.valueAsString)
          .toList();
      expect(variableNames, containsAll(['myLocal', 'value']));
      expect(variableValues, containsAll(['a local value', 'arg1']));
    });

    test('variables in static async loop function', () async {
      stack = await breakAt('staticAsyncLoopFunction', mainScript);
      final variables = getFrameVariables(stack.frames!.first);
      await expectDartVariables(variables);

      final variableNames = variables.keys.toList()..sort();
      final variableValues = variableNames
          .map((name) => variables[name]?.valueAsString)
          .toList();
      expect(variableNames, containsAll(['i', 'myLocal', 'value']));
      // Ensure the loop variable, i, is captued correctly. The value from the
      // first iteration should be captured by the saved closure.
      expect(variableValues, containsAll(['1', 'my local value', 'arg2']));
    });

    test('variables in function', () async {
      stack = await breakAt('nestedFunction', mainScript);
      final variables = getFrameVariables(stack.frames!.first);
      await expectDartVariables(variables);

      final variableNames = variables.keys.toList()..sort();
      expect(
        variableNames,
        containsAll([
          'aClass',
          'another',
          'intLocalInMain',
          'local',
          'localThatsNull',
          'nestedFunction',
          'parameter',
          'testClass',
        ]),
      );
    });

    test('variables in closure nested in method', () async {
      stack = await breakAt('nestedClosure', mainScript);
      final variables = getFrameVariables(stack.frames!.first);
      await expectDartVariables(variables);

      final variableNames = variables.keys.toList()..sort();
      expect(variableNames, [
        'closureLocalInsideMethod',
        'local',
        'parameter',
        'this',
      ]);
    });

    test('variables in method', () async {
      stack = await breakAt('printMethod', mainScript);
      final variables = getFrameVariables(stack.frames!.first);
      await expectDartVariables(variables);

      final variableNames = variables.keys.toList()..sort();
      expect(variableNames, ['this']);
    });

    test('variables in extension method', () async {
      stack = await breakAt('extension', mainScript);
      final variables = getFrameVariables(stack.frames!.first);
      await expectDartVariables(variables);

      final variableNames = variables.keys.toList()..sort();
      // Note: '$this' should change to 'this', and 'return' should
      // disappear after debug symbols are available.
      // https://github.com/dart-lang/webdev/issues/1371
      expect(variableNames, ['\$this', 'ret', 'return']);
    });

    test('evaluateJsOnCallFrame', () async {
      stack = await breakAt('nestedFunction', mainScript);
      final debugger = await service.debuggerFuture;
      final parameter = await debugger.evaluateJsOnCallFrameIndex(
        0,
        'parameter',
      );
      expect(parameter.value, matches(RegExp(r'\d+ world')));
      final ticks = await debugger.evaluateJsOnCallFrameIndex(1, 'ticks');
      // We don't know how many ticks there were before we stopped, but it should
      // be a positive number.
      expect(ticks.value, isPositive);
    });
  });
}
