// Copyright (c) 2017, 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 'package:build_config/build_config.dart';
import 'package:build_config/src/expandos.dart';
import 'package:test/test.dart';

void main() {
  test('build.yaml can be parsed', () {
    final buildConfig = BuildConfig.parse('example', ['a', 'b'], buildYaml);
    expectBuildTargets(buildConfig.buildTargets, {
      'example:a': createBuildTarget(
        'example',
        key: 'example:a',
        builders: {
          'b:b': TargetBuilderConfig(
            isEnabled: true,
            generateFor: const InputSet(include: ['lib/a.dart']),
          ),
          'c:c': TargetBuilderConfig(isEnabled: false),
          'example:h': TargetBuilderConfig(
            isEnabled: true,
            options: {'foo': 'bar'},
          ),
          'example:p': TargetBuilderConfig(
            isEnabled: true,
            options: {'baz': 'zap'},
          ),
        },
        // Expecting $default => example:example
        dependencies: ['example:example', 'b:b', 'c:d'],
        sources: const InputSet(include: ['lib/a.dart', 'lib/src/a/**']),
      ),
      'example:example': createBuildTarget(
        'example',
        dependencies: ['f:f', 'example:a'],
        sources: const InputSet(
          include: ['lib/e.dart', 'lib/src/e/**'],
          exclude: ['lib/src/e/g.dart'],
        ),
      ),
      'example:b': createBuildTarget(
        'example',
        sources: const InputSet(include: ['lib/b.dart']),
        dependencies: [],
        autoApplyBuilders: false,
        builders: {
          'b:b': TargetBuilderConfig(isEnabled: true, options: {'zip': 'zorp'}),
        },
      ),
    });
    expectBuilderDefinitions(buildConfig.builderDefinitions, {
      'example:h': createBuilderDefinition(
        'example',
        key: 'example:h',
        builderFactories: ['createBuilder'],
        autoApply: AutoApply.dependents,
        isOptional: true,
        buildTo: BuildTo.cache,
        import: 'package:example/e.dart',
        buildExtensions: {
          '.dart': ['.g.dart', '.json'],
        },
        requiredInputs: ['.dart'],
        runsBefore: {'foo_builder:foo_builder'},
        appliesBuilders: {'foo_builder:foo_builder'},
        defaults: const TargetBuilderConfigDefaults(
          generateFor: InputSet(include: ['lib/**']),
          options: {'foo': 'bar'},
          releaseOptions: {'baz': 'bop'},
        ),
      ),
    });
    expectPostProcessBuilderDefinitions(
      buildConfig.postProcessBuilderDefinitions,
      {
        'example:p': createPostProcessBuilderDefinition(
          'example',
          key: 'example:p',
          builderFactory: 'createPostProcessBuilder',
          import: 'package:example/p.dart',
          defaults: const TargetBuilderConfigDefaults(
            generateFor: InputSet(include: ['web/**']),
            options: {'foo': 'bar'},
            releaseOptions: {'baz': 'bop'},
          ),
        ),
      },
    );
    expectGlobalOptions(buildConfig.globalOptions, {
      'example:h': GlobalBuilderConfig(options: const {'foo': 'global'}),
      'b:b': GlobalBuilderConfig(
        devOptions: const {'foo': 'global_dev'},
        releaseOptions: const {'foo': 'global_release'},
      ),
    });

    expect(buildConfig.additionalPublicAssets, ['test/**']);
  });

  test('build.yaml can omit a targets section', () {
    final buildConfig = BuildConfig.parse('example', [
      'a',
      'b',
    ], buildYamlNoTargets);
    expectBuildTargets(buildConfig.buildTargets, {
      'example:example': createBuildTarget(
        'example',
        dependencies: {'a:a', 'b:b'},
        sources: const InputSet(),
      ),
    });
    expectBuilderDefinitions(buildConfig.builderDefinitions, {
      'example:a': createBuilderDefinition(
        'example',
        key: 'example:a',
        builderFactories: ['createBuilder'],
        autoApply: AutoApply.none,
        isOptional: false,
        buildTo: BuildTo.cache,
        import: 'package:example/builder.dart',
        buildExtensions: {
          '.dart': ['.g.dart', '.json'],
        },
        requiredInputs: const [],
        runsBefore: <String>{},
        appliesBuilders: <String>{},
      ),
    });
  });

  test('build.yaml can be empty', () {
    final buildConfig = BuildConfig.parse('example', ['a', 'b'], '');
    expectBuildTargets(buildConfig.buildTargets, {
      'example:example': createBuildTarget(
        'example',
        dependencies: {'a:a', 'b:b'},
        sources: const InputSet(),
      ),
    });
    expectBuilderDefinitions(buildConfig.builderDefinitions, {});
    expectPostProcessBuilderDefinitions(
      buildConfig.postProcessBuilderDefinitions,
      {},
    );
  });

  test('build.yaml can use | separator in builder keys', () {
    final buildConfig = BuildConfig.parse(
      'example',
      ['a', 'b'],
      '''
builders:
  example|example:
    builder_factories: ["createBuilder"]
    import: package:example/builders.dart
    build_extensions: {".dart": [".g.dart", ".json"]}
    runs_before: ["a|foo_builder"]
    applies_builders: ["a|foo_builder"]
''',
    );
    expect(buildConfig.builderDefinitions.keys, ['example:example']);
    expect(buildConfig.builderDefinitions['example:example']!.runsBefore, [
      'a:foo_builder',
    ]);
    expect(buildConfig.builderDefinitions['example:example']!.appliesBuilders, [
      'a:foo_builder',
    ]);
  });
}

String buildYaml = r'''
global_options:
  ":h":
    options:
      foo: global
  b:b:
    dev_options:
      foo: global_dev
    release_options:
      foo: global_release
targets:
  a:
    builders:
      ":h":
        options:
          foo: bar
      ":p":
        options:
          baz: zap
      b:b:
        generate_for:
          - lib/a.dart
      c:c:
        enabled: false
    dependencies:
      - $default
      - b
      - c:d
    sources:
      - "lib/a.dart"
      - "lib/src/a/**"
  b:
    auto_apply_builders: false
    sources:
      - "lib/b.dart"
    dependencies: []
    builders:
      # Configuring a builder implicitly enables it
      b:b:
        options:
          zip: zorp
  $default:
    dependencies:
      - f
      - :a
    sources:
      include:
        - "lib/e.dart"
        - "lib/src/e/**"
      exclude:
        - "lib/src/e/g.dart"
builders:
  h:
    builder_factories: ["createBuilder"]
    import: package:example/e.dart
    build_extensions: {".dart": [".g.dart", ".json"]}
    auto_apply: dependents
    required_inputs: [".dart"]
    runs_before: ["foo_builder"]
    applies_builders: ["foo_builder"]
    is_optional: True
    defaults:
      generate_for: ["lib/**"]
      options:
        foo: bar
      release_options:
        baz: bop
post_process_builders:
  p:
    builder_factory: "createPostProcessBuilder"
    import: package:example/p.dart
    defaults:
      generate_for: ["web/**"]
      options:
        foo: bar
      release_options:
        baz: bop
additional_public_assets:
  - "test/**"
''';

String buildYamlNoTargets = '''
builders:
  a:
    builder_factories: ["createBuilder"]
    import: package:example/builder.dart
    build_extensions: {".dart": [".g.dart", ".json"]}
''';

void expectBuilderDefinitions(
  Map<String, BuilderDefinition> actual,
  Map<String, BuilderDefinition> expected,
) {
  expect(actual.keys, unorderedEquals(expected.keys));
  for (final p in actual.keys) {
    expect(actual[p], _matchesBuilderDefinition(expected[p]!));
  }
}

void expectPostProcessBuilderDefinitions(
  Map<String, PostProcessBuilderDefinition> actual,
  Map<String, PostProcessBuilderDefinition> expected,
) {
  expect(actual.keys, unorderedEquals(expected.keys));
  for (final p in actual.keys) {
    expect(actual[p], _matchesPostProcessBuilderDefinition(expected[p]!));
  }
}

void expectGlobalOptions(
  Map<String, GlobalBuilderConfig> actual,
  Map<String, GlobalBuilderConfig> expected,
) {
  expect(actual.keys, unorderedEquals(expected.keys));
  for (final p in actual.keys) {
    expect(actual[p], _matchesGlobalBuilderConfig(expected[p]!));
  }
}

Matcher _matchesBuilderDefinition(BuilderDefinition definition) =>
    const TypeMatcher<BuilderDefinition>()
        .having(
          (d) => d.builderFactories,
          'builderFactories',
          definition.builderFactories,
        )
        .having(
          (d) => d.buildExtensions,
          'buildExtensions',
          definition.buildExtensions,
        )
        .having(
          (d) => d.requiredInputs,
          'requiredInputs',
          definition.requiredInputs,
        )
        .having((d) => d.runsBefore, 'runsBefore', definition.runsBefore)
        .having(
          (d) => d.appliesBuilders,
          'appliesBuilders',
          definition.appliesBuilders,
        )
        .having(
          (d) => d.defaults,
          'defaults',
          _matchesBuilderConfigDefaults(definition.defaults),
        )
        .having((d) => d.autoApply, 'autoApply', definition.autoApply)
        .having((d) => d.isOptional, 'isOptional', definition.isOptional)
        .having((d) => d.buildTo, 'buildTo', definition.buildTo)
        .having((d) => d.import, 'import', definition.import)
        .having((d) => d.key, 'key', definition.key)
        .having((d) => d.package, 'package', definition.package);

Matcher _matchesPostProcessBuilderDefinition(
  PostProcessBuilderDefinition definition,
) => const TypeMatcher<PostProcessBuilderDefinition>()
    .having(
      (d) => d.builderFactory,
      'builderFactory',
      definition.builderFactory,
    )
    .having(
      (d) => d.defaults,
      'defaults',
      _matchesBuilderConfigDefaults(definition.defaults),
    )
    .having((d) => d.import, 'import', definition.import)
    .having((d) => d.key, 'key', definition.key)
    .having((d) => d.package, 'package', definition.package);

Matcher _matchesGlobalBuilderConfig(GlobalBuilderConfig config) =>
    const TypeMatcher<GlobalBuilderConfig>()
        .having((c) => c.options, 'options', config.options)
        .having((c) => c.devOptions, 'devOptions', config.devOptions)
        .having(
          (c) => c.releaseOptions,
          'releaseOptions',
          config.releaseOptions,
        );

Matcher _matchesBuilderConfigDefaults(TargetBuilderConfigDefaults defaults) =>
    const TypeMatcher<TargetBuilderConfigDefaults>()
        .having(
          (d) => d.generateFor.include,
          'generateFor.include',
          defaults.generateFor.include,
        )
        .having(
          (d) => d.generateFor.exclude,
          'generateFor.exclude',
          defaults.generateFor.exclude,
        )
        .having((d) => d.options, 'options', defaults.options)
        .having((d) => d.devOptions, 'devOptions', defaults.devOptions)
        .having(
          (d) => d.releaseOptions,
          'releaseOptions',
          defaults.releaseOptions,
        );

void expectBuildTargets(
  Map<String, BuildTarget> actual,
  Map<String, BuildTarget> expected,
) {
  expect(actual.keys, unorderedEquals(expected.keys));
  for (final p in actual.keys) {
    expect(actual[p], _matchesBuildTarget(expected[p]!));
  }
}

Matcher _matchesBuildTarget(
  BuildTarget target,
) => const TypeMatcher<BuildTarget>()
    .having((t) => t.package, 'package', target.package)
    .having(
      (t) => t.builders,
      'builders',
      _matchesBuilderConfigs(target.builders),
    )
    .having((t) => t.dependencies, 'dependencies', target.dependencies)
    .having((t) => t.sources.include, 'sources.include', target.sources.include)
    .having((t) => t.sources.exclude, 'sources.exclude', target.sources.exclude)
    .having(
      (t) => t.autoApplyBuilders,
      'autoApplyBuilders',
      target.autoApplyBuilders,
    );

Matcher _matchesBuilderConfigs(Map<String, TargetBuilderConfig> configs) =>
    equals(configs.map((k, v) => MapEntry(k, _matchesBuilderConfig(v))));

Matcher _matchesBuilderConfig(TargetBuilderConfig expected) =>
    const TypeMatcher<TargetBuilderConfig>()
        .having((c) => c.isEnabled, 'isEnabled', expected.isEnabled)
        .having((c) => c.options, 'options', expected.options)
        .having((c) => c.devOptions, 'devOptions', expected.devOptions)
        .having(
          (c) => c.releaseOptions,
          'releaseOptions',
          expected.releaseOptions,
        )
        .having(
          (c) => c.generateFor?.include,
          'generateFor.include',
          expected.generateFor?.include,
        )
        .having(
          (c) => c.generateFor?.exclude,
          'generateFor.exclude',
          expected.generateFor?.exclude,
        );

BuildTarget createBuildTarget(
  String package, {
  String? key,
  Map<String, TargetBuilderConfig>? builders,
  Iterable<String>? dependencies,
  InputSet? sources,
  bool? autoApplyBuilders,
}) {
  return runInBuildConfigZone(
    () {
      final target = BuildTarget(
        autoApplyBuilders: autoApplyBuilders,
        builders: builders,
        dependencies: dependencies,
        sources: sources,
      );

      packageExpando[target] = package;
      builderKeyExpando[target] = key ?? '$package:$package';

      return target;
    },
    package,
    [],
  );
}

BuilderDefinition createBuilderDefinition(
  String package, {
  required List<String> builderFactories,
  AutoApply? autoApply,
  bool? isOptional,
  BuildTo? buildTo,
  required String import,
  required Map<String, List<String>> buildExtensions,
  String? key,
  Iterable<String>? requiredInputs,
  Iterable<String>? runsBefore,
  Iterable<String>? appliesBuilders,
  TargetBuilderConfigDefaults? defaults,
}) {
  return runInBuildConfigZone(
    () {
      final definition = BuilderDefinition(
        builderFactories: builderFactories,
        autoApply: autoApply,
        isOptional: isOptional,
        buildTo: buildTo,
        import: import,
        buildExtensions: buildExtensions,
        requiredInputs: requiredInputs,
        runsBefore: runsBefore,
        appliesBuilders: appliesBuilders,
        defaults: defaults,
      );
      packageExpando[definition] = package;
      builderKeyExpando[definition] = key ?? '$package:$package';
      return definition;
    },
    package,
    [],
  );
}

PostProcessBuilderDefinition createPostProcessBuilderDefinition(
  String package, {
  required String builderFactory,
  required String import,
  String? key,
  TargetBuilderConfigDefaults? defaults,
}) {
  return runInBuildConfigZone(
    () {
      final definition = PostProcessBuilderDefinition(
        builderFactory: builderFactory,
        import: import,
        defaults: defaults,
      );
      packageExpando[definition] = package;
      builderKeyExpando[definition] = key ?? '$package:$package';
      return definition;
    },
    package,
    [],
  );
}
