// 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:io';

import 'config.dart';
import 'encoded_asset.dart';
import 'extension.dart';
import 'hooks/syntax.g.dart';

/// A list of [String]s describing validation errors.
typedef ValidationErrors = List<String>;

/// The base protocol for `hook/build.dart` and `hook/link.dart` which can be
/// extended with [ProtocolExtension]s.
///
/// This class contains the [HookInput] and [HookOutput] validation rules for
/// the base protocol. (In contrast to [ProtocolExtension]s, it does not contain
/// setup methods for the [HookInput], the base protocol knows how to setup
/// itself.)
class ProtocolBase {
  /// Validates the [BuildInput] for both syntax and semantics.
  static Future<ValidationErrors> validateBuildInput(BuildInput input) async {
    final syntaxErrors = BuildInputSyntax.fromJson(input.json).validate();
    if (syntaxErrors.isNotEmpty) {
      return [...syntaxErrors, _semanticValidationSkippedMessage];
    }

    return _validateHookInput('BuildInput', input);
  }

  /// Validates the [LinkInput] for both syntax and semantics.
  static Future<ValidationErrors> validateLinkInput(LinkInput input) async {
    final syntaxErrors = LinkInputSyntax.fromJson(input.json).validate();
    if (syntaxErrors.isNotEmpty) {
      return [...syntaxErrors, _semanticValidationSkippedMessage];
    }

    final recordUses = input.recordedUsagesFile;
    return <String>[
      ..._validateHookInput('LinkInput', input),
      if (recordUses != null)
        ..._validateDirectory(
          '$LinkInput.recordUses',
          input.outputDirectoryShared,
        ),
    ];
  }

  static ValidationErrors _validateHookInput(
    String inputName,
    HookInput input,
  ) {
    final errors = <String>[
      ..._validateDirectory('$inputName.packageRoot', input.packageRoot),
      ..._validateDirectory(
        '$inputName.outputDirectory',
        input.outputDirectory,
      ),
      ..._validateDirectory(
        '$inputName.outputDirectoryShared',
        input.outputDirectoryShared,
      ),
      ..._validateDirectory(
        '$inputName.outputFile',
        input.outputFile,
        mustExist: false,
      ),
    ];
    return errors;
  }

  static ValidationErrors _validateDirectory(
    String name,
    Uri uri, {
    bool mustExist = true,
    bool mustBeAbsolute = true,
  }) {
    final errors = <String>[];
    if (mustBeAbsolute && !uri.isAbsolute) {
      errors.add('$name (${uri.toFilePath()}) must be an absolute path.');
    }
    if (mustExist && !Directory.fromUri(uri).existsSync()) {
      errors.add('$name (${uri.toFilePath()}) does not exist as a directory.');
    }
    return errors;
  }

  /// Validates the [BuildOutput] for both syntax and semantics.
  static Future<ValidationErrors> validateBuildOutput(
    BuildInput input,
    BuildOutput output,
  ) async {
    final syntaxErrors = BuildOutputSyntax.fromJson(output.json).validate();
    if (syntaxErrors.isNotEmpty) {
      return [...syntaxErrors, _semanticValidationSkippedMessage];
    }

    final errors = [
      ..._validateAssetsForLinking(input, output),
      ..._validateOutputAssetTypes(input, output.assets.encodedAssets),
    ];
    if (input.config.linkingEnabled) {
      for (final assets in output.assets.encodedAssetsForLinking.values) {
        errors.addAll(_validateOutputAssetTypes(input, assets));
      }
    }
    return errors;
  }

  /// Validates the [LinkOutput] for both syntax and semantics.
  static Future<ValidationErrors> validateLinkOutput(
    LinkInput input,
    LinkOutput output,
  ) async {
    final syntaxErrors = LinkOutputSyntax.fromJson(output.json).validate();
    if (syntaxErrors.isNotEmpty) {
      return [...syntaxErrors, _semanticValidationSkippedMessage];
    }

    final errors = [
      ..._validateOutputAssetTypes(input, output.assets.encodedAssets),
    ];
    return errors;
  }

  /// Only output asset types that are supported by the embedder.
  static ValidationErrors _validateOutputAssetTypes(
    HookInput input,
    Iterable<EncodedAsset> assets,
  ) {
    final errors = <String>[];
    final List<String> buildAssetTypes;
    if (input is BuildInput) {
      buildAssetTypes = input.config.buildAssetTypes;
    } else {
      buildAssetTypes = (input as LinkInput).config.buildAssetTypes;
    }
    for (final asset in assets) {
      if (!buildAssetTypes.contains(asset.type)) {
        final error =
            'Asset with type "${asset.type}" is not a supported asset type '
            '(${buildAssetTypes.join(' ')} are supported)';
        errors.add(error);
      }
    }
    return errors;
  }

  /// EncodedAssetsForLinking should be empty if linking is not supported.
  static ValidationErrors _validateAssetsForLinking(
    BuildInput input,
    BuildOutput output,
  ) {
    final errors = <String>[];
    if (!input.config.linkingEnabled) {
      if (output.assets.encodedAssetsForLinking.isNotEmpty) {
        const error =
            'BuildOutput.assets_for_linking is not empty while '
            'BuildInput.config.linkingEnabled is false';
        errors.add(error);
      }
    }
    return errors;
  }

  static const _semanticValidationSkippedMessage =
      'Syntax errors. Semantic validation skipped.';
}

/// A test failure.
///
/// This cannot be `package:test`s `TestFailure` because we want to avoid
/// having a dependency on `package:test`.
///
/// Not exported, because this is not meant to be caught.
final class ValidationFailure implements Exception {
  /// The message describing the validation failure.
  final String? message;

  /// Creates a new [ValidationFailure] with an optional [message].
  ValidationFailure(this.message);

  @override
  String toString() => message.toString();
}
