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

// Verifies that debugger does not pause when exception is caught by stream
// onError.
//
// Regression test for https://github.com/dart-lang/sdk/issues/54788.
import 'dart:async';

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/pause_on_unhandled_async_exceptions6_test.dart
//
const LINE_A = 32;
const LINE_AA = 36;
const LINE_AB = 42;
const LINE_B = 112;
const LINE_C = 116;
// AUTOGENERATED END

Stream<int> _throwingStream1(String exceptionToThrow) async* {
  for (var i = 0; i < 10; i++) {
    yield i;
  }

  throw exceptionToThrow; // LINE_A
}

Stream<int> _throwingStream2(String exceptionToThrow) async* {
  await for (var e in _throwingStream1(exceptionToThrow)) /* LINE_AA */ {
    yield e;
  }
}

Stream<int> _throwingStream3(String exceptionToThrow) async* {
  yield* _throwingStream1(exceptionToThrow); // LINE_AB
}

final streamFactories = [
  _throwingStream1,
  _throwingStream2,
  _throwingStream3,
];

Future<void> testeeMain({
  bool shouldTestUncaught = true,
}) async {
  int testCaseId = 0;
  for (var makeStream in streamFactories) {
    await testStreamCaught(() => makeStream('Caught#${testCaseId++}'));
  }

  if (shouldTestUncaught) {
    int testCaseId = 0;
    for (var makeStream in streamFactories) {
      await testStreamUncaught(() => makeStream('Uncaught#${testCaseId++}'));
    }
  }
}

Future<void> testStreamCaught(Stream<int> Function() makeStream) async {
  final c = Completer<void>();
  makeStream().listen(
    (_) {},
    onError: (_) {
      // Ignore
    },
    onDone: () {
      c.complete();
    },
  );

  await c.future;

  try {
    await makeStream().toList();
  } catch (_) {}

  try {
    await makeStream().last;
  } catch (_) {}

  try {
    await for (var _ in makeStream()) {}
  } catch (_) {}
}

Future<void> runExpectingUncaughtError(Future<void> Function() test) async {
  final done = Completer();
  Zone.current.fork(
    specification: ZoneSpecification(
      handleUncaughtError: (self, parent, zone, error, stackTrace) {
        done.complete();
      },
    ),
  ).runGuarded(test);
  await done.future;
}

Future<void> testStreamUncaught(Stream<int> Function() makeStream) async {
  await runExpectingUncaughtError(() async {
    makeStream().listen((_) {});
  });

  await runExpectingUncaughtError(() async {
    await makeStream().toList(); // LINE_B
  });

  await runExpectingUncaughtError(() async {
    await makeStream().last; // LINE_C
  });
}

final tests = () {
  int nextTestIndex = 0;

  List<IsolateTest> makeTest(List<ExpectedFrame> expectedFrames) {
    final testIndex = nextTestIndex++;
    return [
      // If this is not the very first test we need to skip the
      // uncaught exception from the previous test first.
      if (testIndex > 0)
        resumePastUnhandledException('Uncaught#${testIndex - 1}'),
      expectUnhandledExceptionWithFrames(
        exceptionAsString: 'Uncaught#$testIndex',
        expectedFrames: expectedFrames,
      ),
    ];
  }

  /// Creates expectations correspondong to test cases in [testStreamUncaught]
  /// given the specific stack prefix characteristic for the specific stream
  /// factory (see [streamFactories]).
  List<IsolateTest> makeTests(List<ExpectedFrame> streamFactoryPrefix) => [
        // Testing: _stream().listen((_) {})
        ...makeTest([
          ...streamFactoryPrefix,
          asyncGap,
          (
            functionName:
                'testStreamUncaught.<anonymous closure>.<anonymous closure>',
            line: null
          ),
        ]),

        // Testing: _stream().toList()
        ...makeTest([
          ...streamFactoryPrefix,
          asyncGap,
          (functionName: 'Stream.toList.<anonymous closure>', line: null),
          asyncGap,
          (
            functionName: 'testStreamUncaught.<anonymous closure>',
            line: LINE_B
          ),
        ]),

        // Testing: _stream().last
        ...makeTest([
          ...streamFactoryPrefix,
          asyncGap,
          (functionName: 'Stream.last.<anonymous closure>', line: null),
          asyncGap,
          (
            functionName: 'testStreamUncaught.<anonymous closure>',
            line: LINE_C
          ),
        ]),
      ];

  return [
    ...makeTests([
      (functionName: '_throwingStream1', line: LINE_A),
    ]),
    ...makeTests([
      (functionName: '_throwingStream1', line: LINE_A),
      asyncGap,
      (functionName: '_throwingStream2', line: LINE_AA),
    ]),
    ...makeTests([
      (functionName: '_throwingStream1', line: LINE_A),
      asyncGap,
      (functionName: '_throwingStream3', line: LINE_AB),
    ]),
  ];
}();

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