// Copyright (c) 2020, 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))
import 'dart:async';

import 'package:dwds/src/debugging/debugger.dart';
import 'package:dwds/src/debugging/location.dart';
import 'package:dwds/src/debugging/skip_list.dart';
import 'package:dwds/src/services/batched_expression_evaluator.dart';
import 'package:dwds/src/services/expression_evaluator.dart';

import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart' hide LogRecord;
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';

import 'fixtures/context.dart';
import 'fixtures/fakes.dart';
import 'fixtures/utilities.dart';

late ExpressionEvaluator? _evaluator;
late ExpressionEvaluator? _batchedEvaluator;

void main() async {
  group('expression compiler service with fake asset server', () {
    Future<void> stop() async {
      _evaluator?.close();
      _evaluator = null;
      _batchedEvaluator?.close();
      _batchedEvaluator = null;
    }

    late StreamController<DebuggerPausedEvent> pausedController;
    late StreamController<Event> debugEventController;
    setUp(() async {
      final assetReader = FakeAssetReader(sourceMap: '');
      final toolConfiguration = TestToolConfiguration.withLoadStrategy(
        loadStrategy: FakeStrategy(assetReader),
      );
      setGlobalsForTesting(
        toolConfiguration: toolConfiguration,
      );
      final modules = FakeModules();

      final webkitDebugger = FakeWebkitDebugger();
      pausedController = StreamController<DebuggerPausedEvent>();
      debugEventController = StreamController<Event>();
      webkitDebugger.onPaused = pausedController.stream;

      final root = 'fakeRoot';
      final entry = 'fake_entrypoint';
      final locations = Locations(assetReader, modules, root);
      locations.initialize(entry);

      final skipLists = SkipLists();
      final debugger = await Debugger.create(
        webkitDebugger,
        (_, e) => debugEventController.sink.add(e),
        locations,
        skipLists,
        root,
      );
      final inspector =
          FakeInspector(webkitDebugger, fakeIsolate: simpleIsolate);
      debugger.updateInspector(inspector);

      _evaluator = ExpressionEvaluator(
        entry,
        inspector,
        debugger,
        locations,
        modules,
        FakeExpressionCompiler(),
      );
      _batchedEvaluator = BatchedExpressionEvaluator(
        entry,
        inspector,
        debugger,
        locations,
        modules,
        FakeExpressionCompiler(),
      );
    });

    tearDown(() async {
      await stop();
    });

    group('evaluator', () {
      late ExpressionEvaluator evaluator;

      setUp(() async {
        evaluator = _evaluator!;
      });

      test('can evaluate expression', () async {
        final result =
            await evaluator.evaluateExpression('1', 'main.dart', 'true', {});
        expect(
          result,
          const TypeMatcher<RemoteObject>()
              .having((o) => o.value, 'value', 'true'),
        );
      });

      test('can evaluate expression in frame with null scope', () async {
        // Verify that we don't get the internal error.
        // More extensive testing of 'evaluateExpressionInFrame' is done in
        // evaluation tests for frontend server and build daemon.
        await expectLater(
          evaluator.evaluateExpressionInFrame('1', 0, 'true', null),
          throwsRPCErrorWithMessage(
            'Cannot evaluate on a call frame when the program is not paused',
          ),
        );
      });

      test('cannot evaluate expression in async frame ', () async {
        // Add a DebuggerPausedEvent with no frames provoke an error.
        pausedController.sink.add(
          DebuggerPausedEvent({
            'method': '',
            'params': {
              'reason': 'other',
              'callFrames': [],
            },
          }),
        );

        await debugEventController.stream
            .firstWhere((e) => e.kind == EventKind.kPauseInterrupted);

        // Verify that we get the internal error.
        final result =
            await evaluator.evaluateExpressionInFrame('20', 0, 'true', null);
        expect(
          result,
          isA<RemoteObject>()
              .having((o) => o.json['type'], 'type', 'AsyncFrameError')
              .having(
                (o) => o.json['value'],
                'value',
                'Expression evaluation in async frames is not supported. '
                    'No frame with index 0.',
              ),
        );
      });

      test('can evaluate expression in frame with empty scope', () async {
        // Verify that we don't get the internal error.
        // More extensive testing of 'evaluateExpressionInFrame' is done in
        // evaluation tests for frontend server and build daemon.
        await expectLater(
          evaluator.evaluateExpressionInFrame('1', 0, 'true', {}),
          throwsRPCErrorWithMessage(
            'Cannot evaluate on a call frame when the program is not paused',
          ),
        );
      });

      test('returns error if closed', () async {
        evaluator.close();
        final result =
            await evaluator.evaluateExpression('1', 'main.dart', 'true', {});
        expect(
          result,
          const TypeMatcher<RemoteObject>()
              .having((o) => o.type, 'type', 'InternalError')
              .having((o) => o.value, 'value', contains('evaluator closed')),
        );
      });
    });

    group('batched evaluator', () {
      late ExpressionEvaluator evaluator;

      setUp(() async {
        evaluator = _batchedEvaluator!;
      });

      test('can evaluate expression', () async {
        final result =
            await evaluator.evaluateExpression('1', 'main.dart', 'true', {});
        expect(
          result,
          const TypeMatcher<RemoteObject>()
              .having((o) => o.value, 'value', 'true'),
        );
      });

      test('returns error if closed', () async {
        evaluator.close();
        final result =
            await evaluator.evaluateExpression('1', 'main.dart', 'true', {});
        expect(
          result,
          const TypeMatcher<RemoteObject>()
              .having((o) => o.type, 'type', 'InternalError')
              .having((o) => o.value, 'value', contains('evaluator closed')),
        );
      });
    });
  });
}
