// Copyright (c) 2023, 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 'package:code_assets/code_assets.dart';
import 'package:data_assets/data_assets.dart';
import 'package:file/local.dart' show LocalFileSystem;
import 'package:hooks/hooks.dart';
import 'package:hooks_runner/src/utils/run_process.dart' as run_process;
import 'package:logging/logging.dart';
import 'package:test/test.dart';
import 'package:yaml/yaml.dart';

extension UriExtension on Uri {
  String get name => pathSegments.where((e) => e != '').last;

  Uri get parent => File(toFilePath()).parent.uri;

  FileSystemEntity get fileSystemEntity {
    if (path.endsWith(Platform.pathSeparator) || path.endsWith('/')) {
      return Directory.fromUri(this);
    }
    return File.fromUri(this);
  }
}

const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES';

Future<void> inTempDir(
  Future<void> Function(Uri tempUri) fun, {
  String? prefix,
  bool keepTemp = false,
}) async {
  final tempDir = await Directory.systemTemp.createTemp(prefix);
  // Deal with Windows temp folder aliases.
  final tempUri = Directory(
    await tempDir.resolveSymbolicLinks(),
  ).uri.normalizePath();
  try {
    await fun(tempUri);
  } finally {
    if ((!Platform.environment.containsKey(keepTempKey) ||
            Platform.environment[keepTempKey]!.isEmpty) &&
        !keepTemp) {
      try {
        await tempDir.delete(recursive: true);
      } on FileSystemException {
        // On Windows, the temp dir might still be locked even though all
        // process invocations have finished.
        if (!Platform.isWindows) rethrow;
      }
    } else {
      print('$keepTempKey $tempUri');
    }
  }
}

Future<Uri> tempDirForTest({String? prefix, bool keepTemp = false}) async {
  final tempDir = await Directory.systemTemp.createTemp(prefix);
  // Deal with Windows temp folder aliases.
  final tempUri = Directory(
    await tempDir.resolveSymbolicLinks(),
  ).uri.normalizePath();
  if ((!Platform.environment.containsKey(keepTempKey) ||
          Platform.environment[keepTempKey]!.isEmpty) &&
      !keepTemp) {
    addTearDown(() async {
      try {
        await tempDir.delete(recursive: true);
      } on FileSystemException {
        // On Windows, the temp dir might still be locked even though all
        // process invocations have finished.
        if (!Platform.isWindows) rethrow;
      }
    });
  } else {
    print('$keepTempKey $tempUri');
  }
  return tempUri;
}

/// Runs a [Process].
///
/// If [logger] is provided, stream stdout and stderr to it.
///
/// If [captureOutput], captures stdout and stderr.
Future<run_process.RunProcessResult> runProcess({
  required Uri executable,
  List<String> arguments = const [],
  Uri? workingDirectory,
  Map<String, String>? environment,
  required Logger? logger,
  bool captureOutput = true,
  int expectedExitCode = 0,
  bool throwOnUnexpectedExitCode = false,
}) => run_process.runProcess(
  filesystem: const LocalFileSystem(),
  executable: executable,
  arguments: arguments,
  workingDirectory: workingDirectory,
  environment: environment,
  logger: logger,
  captureOutput: captureOutput,
  expectedExitCode: expectedExitCode,
  throwOnUnexpectedExitCode: throwOnUnexpectedExitCode,
);

/// Test files are run in a variety of ways, find this package root in all.
///
/// Test files can be run from source from any working directory. The Dart SDK
/// `tools/test.py` runs them from the root of the SDK for example.
///
/// Test files can be run from dill from the root of package. `package:test`
/// does this.
///
/// https://github.com/dart-lang/test/issues/110
Uri findPackageRoot(String packageName) {
  final script = Platform.script;
  final fileName = script.name;
  if (fileName.endsWith('.dart')) {
    // We're likely running from source in the package somewhere.
    var directory = script.resolve('.');
    while (true) {
      final dirName = directory.name;
      if (dirName == packageName) {
        return directory;
      }
      final parent = directory.resolve('..');
      if (parent == directory) break;
      directory = parent;
    }
  } else if (fileName.endsWith('.dill')) {
    // Probably from the package root.
    final cwd = Directory.current.uri;
    final dirName = cwd.name;
    if (dirName == packageName) {
      return cwd;
    }
    // Or the workspace root.
    final candidate = cwd.resolve('pkgs/$packageName/');
    if (Directory.fromUri(candidate).existsSync()) {
      return candidate;
    }
  }
  // Or the workspace root.
  final cwd = Directory.current.uri;
  final candidate = cwd.resolve('pkgs/$packageName/');
  if (Directory.fromUri(candidate).existsSync()) {
    return candidate;
  }
  throw StateError(
    "Could not find package root for package '$packageName'. "
    'Tried finding the package root via Platform.script '
    "'${Platform.script.toFilePath()}' and Directory.current "
    "'${Directory.current.uri.toFilePath()}'.",
  );
}

final pkgNativeAssetsBuilderUri = findPackageRoot('hooks_runner');

final testDataUri = pkgNativeAssetsBuilderUri.resolve('test_data/');

/// Archiver provided by the environment.
///
/// Provided on Dart CI.
final Uri? _ar = Platform.environment['DART_HOOK_TESTING_C_COMPILER__AR']
    ?.asFileUri();

/// Compiler provided by the environment.
///
/// Provided on Dart CI.
final Uri? _cc = Platform.environment['DART_HOOK_TESTING_C_COMPILER__CC']
    ?.asFileUri();

/// Linker provided by the environment.
///
/// Provided on Dart CI.
final Uri? _ld = Platform.environment['DART_HOOK_TESTING_C_COMPILER__LD']
    ?.asFileUri();

/// Path to script that sets environment variables for [_cc], [_ld], and [_ar].
///
/// Provided on Dart CI.
final Uri? _envScript = Platform
    .environment['DART_HOOK_TESTING_C_COMPILER__ENV_SCRIPT']
    ?.asFileUri();

/// Arguments for [_envScript] provided by environment.
///
/// Provided on Dart CI.
final List<String>? _envScriptArgs = Platform
    .environment['DART_HOOK_TESTING_C_COMPILER__ENV_SCRIPT_ARGUMENTS']
    ?.split(' ');

/// Configuration for the native toolchain.
///
/// Provided on Dart CI.
final cCompiler = (_cc == null || _ar == null || _ld == null)
    ? null
    : CCompilerConfig(
        compiler: _cc!,
        archiver: _ar!,
        linker: _ld!,
        windows: _envScript == null
            ? null
            : WindowsCCompilerConfig(
                developerCommandPrompt: DeveloperCommandPrompt(
                  script: _envScript!,
                  arguments: _envScriptArgs ?? [],
                ),
              ),
      );

extension on String {
  Uri asFileUri() => Uri.file(this);
}

extension AssetIterable on Iterable<EncodedAsset> {
  Future<bool> allExist() async {
    for (final encodedAsset in this) {
      if (encodedAsset.isDataAsset) {
        final dataAsset = encodedAsset.asDataAsset;
        if (!await dataAsset.file.fileSystemEntity.exists()) {
          return false;
        }
      } else if (encodedAsset.isCodeAsset) {
        final codeAsset = encodedAsset.asCodeAsset;
        if (!await (codeAsset.file?.fileSystemEntity.exists() ?? true)) {
          return false;
        }
      } else {
        throw UnimplementedError('Unknown asset type ${encodedAsset.type}');
      }
    }
    return true;
  }
}

Future<void> copyTestProjects({
  Uri? sourceUri,
  required Uri targetUri,
  bool addDependencyOverrides = true,
}) async {
  sourceUri ??= testDataUri;
  final manifestUri = sourceUri.resolve('manifest.yaml');
  final manifestFile = File.fromUri(manifestUri);
  final manifestString = await manifestFile.readAsString();
  final manifestYaml = loadYamlDocument(manifestString);
  final manifest = [
    for (final path in manifestYaml.contents as List<Object?>)
      Uri(path: path as String),
  ];
  final filesToCopy = manifest
      .where(
        (e) =>
            !(e.pathSegments.last.startsWith('pubspec') &&
                e.pathSegments.last.endsWith('.yaml')),
      )
      .toList();
  final filesToModify = manifest
      .where(
        (e) =>
            e.pathSegments.last.startsWith('pubspec') &&
            e.pathSegments.last.endsWith('.yaml'),
      )
      .toList();

  for (final pathToCopy in filesToCopy) {
    final sourceFile = File.fromUri(sourceUri.resolveUri(pathToCopy));
    final targetFileUri = targetUri.resolveUri(pathToCopy);
    final targetDirUri = targetFileUri.parent;
    final targetDir = Directory.fromUri(targetDirUri);
    if (!(await targetDir.exists())) {
      await targetDir.create(recursive: true);
    }

    // Copying files on MacOS and Windows preserves the source timestamps.
    // The builder will use the cached build if the timestamps are equal.
    // So just write the file instead.
    final targetFile = File.fromUri(targetFileUri);
    await targetFile.writeAsBytes(await sourceFile.readAsBytes());
  }
  for (final pathToModify in filesToModify) {
    final sourceFile = File.fromUri(sourceUri.resolveUri(pathToModify));
    final targetFileUri = targetUri.resolveUri(pathToModify);
    var sourceString = await sourceFile.readAsString();
    if (addDependencyOverrides &&
        !pathToModify.path.contains('native_add_version_skew')) {
      sourceString += '''

dependency_overrides:
''';
      const packagesToOverride = [
        'code_assets',
        'data_assets',
        'hooks',
        'native_toolchain_c',
      ];
      for (final package in packagesToOverride) {
        sourceString +=
            '''
  $package:
    path: ${pkgNativeAssetsBuilderUri.resolve('../$package/').toFilePath()}
''';
      }
    }

    final modifiedString = sourceString.replaceAll('resolution: workspace', '');
    await File.fromUri(
      targetFileUri,
    ).writeAsString(modifiedString, flush: true);
  }
}

extension UnescapePath on String {
  /// Remove double encoding of slashes on windows, for string comparison with
  /// Unix-style encoded strings.
  String unescape() => replaceAll('\\', '/');
}

/// Logger that outputs the full trace when a test fails.
Logger get logger => _logger ??= () {
  // A new logger is lazily created for each test so that the messages
  // captured by printOnFailure are scoped to the correct test.
  addTearDown(() => _logger = null);
  return _createTestLogger();
}();

Logger? _logger;

Logger createCapturingLogger(
  List<String> capturedMessages, {
  Level level = Level.ALL,
}) => _createTestLogger(capturedMessages: capturedMessages, level: level);

Logger _createTestLogger({
  List<String>? capturedMessages,
  Level level = Level.ALL,
}) => Logger.detached('')
  ..level = level
  ..onRecord.listen((record) {
    printOnFailure('${record.level.name}: ${record.time}: ${record.message}');
    capturedMessages?.add(record.message);
  });

final dartExecutable = File(Platform.resolvedExecutable).uri;
