// Copyright 2014 The Flutter Authors. 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:args/args.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/depfile.dart';
import 'package:flutter_tools/src/build_system/targets/assets.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/globals.dart' as globals;

import '../../../src/common.dart';
import '../../../src/context.dart';
import '../../../src/fake_process_manager.dart';

void main() {
  late Environment environment;
  late FileSystem fileSystem;
  late BufferLogger logger;

  setUp(() {
    fileSystem = MemoryFileSystem.test();
    environment = Environment.test(
      fileSystem.currentDirectory,
      processManager: FakeProcessManager.any(),
      artifacts: Artifacts.test(),
      fileSystem: fileSystem,
      logger: BufferLogger.test(),
      platform: FakePlatform(),
      defines: <String, String>{},
    );
    fileSystem.file(environment.buildDir.childFile('app.dill')).createSync(recursive: true);
    fileSystem.file('packages/flutter_tools/lib/src/build_system/targets/assets.dart')
      .createSync(recursive: true);
    fileSystem.file('assets/foo/bar.png')
      .createSync(recursive: true);
    fileSystem.file('assets/wildcard/#bar.png')
      .createSync(recursive: true);
    fileSystem.file('.packages')
      .createSync();
    fileSystem.file('pubspec.yaml')
      ..createSync()
      ..writeAsStringSync('''
name: example

flutter:
  assets:
    - assets/foo/bar.png
    - assets/wildcard/
''');
    logger = BufferLogger.test();
  });

  testUsingContext('includes LICENSE file inputs in dependencies', () async {
    fileSystem.file('.packages')
      .writeAsStringSync('foo:file:///bar/lib');
    fileSystem.file('bar/LICENSE')
      ..createSync(recursive: true)
      ..writeAsStringSync('THIS IS A LICENSE');

    await const CopyAssets().build(environment);

    final File depfile = environment.buildDir.childFile('flutter_assets.d');

    expect(depfile, exists);

    final Depfile dependencies = environment.depFileService.parse(depfile);

    expect(
      dependencies.inputs.firstWhereOrNull((File file) => file.path == '/bar/LICENSE'),
      isNotNull,
    );
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('Copies files to correct asset directory', () async {
    await const CopyAssets().build(environment);

    expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/AssetManifest.json'), exists);
    expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/FontManifest.json'), exists);
    expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/NOTICES.Z'), exists);
    // See https://github.com/flutter/flutter/issues/35293
    expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/foo/bar.png'), exists);
    // See https://github.com/flutter/flutter/issues/46163
    expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/wildcard/%23bar.png'), exists);
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
  });

  group("Only copies assets with a flavor if the assets' flavor matches the flavor in the environment", () {
    testUsingContext('When the environment does not have a flavor defined', () async {
      fileSystem.file('pubspec.yaml')
        ..createSync()
        ..writeAsStringSync('''
  name: example
  flutter:
    assets:
      - assets/common/
      - path: assets/vanilla/
        flavors:
          - vanilla
      - path: assets/strawberry/
        flavors:
          - strawberry
  ''');

      fileSystem.file('assets/common/image.png').createSync(recursive: true);
      fileSystem.file('assets/vanilla/ice-cream.png').createSync(recursive: true);
      fileSystem.file('assets/strawberry/ice-cream.png').createSync(recursive: true);

      await const CopyAssets().build(environment);

      expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/common/image.png'), exists);
      expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/vanilla/ice-cream.png'), isNot(exists));
      expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/strawberry/ice-cream.png'), isNot(exists));
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
    });

    testUsingContext('When the environment has a flavor defined', () async {
      environment.defines[kFlavor] = 'strawberry';
      fileSystem.file('pubspec.yaml')
        ..createSync()
        ..writeAsStringSync('''
  name: example
  flutter:
    assets:
      - assets/common/
      - path: assets/vanilla/
        flavors:
          - vanilla
      - path: assets/strawberry/
        flavors:
          - strawberry
  ''');

      fileSystem.file('assets/common/image.png').createSync(recursive: true);
      fileSystem.file('assets/vanilla/ice-cream.png').createSync(recursive: true);
      fileSystem.file('assets/strawberry/ice-cream.png').createSync(recursive: true);

      await const CopyAssets().build(environment);

      expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/common/image.png'), exists);
      expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/vanilla/ice-cream.png'), isNot(exists));
      expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/strawberry/ice-cream.png'), exists);
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
    });
  });

  testUsingContext('transforms assets declared with transformers', () async {
    Cache.flutterRoot = Cache.defaultFlutterRoot(
      platform: globals.platform,
      fileSystem: fileSystem,
      userMessages: UserMessages(),
    );

    final Environment environment = Environment.test(
      fileSystem.currentDirectory,
      processManager: globals.processManager,
      artifacts: Artifacts.test(),
      fileSystem: fileSystem,
      logger: logger,
      platform: globals.platform,
      defines: <String, String>{},
    );

    await fileSystem.file('.packages').create();

    fileSystem.file('pubspec.yaml')
      ..createSync()
      ..writeAsStringSync('''
name: example
flutter:
  assets:
    - path: input.txt
      transformers:
        - package: my_capitalizer_transformer
          args: ["-a", "-b", "--color", "green"]
''');

    fileSystem.file('input.txt')
      ..createSync(recursive: true)
      ..writeAsStringSync('abc');

    await const CopyAssets().build(environment);

    expect(logger.errorText, isEmpty);
    expect(globals.processManager, hasNoRemainingExpectations);
    expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/input.txt'), exists);
  }, overrides: <Type, Generator> {
    Logger: () => logger,
    FileSystem: () => fileSystem,
    Platform: () => FakePlatform(),
    ProcessManager: () => FakeProcessManager.list(
      <FakeCommand>[
        FakeCommand(
          command: <Pattern>[
            Artifacts.test().getArtifactPath(Artifact.engineDartBinary),
            'run',
            'my_capitalizer_transformer',
            RegExp('--input=.*'),
            RegExp('--output=.*'),
            '-a',
            '-b',
            '--color',
            'green',
          ],
          onRun: (List<String> args) {
            final ArgResults parsedArgs = (ArgParser()
                ..addOption('input')
                ..addOption('output')
                ..addOption('color')
                ..addFlag('aaa', abbr: 'a')
                ..addFlag('bbb', abbr: 'b'))
              .parse(args);

            expect(parsedArgs['aaa'], true);
            expect(parsedArgs['bbb'], true);
            expect(parsedArgs['color'], 'green');

            final File input = fileSystem.file(parsedArgs['input'] as String);
            expect(input, exists);
            final String inputContents = input.readAsStringSync();
            expect(inputContents, 'abc');
            fileSystem.file(parsedArgs['output'])
              ..createSync()
              ..writeAsStringSync(inputContents.toUpperCase());
          },
        ),
      ],
    ),
  });

  testUsingContext('exits tool if an asset transformation fails', () async {
    Cache.flutterRoot = Cache.defaultFlutterRoot(
      platform: globals.platform,
      fileSystem: fileSystem,
      userMessages: UserMessages(),
    );

    final Environment environment = Environment.test(
      fileSystem.currentDirectory,
      processManager: globals.processManager,
      artifacts: Artifacts.test(),
      fileSystem: fileSystem,
      logger: logger,
      platform: globals.platform,
      defines: <String, String>{},
    );

    await fileSystem.file('.packages').create();

    fileSystem.file('pubspec.yaml')
      ..createSync()
      ..writeAsStringSync('''
name: example
flutter:
  assets:
    - path: input.txt
      transformers:
        - package: my_transformer
          args: ["-a", "-b", "--color", "green"]
''');

    await fileSystem.file('input.txt').create(recursive: true);

    await expectToolExitLater(
      const CopyAssets().build(environment),
      startsWith('User-defined transformation of asset "/input.txt" failed.\n'),
    );
    expect(globals.processManager, hasNoRemainingExpectations);
  }, overrides: <Type, Generator> {
    Logger: () => logger,
    FileSystem: () => fileSystem,
    Platform: () => FakePlatform(),
    ProcessManager: () => FakeProcessManager.list(
      <FakeCommand>[
        FakeCommand(
          command: <Pattern>[
            Artifacts.test().getArtifactPath(Artifact.engineDartBinary),
            'run',
            'my_transformer',
            RegExp('--input=.*'),
            RegExp('--output=.*'),
            '-a',
            '-b',
            '--color',
            'green',
          ],
          exitCode: 1,
        ),
      ],
    ),
  });


  testUsingContext('Throws exception if pubspec contains missing files', () async {
    fileSystem.file('pubspec.yaml')
      ..createSync()
      ..writeAsStringSync('''
name: example

flutter:
  assets:
    - assets/foo/bar2.png

''');

    expect(() async => const CopyAssets().build(environment), throwsException);
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
  });

  testWithoutContext('processSkSLBundle returns null if there is no path '
    'to the bundle', () {

    expect(processSkSLBundle(
      null,
      targetPlatform: TargetPlatform.android,
      fileSystem: MemoryFileSystem.test(),
      logger: logger,
    ), isNull);
  });

  testWithoutContext('processSkSLBundle throws exception if bundle file is '
    'missing', () {

    expect(() => processSkSLBundle(
      'does_not_exist.sksl',
      targetPlatform: TargetPlatform.android,
      fileSystem: MemoryFileSystem.test(),
      logger: logger,
    ), throwsException);
  });

  testWithoutContext('processSkSLBundle throws exception if the bundle is not '
    'valid JSON', () {

    final FileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    fileSystem.file('bundle.sksl').writeAsStringSync('{');

    expect(() => processSkSLBundle(
      'bundle.sksl',
      targetPlatform: TargetPlatform.android,
      fileSystem: fileSystem,
      logger: logger,
    ), throwsException);
    expect(logger.errorText, contains('was not a JSON object'));
  });

  testWithoutContext('processSkSLBundle throws exception if the bundle is not '
    'a JSON object', () {

    final FileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    fileSystem.file('bundle.sksl').writeAsStringSync('[]');

    expect(() => processSkSLBundle(
      'bundle.sksl',
      targetPlatform: TargetPlatform.android,
      fileSystem: fileSystem,
      logger: logger,
    ), throwsException);
    expect(logger.errorText, contains('was not a JSON object'));
  });

  testWithoutContext('processSkSLBundle throws an exception if the engine '
    'revision is different', () {

    final FileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    fileSystem.file('bundle.sksl').writeAsStringSync(json.encode(
      <String, String>{
        'engineRevision': '1',
      },
    ));

    expect(() => processSkSLBundle(
      'bundle.sksl',
      targetPlatform: TargetPlatform.android,
      fileSystem: fileSystem,
      logger: logger,
      engineVersion: '2',
    ), throwsException);
    expect(logger.errorText, contains('Expected Flutter 1, but found 2'));
  });

  testWithoutContext('processSkSLBundle warns if the bundle target platform is '
    'different from the current target', () async {

    final FileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    fileSystem.file('bundle.sksl').writeAsStringSync(json.encode(
      <String, Object>{
        'engineRevision': '2',
        'platform': 'fuchsia-arm64',
        'data': <String, Object>{},
      }
    ));

    final DevFSContent content = processSkSLBundle(
      'bundle.sksl',
      targetPlatform: TargetPlatform.android,
      fileSystem: fileSystem,
      logger: logger,
      engineVersion: '2',
    )!;

    expect(await content.contentsAsBytes(), utf8.encode('{"data":{}}'));
    expect(logger.errorText, contains('This may lead to less efficient shader caching'));
  });

  testWithoutContext('processSkSLBundle does not warn and produces bundle', () async {

    final FileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    fileSystem.file('bundle.sksl').writeAsStringSync(json.encode(
      <String, Object>{
        'engineRevision': '2',
        'platform': 'android',
        'data': <String, Object>{},
      },
    ));

    final DevFSContent content = processSkSLBundle(
      'bundle.sksl',
      targetPlatform: TargetPlatform.android,
      fileSystem: fileSystem,
      logger: logger,
      engineVersion: '2',
    )!;

    expect(await content.contentsAsBytes(), utf8.encode('{"data":{}}'));
    expect(logger.errorText, isEmpty);
  });
}
