// 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:file/memory.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/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/exceptions.dart';
import 'package:flutter_tools/src/build_system/targets/common.dart';
import 'package:flutter_tools/src/build_system/targets/ios.dart';
import 'package:flutter_tools/src/compile.dart';

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

const String kBoundaryKey = '4d2d9609-c662-4571-afde-31410f96caa6';
const String kElfAot = '--snapshot_kind=app-aot-elf';
const String kAssemblyAot = '--snapshot_kind=app-aot-assembly';

final Platform macPlatform = FakePlatform(operatingSystem: 'macos', environment: <String, String>{});
void main() {
  late FakeProcessManager processManager;
  late Environment androidEnvironment;
  late Environment iosEnvironment;
  late Artifacts artifacts;
  late FileSystem fileSystem;
  late Logger logger;

  setUp(() {
    processManager = FakeProcessManager.empty();
    logger = BufferLogger.test();
    artifacts = Artifacts.test();
    fileSystem = MemoryFileSystem.test();
    androidEnvironment = Environment.test(
      fileSystem.currentDirectory,
      defines: <String, String>{
        kBuildMode: getNameForBuildMode(BuildMode.profile),
        kTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
      },
      inputs: <String, String>{},
      artifacts: artifacts,
      processManager: processManager,
      fileSystem: fileSystem,
      logger: logger,
    );
    androidEnvironment.buildDir.createSync(recursive: true);
    iosEnvironment = Environment.test(
      fileSystem.currentDirectory,
      defines: <String, String>{
        kBuildMode: getNameForBuildMode(BuildMode.profile),
        kTargetPlatform: getNameForTargetPlatform(TargetPlatform.ios),
      },
      inputs: <String, String>{},
      artifacts: artifacts,
      processManager: processManager,
      fileSystem: fileSystem,
      logger: logger,
    );
    iosEnvironment.buildDir.createSync(recursive: true);
  });

  testWithoutContext('KernelSnapshot throws error if missing build mode', () async {
    androidEnvironment.defines.remove(kBuildMode);
    expect(
      const KernelSnapshot().build(androidEnvironment),
      throwsA(isA<MissingDefineException>()));
  });

  testWithoutContext('KernelSnapshot handles null result from kernel compilation', () async {
    fileSystem.file('.dart_tool/package_config.json')
      ..createSync(recursive: true)
      ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
    final String build = androidEnvironment.buildDir.path;
    final String flutterPatchedSdkPath = artifacts.getArtifactPath(
      Artifact.flutterPatchedSdkPath,
      platform: TargetPlatform.android_arm,
      mode: BuildMode.profile,
    );
    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        artifacts.getHostArtifact(HostArtifact.engineDartBinary).path,
        '--disable-dart-dev',
        artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk),
        '--sdk-root',
        '$flutterPatchedSdkPath/',
        '--target=flutter',
        '--no-print-incremental-dependencies',
        ...buildModeOptions(BuildMode.profile, <String>[]),
        '--track-widget-creation',
        '--aot',
        '--tfa',
        '--packages',
        '/.dart_tool/package_config.json',
        '--output-dill',
        '$build/app.dill',
        '--depfile',
        '$build/kernel_snapshot.d',
        '--verbosity=error',
        'file:///lib/main.dart',
      ], exitCode: 1),
    ]);

    await expectLater(() => const KernelSnapshot().build(androidEnvironment), throwsException);
    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext('KernelSnapshot does use track widget creation on profile builds', () async {
    fileSystem.file('.dart_tool/package_config.json')
      ..createSync(recursive: true)
      ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
    final String build = androidEnvironment.buildDir.path;
    final String flutterPatchedSdkPath = artifacts.getArtifactPath(
      Artifact.flutterPatchedSdkPath,
      platform: TargetPlatform.android_arm,
      mode: BuildMode.profile,
    );
    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        artifacts.getHostArtifact(HostArtifact.engineDartBinary).path,
        '--disable-dart-dev',
        artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk),
        '--sdk-root',
        '$flutterPatchedSdkPath/',
        '--target=flutter',
        '--no-print-incremental-dependencies',
        ...buildModeOptions(BuildMode.profile, <String>[]),
        '--track-widget-creation',
        '--aot',
        '--tfa',
        '--packages',
        '/.dart_tool/package_config.json',
        '--output-dill',
        '$build/app.dill',
        '--depfile',
        '$build/kernel_snapshot.d',
        '--verbosity=error',
        'file:///lib/main.dart',
      ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
    ]);

    await const KernelSnapshot().build(androidEnvironment);

    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext('KernelSnapshot correctly handles an empty string in ExtraFrontEndOptions', () async {
    fileSystem.file('.dart_tool/package_config.json')
      ..createSync(recursive: true)
      ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
    final String build = androidEnvironment.buildDir.path;
    final String flutterPatchedSdkPath = artifacts.getArtifactPath(
      Artifact.flutterPatchedSdkPath,
      platform: TargetPlatform.android_arm,
      mode: BuildMode.profile,
    );
    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        artifacts.getHostArtifact(HostArtifact.engineDartBinary).path,
        '--disable-dart-dev',
        artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk),
        '--sdk-root',
        '$flutterPatchedSdkPath/',
        '--target=flutter',
        '--no-print-incremental-dependencies',
        ...buildModeOptions(BuildMode.profile, <String>[]),
        '--track-widget-creation',
        '--aot',
        '--tfa',
        '--packages',
        '/.dart_tool/package_config.json',
        '--output-dill',
        '$build/app.dill',
        '--depfile',
        '$build/kernel_snapshot.d',
        '--verbosity=error',
        'file:///lib/main.dart',
      ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
    ]);

    await const KernelSnapshot()
      .build(androidEnvironment..defines[kExtraFrontEndOptions] = '');

    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext('KernelSnapshot correctly forwards ExtraFrontEndOptions', () async {
    fileSystem.file('.dart_tool/package_config.json')
      ..createSync(recursive: true)
      ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
    final String build = androidEnvironment.buildDir.path;
    final String flutterPatchedSdkPath = artifacts.getArtifactPath(
      Artifact.flutterPatchedSdkPath,
      platform: TargetPlatform.android_arm,
      mode: BuildMode.profile,
    );
    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        artifacts.getHostArtifact(HostArtifact.engineDartBinary).path,
        '--disable-dart-dev',
        artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk),
        '--sdk-root',
        '$flutterPatchedSdkPath/',
        '--target=flutter',
        '--no-print-incremental-dependencies',
        ...buildModeOptions(BuildMode.profile, <String>[]),
        '--track-widget-creation',
        '--aot',
        '--tfa',
        '--packages',
        '/.dart_tool/package_config.json',
        '--output-dill',
        '$build/app.dill',
        '--depfile',
        '$build/kernel_snapshot.d',
        '--verbosity=error',
        'foo',
        'bar',
        'file:///lib/main.dart',
      ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
    ]);

    await const KernelSnapshot()
      .build(androidEnvironment..defines[kExtraFrontEndOptions] = 'foo,bar');

    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext('KernelSnapshot can disable track-widget-creation on debug builds', () async {
    fileSystem.file('.dart_tool/package_config.json')
      ..createSync(recursive: true)
      ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
    final String build = androidEnvironment.buildDir.path;
    final String flutterPatchedSdkPath = artifacts.getArtifactPath(
      Artifact.flutterPatchedSdkPath,
      platform: TargetPlatform.android_arm,
      mode: BuildMode.debug,
    );
    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        artifacts.getHostArtifact(HostArtifact.engineDartBinary).path,
        '--disable-dart-dev',
        artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk),
        '--sdk-root',
        '$flutterPatchedSdkPath/',
        '--target=flutter',
        '--no-print-incremental-dependencies',
        ...buildModeOptions(BuildMode.debug, <String>[]),
        '--no-link-platform',
        '--packages',
        '/.dart_tool/package_config.json',
        '--output-dill',
        '$build/app.dill',
        '--depfile',
        '$build/kernel_snapshot.d',
        '--incremental',
        '--initialize-from-dill',
        '$build/app.dill',
        '--verbosity=error',
        'file:///lib/main.dart',
      ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
    ]);

    await const KernelSnapshot().build(androidEnvironment
      ..defines[kBuildMode] = getNameForBuildMode(BuildMode.debug)
      ..defines[kTrackWidgetCreation] = 'false');

    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext('KernelSnapshot forces platform linking on debug for darwin target platforms', () async {
    fileSystem.file('.dart_tool/package_config.json')
      ..createSync(recursive: true)
      ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
    final String build = androidEnvironment.buildDir.path;
    final String flutterPatchedSdkPath = artifacts.getArtifactPath(
      Artifact.flutterPatchedSdkPath,
      platform: TargetPlatform.darwin,
      mode: BuildMode.debug,
    );
    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        artifacts.getHostArtifact(HostArtifact.engineDartBinary).path,
        '--disable-dart-dev',
        artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk),
        '--sdk-root',
        '$flutterPatchedSdkPath/',
        '--target=flutter',
        '--no-print-incremental-dependencies',
        ...buildModeOptions(BuildMode.debug, <String>[]),
        '--packages',
        '/.dart_tool/package_config.json',
        '--output-dill',
        '$build/app.dill',
        '--depfile',
        '$build/kernel_snapshot.d',
        '--incremental',
        '--initialize-from-dill',
        '$build/app.dill',
        '--verbosity=error',
        'file:///lib/main.dart',
      ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n'),
    ]);

    await const KernelSnapshot().build(androidEnvironment
      ..defines[kTargetPlatform]  = getNameForTargetPlatform(TargetPlatform.darwin)
      ..defines[kBuildMode] = getNameForBuildMode(BuildMode.debug)
      ..defines[kTrackWidgetCreation] = 'false'
    );

    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext('KernelSnapshot does use track widget creation on debug builds', () async {
    fileSystem.file('.dart_tool/package_config.json')
      ..createSync(recursive: true)
      ..writeAsStringSync('{"configVersion": 2, "packages":[]}');
    final Environment testEnvironment = Environment.test(
      fileSystem.currentDirectory,
      defines: <String, String>{
        kBuildMode: getNameForBuildMode(BuildMode.debug),
        kTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
      },
      processManager: processManager,
      artifacts: artifacts,
      fileSystem: fileSystem,
      logger: logger,
    );
    final String build = testEnvironment.buildDir.path;
    final String flutterPatchedSdkPath = artifacts.getArtifactPath(
      Artifact.flutterPatchedSdkPath,
      platform: TargetPlatform.android_arm,
      mode: BuildMode.debug,
    );
    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        artifacts.getHostArtifact(HostArtifact.engineDartBinary).path,
        '--disable-dart-dev',
        artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk),
        '--sdk-root',
        '$flutterPatchedSdkPath/',
        '--target=flutter',
        '--no-print-incremental-dependencies',
        ...buildModeOptions(BuildMode.debug, <String>[]),
        '--track-widget-creation',
        '--no-link-platform',
        '--packages',
        '/.dart_tool/package_config.json',
        '--output-dill',
        '$build/app.dill',
        '--depfile',
        '$build/kernel_snapshot.d',
        '--incremental',
        '--initialize-from-dill',
        '$build/app.dill',
        '--verbosity=error',
        'file:///lib/main.dart',
      ], stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey /build/653e11a8e6908714056a57cd6b4f602a/app.dill 0\n'),
    ]);

    await const KernelSnapshot().build(testEnvironment);

    expect(processManager, hasNoRemainingExpectations);
  });

  testUsingContext('AotElfProfile Produces correct output directory', () async {
    final String build = androidEnvironment.buildDir.path;
    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        artifacts.getArtifactPath(
          Artifact.genSnapshot,
          platform: TargetPlatform.android_arm,
          mode: BuildMode.profile,
        ),
        '--deterministic',
        kElfAot,
        '--elf=$build/app.so',
        '--strip',
        '--no-sim-use-hardfp',
        '--no-use-integer-division',
        '$build/app.dill',
      ]),
    ]);
    androidEnvironment.buildDir.childFile('app.dill').createSync(recursive: true);

    await const AotElfProfile(TargetPlatform.android_arm).build(androidEnvironment);

    expect(processManager, hasNoRemainingExpectations);
  });

  testUsingContext('AotElfRelease configures gen_snapshot with code size directory', () async {
    androidEnvironment.defines[kCodeSizeDirectory] = 'code_size_1';
    final String build = androidEnvironment.buildDir.path;
    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        artifacts.getArtifactPath(
          Artifact.genSnapshot,
          platform: TargetPlatform.android_arm,
          mode: BuildMode.profile,
        ),
        '--deterministic',
        '--write-v8-snapshot-profile-to=code_size_1/snapshot.android-arm.json',
        '--trace-precompiler-to=code_size_1/trace.android-arm.json',
        kElfAot,
        '--elf=$build/app.so',
        '--strip',
        '--no-sim-use-hardfp',
        '--no-use-integer-division',
        '$build/app.dill',
      ]),
    ]);
    androidEnvironment.buildDir.childFile('app.dill').createSync(recursive: true);

    await const AotElfRelease(TargetPlatform.android_arm).build(androidEnvironment);

    expect(processManager, hasNoRemainingExpectations);
  });

  testUsingContext('AotElfProfile throws error if missing build mode', () async {
    androidEnvironment.defines.remove(kBuildMode);

    expect(const AotElfProfile(TargetPlatform.android_arm).build(androidEnvironment),
      throwsA(isA<MissingDefineException>()));
  });

  testUsingContext('AotElfProfile throws error if missing target platform', () async {
    androidEnvironment.defines.remove(kTargetPlatform);

    expect(const AotElfProfile(TargetPlatform.android_arm).build(androidEnvironment),
      throwsA(isA<MissingDefineException>()));
  });

  testUsingContext('AotAssemblyProfile throws error if missing build mode', () async {
    iosEnvironment.defines.remove(kBuildMode);

    expect(const AotAssemblyProfile().build(iosEnvironment),
      throwsA(isA<MissingDefineException>()));
  }, overrides: <Type, Generator>{
    Platform: () => macPlatform,
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });


  testUsingContext('AotAssemblyProfile throws error if missing target platform', () async {
    iosEnvironment.defines.remove(kTargetPlatform);

    expect(const AotAssemblyProfile().build(iosEnvironment),
      throwsA(isA<MissingDefineException>()));
  }, overrides: <Type, Generator>{
    Platform: () => macPlatform,
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });

  testUsingContext('AotAssemblyProfile throws error if built for non-iOS platform', () async {
    expect(const AotAssemblyProfile().build(androidEnvironment), throwsException);
  }, overrides: <Type, Generator>{
    Platform: () => macPlatform,
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });

  testUsingContext('AotAssemblyProfile with bitcode sends correct argument to snapshotter', () async {
    iosEnvironment.defines[kIosArchs] = 'arm64';
    iosEnvironment.defines[kBitcodeFlag] = 'true';
    iosEnvironment.defines[kSdkRoot] = 'path/to/iPhoneOS.sdk';
    final String build = iosEnvironment.buildDir.path;
    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        // This path is not known by the cache due to the iOS gen_snapshot split.
        'Artifact.genSnapshot.TargetPlatform.ios.profile_arm64',
        '--deterministic',
        kAssemblyAot,
        '--assembly=$build/arm64/snapshot_assembly.S',
        '--strip',
        '$build/app.dill',
      ]),
      FakeCommand(command: <String>[
        'xcrun',
        'cc',
        '-arch',
        'arm64',
        '-miphoneos-version-min=11.0',
        '-isysroot',
        'path/to/iPhoneOS.sdk',
        // Contains bitcode flag.
        '-fembed-bitcode',
        '-c',
        '$build/arm64/snapshot_assembly.S',
        '-o',
        '$build/arm64/snapshot_assembly.o',
      ]),
      FakeCommand(command: <String>[
        'xcrun',
        'clang',
        '-arch',
        'arm64',
        '-miphoneos-version-min=11.0',
        '-isysroot',
        'path/to/iPhoneOS.sdk',
        '-dynamiclib',
        '-Xlinker',
        '-rpath',
        '-Xlinker',
        '@executable_path/Frameworks',
        '-Xlinker',
        '-rpath',
        '-Xlinker',
        '@loader_path/Frameworks',
        '-install_name',
        '@rpath/App.framework/App',
        // Contains bitcode flag.
        '-fembed-bitcode',
        '-o',
        '$build/arm64/App.framework/App',
        '$build/arm64/snapshot_assembly.o',
      ]),
      FakeCommand(command: <String>[
        'lipo',
        '$build/arm64/App.framework/App',
        '-create',
        '-output',
        '$build/App.framework/App',
      ]),
    ]);

    await const AotAssemblyProfile().build(iosEnvironment);

    expect(processManager, hasNoRemainingExpectations);
  }, overrides: <Type, Generator>{
    Platform: () => macPlatform,
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });

  testUsingContext('AotAssemblyRelease configures gen_snapshot with code size directory', () async {
    iosEnvironment.defines[kCodeSizeDirectory] = 'code_size_1';
    iosEnvironment.defines[kIosArchs] = 'arm64';
    iosEnvironment.defines[kSdkRoot] = 'path/to/iPhoneOS.sdk';
    iosEnvironment.defines[kBitcodeFlag] = 'true';
    final String build = iosEnvironment.buildDir.path;
    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        // This path is not known by the cache due to the iOS gen_snapshot split.
        'Artifact.genSnapshot.TargetPlatform.ios.profile_arm64',
        '--deterministic',
        '--write-v8-snapshot-profile-to=code_size_1/snapshot.arm64.json',
        '--trace-precompiler-to=code_size_1/trace.arm64.json',
        kAssemblyAot,
        '--assembly=$build/arm64/snapshot_assembly.S',
        '--strip',
        '$build/app.dill',
      ]),
      FakeCommand(command: <String>[
        'xcrun',
        'cc',
        '-arch',
        'arm64',
        '-miphoneos-version-min=11.0',
        '-isysroot',
        'path/to/iPhoneOS.sdk',
        // Contains bitcode flag.
        '-fembed-bitcode',
        '-c',
        '$build/arm64/snapshot_assembly.S',
        '-o',
        '$build/arm64/snapshot_assembly.o',
      ]),
      FakeCommand(command: <String>[
        'xcrun',
        'clang',
        '-arch',
        'arm64',
        '-miphoneos-version-min=11.0',
        '-isysroot',
        'path/to/iPhoneOS.sdk',
        '-dynamiclib',
        '-Xlinker',
        '-rpath',
        '-Xlinker',
        '@executable_path/Frameworks',
        '-Xlinker',
        '-rpath',
        '-Xlinker',
        '@loader_path/Frameworks',
        '-install_name',
        '@rpath/App.framework/App',
        // Contains bitcode flag.
        '-fembed-bitcode',
        '-o',
        '$build/arm64/App.framework/App',
        '$build/arm64/snapshot_assembly.o',
      ]),
      FakeCommand(command: <String>[
        'lipo',
        '$build/arm64/App.framework/App',
        '-create',
        '-output',
        '$build/App.framework/App',
      ]),
    ]);

    await const AotAssemblyProfile().build(iosEnvironment);

    expect(processManager, hasNoRemainingExpectations);
  }, overrides: <Type, Generator>{
    Platform: () => macPlatform,
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
  });

  testUsingContext('kExtraGenSnapshotOptions passes values to gen_snapshot', () async {
    androidEnvironment.defines[kExtraGenSnapshotOptions] = 'foo,bar,baz=2';
    androidEnvironment.defines[kBuildMode] = getNameForBuildMode(BuildMode.profile);
    final String build = androidEnvironment.buildDir.path;

    processManager.addCommands(<FakeCommand>[
      FakeCommand(command: <String>[
        artifacts.getArtifactPath(
          Artifact.genSnapshot,
          platform: TargetPlatform.android_arm,
          mode: BuildMode.profile,
        ),
        '--deterministic',
        'foo',
        'bar',
        'baz=2',
        kElfAot,
        '--elf=$build/app.so',
        '--strip',
        '--no-sim-use-hardfp',
        '--no-use-integer-division',
        '$build/app.dill',
      ]),
    ]);

    await const AotElfRelease(TargetPlatform.android_arm).build(androidEnvironment);

    expect(processManager, hasNoRemainingExpectations);
  });
}