// 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/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/ios/xcode_build_settings.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';

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

const String xcodebuild = '/usr/bin/xcodebuild';

void main() {
  group('MockProcessManager', () {
    setUp(() {
      final FileSystem fileSystem = MemoryFileSystem.test();
      fileSystem.file(xcodebuild).createSync(recursive: true);
    });
  });

  const FakeCommand kWhichSysctlCommand = FakeCommand(
    command: <String>[
      'which',
      'sysctl',
    ],
  );

  // x64 host.
  const FakeCommand kx64CheckCommand = FakeCommand(
    command: <String>[
      'sysctl',
      'hw.optional.arm64',
    ],
    exitCode: 1,
  );

  // ARM host.
  const FakeCommand kARMCheckCommand = FakeCommand(
    command: <String>[
      'sysctl',
      'hw.optional.arm64',
    ],
    stdout: 'hw.optional.arm64: 1',
  );

  late FakeProcessManager fakeProcessManager;
  late XcodeProjectInterpreter xcodeProjectInterpreter;
  late FakePlatform platform;
  late FileSystem fileSystem;
  late BufferLogger logger;

  setUp(() {
    fakeProcessManager = FakeProcessManager.empty();
    platform = FakePlatform(operatingSystem: 'macos');
    fileSystem = MemoryFileSystem.test();
    fileSystem.file(xcodebuild).createSync(recursive: true);
    logger = BufferLogger.test();
    xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      processManager: fakeProcessManager,
      usage: TestUsage(),
    );
  });

  testWithoutContext('xcodebuild versionText returns null when xcodebuild is not fully installed', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, "
            "but active developer directory '/Library/Developer/CommandLineTools' "
            'is a command line tools instance',
        exitCode: 1,
      ),
    ]);

    expect(xcodeProjectInterpreter.versionText, isNull);
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild versionText returns null when xcodebuild is not installed', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        exception: ProcessException(xcodebuild, <String>['-version']),
      ),
    ]);

    expect(xcodeProjectInterpreter.versionText, isNull);
  });

  testWithoutContext('xcodebuild versionText returns formatted version text', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode 8.3.3\nBuild version 8E3004b',
      ),
    ]);

    expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b');
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild versionText handles Xcode version string with unexpected format', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
      ),
    ]);

    expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b');
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild version parts can be parsed', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode 11.4.1\nBuild version 11N111s',
      ),
    ]);

    expect(xcodeProjectInterpreter.version, Version(11, 4, 1));
    expect(xcodeProjectInterpreter.build, '11N111s');
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild minor and patch version default to 0', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode 11\nBuild version 11N111s',
      ),
    ]);

    expect(xcodeProjectInterpreter.version, Version(11, 0, 0));
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild version parts is null when version has unexpected format', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
      ),
    ]);
    expect(xcodeProjectInterpreter.version, isNull);
    expect(xcodeProjectInterpreter.build, isNull);
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild isInstalled is false when not on MacOS', () {
    final Platform platform = FakePlatform(operatingSystem: 'notMacOS');
    xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      processManager: fakeProcessManager,
      usage: TestUsage(),
    );
    fileSystem.file(xcodebuild).deleteSync();

    expect(xcodeProjectInterpreter.isInstalled, isFalse);
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild isInstalled is false when xcodebuild does not exist', () {
    fileSystem.file(xcodebuild).deleteSync();

    expect(xcodeProjectInterpreter.isInstalled, isFalse);
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext(
      'xcodebuild isInstalled is false when Xcode is not fully installed', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, "
            "but active developer directory '/Library/Developer/CommandLineTools' "
            'is a command line tools instance',
        exitCode: 1,
      ),
    ]);

    expect(xcodeProjectInterpreter.isInstalled, isFalse);
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild isInstalled is false when version has unexpected format', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
      ),
    ]);

    expect(xcodeProjectInterpreter.isInstalled, isFalse);
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild isInstalled is true when version has expected format', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-version'],
        stdout: 'Xcode 8.3.3\nBuild version 8E3004b',
      ),
    ]);

    expect(xcodeProjectInterpreter.isInstalled, isTrue);
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcrun runs natively on arm64', () {
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kARMCheckCommand,
    ]);

    expect(xcodeProjectInterpreter.xcrunCommand(), <String>[
      '/usr/bin/arch',
      '-arm64e',
      'xcrun',
    ]);
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testUsingContext('xcodebuild build settings is empty when xcodebuild failed to get the build settings', () async {
    platform.environment = const <String, String>{};

    fakeProcessManager.addCommands(<FakeCommand>[
      kWhichSysctlCommand,
      const FakeCommand(
        command: <String>[
          'sysctl',
          'hw.optional.arm64',
        ],
        exitCode: 1,
      ),
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-project',
          '/',
          '-scheme',
          'Free',
          '-destination',
          'id=123',
          '-showBuildSettings',
          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
        ],
        exitCode: 1,
      ),
    ]);

    expect(
        await xcodeProjectInterpreter.getBuildSettings('', buildContext: const XcodeProjectBuildContext(deviceId: '123', scheme: 'Free')),
        const <String, String>{});
    expect(fakeProcessManager, hasNoRemainingExpectations);
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('build settings passes in the simulator SDK', () async {
    platform.environment = const <String, String>{};

    fakeProcessManager.addCommands(<FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-project',
          '/',
          '-sdk',
          'iphonesimulator',
          '-destination',
          'generic/platform=iOS Simulator',
          '-showBuildSettings',
          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
        ],
        exitCode: 1,
      ),
    ]);

    expect(
      await xcodeProjectInterpreter.getBuildSettings(
        '',
        buildContext: const XcodeProjectBuildContext(environmentType: EnvironmentType.simulator),
      ),
      const <String, String>{},
    );
    expect(fakeProcessManager, hasNoRemainingExpectations);
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('build settings accepts an empty scheme', () async {
    platform.environment = const <String, String>{};

    fakeProcessManager.addCommands(<FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-project',
          '/',
          '-destination',
          'generic/platform=iOS',
          '-showBuildSettings',
          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
        ],
        exitCode: 1,
      ),
    ]);

    expect(await xcodeProjectInterpreter.getBuildSettings('', buildContext: const XcodeProjectBuildContext()),
        const <String, String>{});
    expect(fakeProcessManager, hasNoRemainingExpectations);
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('xcodebuild build settings contains Flutter Xcode environment variables', () async {
    platform.environment = const <String, String>{
      'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
      'FLUTTER_XCODE_ARCHS': 'arm64',
    };
    fakeProcessManager.addCommands(<FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-project',
          fileSystem.path.separator,
          '-scheme',
          'Free',
          '-destination',
          'generic/platform=iOS',
          '-showBuildSettings',
          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
          'CODE_SIGN_STYLE=Manual',
          'ARCHS=arm64',
        ],
      ),
    ]);
    expect(
        await xcodeProjectInterpreter.getBuildSettings('', buildContext: const XcodeProjectBuildContext(scheme: 'Free')),
        const <String, String>{});
    expect(fakeProcessManager, hasNoRemainingExpectations);
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('build settings uses watch destination if isWatch is true', () async {
    platform.environment = const <String, String>{};

    fakeProcessManager.addCommands(<FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-project',
          '/',
          '-destination',
          'generic/platform=watchOS',
          '-showBuildSettings',
          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
        ],
        exitCode: 1,
      ),
    ]);

    expect(
      await xcodeProjectInterpreter.getBuildSettings(
        '',
        buildContext: const XcodeProjectBuildContext(isWatch: true),
      ),
      const <String, String>{},
    );
    expect(fakeProcessManager, hasNoRemainingExpectations);
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
  });

  testUsingContext('build settings uses watch simulator destination if isWatch is true and environment type is simulator', () async {
    platform.environment = const <String, String>{};

    fakeProcessManager.addCommands(<FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-project',
          '/',
          '-sdk',
          'iphonesimulator',
          '-destination',
          'generic/platform=watchOS Simulator',
          '-showBuildSettings',
          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
        ],
        exitCode: 1,
      ),
    ]);

    expect(
      await xcodeProjectInterpreter.getBuildSettings(
        '',
        buildContext: const XcodeProjectBuildContext(environmentType: EnvironmentType.simulator, isWatch: true),
      ),
      const <String, String>{},
    );
    expect(fakeProcessManager, hasNoRemainingExpectations);
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
  });

  testWithoutContext('xcodebuild clean contains Flutter Xcode environment variables', () async {
    platform.environment = const <String, String>{
      'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
      'FLUTTER_XCODE_ARCHS': 'arm64',
    };

    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>[
          'xcrun',
          'xcodebuild',
          '-workspace',
          'workspace_path',
          '-scheme',
          'Free',
          '-quiet',
          'clean',
          'CODE_SIGN_STYLE=Manual',
          'ARCHS=arm64',
        ],
      ),
    ]);

    await xcodeProjectInterpreter.cleanWorkspace('workspace_path', 'Free');
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild -list getInfo returns something when xcodebuild -list succeeds', () async {
    const String workingDirectory = '/';
    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-list'],
      ),
    ]);

    final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      processManager: fakeProcessManager,
      usage: TestUsage(),
    );

    expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull);
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild -list getInfo throws a tool exit when it is unable to find a project', () async {
    const String workingDirectory = '/';
    const String stderr = 'Useful Xcode failure message about missing project.';

    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-list'],
        exitCode: 66,
        stderr: stderr,
      ),
    ]);

    final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      processManager: fakeProcessManager,
      usage: TestUsage(),
    );

    expect(() => xcodeProjectInterpreter.getInfo(workingDirectory), throwsToolExit(message: stderr));
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('xcodebuild -list getInfo throws a tool exit when project is corrupted', () async {
    const String workingDirectory = '/';
    const String stderr = 'Useful Xcode failure message about corrupted project.';

    fakeProcessManager.addCommands(const <FakeCommand>[
      kWhichSysctlCommand,
      kx64CheckCommand,
      FakeCommand(
        command: <String>['xcrun', 'xcodebuild', '-list'],
        exitCode: 74,
        stderr: stderr,
      ),
    ]);

    final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      processManager: fakeProcessManager,
      usage: TestUsage(),
    );

    expect(() => xcodeProjectInterpreter.getInfo(workingDirectory), throwsToolExit(message: stderr));
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('Xcode project properties from default project can be parsed', () {
    const String output = '''
Information about project "Runner":
    Targets:
        Runner

    Build Configurations:
        Debug
        Release

    If no build configuration is specified and -scheme is not passed then "Release" is used.

    Schemes:
        Runner

''';
    final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output, logger);
    expect(info.targets, <String>['Runner']);
    expect(info.schemes, <String>['Runner']);
    expect(info.buildConfigurations, <String>['Debug', 'Release']);
  });

  testWithoutContext('Xcode project properties from project with custom schemes can be parsed', () {
    const String output = '''
Information about project "Runner":
    Targets:
        Runner

    Build Configurations:
        Debug (Free)
        Debug (Paid)
        Release (Free)
        Release (Paid)

    If no build configuration is specified and -scheme is not passed then "Release (Free)" is used.

    Schemes:
        Free
        Paid

''';
    final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output, logger);
    expect(info.targets, <String>['Runner']);
    expect(info.schemes, <String>['Free', 'Paid']);
    expect(info.buildConfigurations, <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)']);
  });

  testWithoutContext('expected scheme for non-flavored build is Runner', () {
    expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.debug), 'Runner');
    expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.profile), 'Runner');
    expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.release), 'Runner');
  });

  testWithoutContext('expected build configuration for non-flavored build is derived from BuildMode', () {
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
  });

  testWithoutContext('expected scheme for flavored build is the title-cased flavor', () {
    expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.debug, 'hello', treeShakeIcons: false)), 'Hello');
    expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.profile, 'HELLO', treeShakeIcons: false)), 'HELLO');
    expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.release, 'Hello', treeShakeIcons: false)), 'Hello');
  });
  testWithoutContext('expected build configuration for flavored build is Mode-Flavor', () {
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.debug, 'hello', treeShakeIcons: false), 'Hello'), 'Debug-Hello');
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.profile, 'HELLO', treeShakeIcons: false), 'Hello'), 'Profile-Hello');
    expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.release, 'Hello', treeShakeIcons: false), 'Hello'), 'Release-Hello');
  });

  testWithoutContext('scheme for default project is Runner', () {
    final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Release'], <String>['Runner'], logger);

    expect(info.schemeFor(BuildInfo.debug), 'Runner');
    expect(info.schemeFor(BuildInfo.profile), 'Runner');
    expect(info.schemeFor(BuildInfo.release), 'Runner');
    expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown', treeShakeIcons: false)), isNull);
  });

  testWithoutContext('build configuration for default project is matched against BuildMode', () {
    final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Profile', 'Release'], <String>['Runner'], logger);

    expect(info.buildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
    expect(info.buildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
    expect(info.buildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
  });

  testWithoutContext('scheme for project with custom schemes is matched against flavor', () {
    final XcodeProjectInfo info = XcodeProjectInfo(
      <String>['Runner'],
      <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)'],
      <String>['Free', 'Paid'],
      logger,
    );

    expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false)), 'Free');
    expect(info.schemeFor(const BuildInfo(BuildMode.profile, 'Free', treeShakeIcons: false)), 'Free');
    expect(info.schemeFor(const BuildInfo(BuildMode.release, 'paid', treeShakeIcons: false)), 'Paid');
    expect(info.schemeFor(BuildInfo.debug), isNull);
    expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown', treeShakeIcons: false)), isNull);
  });

  testWithoutContext('reports default scheme error and exit', () {
    final XcodeProjectInfo defaultInfo = XcodeProjectInfo(
      <String>[],
      <String>[],
      <String>['Runner'],
      logger,
    );

    expect(
      defaultInfo.reportFlavorNotFoundAndExit,
      throwsToolExit(
        message: 'The Xcode project does not define custom schemes. You cannot use the --flavor option.'
      ),
    );
  });

  testWithoutContext('reports custom scheme error and exit', () {
    final XcodeProjectInfo info = XcodeProjectInfo(
      <String>[],
      <String>[],
      <String>['Free', 'Paid'],
      logger,
    );

    expect(
      info.reportFlavorNotFoundAndExit,
      throwsToolExit(
        message: 'You must specify a --flavor option to select one of the available schemes.'
      ),
    );
  });

  testWithoutContext('build configuration for project with custom schemes is matched against BuildMode and flavor', () {
    final XcodeProjectInfo info = XcodeProjectInfo(
      <String>['Runner'],
      <String>['debug (free)', 'Debug paid', 'profile - Free', 'Profile-Paid', 'release - Free', 'Release-Paid'],
      <String>['Free', 'Paid'],
      logger,
    );

    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false), 'Free'), 'debug (free)');
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Paid', treeShakeIcons: false), 'Paid'), 'Debug paid');
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'FREE', treeShakeIcons: false), 'Free'), 'profile - Free');
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'paid', treeShakeIcons: false), 'Paid'), 'Release-Paid');
  });

  testWithoutContext('build configuration for project with inconsistent naming is null', () {
    final XcodeProjectInfo info = XcodeProjectInfo(
      <String>['Runner'],
      <String>['Debug-F', 'Dbg Paid', 'Rel Free', 'Release Full'],
      <String>['Free', 'Paid'],
      logger,
    );
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Free', treeShakeIcons: false), 'Free'), null);
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'Free', treeShakeIcons: false), 'Free'), null);
    expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'Paid', treeShakeIcons: false), 'Paid'), null);
  });
 group('environmentVariablesAsXcodeBuildSettings', () {
    late FakePlatform platform;

    setUp(() {
      platform = FakePlatform();
    });

    testWithoutContext('environment variables as Xcode build settings', () {
      platform.environment = const <String, String>{
        'Ignored': 'Bogus',
        'FLUTTER_NOT_XCODE': 'Bogus',
        'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
        'FLUTTER_XCODE_ARCHS': 'arm64',
      };
      final List<String> environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings(platform);
      expect(environmentVariablesAsBuildSettings, <String>['CODE_SIGN_STYLE=Manual', 'ARCHS=arm64']);
    });
  });

  group('updateGeneratedXcodeProperties', () {
    late Artifacts localIosArtifacts;
    late FakePlatform macOS;
    late FileSystem fs;

    setUp(() {
      fs = MemoryFileSystem.test();
      localIosArtifacts = Artifacts.test(localEngine: 'out/ios_profile_arm64');
      macOS = FakePlatform(operatingSystem: 'macos');
      fs.file(xcodebuild).createSync(recursive: true);
    });

    group('arm simulator', () {
      late FakeProcessManager fakeProcessManager;
      late XcodeProjectInterpreter xcodeProjectInterpreter;

      setUp(() {
        fakeProcessManager = FakeProcessManager.empty();
        xcodeProjectInterpreter = XcodeProjectInterpreter.test(processManager: fakeProcessManager);
      });

      testUsingContext('does not exclude arm64 simulator when supported by all plugins', () async {
        const BuildInfo buildInfo = BuildInfo.debug;
        final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
        final Directory podXcodeProject = project.ios.hostAppRoot.childDirectory('Pods').childDirectory('Pods.xcodeproj')
          ..createSync(recursive: true);

        final String buildDirectory = fileSystem.path.absolute('build', 'ios');
        fakeProcessManager.addCommands(<FakeCommand>[
          kWhichSysctlCommand,
          kARMCheckCommand,
          FakeCommand(
            command: <String>[
              '/usr/bin/arch',
              '-arm64e',
              'xcrun',
              'xcodebuild',
              '-alltargets',
              '-sdk',
              'iphonesimulator',
              '-project',
              podXcodeProject.path,
              '-showBuildSettings',
              'BUILD_DIR=$buildDirectory',
              'OBJROOT=$buildDirectory',
            ],
            stdout: '''
Build settings for action build and target plugin1:
    ENABLE_BITCODE = NO;
    EXCLUDED_ARCHS = i386;
    INFOPLIST_FILE = Runner/Info.plist;
    UNRELATED_BUILD_SETTING = arm64;

Build settings for action build and target plugin2:
    ENABLE_BITCODE = NO;
    EXCLUDED_ARCHS = i386;
    INFOPLIST_FILE = Runner/Info.plist;
    UNRELATED_BUILD_SETTING = arm64;
				'''
          ),
        ]);
        await updateGeneratedXcodeProperties(
          project: project,
          buildInfo: buildInfo,
        );

        final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
        expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386\n'));
        expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphoneos*]=armv7\n'));
        expect(fakeProcessManager, hasNoRemainingExpectations);
      }, overrides: <Type, Generator>{
        Artifacts: () => localIosArtifacts,
        Platform: () => macOS,
        FileSystem: () => fs,
        ProcessManager: () => fakeProcessManager,
        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
      });

      testUsingContext('excludes arm64 simulator when build setting fetch fails', () async {
        const BuildInfo buildInfo = BuildInfo.debug;
        final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
        final Directory podXcodeProject = project.ios.hostAppRoot.childDirectory('Pods').childDirectory('Pods.xcodeproj')
          ..createSync(recursive: true);

        final String buildDirectory = fileSystem.path.absolute('build', 'ios');
        fakeProcessManager.addCommands(<FakeCommand>[
          kWhichSysctlCommand,
          kARMCheckCommand,
          FakeCommand(
              command: <String>[
                '/usr/bin/arch',
                '-arm64e',
                'xcrun',
                'xcodebuild',
                '-alltargets',
                '-sdk',
                'iphonesimulator',
                '-project',
                podXcodeProject.path,
                '-showBuildSettings',
                'BUILD_DIR=$buildDirectory',
                'OBJROOT=$buildDirectory',
              ],
              exitCode: 1,
          ),
        ]);
        await updateGeneratedXcodeProperties(
          project: project,
          buildInfo: buildInfo,
        );

        final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
        expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 arm64\n'));
        expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphoneos*]=armv7\n'));
        expect(fakeProcessManager, hasNoRemainingExpectations);
      }, overrides: <Type, Generator>{
        Artifacts: () => localIosArtifacts,
        Platform: () => macOS,
        FileSystem: () => fs,
        ProcessManager: () => fakeProcessManager,
        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
      });

      testUsingContext('excludes arm64 simulator when unsupported by plugins', () async {
        const BuildInfo buildInfo = BuildInfo.debug;
        final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
        final Directory podXcodeProject = project.ios.hostAppRoot.childDirectory('Pods').childDirectory('Pods.xcodeproj')
          ..createSync(recursive: true);

        final String buildDirectory = fileSystem.path.absolute('build', 'ios');
        fakeProcessManager.addCommands(<FakeCommand>[
          kWhichSysctlCommand,
          kARMCheckCommand,
          FakeCommand(
              command: <String>[
                '/usr/bin/arch',
                '-arm64e',
                'xcrun',
                'xcodebuild',
                '-alltargets',
                '-sdk',
                'iphonesimulator',
                '-project',
                podXcodeProject.path,
                '-showBuildSettings',
                'BUILD_DIR=$buildDirectory',
                'OBJROOT=$buildDirectory',
              ],
              stdout: '''
Build settings for action build and target plugin1:
    ENABLE_BITCODE = NO;
    EXCLUDED_ARCHS = i386;
    INFOPLIST_FILE = Runner/Info.plist;
    UNRELATED_BUILD_SETTING = arm64;

Build settings for action build and target plugin2:
    ENABLE_BITCODE = NO;
    EXCLUDED_ARCHS = i386 arm64;
    INFOPLIST_FILE = Runner/Info.plist;
    UNRELATED_BUILD_SETTING = arm64;
				'''
          ),
        ]);
        await updateGeneratedXcodeProperties(
          project: project,
          buildInfo: buildInfo,
        );

        final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
        expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 arm64\n'));
        expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphoneos*]=armv7\n'));
        expect(fakeProcessManager, hasNoRemainingExpectations);
      }, overrides: <Type, Generator>{
        Artifacts: () => localIosArtifacts,
        Platform: () => macOS,
        FileSystem: () => fs,
        ProcessManager: () => fakeProcessManager,
        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
      });
    });

    void testUsingOsxContext(String description, dynamic Function() testMethod) {
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
        Artifacts: () => localIosArtifacts,
        Platform: () => macOS,
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
      });
    }

    testUsingOsxContext('exits when armv7 local engine is set', () async {
      localIosArtifacts = Artifacts.test(localEngine: 'out/ios_profile_arm');
      const BuildInfo buildInfo = BuildInfo.debug;
      final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
      await expectLater(() =>
        updateGeneratedXcodeProperties(
          project: project,
          buildInfo: buildInfo,
        ),
        throwsToolExit(message: '32-bit iOS local engine binaries are not supported.'),
      );
    });

    testUsingContext('sets ARCHS=arm64 when arm64 local host engine is set', () async {
      const BuildInfo buildInfo = BuildInfo.debug;
      final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
      await updateGeneratedXcodeProperties(
        project: project,
        buildInfo: buildInfo,
        useMacOSConfig: true,
      );

      final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('ARCHS=arm64\n'), isTrue);

      final File buildPhaseScript = fs.file('path/to/project/macos/Flutter/ephemeral/flutter_export_environment.sh');
      expect(buildPhaseScript.existsSync(), isTrue);

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('export "ARCHS=arm64"'), isTrue);
    }, overrides: <Type, Generator>{
      Artifacts: () => Artifacts.test(localEngine: 'out/host_profile_arm64'),
      Platform: () => macOS,
      FileSystem: () => fs,
      ProcessManager: () => FakeProcessManager.any(),
    });

    testUsingContext('sets ARCHS=x86_64 when x64 local host engine is set', () async {
      const BuildInfo buildInfo = BuildInfo.debug;
      final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
      await updateGeneratedXcodeProperties(
        project: project,
        buildInfo: buildInfo,
        useMacOSConfig: true,
      );

      final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('ARCHS=x86_64\n'), isTrue);

      final File buildPhaseScript = fs.file('path/to/project/macos/Flutter/ephemeral/flutter_export_environment.sh');
      expect(buildPhaseScript.existsSync(), isTrue);

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('export "ARCHS=x86_64"'), isTrue);
    }, overrides: <Type, Generator>{
      Artifacts: () => Artifacts.test(localEngine: 'out/host_profile'),
      Platform: () => macOS,
      FileSystem: () => fs,
      ProcessManager: () => FakeProcessManager.any(),
    });

    testUsingOsxContext('does not exclude arm64 simulator when there are no plugins', () async {
      const BuildInfo buildInfo = BuildInfo.debug;
      final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
      await updateGeneratedXcodeProperties(
        project: project,
        buildInfo: buildInfo,
      );

      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
      expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386\n'));
      expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphoneos*]=armv7\n'));

      final File buildPhaseScript = fs.file('path/to/project/ios/Flutter/flutter_export_environment.sh');
      expect(buildPhaseScript.readAsStringSync(), isNot(contains('EXCLUDED_ARCHS')));
    });

    testUsingOsxContext('sets TRACK_WIDGET_CREATION=true when trackWidgetCreation is true', () async {
      const BuildInfo buildInfo = BuildInfo.debug;
      final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
      await updateGeneratedXcodeProperties(
        project: project,
        buildInfo: buildInfo,
      );

      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('TRACK_WIDGET_CREATION=true'), isTrue);

      final File buildPhaseScript = fs.file('path/to/project/ios/Flutter/flutter_export_environment.sh');
      expect(buildPhaseScript.existsSync(), isTrue);

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('TRACK_WIDGET_CREATION=true'), isTrue);
    });

    testUsingOsxContext('does not set TRACK_WIDGET_CREATION when trackWidgetCreation is false', () async {
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
      final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
      await updateGeneratedXcodeProperties(
        project: project,
        buildInfo: buildInfo,
      );

      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('TRACK_WIDGET_CREATION=true'), isFalse);

      final File buildPhaseScript = fs.file('path/to/project/ios/Flutter/flutter_export_environment.sh');
      expect(buildPhaseScript.existsSync(), isTrue);

      final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
      expect(buildPhaseScriptContents.contains('TRACK_WIDGET_CREATION=true'), isFalse);
    });

    group('sim local engine', () {
      testUsingContext('sets ARCHS=x86_64 when x86 sim local engine is set', () async {
        const BuildInfo buildInfo = BuildInfo.debug;
        final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
        await updateGeneratedXcodeProperties(
          project: project,
          buildInfo: buildInfo,
        );

        final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
        expect(config.existsSync(), isTrue);

        final String contents = config.readAsStringSync();
        expect(contents.contains('ARCHS=x86_64'), isTrue);

        final File buildPhaseScript = fs.file('path/to/project/ios/Flutter/flutter_export_environment.sh');
        expect(buildPhaseScript.existsSync(), isTrue);

        final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
        expect(buildPhaseScriptContents.contains('ARCHS=x86_64'), isTrue);
      }, overrides: <Type, Generator>{
        Artifacts: () => Artifacts.test(localEngine: 'out/ios_debug_sim_unopt'),
        Platform: () => macOS,
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
      });

      testUsingContext('sets ARCHS=arm64 when arm64 sim local engine is set', () async {
        const BuildInfo buildInfo = BuildInfo.debug;
        final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
        await updateGeneratedXcodeProperties(
          project: project,
          buildInfo: buildInfo,
        );

        final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
        expect(config.existsSync(), isTrue);

        final String contents = config.readAsStringSync();
        expect(contents.contains('ARCHS=arm64'), isTrue);

        final File buildPhaseScript = fs.file('path/to/project/ios/Flutter/flutter_export_environment.sh');
        expect(buildPhaseScript.existsSync(), isTrue);

        final String buildPhaseScriptContents = buildPhaseScript.readAsStringSync();
        expect(buildPhaseScriptContents.contains('ARCHS=arm64'), isTrue);
      }, overrides: <Type, Generator>{
        Artifacts: () => Artifacts.test(localEngine: 'out/ios_debug_sim_arm64'),
        Platform: () => macOS,
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
      });
    });

    String? propertyFor(String key, File file) {
      final List<String> properties = file
          .readAsLinesSync()
          .where((String line) => line.startsWith('$key='))
          .map((String line) => line.split('=')[1])
          .toList();
      return properties.isEmpty ? null : properties.first;
    }

    Future<void> checkBuildVersion({
      required String manifestString,
      required BuildInfo buildInfo,
      String? expectedBuildName,
      String? expectedBuildNumber,
    }) async {
      final File manifestFile = fs.file('path/to/project/pubspec.yaml');
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync(manifestString);

      await updateGeneratedXcodeProperties(
        project: FlutterProject.fromDirectoryTest(fs.directory('path/to/project')),
        buildInfo: buildInfo,
      );

      final File localPropertiesFile = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
      expect(propertyFor('FLUTTER_BUILD_NAME', localPropertiesFile), expectedBuildName);
      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), expectedBuildNumber);
      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), isNotNull);
    }

    testUsingOsxContext('extract build name and number from pubspec.yaml', () async {
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';

      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });

    testUsingOsxContext('extract build name from pubspec.yaml', () async {
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1.0.0',
      );
    });

    testUsingOsxContext('allow build info to override build name', () async {
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', treeShakeIcons: false);
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1',
      );
    });

    testUsingOsxContext('allow build info to override build name with build number fallback', () async {
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', treeShakeIcons: false);
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1.0.2',
      );
    });

    testUsingOsxContext('allow build info to override build number', () async {
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3', treeShakeIcons: false);
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '3',
      );
    });

    testUsingOsxContext('allow build info to override build name and number', () async {
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

    testUsingOsxContext('allow build info to override build name and set number', () async {
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

    testUsingOsxContext('allow build info to set build name and number', () async {
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

    testUsingOsxContext('default build name and number when version is missing', () async {
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
      await checkBuildVersion(
        manifestString: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });
  });
}
