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

import 'package:path/path.dart' as path;

final thisDirectory = path.join('runtime', 'tests', 'concurrency');
final stressTestListJson = path.join(thisDirectory, 'stress_test_list.json');
final generatedTest = path.join(
  path.join(thisDirectory, 'generated_stress_test.dart'),
);

final List<String> testFiles = List<String>.from(
  json.decode(File(stressTestListJson).readAsStringSync()),
);
final dart = 'tools/sdks/dart-sdk/bin/dart';

main(List<String> args) async {
  File(generatedTest).writeAsStringSync(await generateStressTest(testFiles));
}

Future<String> generateStressTest(List<String> testFiles) async {
  testFiles = List.from(testFiles)..shuffle();

  final sb = StringBuffer();
  sb.writeln(r'''
import 'dart:async';
import 'dart:isolate';
import 'dart:io';

''');
  for (int i = 0; i < testFiles.length; ++i) {
    final testFile = testFiles[i];
    sb.writeln('import "$testFile" as test$i;');
  }
  for (int i = 0; i < testFiles.length; ++i) {
    final testFile = path.normalize(path.join(thisDirectory, testFiles[i]));
    sb.writeln('''
        wrapper$i(dynamic _) {
          print('[$testFile] starting ...');
          runZoned(() {
            test$i.main();
          }, zoneSpecification: ZoneSpecification(
            handleUncaughtError: (_, ZoneDelegate parent, Zone zone, error, stack) {
              parent.print(zone, 'Error($testFile): \$error \\nstack:\$stack');
              parent.handleUncaughtError(zone, error, stack);
            },
            print: (_, _2, _3, String line) {}));
        }
        ''');
  }
  sb.writeln('');
  sb.writeln(r'''
    class Test {
      final String name;
      final dynamic Function(dynamic) fun;
      Test(this.name, this.fun);
    }
  ''');
  sb.writeln('final List<Test> tests = [');
  for (int i = 0; i < testFiles.length; ++i) {
    final testFile = path.normalize(path.join(thisDirectory, testFiles[i]));
    sb.writeln('  Test("$testFile", wrapper$i),');
  }
  sb.writeln('];');
  sb.writeln('');

  sb.writeln('''
class Runner {
  static const progressEvery = 100;

  final List<Test> tests;
  late final ReceivePort onExit;
  late final List<ReceivePort> onExits;
  late final List<ReceivePort> onErrors;
  final List<String> errorLog = [];

  Runner(this.tests) {
    onExit = ReceivePort();
    onExits = List<ReceivePort>.generate(tests.length, (int i) {
      return ReceivePort()..listen((_) {
        print('[\${tests[i].name}] finished');
        onExit.sendPort.send(null);
      });
    });
    onErrors = List<ReceivePort>.generate(tests.length, (int i) {
      return ReceivePort()..listen((error) {
        errorLog.add('[\${tests[i].name}] error: \$error');
      });
    });
  }

  Future runWithUnlimitedParallelism() async {
    for (int i = 0; i < tests.length; ++i) {
      await Isolate.spawn(
          tests[i].fun,
          null,
          onExit: onExits[i].sendPort,
          onError: onErrors[i].sendPort);
    }
    await waitUntilDone();
  }

  Future runWithParallelism(int parallelism) async {
    int _current = 0;

    Future run() async {
      final int current = _current++;
      await Isolate.spawn(
          tests[current].fun,
          null,
          onExit: onExits[current].sendPort,
          onError: onErrors[current].sendPort);
    }
    void scheduleNext() {
      if (_current == tests.length) return;
      run().whenComplete(scheduleNext);
    }

    for (int i = 0; i < parallelism; ++i) {
      scheduleNext();
    }
    await waitUntilDone();
  }

  Future waitUntilDone() async {
    final exitSi = StreamIterator(onExit);
    for (int i = 0; i < tests.length; ++i) {
      await exitSi.moveNext();
    }
    await exitSi.cancel();
    onExit.close();
    onExits.forEach((rp) => rp.close());
    onErrors.forEach((rp) => rp.close());

    if (!errorLog.isEmpty) {
      print('Spawning tests in isolates resulted in the following errors:');
      print('------------------------------------------------------------');
      errorLog.forEach(print);
      print('------------------------------------------------------------');
      print('-> Setting exitCode to 255');
      exitCode = 255;
    }
  }

}

main() async {
  final shards = int.fromEnvironment(
      'shards', defaultValue: 1);
  final shard = int.fromEnvironment(
      'shard', defaultValue: 0);

  final repeat = int.fromEnvironment(
      'repeat', defaultValue: 1);

  final parallelism = int.fromEnvironment(
      'parallelism', defaultValue: 0);

  final filteredTests = <Test>[];
  for (int i = 0; i < tests.length; ++i) {
    if ((i % shards) == shard) {
      final test = tests[i];
      for (int j = 0; j < repeat; ++j) {
        filteredTests.add(test);
      }
    }
  }

  final runner = Runner(filteredTests);
  if (parallelism <= 0) {
    await runner.runWithUnlimitedParallelism();
  } else {
    await runner.runWithParallelism(parallelism);
  }
}
  ''');
  return format(sb.toString());
}

Future<String> format(String generatedSource) async {
  try {
    final result = await Process.start(dart, ['format']);
    result.stdin.writeln(generatedSource);

    final results = await Future.wait([
      result.stdin.close(),
      result.stdout.transform(utf8.decoder).join(''),
      result.stderr.transform(utf8.decoder).join(''),
      result.exitCode,
    ]);

    final exitCode = results[3] as int;
    if (exitCode != 0) {
      print('Note: Failed to format source code. Dart format exited non-0.');
      return generatedSource;
    }
    final stdout = results[1] as String;
    final stderr = results[2] as String;
    if (stderr.trim().length != 0) {
      print(
        'Note: Failed to format source code. Dart format had stderr: '
        '$stderr',
      );
      return generatedSource;
    }
    return stdout;
  } catch (e) {
    print('Note: Failed to format source code: $e');
    return generatedSource;
  }
}
