// 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: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/build_system/tools/asset_transformer.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';

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

void main() {
  testWithoutContext('Invokes dart properly', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    final Artifacts artifacts = Artifacts.test();

    final File asset = fileSystem.file('asset.txt')..createSync()..writeAsStringSync('hello world');
    const String outputPath = 'output.txt';

    final FakeProcessManager processManager =
        FakeProcessManager.list(<FakeCommand>[
      FakeCommand(
        command: <String>[
          artifacts.getArtifactPath(Artifact.engineDartBinary),
          'run',
          'my_copy_transformer',
          '--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
          '--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
          '-f',
          '--my_option',
          'my_option_value',
        ],
        onRun: (List<String> args) {
          final ArgResults parsedArgs = (ArgParser()
            ..addOption('input')
            ..addOption('output')
            ..addFlag('foo', abbr: 'f')
            ..addOption('my_option'))
            .parse(args);

          fileSystem.file(parsedArgs['input']).copySync(parsedArgs['output'] as String);
        },
      ),
    ]);

    final AssetTransformer transformer = AssetTransformer(
      processManager: processManager,
      fileSystem: fileSystem,
      dartBinaryPath: artifacts.getArtifactPath(Artifact.engineDartBinary),
    );

    final AssetTransformationFailure? transformationFailure = await transformer.transformAsset(
      asset: asset,
      outputPath: outputPath,
      workingDirectory: fileSystem.currentDirectory.path,
      transformerEntries: <AssetTransformerEntry>[
        const AssetTransformerEntry(
          package: 'my_copy_transformer',
          args: <String>[
            '-f',
            '--my_option',
            'my_option_value',
          ],
        )
      ],
    );

    expect(transformationFailure, isNull, reason: logger.errorText);
    expect(processManager, hasNoRemainingExpectations);
    expect(fileSystem.file(outputPath).readAsStringSync(), 'hello world');
    expect(fileSystem.directory('.tmp_rand0').listSync(), isEmpty, reason: 'Transformer did not clean up after itself.');
  });

  testWithoutContext('logs useful error information when transformation process returns a nonzero exit code', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Artifacts artifacts = Artifacts.test();

    final File asset = fileSystem.file('asset.txt')..createSync();
    const String outputPath = 'output.txt';

    final String dartBinaryPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
    final FakeProcessManager processManager =
        FakeProcessManager.list(<FakeCommand>[
      FakeCommand(
        command: <String>[
          dartBinaryPath,
          'run',
          'my_copy_transformer',
          '--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
          '--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
        ],
        onRun: (List<String> args) {
          final ArgResults parsedArgs = (ArgParser()
            ..addOption('input')
            ..addOption('output'))
            .parse(args);
          fileSystem.file(parsedArgs['input']).copySync(parsedArgs['output'] as String);
        },
        exitCode: 1,
        stdout: 'Beginning transformation',
        stderr: 'Something went wrong',
      ),
    ]);

    final AssetTransformer transformer = AssetTransformer(
      processManager: processManager,
      fileSystem: fileSystem,
      dartBinaryPath: dartBinaryPath,
    );

    final AssetTransformationFailure? failure = await transformer.transformAsset(
      asset: asset,
      outputPath: outputPath,
      workingDirectory: fileSystem.currentDirectory.path,
      transformerEntries: <AssetTransformerEntry>[
        const AssetTransformerEntry(
          package: 'my_copy_transformer',
          args: <String>[],
        )
      ],
    );

    expect(asset, exists);
    expect(processManager, hasNoRemainingExpectations);
    expect(failure, isNotNull);
    expect(failure!.message,
'''
User-defined transformation of asset "asset.txt" failed.
Transformer process terminated with non-zero exit code: 1
Transformer package: my_copy_transformer
Full command: $dartBinaryPath run my_copy_transformer --input=/.tmp_rand0/asset.txt-transformOutput0.txt --output=/.tmp_rand0/asset.txt-transformOutput1.txt
stdout:
Beginning transformation
stderr:
Something went wrong''');
    expect(fileSystem.directory('.tmp_rand0').listSync(), isEmpty, reason: 'Transformer did not clean up after itself.');
  });

  testWithoutContext('prints error message when the transformer does not produce an output file', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Artifacts artifacts = Artifacts.test();

    final File asset = fileSystem.file('asset.txt')..createSync();
    const String outputPath = 'output.txt';

    final String dartBinaryPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
    final FakeProcessManager processManager =
        FakeProcessManager.list(<FakeCommand>[
      FakeCommand(
        command: <String>[
          dartBinaryPath,
          'run',
          'my_transformer',
          '--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
          '--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
        ],
        onRun: (_) {
          // Do nothing.
        },
        stderr: 'Transformation failed, but I forgot to exit with a non-zero code.'
      ),
    ]);

    final AssetTransformer transformer = AssetTransformer(
      processManager: processManager,
      fileSystem: fileSystem,
      dartBinaryPath: dartBinaryPath,
    );

    final AssetTransformationFailure? failure = await transformer.transformAsset(
      asset: asset,
      outputPath: outputPath,
      workingDirectory: fileSystem.currentDirectory.path,
      transformerEntries: <AssetTransformerEntry>[
        const AssetTransformerEntry(
          package: 'my_transformer',
          args: <String>[],
        )
      ],
    );

    expect(processManager, hasNoRemainingExpectations);
    expect(failure, isNotNull);
    expect(failure!.message,
'''
User-defined transformation of asset "asset.txt" failed.
Asset transformer my_transformer did not produce an output file.
Input file provided to transformer: "/.tmp_rand0/asset.txt-transformOutput0.txt"
Expected output file at: "/.tmp_rand0/asset.txt-transformOutput1.txt"
Full command: $dartBinaryPath run my_transformer --input=/.tmp_rand0/asset.txt-transformOutput0.txt --output=/.tmp_rand0/asset.txt-transformOutput1.txt
stdout:

stderr:
Transformation failed, but I forgot to exit with a non-zero code.'''
    );
    expect(fileSystem.directory('.tmp_rand0').listSync(), isEmpty, reason: 'Transformer did not clean up after itself.');
  });

  testWithoutContext('correctly chains transformations when there are multiple of them', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Artifacts artifacts = Artifacts.test();

    final File asset = fileSystem.file('asset.txt')
      ..createSync()
      ..writeAsStringSync('ABC');
    const String outputPath = 'output.txt';

    final String dartBinaryPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      FakeCommand(
        command: <String>[
          dartBinaryPath,
          'run',
          'my_lowercase_transformer',
          '--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
          '--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
        ],
        onRun: (List<String> args) {
          final ArgResults parsedArgs = (ArgParser()
              ..addOption('input')
              ..addOption('output'))
            .parse(args);

          final String inputFileContents = fileSystem.file(parsedArgs['input']).readAsStringSync();
          fileSystem.file(parsedArgs['output'])
            ..createSync()
            ..writeAsStringSync(inputFileContents.toLowerCase());
        },
      ),
      FakeCommand(
        command: <String>[
          dartBinaryPath,
          'run',
          'my_distance_from_ascii_a_transformer',
          '--input=/.tmp_rand0/asset.txt-transformOutput1.txt',
          '--output=/.tmp_rand0/asset.txt-transformOutput2.txt',
        ],
        onRun: (List<String> args) {
          final ArgResults parsedArgs = (ArgParser()
              ..addOption('input')
              ..addOption('output'))
            .parse(args);

          final String inputFileContents = fileSystem.file(parsedArgs['input']).readAsStringSync();
          final StringBuffer outputContents = StringBuffer();

          for (int i = 0; i < inputFileContents.length; i++) {
            outputContents.write(inputFileContents.codeUnitAt(i) - 'a'.codeUnits.first);
          }

          fileSystem.file(parsedArgs['output'])
            ..createSync()
            ..writeAsStringSync(outputContents.toString());
        },
      ),
    ]);

    final AssetTransformer transformer = AssetTransformer(
      processManager: processManager,
      fileSystem: fileSystem,
      dartBinaryPath: dartBinaryPath,
    );

    final AssetTransformationFailure? failure = await transformer.transformAsset(
      asset: asset,
      outputPath: outputPath,
      workingDirectory: fileSystem.currentDirectory.path,
      transformerEntries: <AssetTransformerEntry>[
        const AssetTransformerEntry(
          package: 'my_lowercase_transformer',
          args: <String>[],
        ),
        const AssetTransformerEntry(
          package: 'my_distance_from_ascii_a_transformer',
          args: <String>[],
        ),
      ],
    );

    expect(processManager, hasNoRemainingExpectations);
    expect(failure, isNull);
    expect(fileSystem.file(outputPath).readAsStringSync(), '012');
    expect(fileSystem.directory('.tmp_rand0').listSync(), isEmpty, reason: 'Transformer did not clean up after itself.');
  });

  testWithoutContext('prints an error when a transformer in a chain (thats not the first) does not produce an output', () async {
    final FileSystem fileSystem = MemoryFileSystem();
    final Artifacts artifacts = Artifacts.test();

    final File asset = fileSystem.file('asset.txt')
      ..createSync()
      ..writeAsStringSync('ABC');
    const String outputPath = 'output.txt';

    final String dartBinaryPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      FakeCommand(
        command: <String>[
          dartBinaryPath,
          'run',
          'my_lowercase_transformer',
          '--input=/.tmp_rand0/asset.txt-transformOutput0.txt',
          '--output=/.tmp_rand0/asset.txt-transformOutput1.txt',
        ],
        onRun: (List<String> args) {
          final ArgResults parsedArgs = (ArgParser()
              ..addOption('input')
              ..addOption('output'))
            .parse(args);

          final String inputFileContents = fileSystem.file(parsedArgs['input']).readAsStringSync();
          fileSystem.file(parsedArgs['output'])
            ..createSync()
            ..writeAsStringSync(inputFileContents.toLowerCase());
        },
      ),
      FakeCommand(
        command: <String>[
          dartBinaryPath,
          'run',
          'my_distance_from_ascii_a_transformer',
          '--input=/.tmp_rand0/asset.txt-transformOutput1.txt',
          '--output=/.tmp_rand0/asset.txt-transformOutput2.txt',
        ],
        onRun: (List<String> args) {
          // Do nothing.
        },
        stderr: 'Transformation failed, but I forgot to exit with a non-zero code.'
      ),
    ]);

    final AssetTransformer transformer = AssetTransformer(
      processManager: processManager,
      fileSystem: fileSystem,
      dartBinaryPath: dartBinaryPath,
    );

    final AssetTransformationFailure? failure = await transformer.transformAsset(
      asset: asset,
      outputPath: outputPath,
      workingDirectory: fileSystem.currentDirectory.path,
      transformerEntries: <AssetTransformerEntry>[
        const AssetTransformerEntry(
          package: 'my_lowercase_transformer',
          args: <String>[],
        ),
        const AssetTransformerEntry(
          package: 'my_distance_from_ascii_a_transformer',
          args: <String>[],
        ),
      ],
    );

    expect(failure, isNotNull);
    expect(failure!.message,
'''
User-defined transformation of asset "asset.txt" failed.
Asset transformer my_distance_from_ascii_a_transformer did not produce an output file.
Input file provided to transformer: "/.tmp_rand0/asset.txt-transformOutput1.txt"
Expected output file at: "/.tmp_rand0/asset.txt-transformOutput2.txt"
Full command: Artifact.engineDartBinary run my_distance_from_ascii_a_transformer --input=/.tmp_rand0/asset.txt-transformOutput1.txt --output=/.tmp_rand0/asset.txt-transformOutput2.txt
stdout:

stderr:
Transformation failed, but I forgot to exit with a non-zero code.'''
    );
    expect(processManager, hasNoRemainingExpectations);
    expect(fileSystem.file(outputPath), isNot(exists));
    expect(fileSystem.directory('.tmp_rand0').listSync(), isEmpty, reason: 'Transformer did not clean up after itself.');
  });
}
