// 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/command_runner.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/build_system/build_system.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/assemble.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/globals.dart' as globals;

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';
import '../../src/test_build_system.dart';
import '../../src/test_flutter_command_runner.dart';

void main() {
  Cache.disableLocking();
  Cache.flutterRoot = '';
  final StackTrace stackTrace = StackTrace.current;

  testUsingContext('flutter assemble can run a build', () async {
    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(
      buildSystem: TestBuildSystem.all(BuildResult(success: true)),
    ));
    await commandRunner.run(<String>['assemble', '-o Output', 'debug_macos_bundle_flutter_assets']);

    expect(testLogger.traceText, contains('build succeeded.'));
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('flutter assemble can parse defines whose values contain =', () async {
    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(
      buildSystem: TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) {
        expect(environment.defines, containsPair('FooBar', 'fizz=2'));
      })
    ));
    await commandRunner.run(<String>['assemble', '-o Output', '-dFooBar=fizz=2', 'debug_macos_bundle_flutter_assets']);

    expect(testLogger.traceText, contains('build succeeded.'));
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('flutter assemble can parse inputs', () async {
    final AssembleCommand command = AssembleCommand(
      buildSystem: TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) {
        expect(environment.inputs, containsPair('Foo', 'Bar.txt'));
    }));
    final CommandRunner<void> commandRunner = createTestCommandRunner(command);
    await commandRunner.run(<String>['assemble', '-o Output', '-iFoo=Bar.txt', 'debug_macos_bundle_flutter_assets']);

    expect(testLogger.traceText, contains('build succeeded.'));
    expect(await command.requiredArtifacts, isEmpty);
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('flutter assemble sets required artifacts from target platform', () async {
    final AssembleCommand command = AssembleCommand(
        buildSystem: TestBuildSystem.all(BuildResult(success: true)));
    final CommandRunner<void> commandRunner = createTestCommandRunner(command);
    await commandRunner.run(<String>['assemble', '-o Output', '-dTargetPlatform=darwin', '-dDarwinArchs=x86_64', 'debug_macos_bundle_flutter_assets']);

    expect(await command.requiredArtifacts, <DevelopmentArtifact>{
      DevelopmentArtifact.macOS,
    });
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
  });

  testUsingContext('flutter assemble throws ToolExit if not provided with output', () async {
    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(
      buildSystem: TestBuildSystem.all(BuildResult(success: true)),
    ));

    expect(commandRunner.run(<String>['assemble', 'debug_macos_bundle_flutter_assets']), throwsToolExit());
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('flutter assemble throws ToolExit if dart-defines are not base64 encoded', () async {
    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(
      buildSystem: TestBuildSystem.all(BuildResult(success: true)),
    ));

    final List<String> command = <String>[
      'assemble',
      '--output',
      'Output',
      '--DartDefines=flutter.inspector.structuredErrors%3Dtrue',
      'debug_macos_bundle_flutter_assets',
    ];
    expect(
      commandRunner.run(command),
      throwsToolExit(message: 'Error parsing assemble command: your generated configuration may be out of date')
    );
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('flutter assemble throws ToolExit if called with non-existent rule', () async {
    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(
      buildSystem: TestBuildSystem.all(BuildResult(success: true)),
    ));

    expect(commandRunner.run(<String>['assemble', '-o Output', 'undefined']),
      throwsToolExit());
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('flutter assemble does not log stack traces during build failure', () async {
    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(
      buildSystem: TestBuildSystem.all(BuildResult(success: false, exceptions: <String, ExceptionMeasurement>{
        'hello': ExceptionMeasurement('hello', 'bar', stackTrace, fatal: true),
      }))
    ));

    await expectLater(commandRunner.run(<String>['assemble', '-o Output', 'debug_macos_bundle_flutter_assets']),
      throwsToolExit());
    expect(testLogger.errorText, contains('Target hello failed: bar'));
    expect(testLogger.errorText, isNot(contains(stackTrace.toString())));
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('flutter assemble outputs JSON performance data to provided file', () async {
    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(
      buildSystem: TestBuildSystem.all(
        BuildResult(success: true, performance: <String, PerformanceMeasurement>{
          'hello': PerformanceMeasurement(
            target: 'hello',
            analyticsName: 'bar',
            elapsedMilliseconds: 123,
            skipped: false,
            succeeded: true,
          ),
        }),
      ),
    ));

    await commandRunner.run(<String>[
      'assemble',
      '-o Output',
      '--performance-measurement-file=out.json',
      'debug_macos_bundle_flutter_assets',
    ]);

    expect(globals.fs.file('out.json'), exists);
    expect(
      json.decode(globals.fs.file('out.json').readAsStringSync()),
      containsPair('targets', contains(
        containsPair('name', 'bar'),
      )),
    );
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('flutter assemble does not inject engine revision with local-engine', () async {
    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(
      buildSystem: TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) {
        expect(environment.engineVersion, isNull);
      })
    ));
    await commandRunner.run(<String>['assemble', '-o Output', 'debug_macos_bundle_flutter_assets']);
  }, overrides: <Type, Generator>{
    Artifacts: () => Artifacts.test(localEngine: 'out/host_release'),
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('flutter assemble only writes input and output files when the values change', () async {
    final BuildSystem buildSystem = TestBuildSystem.list(<BuildResult>[
      BuildResult(
        success: true,
        inputFiles: <File>[globals.fs.file('foo')..createSync()],
        outputFiles: <File>[globals.fs.file('bar')..createSync()],
      ),
      BuildResult(
        success: true,
        inputFiles: <File>[globals.fs.file('foo')..createSync()],
        outputFiles: <File>[globals.fs.file('bar')..createSync()],
      ),
      BuildResult(
        success: true,
        inputFiles: <File>[globals.fs.file('foo'), globals.fs.file('fizz')..createSync()],
        outputFiles: <File>[globals.fs.file('bar'), globals.fs.file(globals.fs.path.join('.dart_tool', 'fizz2'))..createSync(recursive: true)],
      ),
    ]);
    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(buildSystem: buildSystem));
    await commandRunner.run(<String>[
      'assemble',
      '-o Output',
      '--build-outputs=outputs',
      '--build-inputs=inputs',
      'debug_macos_bundle_flutter_assets',
    ]);

    final File inputs = globals.fs.file('inputs');
    final File outputs = globals.fs.file('outputs');
    expect(inputs.readAsStringSync(), contains('foo'));
    expect(outputs.readAsStringSync(), contains('bar'));

    final DateTime theDistantPast = DateTime(1991, 8, 23);
    inputs.setLastModifiedSync(theDistantPast);
    outputs.setLastModifiedSync(theDistantPast);
    await commandRunner.run(<String>[
      'assemble',
      '-o Output',
      '--build-outputs=outputs',
      '--build-inputs=inputs',
      'debug_macos_bundle_flutter_assets',
    ]);

    expect(inputs.lastModifiedSync(), theDistantPast);
    expect(outputs.lastModifiedSync(), theDistantPast);

    await commandRunner.run(<String>[
      'assemble',
      '-o Output',
      '--build-outputs=outputs',
      '--build-inputs=inputs',
      'debug_macos_bundle_flutter_assets',
    ]);

    expect(inputs.readAsStringSync(), contains('foo'));
    expect(inputs.readAsStringSync(), contains('fizz'));
    expect(inputs.lastModifiedSync(), isNot(theDistantPast));
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testWithoutContext('writePerformanceData outputs performance data in JSON form', () {
    final List<PerformanceMeasurement> performanceMeasurement = <PerformanceMeasurement>[
      PerformanceMeasurement(
        analyticsName: 'foo',
        target: 'hidden',
        skipped: false,
        succeeded: true,
        elapsedMilliseconds: 123,
      ),
    ];
    final FileSystem fileSystem = MemoryFileSystem.test();
    final File outFile = fileSystem.currentDirectory
      .childDirectory('foo')
      .childFile('out.json');

    writePerformanceData(performanceMeasurement, outFile);

    expect(outFile, exists);
    expect(json.decode(outFile.readAsStringSync()), <String, Object>{
      'targets': <Object>[
        <String, Object>{
          'name': 'foo',
          'skipped': false,
          'succeeded': true,
          'elapsedMilliseconds': 123,
        },
      ],
    });
  });

  testUsingContext('test --dart-define-from-file option with err file format', () {
    globals.fs.directory('config').createSync();
    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(
      buildSystem: TestBuildSystem.all(BuildResult(success: true)),
    ));

    expect(commandRunner.run(<String>['assemble',
      '-o Output',
      'debug_macos_bundle_flutter_assets',
      '--dart-define=k=v',
      '--dart-define-from-file=config']),
        throwsToolExit(message: 'Json config define file "--dart-define-from-file=config" is not a file, please fix first!'));
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('test --dart-define-from-file option with err json format', () async {
    await globals.fs.file('config.json').writeAsString(
        '''
          {
            "kInt": 1,
            "kDouble": 1.1,
            "name": "err json format,
            "title": "this is title from config json file"
          }
        '''
    );
    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(
      buildSystem: TestBuildSystem.all(BuildResult(success: true)),
    ));

    expect(commandRunner.run(<String>['assemble',
      '-o Output',
      'debug_macos_bundle_flutter_assets',
      '--dart-define=k=v',
      '--dart-define-from-file=config.json']),
        throwsToolExit(message: 'Json config define file "--dart-define-from-file=config.json" format err, please fix first! format err:'));
  }, overrides: <Type, Generator>{
    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
    FileSystem: () => MemoryFileSystem.test(),
    ProcessManager: () => FakeProcessManager.any(),
  });
}
