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

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:yaml/yaml.dart';

import 'config.dart';
import 'encoded_asset.dart';
import 'extension.dart';
import 'user_defines.dart';
import 'validation.dart';

/// Tests the main function of a `hook/build.dart`.
///
/// This method will throw an exception on validation errors.
///
/// This is intended to be used from tests, e.g.:
///
/// <!-- file://./../../example/api/test_snippet.dart -->
/// ```dart
/// import 'package:hooks/src/test.dart';
/// import 'package:test/test.dart';
///
/// void main() {
///   test('test my build hook', () async {
///     await testBuildHook(
///       mainMethod: (args) {},
///       check: (input, output) {},
///       extensions: [],
///     );
///   });
/// }
/// ```
///
/// The hook is run in isolation. No user-defines are read from the pubspec,
/// they must be provided via [userDefines]. No other hooks are run, if the hook
/// requires assets from other build hooks, the must be provided in [assets].
Future<void> testBuildHook({
  required FutureOr<void> Function(List<String> arguments) mainMethod,
  required FutureOr<void> Function(BuildInput input, BuildOutput output) check,
  bool? linkingEnabled,
  required List<ProtocolExtension> extensions,
  // TODO(https://github.com/dart-lang/native/issues/2241): Cleanup how the
  // following parameters are passed in.
  PackageUserDefines? userDefines,
  Map<String, List<EncodedAsset>>? assets,
}) async {
  linkingEnabled ??= false;
  const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES';

  final tempDir = await Directory.systemTemp.createTemp();

  try {
    // Deal with Windows temp folder aliases.
    final tempUri = Directory(
      await tempDir.resolveSymbolicLinks(),
    ).uri.normalizePath();
    final outputDirectoryShared = tempUri.resolve('output_shared/');
    final outputFile = tempUri.resolve('output.json');

    await Directory.fromUri(outputDirectoryShared).create();

    final inputBuilder = BuildInputBuilder();
    inputBuilder
      ..setupShared(
        packageRoot: Directory.current.uri,
        packageName: _readPackageNameFromPubspec(),
        outputFile: outputFile,
        outputDirectoryShared: outputDirectoryShared,
        userDefines: userDefines,
      )
      ..setupBuildInput(assets: assets)
      ..config.setupBuild(linkingEnabled: linkingEnabled);
    for (final extension in extensions) {
      extension.setupBuildInput(inputBuilder);
    }
    final input = inputBuilder.build();

    final inputErrors = [
      for (final extension in extensions)
        ...await extension.validateBuildInput(input),
    ];
    if (inputErrors.isNotEmpty) {
      throw ValidationFailure(
        'Encountered build input validation issues: $inputErrors',
      );
    }

    final inputUri = tempUri.resolve('input.json');
    _writeJsonTo(inputUri, input.json);
    await mainMethod(['--config=${inputUri.toFilePath()}']);
    final output = BuildOutput(_readJsonFrom(input.outputFile));

    final outputErrors = [
      ...await ProtocolBase.validateBuildOutput(input, output),
      for (final extension in extensions) ...[
        ...await extension.validateBuildOutput(input, output),
        ...await extension.validateApplicationAssets(
          output.assets.encodedAssets,
        ),
      ],
    ];
    if (outputErrors.isNotEmpty) {
      throw ValidationFailure(
        'Encountered build output validation issues: $outputErrors',
      );
    }

    await check(input, output);
  } finally {
    final keepTempDir = (Platform.environment[keepTempKey] ?? '').isNotEmpty;
    if (!keepTempDir) {
      tempDir.deleteSync(recursive: true);
    } else {
      print('$keepTempKey ${tempDir.uri}');
    }
  }
}

void _writeJsonTo(Uri uri, Map<String, Object?> json) {
  final encoder = const JsonEncoder().fuse(const Utf8Encoder());
  File.fromUri(uri).writeAsBytesSync(encoder.convert(json));
}

Map<String, Object?> _readJsonFrom(Uri uri) {
  final decoder = const Utf8Decoder().fuse(const JsonDecoder());
  final bytes = File.fromUri(uri).readAsBytesSync();
  return decoder.convert(bytes) as Map<String, Object?>;
}

String _readPackageNameFromPubspec() {
  final uri = Directory.current.uri.resolve('pubspec.yaml');
  final readAsString = File.fromUri(uri).readAsStringSync();
  final yaml = loadYaml(readAsString) as YamlMap;
  return yaml['name'] as String;
}
