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

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

import 'use_flag_test_helper.dart';
export 'use_flag_test_helper.dart';

class Result {
  final String cmdline;
  final ProcessResult processResult;

  Result(this.cmdline, this.processResult);

  String get output => processResult.stdout.trim();
}

void reportError(Result result, String msg) {
  print('running ${result.cmdline}:');
  if (result.processResult.stdout.isNotEmpty) {
    print('''

Command stdout:
${result.processResult.stdout}''');
  }

  if (result.processResult.stderr.isNotEmpty) {
    print('''

Command stderr:
${result.processResult.stderr}''');
  }

  Expect.fail(msg);
}

void expectOutput(String what, Result result) {
  if (result.output != what) {
    reportError(
      result,
      'Expected test to print \'${what}\' to stdout. '
      'Actual: ${result.output}',
    );
  }
}

Future<Result> runDart(
  String prefix,
  List<String> arguments, {
  bool printOut = true,
  String? dartExecutable,
}) {
  final augmentedArguments = <String>[]
    ..addAll(Platform.executableArguments)
    ..add('--verbosity=warning')
    ..addAll(arguments);
  return runBinary(
    prefix,
    dartExecutable ?? Platform.executable,
    augmentedArguments,
    printOut: printOut,
  );
}

Future<Result> runGenKernel(String prefix, List<String> arguments) {
  final augmentedArguments = <String>[
    "--platform",
    platformDill,
    ...Platform.executableArguments.where(
      (arg) =>
          arg.startsWith('--enable-experiment=') ||
          arg == '--sound-null-safety' ||
          arg == '--no-sound-null-safety',
    ),
    ...arguments,
  ];
  return runGenKernelWithoutStandardOptions(prefix, augmentedArguments);
}

Future<Result> runGenKernelWithoutStandardOptions(
  String prefix,
  List<String> arguments,
) {
  return runBinary(prefix, checkedInDartVM, [genKernelDart, ...arguments]);
}

Future<Result> runGenSnapshot(String prefix, List<String> arguments) {
  return runBinary(prefix, genSnapshot, arguments);
}

Future<Result> runBinary(
  String prefix,
  String binary,
  List<String> arguments, {
  Map<String, String>? environment,
  bool runInShell = false,
  bool printOut = true,
  bool allowNonZeroExitCode = false,
}) async {
  print("+ $binary " + arguments.join(" "));
  final processResult = await Process.run(
    binary,
    arguments,
    environment: environment,
    runInShell: runInShell,
  );
  final result = new Result(
    '[$prefix] ${binary} ${arguments.join(' ')}',
    processResult,
  );

  if (printOut && processResult.stdout.isNotEmpty) {
    print('''

Command stdout:
${processResult.stdout}''');
  }

  if (printOut && processResult.stderr.isNotEmpty) {
    print('''

Command stderr:
${processResult.stderr}''');
  }

  if (!allowNonZeroExitCode && result.processResult.exitCode != 0) {
    reportError(
      result,
      '[$prefix] Process finished with non-zero exit code ${result.processResult.exitCode}',
    );
  }
  return result;
}

checkDeterministicSnapshot(String snapshotKind, String expectedStdout) async {
  await withTempDir('snapshot', (String temp) async {
    final snapshot1Path = p.join(temp, 'snapshot1');
    final snapshot2Path = p.join(temp, 'snapshot2');

    print("Version ${Platform.version}");

    final generate1Result = await runDart('GENERATE SNAPSHOT 1', [
      '--deterministic',
      '--trace_class_finalization',
      '--trace_type_finalization',
      '--trace_compiler',
      '--verbose_gc',
      '--verbosity=warning',
      '--snapshot=$snapshot1Path',
      '--snapshot-kind=$snapshotKind',
      Platform.script.toFilePath(),
      '--child',
    ]);
    expectOutput(expectedStdout, generate1Result);

    final generate2Result = await runDart('GENERATE SNAPSHOT 2', [
      '--deterministic',
      '--trace_class_finalization',
      '--trace_type_finalization',
      '--trace_compiler',
      '--verbose_gc',
      '--verbosity=warning',
      '--snapshot=$snapshot2Path',
      '--snapshot-kind=$snapshotKind',
      Platform.script.toFilePath(),
      '--child',
    ]);
    expectOutput(expectedStdout, generate2Result);

    var snapshot1Bytes = await new File(snapshot1Path).readAsBytes();
    var snapshot2Bytes = await new File(snapshot2Path).readAsBytes();

    var minLength = min(snapshot1Bytes.length, snapshot2Bytes.length);
    for (var i = 0; i < minLength; i++) {
      if (snapshot1Bytes[i] != snapshot2Bytes[i]) {
        Expect.fail("Snapshots differ at byte $i");
      }
    }
    Expect.equals(snapshot1Bytes.length, snapshot2Bytes.length);
  });
}

runAppJitTest(
  Uri testScriptUri, {
  Future<Result> Function(String snapshotPath)? runSnapshot,
}) async {
  runSnapshot ??= (snapshotPath) =>
      runDart('RUN FROM SNAPSHOT', [snapshotPath]);

  await withTempDir('snapshot', (String temp) async {
    final snapshotPath = p.join(temp, 'app.jit');
    final testPath = testScriptUri.toFilePath();

    final trainingResult = await runDart('TRAINING RUN', [
      '--snapshot=$snapshotPath',
      '--snapshot-kind=app-jit',
      '--verbosity=warning',
      testPath,
      '--train',
    ]);
    expectOutput("OK(Trained)", trainingResult);
    final runResult = await runSnapshot!(snapshotPath);
    expectOutput("OK(Run)", runResult);
  });
}
