// 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/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart' as gradle_utils;
import 'package:flutter_tools/src/android/java.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:meta/meta.dart';
import 'package:test/fake.dart';

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

void main() {
  // TODO(zanderso): remove once FlutterProject is fully refactored.
  // this is safe since no tests have expectations on the test logger.
  final BufferLogger logger = BufferLogger.test();

  group('Project', () {
    group('construction', () {
      testWithoutContext('invalid utf8 throws a tool exit', () {
        final FileSystem fileSystem = MemoryFileSystem.test();
        final FlutterProjectFactory projectFactory = FlutterProjectFactory(
          fileSystem: fileSystem,
          logger: BufferLogger.test(),
        );
        fileSystem.file('pubspec.yaml').writeAsBytesSync(<int>[0xFFFE]);

        /// Technically this should throw a FileSystemException but this is
        /// currently a bug in package:file.
        expect(
          () => projectFactory.fromDirectory(fileSystem.currentDirectory),
          throwsToolExit(),
        );
      });

      _testInMemory('fails on invalid pubspec.yaml', () async {
        final Directory directory = globals.fs.directory('myproject');
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(invalidPubspec);

        expect(
          () => FlutterProject.fromDirectory(directory),
          throwsToolExit(),
        );
      });

      _testInMemory('fails on pubspec.yaml parse failure', () async {
        final Directory directory = globals.fs.directory('myproject');
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(parseErrorPubspec);

        expect(
          () => FlutterProject.fromDirectory(directory),
          throwsToolExit(),
        );
      });

      _testInMemory('fails on invalid example/pubspec.yaml', () async {
        final Directory directory = globals.fs.directory('myproject');
        directory.childDirectory('example').childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(invalidPubspec);

        expect(
          () => FlutterProject.fromDirectory(directory),
          throwsToolExit(),
        );
      });

      _testInMemory('treats missing pubspec.yaml as empty', () async {
        final Directory directory = globals.fs.directory('myproject')
          ..createSync(recursive: true);
        expect(FlutterProject.fromDirectory(directory).manifest.isEmpty,
          true,
        );
      });

      _testInMemory('reads valid pubspec.yaml', () async {
        final Directory directory = globals.fs.directory('myproject');
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(validPubspec);
        expect(
          FlutterProject.fromDirectory(directory).manifest.appName,
          'hello',
        );
      });

      _testInMemory('reads dependencies from pubspec.yaml', () async {
        final Directory directory = globals.fs.directory('myproject');
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(validPubspecWithDependencies);
        expect(
          FlutterProject.fromDirectory(directory).manifest.dependencies,
          <String>{'plugin_a', 'plugin_b'},
        );
      });

      _testInMemory('sets up location', () async {
        final Directory directory = globals.fs.directory('myproject');
        expect(
          FlutterProject.fromDirectory(directory).directory.absolute.path,
          directory.absolute.path,
        );
        expect(
          FlutterProject.fromDirectoryTest(directory).directory.absolute.path,
          directory.absolute.path,
        );
        expect(
          FlutterProject.current().directory.absolute.path,
          globals.fs.currentDirectory.absolute.path,
        );
      });
    });

    group('ensure ready for platform-specific tooling', () {
      _testInMemory('does nothing, if project is not created', () async {
        final FlutterProject project = FlutterProject(
          globals.fs.directory('not_created'),
          FlutterManifest.empty(logger: logger),
          FlutterManifest.empty(logger: logger),
        );
        await project.regeneratePlatformSpecificTooling();
        expectNotExists(project.directory);
      });
      _testInMemory('does nothing in plugin or package root project', () async {
        final FlutterProject project = await aPluginProject();
        await project.regeneratePlatformSpecificTooling();
        expectNotExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
        expectNotExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
        expectNotExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
        expectNotExists(project.android.hostAppGradleRoot.childFile('local.properties'));
      });
      _testInMemory('works if there is an "example" folder', () async {
        final FlutterProject project = await someProject();
        // The presence of an "example" folder used to be used as an indicator
        // that a project was a plugin, but shouldn't be as this creates false
        // positives.
        project.directory.childDirectory('example').createSync();
        await project.regeneratePlatformSpecificTooling();
        expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
        expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
      });
      _testInMemory('injects plugins for iOS', () async {
        final FlutterProject project = await someProject();
        await project.regeneratePlatformSpecificTooling();
        expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
      });
      _testInMemory('generates Xcode configuration for iOS', () async {
        final FlutterProject project = await someProject();
        await project.regeneratePlatformSpecificTooling();
        expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
      });
      _testInMemory('injects plugins for Android', () async {
        final FlutterProject project = await someProject();
        await project.regeneratePlatformSpecificTooling();
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
      });
      _testInMemory('updates local properties for Android', () async {
        final FlutterProject project = await someProject();
        await project.regeneratePlatformSpecificTooling();
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
      });
      _testInMemory('checkForDeprecation fails on invalid android app manifest file', () async {
        // This is not a valid Xml document
        const String invalidManifest = '<manifest></application>';
        final FlutterProject project = await someProject(androidManifestOverride: invalidManifest, includePubspec: true);

        expect(
          () => project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.ignore),
          throwsToolExit(message: 'Please ensure that the android manifest is a valid XML document and try again.'),
        );
      });
      _testInMemory('Project not on v2 embedding does not warn if deprecation status is irrelevant', () async {
        final FlutterProject project = await someProject(includePubspec: true);
        // The default someProject with an empty <manifest> already indicates
        // v1 embedding, as opposed to having <meta-data
        // android:name="flutterEmbedding" android:value="2" />.

        // Default is "DeprecationBehavior.none"
        project.checkForDeprecation();
        expect(testLogger.statusText, isEmpty);
      });
      _testInMemory('Android project no pubspec continues', () async {
        final FlutterProject project = await someProject();
        // The default someProject with an empty <manifest> already indicates
        // v1 embedding, as opposed to having <meta-data
        // android:name="flutterEmbedding" android:value="2" />.

        project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.ignore);
        expect(testLogger.statusText, isNot(contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects')));
      });
      _testInMemory('Android plugin project does not throw v1 embedding deprecation warning', () async {
        final FlutterProject project = await aPluginProject();

        project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.exit);
        expect(testLogger.statusText, isNot(contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects')));
        expect(testLogger.statusText, isNot(contains('No `<meta-data android:name="flutterEmbedding" android:value="2"/>` in ')));
      });
      _testInMemory('Android plugin without example app does not show a warning', () async {
        final FlutterProject project = await aPluginProject();
        project.example.directory.deleteSync();

        await project.regeneratePlatformSpecificTooling();
        expect(testLogger.statusText, isNot(contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects')));
      });
      _testInMemory('updates local properties for Android', () async {
        final FlutterProject project = await someProject();
        await project.regeneratePlatformSpecificTooling();
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
      });
      testUsingContext('injects plugins for macOS', () async {
        final FlutterProject project = await someProject();
        project.macos.managedDirectory.createSync(recursive: true);
        await project.regeneratePlatformSpecificTooling();
        expectExists(project.macos.pluginRegistrantImplementation);
      }, overrides: <Type, Generator>{
        FileSystem: () => MemoryFileSystem.test(),
        ProcessManager: () => FakeProcessManager.any(),
        FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
      });
      testUsingContext('generates Xcode configuration for macOS', () async {
        final FlutterProject project = await someProject();
        project.macos.managedDirectory.createSync(recursive: true);
        await project.regeneratePlatformSpecificTooling();
        expectExists(project.macos.generatedXcodePropertiesFile);
      }, overrides: <Type, Generator>{
        FileSystem: () => MemoryFileSystem.test(),
        ProcessManager: () => FakeProcessManager.any(),
        FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
      });
      testUsingContext('injects plugins for Linux', () async {
        final FlutterProject project = await someProject();
        project.linux.cmakeFile.createSync(recursive: true);
        await project.regeneratePlatformSpecificTooling();
        expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.h'));
        expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.cc'));
      }, overrides: <Type, Generator>{
        FileSystem: () => MemoryFileSystem.test(),
        ProcessManager: () => FakeProcessManager.any(),
        FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true),
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
      });
      testUsingContext('injects plugins for Windows', () async {
        final FlutterProject project = await someProject();
        project.windows.cmakeFile.createSync(recursive: true);
        await project.regeneratePlatformSpecificTooling();
        expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.h'));
        expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.cc'));
      }, overrides: <Type, Generator>{
        FileSystem: () => MemoryFileSystem.test(),
        ProcessManager: () => FakeProcessManager.any(),
        FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true),
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
      });
      _testInMemory('creates Android library in module', () async {
        final FlutterProject project = await aModuleProject();
        await project.regeneratePlatformSpecificTooling();
        expectExists(project.android.hostAppGradleRoot.childFile('settings.gradle'));
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('Flutter')));
      });
      _testInMemory('creates iOS pod in module', () async {
        final FlutterProject project = await aModuleProject();
        await project.regeneratePlatformSpecificTooling();
        final Directory flutter = project.ios.hostAppRoot.childDirectory('Flutter');
        expectExists(flutter.childFile('podhelper.rb'));
        expectExists(flutter.childFile('flutter_export_environment.sh'));
        expectExists(flutter.childFile('Generated.xcconfig'));
        final Directory pluginRegistrantClasses = flutter
            .childDirectory('FlutterPluginRegistrant')
            .childDirectory('Classes');
        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.h'));
        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.m'));
      });

      testUsingContext('Version.json info is correct', () {
        final MemoryFileSystem fileSystem = MemoryFileSystem.test();
        final FlutterManifest manifest = FlutterManifest.createFromString('''
    name: test
    version: 1.0.0+3
    ''', logger: BufferLogger.test())!;
        final FlutterProject project = FlutterProject(fileSystem.systemTempDirectory, manifest, manifest);
        final Map<String, dynamic> versionInfo = jsonDecode(project.getVersionInfo()) as Map<String, dynamic>;
        expect(versionInfo['app_name'],'test');
        expect(versionInfo['version'],'1.0.0');
        expect(versionInfo['build_number'],'3');
        expect(versionInfo['package_name'],'test');
      });
      _testInMemory('gets xcworkspace directory', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        project.ios.hostAppRoot.childFile('._Runner.xcworkspace').createSync(recursive: true);
        project.ios.hostAppRoot.childFile('Runner.xcworkspace').createSync(recursive: true);

        expect(project.ios.xcodeWorkspace?.basename, 'Runner.xcworkspace');
      });
      _testInMemory('no xcworkspace directory found', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        expect(project.ios.xcodeWorkspace?.basename, null);
      });
    });

    group('module status', () {
      _testInMemory('is known for module', () async {
        final FlutterProject project = await aModuleProject();
        expect(project.isModule, isTrue);
        expect(project.android.isModule, isTrue);
        expect(project.ios.isModule, isTrue);
        expect(project.android.hostAppGradleRoot.basename, '.android');
        expect(project.ios.hostAppRoot.basename, '.ios');
      });
      _testInMemory('is known for non-module', () async {
        final FlutterProject project = await someProject();
        expect(project.isModule, isFalse);
        expect(project.android.isModule, isFalse);
        expect(project.ios.isModule, isFalse);
        expect(project.android.hostAppGradleRoot.basename, 'android');
        expect(project.ios.hostAppRoot.basename, 'ios');
      });
    });

    group('example', () {
      _testInMemory('exists for plugin in legacy format', () async {
        final FlutterProject project = await aPluginProject();
        expect(project.isPlugin, isTrue);
        expect(project.hasExampleApp, isTrue);
      });
      _testInMemory('exists for plugin in multi-platform format', () async {
        final FlutterProject project = await aPluginProject(legacy: false);
        expect(project.hasExampleApp, isTrue);
      });
      _testInMemory('does not exist for non-plugin', () async {
        final FlutterProject project = await someProject();
        expect(project.isPlugin, isFalse);
        expect(project.hasExampleApp, isFalse);
      });
    });

    group('java gradle agp compatibility', () {
      Future<FlutterProject?> configureGradleAgpForTest({
        required String gradleV,
        required String agpV,
      }) async {
        final FlutterProject project = await someProject();
        addRootGradleFile(project.directory, gradleFileContent: () {
          return '''
dependencies {
    classpath 'com.android.tools.build:gradle:$agpV'
}
''';
        });
        addGradleWrapperFile(project.directory, gradleV);
        return project;
      }

      // Tests in this group that use overrides and _testInMemory should
      // be placed in their own group to avoid test pollution. This is
      // especially important for filesystem.
      group('_', () {
        final FakeProcessManager processManager;
        final Java java;
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
        java = FakeJava(version: Version(17, 0, 2));
        processManager = FakeProcessManager.empty();
        androidStudio = FakeAndroidStudio();
        androidSdk =
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'flamingo values are compatible',
          () async {
            final FlutterProject? project = await configureGradleAgpForTest(
              gradleV: '8.0',
              agpV: '7.4.2',
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isTrue);
          },
          java: java,
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });
      group('_', () {
        final FakeProcessManager processManager;
        final Java java;
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
        java = FakeJava(version: const Version.withText(1, 8, 0, '1.8.0_242'));
        processManager = FakeProcessManager.empty();
        androidStudio = FakeAndroidStudio();
        androidSdk =
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'java 8 era values are compatible',
          () async {
            final FlutterProject? project = await configureGradleAgpForTest(
              gradleV: '6.7.1',
              agpV: '4.2.0',
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isTrue);
          },
          java: java,
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });

      group('_', () {
        final FakeProcessManager processManager;
        final Java java;
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
        processManager = FakeProcessManager.empty();
        java = FakeJava(version: Version(11, 0, 14));
        androidStudio = FakeAndroidStudio();
        androidSdk =
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'electric eel era values are compatible',
          () async {
            final FlutterProject? project = await configureGradleAgpForTest(
              gradleV: '7.3.3',
              agpV: '7.2.0',
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isTrue);
          },
          java: java,
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });
      group('_', () {
        const String javaV = '17.0.2';
        const String gradleV = '6.7.3';
        const String agpV = '7.2.0';

        final FakeProcessManager processManager;
        final Java java;
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
        processManager = FakeProcessManager.empty();
        java = FakeJava(version: Version.parse(javaV));
        androidStudio = FakeAndroidStudio();
        androidSdk =
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'incompatible everything',
          () async {

            final FlutterProject? project = await configureGradleAgpForTest(
              gradleV: gradleV,
              agpV: agpV,
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isFalse);
            // Should not have the valid string
            expect(
                value.description,
                isNot(
                    contains(RegExp(AndroidProject.validJavaGradleAgpString))));
            // On gradle/agp error print help url and gradle and agp versions.
            expect(value.description,
                contains(RegExp(AndroidProject.gradleAgpCompatUrl)));
            expect(value.description, contains(RegExp(gradleV)));
            expect(value.description, contains(RegExp(agpV)));
            // On gradle/agp error print help url and java and gradle versions.
            expect(value.description,
                contains(RegExp(AndroidProject.javaGradleCompatUrl)));
            expect(value.description, contains(RegExp(javaV)));
            expect(value.description, contains(RegExp(gradleV)));
          },
          java: java,
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });
      group('_', () {
        const String javaV = '17.0.2';
        const String gradleV = '6.7.3';
        const String agpV = '4.2.0';

        final FakeProcessManager processManager;
        final Java java;
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
        processManager = FakeProcessManager.empty();
        java = FakeJava(version: Version(17, 0, 2));
        androidStudio = FakeAndroidStudio();
        androidSdk =
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'incompatible java/gradle only',
          () async {
            final FlutterProject? project = await configureGradleAgpForTest(
              gradleV: gradleV,
              agpV: agpV,
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isFalse);
            // Should not have the valid string.
            expect(
                value.description,
                isNot(
                    contains(RegExp(AndroidProject.validJavaGradleAgpString))));
            // On gradle/agp error print help url and java and gradle versions.
            expect(value.description,
                contains(RegExp(AndroidProject.javaGradleCompatUrl)));
            expect(value.description, contains(RegExp(javaV)));
            expect(value.description, contains(RegExp(gradleV)));
          },
          java: java,
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });
      group('_', () {
        final FakeProcessManager processManager;
        final Java java;
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
        java = FakeJava(version: Version(11, 0, 2));
        processManager = FakeProcessManager.empty();
        androidStudio = FakeAndroidStudio();
        androidSdk =
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'incompatible gradle/agp only',
          () async {
            const String gradleV = '7.0.3';
            const String agpV = '7.1.0';
            final FlutterProject? project = await configureGradleAgpForTest(
              gradleV: gradleV,
              agpV: agpV,
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isFalse);
            // Should not have the valid string.
            expect(
                value.description,
                isNot(
                    contains(RegExp(AndroidProject.validJavaGradleAgpString))));
            // On gradle/agp error print help url and gradle and agp versions.
            expect(value.description,
                contains(RegExp(AndroidProject.gradleAgpCompatUrl)));
            expect(value.description, contains(RegExp(gradleV)));
            expect(value.description, contains(RegExp(agpV)));
          },
          java: java,
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });
      group('_', () {
        final FakeProcessManager processManager;
        final Java java;
        final AndroidStudio androidStudio;
        final FakeAndroidSdkWithDir androidSdk;
        final FileSystem fileSystem = getFileSystemForPlatform();
        java = FakeJava(version: Version(11, 0, 2));
        processManager = FakeProcessManager.empty();
        androidStudio = FakeAndroidStudio();
        androidSdk =
            FakeAndroidSdkWithDir(fileSystem.currentDirectory);
        fileSystem.currentDirectory
            .childDirectory(androidStudio.javaPath!)
            .createSync();
        _testInMemory(
          'null agp only',
          () async {
            const String gradleV = '7.0.3';
            final FlutterProject? project = await configureGradleAgpForTest(
              gradleV: gradleV,
              agpV: '',
            );
            final CompatibilityResult value =
                await project!.android.hasValidJavaGradleAgpVersions();
            expect(value.success, isFalse);
            // Should not have the valid string.
            expect(
                value.description,
                isNot(
                    contains(RegExp(AndroidProject.validJavaGradleAgpString))));
            // On gradle/agp error print help url null value for agp.
            expect(value.description,
                contains(RegExp(AndroidProject.gradleAgpCompatUrl)));
            expect(value.description, contains(RegExp(gradleV)));
            expect(value.description, contains(RegExp('null')));
          },
          java: java,
          androidStudio: androidStudio,
          processManager: processManager,
          androidSdk: androidSdk,
        );
      });
    });

    group('language', () {
      late XcodeProjectInterpreter xcodeProjectInterpreter;
      late MemoryFileSystem fs;
      late FlutterProjectFactory flutterProjectFactory;
      setUp(() {
        fs = MemoryFileSystem.test();
        xcodeProjectInterpreter = XcodeProjectInterpreter.test(processManager: FakeProcessManager.any());
        flutterProjectFactory = FlutterProjectFactory(
          logger: logger,
          fileSystem: fs,
        );
      });

      _testInMemory('default host app language', () async {
        final FlutterProject project = await someProject();
        expect(project.android.isKotlin, isFalse);
      });

      testUsingContext('kotlin host app language', () async {
        final FlutterProject project = await someProject();

        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return '''
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
''';
        });
        expect(project.android.isKotlin, isTrue);
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

    testUsingContext('kotlin host app language with Gradle Kotlin DSL', () async {
      final FlutterProject project = await someProject();

        addAndroidGradleFile(project.directory,
          kotlinDsl: true,
          gradleFileContent: () {
            return '''
plugins {
    id "com.android.application"
    id "kotlin-android"
    id "dev.flutter.flutter-gradle-plugin"
}
''';
        });
        expect(project.android.isKotlin, isTrue);
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

    testUsingContext('Gradle Groovy files are preferred to Gradle Kotlin files', () async {
      final FlutterProject project = await someProject();

        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return '''
plugins {
    id "com.android.application"
    id "dev.flutter.flutter-gradle-plugin"
}
''';
        });
        addAndroidGradleFile(project.directory,
          kotlinDsl: true,
          gradleFileContent: () {
            return '''
plugins {
    id("com.android.application")
    id("kotlin-android")
    id("dev.flutter.flutter-gradle-plugin")
}
''';
        });

        expect(project.android.isKotlin, isFalse);
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });
    });

    group('With mocked context', () {
      late MemoryFileSystem fs;
      late FakePlistParser testPlistUtils;
      late FakeXcodeProjectInterpreter xcodeProjectInterpreter;
      late FlutterProjectFactory flutterProjectFactory;
      setUp(() {
        fs = MemoryFileSystem.test();
        testPlistUtils = FakePlistParser();
        xcodeProjectInterpreter = FakeXcodeProjectInterpreter();
        flutterProjectFactory = FlutterProjectFactory(
          fileSystem: fs,
          logger: logger,
        );
      });

      void testWithMocks(String description, Future<void> Function() testMethod) {
        testUsingContext(description, testMethod, overrides: <Type, Generator>{
          FileSystem: () => fs,
          ProcessManager: () => FakeProcessManager.any(),
          PlistParser: () => testPlistUtils,
          XcodeProjectInterpreter: () => xcodeProjectInterpreter,
          FlutterProjectFactory: () => flutterProjectFactory,
        });
      }

      group('universal link', () {
        testWithMocks('build with flavor', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          project.ios.defaultHostInfoPlist.createSync(recursive: true);
          const String entitlementFilePath = 'myEntitlement.Entitlement';
          project.ios.hostAppRoot.childFile(entitlementFilePath).createSync(recursive: true);

          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
            target: 'Runner',
            configuration: 'config',
          );
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
            IosProject.kTeamIdKey: 'ABC',
            IosProject.kEntitlementFilePathKey: entitlementFilePath,
            'SUFFIX': 'suffix',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
          testPlistUtils.setProperty(
            PlistParser.kAssociatedDomainsKey,
            <String>[
              'applinks:example.com',
              'applinks:example2.com',
            ],
          );
          final String outputFilePath = await project.ios.outputsUniversalLinkSettings(
            target: 'Runner',
            configuration: 'config',
          );
          final File outputFile = fs.file(outputFilePath);
          final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;

          expect(
            json['associatedDomains'],
            unorderedEquals(
              <String>[
                'example.com',
                'example2.com',
              ],
            ),
          );
          expect(json['teamIdentifier'], 'ABC');
          expect(json['bundleIdentifier'], 'io.flutter.someProject.suffix');
        });

        testWithMocks('can handle entitlement file in nested directory structure.', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          project.ios.defaultHostInfoPlist.createSync(recursive: true);
          const String entitlementFilePath = 'nested/somewhere/myEntitlement.Entitlement';
          project.ios.hostAppRoot.childFile(entitlementFilePath).createSync(recursive: true);

          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
            target: 'Runner',
            configuration: 'config',
          );
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
            IosProject.kTeamIdKey: 'ABC',
            IosProject.kEntitlementFilePathKey: entitlementFilePath,
            'SUFFIX': 'suffix',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
          testPlistUtils.setProperty(
            PlistParser.kAssociatedDomainsKey,
            <String>[
              'applinks:example.com',
              'applinks:example2.com',
            ],
          );

          final String outputFilePath = await project.ios.outputsUniversalLinkSettings(
            target: 'Runner',
            configuration: 'config',
          );
          final File outputFile = fs.file(outputFilePath);
          final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;
          expect(
            json['associatedDomains'],
            unorderedEquals(
              <String>[
                'example.com',
                'example2.com',
              ],
            ),
          );
          expect(json['teamIdentifier'], 'ABC');
          expect(json['bundleIdentifier'], 'io.flutter.someProject.suffix');
        });

        testWithMocks('return empty when no entitlement', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          project.ios.defaultHostInfoPlist.createSync(recursive: true);

          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
            target: 'Runner',
            configuration: 'config',
          );
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
            IosProject.kTeamIdKey: 'ABC',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER)');
          final String outputFilePath = await project.ios.outputsUniversalLinkSettings(
            target: 'Runner',
            configuration: 'config',
          );
          final File outputFile = fs.file(outputFilePath);
          final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;
          expect(json['teamIdentifier'], 'ABC');
          expect(json['bundleIdentifier'], 'io.flutter.someProject');
          expect(json['associatedDomains'], unorderedEquals(<String>[]));
        });
      });

      group('product bundle identifier', () {
        testWithMocks('null, if no build settings or plist entries', () async {
          final FlutterProject project = await someProject();
          expect(await project.ios.productBundleIdentifier(null), isNull);
        });

        testWithMocks('from build settings, if no plist', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] =
          <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);

          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
        });

        testWithMocks('from project file, if no plist or build settings', () async {
          final FlutterProject project = await someProject();
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);

          addIosProjectFile(project.directory, projectFileContent: () {
            return projectFileWithBundleId('io.flutter.someProject');
          });
          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
        });

        testWithMocks('from plist, if no variables', () async {
          final FlutterProject project = await someProject();
          project.ios.defaultHostInfoPlist.createSync(recursive: true);
          testPlistUtils.setProperty('CFBundleIdentifier', 'io.flutter.someProject');
          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
        });

        testWithMocks('from build settings and plist, if default variable', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');

          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
        });

        testWithMocks('from build settings and plist, by substitution', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          project.ios.defaultHostInfoPlist.createSync(recursive: true);
          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
            'SUFFIX': 'suffix',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');

          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject.suffix');
        });

        testWithMocks('Always pass parsing org on ios project with flavors', () async {
          final FlutterProject project = await someProject();
          addIosProjectFile(project.directory, projectFileContent: () {
            return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
          });
          project.ios.xcodeProject.createSync();
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);

          expect(await project.organizationNames, <String>[]);
        });

        testWithMocks('fails with no flavor and defined schemes', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);

          await expectToolExitLater(
            project.ios.productBundleIdentifier(null),
            contains('You must specify a --flavor option to select one of the available schemes.'),
          );
        });

        testWithMocks('handles case insensitive flavor', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Free');
          xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
            IosProject.kProductBundleIdKey: 'io.flutter.someProject',
          };
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Free'], logger);
          const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);

          expect(await project.ios.productBundleIdentifier(buildInfo), 'io.flutter.someProject');
        });

        testWithMocks('fails with flavor and default schemes', () async {
          final FlutterProject project = await someProject();
          project.ios.xcodeProject.createSync();
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);

          await expectToolExitLater(
            project.ios.productBundleIdentifier(buildInfo),
            contains('The Xcode project does not define custom schemes. You cannot use the --flavor option.'),
          );
        });

        testWithMocks('empty surrounded by quotes', () async {
          final FlutterProject project = await someProject();
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          addIosProjectFile(project.directory, projectFileContent: () {
            return projectFileWithBundleId('', qualifier: '"');
          });
          expect(await project.ios.productBundleIdentifier(null), '');
        });

        testWithMocks('surrounded by double quotes', () async {
          final FlutterProject project = await someProject();
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          addIosProjectFile(project.directory, projectFileContent: () {
            return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
          });
          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
        });

        testWithMocks('surrounded by single quotes', () async {
          final FlutterProject project = await someProject();
          xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
          addIosProjectFile(project.directory, projectFileContent: () {
            return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
          });
          expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
        });
      });
    });

    group('application bundle name', () {
      late MemoryFileSystem fs;
      late FakeXcodeProjectInterpreter mockXcodeProjectInterpreter;
      setUp(() {
        fs = MemoryFileSystem.test();
        mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter();
      });

      testUsingContext('app product name defaults to Runner.app', () async {
        final FlutterProject project = await someProject();
        expect(await project.ios.hostAppBundleName(null), 'Runner.app');
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
      });

      testUsingContext('app product name xcodebuild settings', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
          'FULL_PRODUCT_NAME': 'My App.app',
        };
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);

        expect(await project.ios.hostAppBundleName(null), 'My App.app');
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
      });
    });

    group('organization names set', () {
      _testInMemory('is empty, if project not created', () async {
        final FlutterProject project = await someProject();
        expect(await project.organizationNames, isEmpty);
      });
      _testInMemory('is empty, if no platform folders exist', () async {
        final FlutterProject project = await someProject();
        project.directory.createSync();
        expect(await project.organizationNames, isEmpty);
      });
      _testInMemory('is populated from iOS bundle identifier', () async {
        final FlutterProject project = await someProject();
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
        });
        expect(await project.organizationNames, <String>['io.flutter']);
      });
      _testInMemory('is populated from Android application ID', () async {
        final FlutterProject project = await someProject();
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
        expect(await project.organizationNames, <String>['io.flutter']);
      });
      _testInMemory('is populated from iOS bundle identifier in plugin example', () async {
        final FlutterProject project = await someProject();
        addIosProjectFile(project.example.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
        });
        expect(await project.organizationNames, <String>['io.flutter']);
      });
      _testInMemory('is populated from Android application ID in plugin example', () async {
        final FlutterProject project = await someProject();
        addAndroidGradleFile(project.example.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
        expect(await project.organizationNames, <String>['io.flutter']);
      });
      _testInMemory('is populated from Android group in plugin', () async {
        final FlutterProject project = await someProject();
        addAndroidWithGroup(project.directory, 'io.flutter.someproject');
        expect(await project.organizationNames, <String>['io.flutter']);
      });
      _testInMemory('is singleton, if sources agree', () async {
        final FlutterProject project = await someProject();
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
        expect(await project.organizationNames, <String>['io.flutter']);
      });
      _testInMemory('is non-singleton, if sources disagree', () async {
        final FlutterProject project = await someProject();
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.clutter.someproject');
          });
        expect(
          await project.organizationNames,
          <String>['io.flutter', 'io.clutter'],
        );
      });
    });
  });
  group('watch companion', () {
    late MemoryFileSystem fs;
    late FakePlistParser testPlistParser;
    late FakeXcodeProjectInterpreter mockXcodeProjectInterpreter;
    late FlutterProjectFactory flutterProjectFactory;
    setUp(() {
      fs = MemoryFileSystem.test();
      testPlistParser = FakePlistParser();
      mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter();
      flutterProjectFactory = FlutterProjectFactory(
        fileSystem: fs,
        logger: logger,
      );
    });

    testUsingContext('cannot find bundle identifier', () async {
      final FlutterProject project = await someProject();
      final XcodeProjectInfo projectInfo = XcodeProjectInfo(<String>['WatchTarget'], <String>[], <String>[], logger);
      expect(
        await project.ios.containsWatchCompanion(
          projectInfo: projectInfo,
          buildInfo: BuildInfo.debug,
          deviceId: '123',
        ),
        isFalse,
      );
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => FakeProcessManager.any(),
      PlistParser: () => testPlistParser,
      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
      FlutterProjectFactory: () => flutterProjectFactory,
    });

    group('with bundle identifier', () {
      setUp(() {
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
        };
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>['Runner', 'WatchTarget'], <String>[], <String>['Runner', 'WatchScheme'], logger);
      });

      testUsingContext('no Info.plist in target', () async {
        final FlutterProject project = await someProject();
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('Info.plist in target does not contain WKCompanionAppBundleIdentifier', () async {
        final FlutterProject project = await someProject();
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);

        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('target WKCompanionAppBundleIdentifier is not project bundle identifier', () async {
        final FlutterProject project = await someProject();
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);

        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someOTHERproject');
        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isFalse,
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('has watch companion in plist', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someProject');

        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('has watch companion in plist with xcode variable', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
          scheme: 'Runner',
          deviceId: '123',
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
        };
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');

        expect(
          await project.ios.containsWatchCompanion(
            projectInfo:  mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('has watch companion in other scheme build settings', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        project.ios.xcodeProjectInfoFile.writeAsStringSync('''
        Build settings for action build and target "WatchTarget":
            INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.flutter.someProject
''');

        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
          scheme: 'Runner',
          deviceId: '123',
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
        };

        const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
          scheme: 'WatchScheme',
          deviceId: '123',
          isWatch: true,
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = <String, String>{
          'INFOPLIST_KEY_WKCompanionAppBundleIdentifier': 'io.flutter.someProject',
        };

        expect(
          await project.ios.containsWatchCompanion(
            projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('has watch companion in other scheme build settings with xcode variable', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        project.ios.xcodeProjectInfoFile.writeAsStringSync(r'''
        Build settings for action build and target "WatchTarget":
            INFOPLIST_KEY_WKCompanionAppBundleIdentifier = $(PRODUCT_BUNDLE_IDENTIFIER)
''');
        const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
          scheme: 'Runner',
          deviceId: '123'
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
        };

        const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
          scheme: 'WatchScheme',
          deviceId: '123',
          isWatch: true,
        );
        mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = <String, String>{
          IosProject.kProductBundleIdKey: 'io.flutter.someProject',
          'INFOPLIST_KEY_WKCompanionAppBundleIdentifier': r'$(PRODUCT_BUNDLE_IDENTIFIER)',
        };

        expect(
          await project.ios.containsWatchCompanion(
            projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
            buildInfo: BuildInfo.debug,
            deviceId: '123',
          ),
          isTrue,
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });
    });
  });
}

Future<FlutterProject> someProject({
  String? androidManifestOverride,
  bool includePubspec = false,
}) async {
  final Directory directory = globals.fs.directory('some_project');
  directory.childDirectory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
  if (includePubspec) {
    directory.childFile('pubspec.yaml')
    ..createSync(recursive: true)
    ..writeAsStringSync(validPubspec);
  }
  directory.childDirectory('ios').createSync(recursive: true);
  final Directory androidDirectory = directory
      .childDirectory('android')
      ..createSync(recursive: true);
  androidDirectory
    .childFile('AndroidManifest.xml')
    .writeAsStringSync(androidManifestOverride ?? '<manifest></manifest>');
  return FlutterProject.fromDirectory(directory);
}

Future<FlutterProject> aPluginProject({bool legacy = true}) async {
  final Directory directory = globals.fs.directory('plugin_project');
  directory.childDirectory('ios').createSync(recursive: true);
  directory.childDirectory('android').createSync(recursive: true);
  directory.childDirectory('example').createSync(recursive: true);
  String pluginPubSpec;
  if (legacy) {
    pluginPubSpec = '''
name: my_plugin
flutter:
  plugin:
    androidPackage: com.example
    pluginClass: MyPlugin
    iosPrefix: FLT
''';
  } else {
    pluginPubSpec = '''
name: my_plugin
flutter:
  plugin:
    platforms:
      android:
        package: com.example
        pluginClass: MyPlugin
      ios:
        pluginClass: MyPlugin
      linux:
        pluginClass: MyPlugin
      macos:
        pluginClass: MyPlugin
      windows:
        pluginClass: MyPlugin
''';
  }
  directory.childFile('pubspec.yaml').writeAsStringSync(pluginPubSpec);
  return FlutterProject.fromDirectory(directory);
}

Future<FlutterProject> aModuleProject() async {
  final Directory directory = globals.fs.directory('module_project');
  directory
    .childDirectory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
  directory.childFile('pubspec.yaml').writeAsStringSync('''
name: my_module
flutter:
  module:
    androidPackage: com.example
''');
  return FlutterProject.fromDirectory(directory);
}

/// Executes the [testMethod] in a context where the file system
/// is in memory.
@isTest
void _testInMemory(
  String description,
  Future<void> Function() testMethod, {
  FileSystem? fileSystem,
  Java? java,
  AndroidStudio? androidStudio,
  ProcessManager? processManager,
  AndroidSdk? androidSdk,
}) {
  Cache.flutterRoot = getFlutterRoot();
  final FileSystem testFileSystem = fileSystem ?? getFileSystemForPlatform();
  testFileSystem.directory('.dart_tool').childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
  // Transfer needed parts of the Flutter installation folder
  // to the in-memory file system used during testing.
  final Logger logger = BufferLogger.test();
  transfer(
      Cache(
        fileSystem: globals.fs,
        logger: logger,
        artifacts: <ArtifactSet>[],
        osUtils: OperatingSystemUtils(
          fileSystem: globals.fs,
          logger: logger,
          platform: globals.platform,
          processManager: globals.processManager,
        ),
        platform: globals.platform,
      ).getArtifactDirectory('gradle_wrapper'),
      testFileSystem);
  transfer(
      globals.fs
          .directory(Cache.flutterRoot)
          .childDirectory('packages')
          .childDirectory('flutter_tools')
          .childDirectory('templates'),
      testFileSystem);
  // Set up enough of the packages to satisfy the templating code.
  final File packagesFile = testFileSystem
      .directory(Cache.flutterRoot)
      .childDirectory('packages')
      .childDirectory('flutter_tools')
      .childDirectory('.dart_tool')
      .childFile('package_config.json');
  final Directory dummyTemplateImagesDirectory =
      testFileSystem.directory(Cache.flutterRoot).parent;
  dummyTemplateImagesDirectory.createSync(recursive: true);
  packagesFile.createSync(recursive: true);
  packagesFile.writeAsStringSync(json.encode(<String, Object>{
    'configVersion': 2,
    'packages': <Object>[
      <String, Object>{
        'name': 'flutter_template_images',
        'rootUri': dummyTemplateImagesDirectory.uri.toString(),
        'packageUri': 'lib/',
        'languageVersion': '2.6',
      },
    ],
  }));

  testUsingContext(
    description,
    testMethod,
    overrides: <Type, Generator>{
      FileSystem: () => testFileSystem,
      ProcessManager: () => processManager ?? FakeProcessManager.any(),
      Java : () => java,
      AndroidStudio: () => androidStudio ?? FakeAndroidStudio(),
      // Intentionally null if not set. Some ios tests fail if this is a fake.
      AndroidSdk: () => androidSdk,
      Cache: () => Cache(
            logger: globals.logger,
            fileSystem: testFileSystem,
            osUtils: globals.os,
            platform: globals.platform,
            artifacts: <ArtifactSet>[],
          ),
      FlutterProjectFactory: () => FlutterProjectFactory(
            fileSystem: testFileSystem,
            logger: globals.logger,
          ),
    },
  );
}

/// Transfers files and folders from the local file system's Flutter
/// installation to an (in-memory) file system used for testing.
void transfer(FileSystemEntity entity, FileSystem target) {
  if (entity is Directory) {
    target.directory(entity.absolute.path).createSync(recursive: true);
    for (final FileSystemEntity child in entity.listSync()) {
      transfer(child, target);
    }
  } else if (entity is File) {
    target.file(entity.absolute.path).writeAsBytesSync(entity.readAsBytesSync(), flush: true);
  } else {
    throw Exception('Unsupported FileSystemEntity ${entity.runtimeType}');
  }
}

void expectExists(FileSystemEntity entity) {
  expect(entity.existsSync(), isTrue);
}

void expectNotExists(FileSystemEntity entity) {
  expect(entity.existsSync(), isFalse);
}

void addIosProjectFile(Directory directory, {required String Function() projectFileContent}) {
  directory
      .childDirectory('ios')
      .childDirectory('Runner.xcodeproj')
      .childFile('project.pbxproj')
        ..createSync(recursive: true)
    ..writeAsStringSync(projectFileContent());
}

/// Adds app-level Gradle Groovy build file (build.gradle) to [directory].
///
/// If [kotlinDsl] is true, then build.gradle.kts is created instead of
/// build.gradle. It's the caller's responsibility to make sure that
/// [gradleFileContent] is consistent with the value of the [kotlinDsl] flag.
void addAndroidGradleFile(Directory directory, {
  required String Function() gradleFileContent, bool kotlinDsl = false,
}) {
  directory
      .childDirectory('android')
      .childDirectory('app')
      .childFile(kotlinDsl ? 'build.gradle.kts' : 'build.gradle')
    ..createSync(recursive: true)
    ..writeAsStringSync(gradleFileContent());
}

void addRootGradleFile(Directory directory,
    {required String Function() gradleFileContent}) {
  directory.childDirectory('android').childFile('build.gradle')
    ..createSync(recursive: true)
    ..writeAsStringSync(gradleFileContent());
}

void addGradleWrapperFile(Directory directory, String gradleVersion) {
  directory
      .childDirectory('android')
      .childDirectory(gradle_utils.gradleDirectoryName)
      .childDirectory(gradle_utils.gradleWrapperDirectoryName)
      .childFile(gradle_utils.gradleWrapperPropertiesFilename)
    ..createSync(recursive: true)
    ..writeAsStringSync('''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip
''');
}

FileSystem getFileSystemForPlatform() {
  return MemoryFileSystem(
    style: globals.platform.isWindows
        ? FileSystemStyle.windows
        : FileSystemStyle.posix,
  );
}

void addAndroidWithGroup(Directory directory, String id, {bool kotlinDsl = false}) {
  directory.childDirectory('android').childFile(kotlinDsl ? 'build.gradle.kts' : 'build.gradle')
    ..createSync(recursive: true)
    ..writeAsStringSync(gradleFileWithGroupId(id));
}

String get validPubspec => '''
name: hello
flutter:
''';

String get validPubspecWithDependencies => '''
name: hello
flutter:

dependencies:
  plugin_a:
  plugin_b:
''';


String get invalidPubspec => '''
name: hello
flutter:
  invalid:
''';

String get parseErrorPubspec => '''
name: hello
# Whitespace is important.
flutter:
    something:
  something_else:
''';

String projectFileWithBundleId(String id, {String? qualifier}) {
  return '''
97C147061CF9000F007C117D /* Debug */ = {
  isa = XCBuildConfiguration;
  baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
  buildSettings = {
    PRODUCT_BUNDLE_IDENTIFIER = ${qualifier ?? ''}$id${qualifier ?? ''};
    PRODUCT_NAME = "\$(TARGET_NAME)";
  };
  name = Debug;
};
''';
}

String gradleFileWithApplicationId(String id) {
  return '''
apply plugin: 'com.android.application'
android {
    compileSdk 34

    defaultConfig {
        applicationId '$id'
    }
}
''';
}

String gradleFileWithGroupId(String id) {
  return '''
group '$id'
version '1.0-SNAPSHOT'

apply plugin: 'com.android.library'

android {
    compileSdk 34
}
''';
}

File androidPluginRegistrant(Directory parent) {
  return parent.childDirectory('src')
    .childDirectory('main')
    .childDirectory('java')
    .childDirectory('io')
    .childDirectory('flutter')
    .childDirectory('plugins')
    .childFile('GeneratedPluginRegistrant.java');
}

class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
  final Map<XcodeProjectBuildContext, Map<String, String>> buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};
  late XcodeProjectInfo xcodeProjectInfo;

  @override
  Future<Map<String, String>> getBuildSettings(String projectPath, {
    XcodeProjectBuildContext? buildContext,
    Duration timeout = const Duration(minutes: 1),
  }) async {
    if (buildSettingsByBuildContext[buildContext] == null) {
      return <String, String>{};
    }
    return buildSettingsByBuildContext[buildContext]!;
  }

  @override
  Future<XcodeProjectInfo> getInfo(String projectPath, {String? projectFilename}) async {
    return xcodeProjectInfo;
  }

  @override
  bool get isInstalled => true;
}

class FakeAndroidSdkWithDir extends Fake implements AndroidSdk {
  FakeAndroidSdkWithDir(this._directory);

  final Directory _directory;

  @override
  Directory get directory => _directory;
}
