// 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/android/gradle_utils.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/version_range.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/project.dart';
import '../../src/common.dart';
import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart';

void main() {
  group('injectGradleWrapperIfNeeded', () {
    late MemoryFileSystem fileSystem;
    late Directory gradleWrapperDirectory;
    late GradleUtils gradleUtils;

    setUp(() {
      fileSystem = MemoryFileSystem.test();
      gradleWrapperDirectory =
          fileSystem.directory('cache/bin/cache/artifacts/gradle_wrapper');
      gradleWrapperDirectory.createSync(recursive: true);
      gradleWrapperDirectory
          .childFile('gradlew')
          .writeAsStringSync('irrelevant');
      gradleWrapperDirectory
          .childDirectory('gradle')
          .childDirectory('wrapper')
          .createSync(recursive: true);
      gradleWrapperDirectory
          .childDirectory('gradle')
          .childDirectory('wrapper')
          .childFile('gradle-wrapper.jar')
          .writeAsStringSync('irrelevant');
      gradleUtils = GradleUtils(
        cache: Cache.test(
            processManager: FakeProcessManager.any(), fileSystem: fileSystem),
        platform: FakePlatform(environment: <String, String>{}),
        logger: BufferLogger.test(),
        operatingSystemUtils: FakeOperatingSystemUtils(),
      );
    });

    testWithoutContext('injects the wrapper when all files are missing', () {
      final Directory sampleAppAndroid =
          fileSystem.directory('/sample-app/android');
      sampleAppAndroid.createSync(recursive: true);

      gradleUtils.injectGradleWrapperIfNeeded(sampleAppAndroid);

      expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);

      expect(
          sampleAppAndroid
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.jar')
              .existsSync(),
          isTrue);

      expect(
          sampleAppAndroid
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.properties')
              .existsSync(),
          isTrue);

      expect(
          sampleAppAndroid
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.properties')
              .readAsStringSync(),
          'distributionBase=GRADLE_USER_HOME\n'
          'distributionPath=wrapper/dists\n'
          'zipStoreBase=GRADLE_USER_HOME\n'
          'zipStorePath=wrapper/dists\n'
          'distributionUrl=https\\://services.gradle.org/distributions/gradle-7.6.3-all.zip\n');
    });

    testWithoutContext('injects the wrapper when some files are missing', () {
      final Directory sampleAppAndroid =
          fileSystem.directory('/sample-app/android');
      sampleAppAndroid.createSync(recursive: true);

      // There's an existing gradlew
      sampleAppAndroid
          .childFile('gradlew')
          .writeAsStringSync('existing gradlew');

      gradleUtils.injectGradleWrapperIfNeeded(sampleAppAndroid);

      expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);
      expect(sampleAppAndroid.childFile('gradlew').readAsStringSync(),
          equals('existing gradlew'));

      expect(
          sampleAppAndroid
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.jar')
              .existsSync(),
          isTrue);

      expect(
          sampleAppAndroid
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.properties')
              .existsSync(),
          isTrue);

      expect(
          sampleAppAndroid
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.properties')
              .readAsStringSync(),
          'distributionBase=GRADLE_USER_HOME\n'
          'distributionPath=wrapper/dists\n'
          'zipStoreBase=GRADLE_USER_HOME\n'
          'zipStorePath=wrapper/dists\n'
          'distributionUrl=https\\://services.gradle.org/distributions/gradle-7.6.3-all.zip\n');
    });

    testWithoutContext(
        'injects the wrapper and the Gradle version is derivated from the AGP version',
        () {
      const Map<String, String> testCases = <String, String>{
        // AGP version : Gradle version
        '1.0.0': '2.3',
        '3.3.1': '4.10.2',
        '3.0.0': '4.1',
        '3.0.5': '4.1',
        '3.0.9': '4.1',
        '3.1.0': '4.4',
        '3.2.0': '4.6',
        '3.3.0': '4.10.2',
        '3.4.0': '5.6.2',
        '3.5.0': '5.6.2',
        '4.0.0': '6.7',
        '4.0.5': '6.7',
        '4.1.0': '6.7',
      };

      for (final MapEntry<String, String> entry in testCases.entries) {
        final Directory sampleAppAndroid =
            fileSystem.systemTempDirectory.createTempSync('flutter_android.');
        sampleAppAndroid.childFile('build.gradle').writeAsStringSync('''
  buildscript {
      dependencies {
          classpath 'com.android.tools.build:gradle:${entry.key}'
      }
  }
  ''');
        gradleUtils.injectGradleWrapperIfNeeded(sampleAppAndroid);

        expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);

        expect(
            sampleAppAndroid
                .childDirectory('gradle')
                .childDirectory('wrapper')
                .childFile('gradle-wrapper.jar')
                .existsSync(),
            isTrue);

        expect(
            sampleAppAndroid
                .childDirectory('gradle')
                .childDirectory('wrapper')
                .childFile('gradle-wrapper.properties')
                .existsSync(),
            isTrue);

        expect(
            sampleAppAndroid
                .childDirectory('gradle')
                .childDirectory('wrapper')
                .childFile('gradle-wrapper.properties')
                .readAsStringSync(),
            'distributionBase=GRADLE_USER_HOME\n'
            'distributionPath=wrapper/dists\n'
            'zipStoreBase=GRADLE_USER_HOME\n'
            'zipStorePath=wrapper/dists\n'
            'distributionUrl=https\\://services.gradle.org/distributions/gradle-${entry.value}-all.zip\n');
      }
    });

    testWithoutContext('returns the gradlew path', () {
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      androidDirectory.childFile('gradlew').createSync();
      androidDirectory.childFile('gradlew.bat').createSync();
      androidDirectory.childFile('gradle.properties').createSync();

      final FlutterProject flutterProject = FlutterProjectFactory(
        logger: BufferLogger.test(),
        fileSystem: fileSystem,
      ).fromDirectory(fileSystem.currentDirectory);

      expect(
        gradleUtils.getExecutable(flutterProject),
        androidDirectory.childFile('gradlew').path,
      );
    });
    testWithoutContext('getGradleFileName for notWindows', () {
      expect(getGradlewFileName(notWindowsPlatform), 'gradlew');
    });
    testWithoutContext('getGradleFileName for windows', () {
      expect(getGradlewFileName(windowsPlatform), 'gradlew.bat');
    });

    testWithoutContext('returns the gradle properties file', () async {
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      final Directory wrapperDirectory = androidDirectory
          .childDirectory(gradleDirectoryName)
          .childDirectory(gradleWrapperDirectoryName)
        ..createSync(recursive: true);
      final File expectedFile = await wrapperDirectory
          .childFile(gradleWrapperPropertiesFilename)
          .create();
      final File gradleWrapperFile = getGradleWrapperFile(androidDirectory);
      expect(gradleWrapperFile.path, expectedFile.path);
    });

    testWithoutContext('returns the gradle wrapper version', () async {
      const String expectedVersion = '7.4.2';
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      final Directory wrapperDirectory = androidDirectory
          .childDirectory('gradle')
          .childDirectory('wrapper')
        ..createSync(recursive: true);
      wrapperDirectory
          .childFile('gradle-wrapper.properties')
          .writeAsStringSync('''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\\://services.gradle.org/distributions/gradle-$expectedVersion-all.zip
''');

      expect(
        await getGradleVersion(
            androidDirectory, BufferLogger.test(), FakeProcessManager.empty()),
        expectedVersion,
      );
    });

        testWithoutContext('ignores gradle comments', () async {
      const String expectedVersion = '7.4.2';
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      final Directory wrapperDirectory = androidDirectory
          .childDirectory('gradle')
          .childDirectory('wrapper')
        ..createSync(recursive: true);
      wrapperDirectory
          .childFile('gradle-wrapper.properties')
          .writeAsStringSync('''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
# distributionUrl=https\\://services.gradle.org/distributions/gradle-8.0.2-all.zip
distributionUrl=https\\://services.gradle.org/distributions/gradle-$expectedVersion-all.zip
# distributionUrl=https\\://services.gradle.org/distributions/gradle-8.0.2-all.zip
''');

      expect(
        await getGradleVersion(
            androidDirectory, BufferLogger.test(), FakeProcessManager.empty()),
        expectedVersion,
      );
    });

    testWithoutContext('returns gradlew version, whitespace, location', () async {
      const String expectedVersion = '7.4.2';
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      final Directory wrapperDirectory = androidDirectory
          .childDirectory('gradle')
          .childDirectory('wrapper')
        ..createSync(recursive: true);
      // Distribution url is not the last line.
      // Whitespace around distribution url.
      wrapperDirectory
          .childFile('gradle-wrapper.properties')
          .writeAsStringSync('''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl = https\\://services.gradle.org/distributions/gradle-$expectedVersion-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
''');

      expect(
        await getGradleVersion(
            androidDirectory, BufferLogger.test(), FakeProcessManager.empty()),
        expectedVersion,
      );
    });

    testWithoutContext('does not crash on hypothetical new format', () async {
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      final Directory wrapperDirectory = androidDirectory
          .childDirectory('gradle')
          .childDirectory('wrapper')
        ..createSync(recursive: true);
      // Distribution url is not the last line.
      // Whitespace around distribution url.
      wrapperDirectory
          .childFile('gradle-wrapper.properties')
          .writeAsStringSync(r'distributionUrl=https\://services.gradle.org/distributions/gradle_7.4.2_all.zip');

      // FakeProcessManager.any is used here and not in other getGradleVersion
      // tests because this test does not care about process fallback logic.
      expect(
        await getGradleVersion(
            androidDirectory, BufferLogger.test(), FakeProcessManager.any()),
        isNull,
      );
    });

    testWithoutContext('returns the installed gradle version', () async {
      const String expectedVersion = '7.4.2';
      const String gradleOutput = '''

------------------------------------------------------------
Gradle $expectedVersion
------------------------------------------------------------

Build time:   2022-03-31 15:25:29 UTC
Revision:     540473b8118064efcc264694cbcaa4b677f61041

Kotlin:       1.5.31
Groovy:       3.0.9
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          11.0.18 (Azul Systems, Inc. 11.0.18+10-LTS)
OS:           Mac OS X 13.2.1 aarch64
''';
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      final ProcessManager processManager = FakeProcessManager.empty()
        ..addCommand(const FakeCommand(
            command: <String>['gradle', gradleVersionFlag],
            stdout: gradleOutput));

      expect(
        await getGradleVersion(
          androidDirectory,
          BufferLogger.test(),
          processManager,
        ),
        expectedVersion,
      );
    });

    testWithoutContext('returns the installed gradle with whitespace formatting', () async {
      const String expectedVersion = '7.4.2';
      const String gradleOutput = 'Gradle   $expectedVersion';
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      final ProcessManager processManager = FakeProcessManager.empty()
        ..addCommand(const FakeCommand(
            command: <String>['gradle', gradleVersionFlag],
            stdout: gradleOutput));

      expect(
        await getGradleVersion(
          androidDirectory,
          BufferLogger.test(),
          processManager,
        ),
        expectedVersion,
      );
    });

    testWithoutContext('returns the AGP version when set', () async {
      const String expectedVersion = '7.3.0';
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      androidDirectory.childFile('build.gradle').writeAsStringSync('''
buildscript {
    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:$expectedVersion'
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}
''');

      expect(
        getAgpVersion(androidDirectory, BufferLogger.test()),
        expectedVersion,
      );
    });
    testWithoutContext('returns null when AGP version not set', () async {
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      androidDirectory.childFile('build.gradle').writeAsStringSync('''
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
    }
}
allprojects {
    repositories {
        google()
        mavenCentral()
    }
}
''');

      expect(
        getAgpVersion(androidDirectory, BufferLogger.test()),
        null,
      );
    });
    testWithoutContext('returns the AGP version when beta', () async {
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      androidDirectory.childFile('build.gradle').writeAsStringSync(r'''
buildscript {
    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:7.3.0-beta03'
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}
''');

      expect(
        getAgpVersion(androidDirectory, BufferLogger.test()),
        '7.3.0',
      );
    });

    testWithoutContext('returns the AGP version when in settings', () async {
      final Directory androidDirectory = fileSystem.directory('/android')
        ..createSync();
      // File must exist and can not have agp defined.
      androidDirectory.childFile('build.gradle').writeAsStringSync(r'');
      androidDirectory.childFile('settings.gradle').writeAsStringSync(r'''
pluginManagement {
    def flutterSdkPath = {
        def properties = new Properties()
        file("local.properties").withInputStream { properties.load(it) }
        def flutterSdkPath = properties.getProperty("flutter.sdk")
        assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
        return flutterSdkPath
    }
    settings.ext.flutterSdkPath = flutterSdkPath()

    includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")

    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }

    plugins {
        id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
    }
}

plugins {
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
    // Decoy value to ensure we ignore commented out lines.
    // id "com.android.application" version "6.1.0" apply false
    id "com.android.application" version "7.3.0" apply false
}

include ":app"
''');

      expect(
        getAgpVersion(androidDirectory, BufferLogger.test()),
        '7.3.0',
      );
    });

    group('validates gradle/agp versions', () {
      final List<GradleAgpTestData> testData = <GradleAgpTestData>[
        // Values too new *these need to be updated* when
        // max known gradle and max known agp versions are updated:
        // Newer tools version supports max gradle version.
        GradleAgpTestData(true, agpVersion: '8.2', gradleVersion: '8.0'),
        // Newer tools version does not even meet current gradle version requirements.
        GradleAgpTestData(false, agpVersion: '8.2', gradleVersion: '7.3'),
        // Newer tools version requires newer gradle version.
        GradleAgpTestData(true, agpVersion: '8.3', gradleVersion: '8.1'),

        // Template versions of Gradle/AGP.
        GradleAgpTestData(true, agpVersion: templateAndroidGradlePluginVersion, gradleVersion: templateDefaultGradleVersion),
        GradleAgpTestData(true, agpVersion: templateAndroidGradlePluginVersionForModule, gradleVersion: templateDefaultGradleVersion),

        // Minimums as defined in
        // https://developer.android.com/studio/releases/gradle-plugin#updating-gradle
        GradleAgpTestData(true, agpVersion: '8.1', gradleVersion: '8.0'),
        GradleAgpTestData(true, agpVersion: '8.0', gradleVersion: '8.0'),
        GradleAgpTestData(true, agpVersion: '7.4', gradleVersion: '7.5'),
        GradleAgpTestData(true, agpVersion: '7.3', gradleVersion: '7.4'),
        GradleAgpTestData(true, agpVersion: '7.2', gradleVersion: '7.3.3'),
        GradleAgpTestData(true, agpVersion: '7.1', gradleVersion: '7.2'),
        GradleAgpTestData(true, agpVersion: '7.0', gradleVersion: '7.0'),
        GradleAgpTestData(true, agpVersion: '4.2.0', gradleVersion: '6.7.1'),
        GradleAgpTestData(true, agpVersion: '4.1.0', gradleVersion: '6.5'),
        GradleAgpTestData(true, agpVersion: '4.0.0', gradleVersion: '6.1.1'),
        GradleAgpTestData(true, agpVersion: '3.6.0', gradleVersion: '5.6.4'),
        GradleAgpTestData(true, agpVersion: '3.5.0', gradleVersion: '5.4.1'),
        GradleAgpTestData(true, agpVersion: '3.4.0', gradleVersion: '5.1.1'),
        GradleAgpTestData(true, agpVersion: '3.3.0', gradleVersion: '4.10.1'),
        // Values too old:
        GradleAgpTestData(false, agpVersion: '3.3.0', gradleVersion: '4.9'),
        GradleAgpTestData(false, agpVersion: '7.3', gradleVersion: '7.2'),
        GradleAgpTestData(false, agpVersion: '3.0.0', gradleVersion: '7.2'),
        // Null values:
        // ignore: avoid_redundant_argument_values
        GradleAgpTestData(false, agpVersion: null, gradleVersion: '7.2'),
        // ignore: avoid_redundant_argument_values
        GradleAgpTestData(false, agpVersion: '3.0.0', gradleVersion: null),
        // ignore: avoid_redundant_argument_values
        GradleAgpTestData(false, agpVersion: null, gradleVersion: null),
        // Middle AGP cases:
        GradleAgpTestData(true, agpVersion: '8.0.1', gradleVersion: '8.0'),
        GradleAgpTestData(true, agpVersion: '7.4.1', gradleVersion: '7.5'),
        GradleAgpTestData(true, agpVersion: '7.3.1', gradleVersion: '7.4'),
        GradleAgpTestData(true, agpVersion: '7.2.1', gradleVersion: '7.3.3'),
        GradleAgpTestData(true, agpVersion: '7.1.1', gradleVersion: '7.2'),
        GradleAgpTestData(true, agpVersion: '7.0.1', gradleVersion: '7.0'),
        GradleAgpTestData(true, agpVersion: '4.2.1', gradleVersion: '6.7.1'),
        GradleAgpTestData(true, agpVersion: '4.1.1', gradleVersion: '6.5'),
        GradleAgpTestData(true, agpVersion: '4.0.1', gradleVersion: '6.1.1'),
        GradleAgpTestData(true, agpVersion: '3.6.1', gradleVersion: '5.6.4'),
        GradleAgpTestData(true, agpVersion: '3.5.1', gradleVersion: '5.4.1'),
        GradleAgpTestData(true, agpVersion: '3.4.1', gradleVersion: '5.1.1'),
        GradleAgpTestData(true, agpVersion: '3.3.1', gradleVersion: '4.10.1'),

        // Higher gradle cases:
        GradleAgpTestData(true, agpVersion: '7.4', gradleVersion: '8.0'),
        GradleAgpTestData(true, agpVersion: '7.3', gradleVersion: '7.5'),
        GradleAgpTestData(true, agpVersion: '7.2', gradleVersion: '7.4'),
        GradleAgpTestData(true, agpVersion: '7.1', gradleVersion: '7.3.3'),
        GradleAgpTestData(true, agpVersion: '7.0', gradleVersion: '7.2'),
        GradleAgpTestData(true, agpVersion: '4.2.0', gradleVersion: '7.0'),
        GradleAgpTestData(true, agpVersion: '4.1.0', gradleVersion: '6.7.1'),
        GradleAgpTestData(true, agpVersion: '4.0.0', gradleVersion: '6.5'),
        GradleAgpTestData(true, agpVersion: '3.6.0', gradleVersion: '6.1.1'),
        GradleAgpTestData(true, agpVersion: '3.5.0', gradleVersion: '5.6.4'),
        GradleAgpTestData(true, agpVersion: '3.4.0', gradleVersion: '5.4.1'),
        GradleAgpTestData(true, agpVersion: '3.3.0', gradleVersion: '5.1.1'),
      ];
      for (final GradleAgpTestData data in testData) {
        test('(gradle, agp): (${data.gradleVersion}, ${data.agpVersion})', () {
          expect(
              validateGradleAndAgp(
                BufferLogger.test(),
                gradleV: data.gradleVersion,
                agpV: data.agpVersion,
              ),
              data.validPair ? isTrue : isFalse,
              reason: 'G: ${data.gradleVersion}, AGP: ${data.agpVersion}');
        });
      }
    });

    group('Parse gradle version from distribution url', () {
      testWithoutContext('null distribution url returns null version', () {
        expect(parseGradleVersionFromDistributionUrl(null), null);
      });

      testWithoutContext('unparseable format returns null', () {
        const String distributionUrl = 'aString';
        expect(parseGradleVersionFromDistributionUrl(distributionUrl), null);
      });

      testWithoutContext("recognizable 'all' format returns correct version", () {
        const String distributionUrl = r'distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip';
        expect(parseGradleVersionFromDistributionUrl(distributionUrl), '6.7');
      });

      testWithoutContext("recognizable 'bin' format returns correct version", () {
        const String distributionUrl = r'distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip';
        expect(parseGradleVersionFromDistributionUrl(distributionUrl), '6.7');
      });

      testWithoutContext("recognizable 'rc' format returns correct version", () {
        const String distributionUrl = r'distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-rc-3-all.zip';
        expect(parseGradleVersionFromDistributionUrl(distributionUrl), '8.1');
      });
    });

    group('validates java/gradle versions', () {
      final List<JavaGradleTestData> testData = <JavaGradleTestData>[
        // Values too new *these need to be updated* when
        // max supported java and max known gradle versions are updated:
        // Newer tools version does not even meet current gradle version requiremnts.
        JavaGradleTestData(false, javaVersion: '20', gradleVersion: '7.5'),
        // Newer tools version requires newer gradle version.
        JavaGradleTestData(true, javaVersion: '20', gradleVersion: '8.1'),
        // Max known unsupported Java version.
        JavaGradleTestData(true, javaVersion: '24', gradleVersion: maxKnownAndSupportedGradleVersion),
        // Minimums as defined in
        // https://docs.gradle.org/current/userguide/compatibility.html#java
        JavaGradleTestData(true, javaVersion: '19', gradleVersion: '7.6'),
        JavaGradleTestData(true, javaVersion: '18', gradleVersion: '7.5'),
        JavaGradleTestData(true, javaVersion: '17', gradleVersion: '7.3'),
        JavaGradleTestData(true, javaVersion: '16', gradleVersion: '7.0'),
        JavaGradleTestData(true, javaVersion: '15', gradleVersion: '6.7'),
        JavaGradleTestData(true, javaVersion: '14', gradleVersion: '6.3'),
        JavaGradleTestData(true, javaVersion: '13', gradleVersion: '6.0'),
        JavaGradleTestData(true, javaVersion: '12', gradleVersion: '5.4'),
        JavaGradleTestData(true, javaVersion: '11', gradleVersion: '5.0'),
        JavaGradleTestData(true, javaVersion: '1.10', gradleVersion: '4.7'),
        JavaGradleTestData(true, javaVersion: '1.9', gradleVersion: '4.3'),
        JavaGradleTestData(true, javaVersion: '1.8', gradleVersion: '2.0'),
        // Gradle too old for Java version.
        JavaGradleTestData(false, javaVersion: '19', gradleVersion: '6.7'),
        JavaGradleTestData(false, javaVersion: '11', gradleVersion: '4.10.1'),
        JavaGradleTestData(false, javaVersion: '1.9', gradleVersion: '4.1'),
        // Null values:
        // ignore: avoid_redundant_argument_values
        JavaGradleTestData(false, javaVersion: null, gradleVersion: '7.2'),
        // ignore: avoid_redundant_argument_values
        JavaGradleTestData(false, javaVersion: '11', gradleVersion: null),
        // ignore: avoid_redundant_argument_values
        JavaGradleTestData(false, javaVersion: null, gradleVersion: null),
        // Middle Java cases:
        // https://www.java.com/releases/
        JavaGradleTestData(true, javaVersion: '19.0.2', gradleVersion: '8.0.2'),
        JavaGradleTestData(true, javaVersion: '19.0.2', gradleVersion: '8.0.0'),
        JavaGradleTestData(true, javaVersion: '18.0.2', gradleVersion: '8.0.2'),
        JavaGradleTestData(true, javaVersion: '17.0.3', gradleVersion: '7.5'),
        JavaGradleTestData(true, javaVersion: '16.0.1', gradleVersion: '7.3'),
        JavaGradleTestData(true, javaVersion: '15.0.2', gradleVersion: '7.3'),
        JavaGradleTestData(true, javaVersion: '14.0.1', gradleVersion: '7.0'),
        JavaGradleTestData(true, javaVersion: '13.0.2', gradleVersion: '6.7'),
        JavaGradleTestData(true, javaVersion: '12.0.2', gradleVersion: '6.3'),
        JavaGradleTestData(true, javaVersion: '11.0.18', gradleVersion: '6.0'),
        // Higher gradle cases:
        JavaGradleTestData(true, javaVersion: '19', gradleVersion: '8.0'),
        JavaGradleTestData(true, javaVersion: '18', gradleVersion: '8.0'),
        JavaGradleTestData(true, javaVersion: '17', gradleVersion: '7.5'),
        JavaGradleTestData(true, javaVersion: '16', gradleVersion: '7.3'),
        JavaGradleTestData(true, javaVersion: '15', gradleVersion: '7.3'),
        JavaGradleTestData(true, javaVersion: '14', gradleVersion: '7.0'),
        JavaGradleTestData(true, javaVersion: '13', gradleVersion: '6.7'),
        JavaGradleTestData(true, javaVersion: '12', gradleVersion: '6.3'),
        JavaGradleTestData(true, javaVersion: '11', gradleVersion: '6.0'),
        JavaGradleTestData(true, javaVersion: '1.10', gradleVersion: '5.4'),
        JavaGradleTestData(true, javaVersion: '1.9', gradleVersion: '5.0'),
        JavaGradleTestData(true, javaVersion: '1.8', gradleVersion: '4.3'),
      ];

      for (final JavaGradleTestData data in testData) {
        testWithoutContext(
            '(Java, gradle): (${data.javaVersion}, ${data.gradleVersion})', () {
          expect(
              validateJavaAndGradle(
                BufferLogger.test(),
                javaV: data.javaVersion,
                gradleV: data.gradleVersion,
              ),
              data.validPair ? isTrue : isFalse,
              reason: 'J: ${data.javaVersion}, G: ${data.gradleVersion}');
        });
      }
    });
  });

  group('validates java/AGP versions', () {
    final List<JavaAgpTestData> testData = <JavaAgpTestData>[
      // Strictly too old Java versions for known AGP versions.
      JavaAgpTestData(false, javaVersion: '1.6', agpVersion: maxKnownAgpVersion),
      JavaAgpTestData(false, javaVersion: '1.6', agpVersion: maxKnownAndSupportedAgpVersion),
      JavaAgpTestData(false, javaVersion: '1.6', agpVersion: '4.2'),
      // Strictly too old AGP versions.
      JavaAgpTestData(false, javaVersion: '1.8', agpVersion: '1.0'),
      JavaAgpTestData(false, javaVersion: '1.8', agpVersion: '4.1'),
      JavaAgpTestData(false, javaVersion: '1.8', agpVersion: '2.3'),
      // Strictly too new Java versions for defined AGP versions.
      JavaAgpTestData(true, javaVersion: '18', agpVersion: '8.1'),
      JavaAgpTestData(true, javaVersion: '18', agpVersion: '7.4'),
      JavaAgpTestData(true, javaVersion: '18', agpVersion: '4.2'),
      // Strictly too new AGP versions.
      // *The tests that follow need to be updated* when max supported AGP versions are updated:
      JavaAgpTestData(false, javaVersion: '24', agpVersion: '8.3'),
      JavaAgpTestData(false, javaVersion: '20', agpVersion: '8.3'),
      JavaAgpTestData(false, javaVersion: '17', agpVersion: '8.3'),
      // Java 17 & patch versions compatibility cases
      // *The tests that follow need to be updated* when maxKnownAndSupportedAgpVersion is
      // updated:
      JavaAgpTestData(false, javaVersion: '17', agpVersion: '8.2'),
      JavaAgpTestData(true, javaVersion: '17', agpVersion: maxKnownAndSupportedAgpVersion),
      JavaAgpTestData(true, javaVersion: '17', agpVersion: '8.1'),
      JavaAgpTestData(true, javaVersion: '17', agpVersion: '8.0'),
      JavaAgpTestData(true, javaVersion: '17', agpVersion: '7.4'),
      JavaAgpTestData(false, javaVersion: '17.0.3', agpVersion: '8.2'),
      JavaAgpTestData(true, javaVersion: '17.0.3', agpVersion: maxKnownAndSupportedAgpVersion),
      JavaAgpTestData(true, javaVersion: '17.0.3', agpVersion: '8.1'),
      JavaAgpTestData(true, javaVersion: '17.0.3', agpVersion: '8.0'),
      JavaAgpTestData(true, javaVersion: '17.0.3', agpVersion: '7.4'),
      // Java 11 & patch versions compatibility cases
      JavaAgpTestData(false, javaVersion: '11', agpVersion: '8.0'),
      JavaAgpTestData(true, javaVersion: '11', agpVersion: '7.4'),
      JavaAgpTestData(true, javaVersion: '11', agpVersion: '7.2'),
      JavaAgpTestData(true, javaVersion: '11', agpVersion: '7.0'),
      JavaAgpTestData(true, javaVersion: '11', agpVersion: '4.2'),
      JavaAgpTestData(false, javaVersion: '11.0.18', agpVersion: '8.0'),
      JavaAgpTestData(true, javaVersion: '11.0.18', agpVersion: '7.4'),
      JavaAgpTestData(true, javaVersion: '11.0.18', agpVersion: '7.2'),
      JavaAgpTestData(true, javaVersion: '11.0.18', agpVersion: '7.0'),
      JavaAgpTestData(true, javaVersion: '11.0.18', agpVersion: '4.2'),
      // Java 8 compatibility cases
      JavaAgpTestData(false, javaVersion: '1.8', agpVersion: '7.0'),
      JavaAgpTestData(true, javaVersion: '1.8', agpVersion: oldestDocumentedJavaAgpCompatibilityVersion), // agpVersion = 4.2
      JavaAgpTestData(false, javaVersion: '1.8', agpVersion: '4.1'),
      // Null value cases
      // ignore: avoid_redundant_argument_values
      JavaAgpTestData(false, javaVersion: null, agpVersion: '4.2'),
      // ignore: avoid_redundant_argument_values
      JavaAgpTestData(false, javaVersion: '1.8', agpVersion: null),
      // ignore: avoid_redundant_argument_values
      JavaAgpTestData(false, javaVersion: null, agpVersion: null),
    ];

      for (final JavaAgpTestData data in testData) {
        testWithoutContext(
            '(Java, agp): (${data.javaVersion}, ${data.agpVersion})', () {
          expect(
              validateJavaAndAgp(
                BufferLogger.test(),
                javaV: data.javaVersion,
                agpV: data.agpVersion,
              ),
              data.validPair ? isTrue : isFalse,
              reason: 'J: ${data.javaVersion}, G: ${data.agpVersion}');
        });
      }
  });

  group('detecting valid Gradle/AGP versions for given Java version and vice versa', () {
    testWithoutContext('getValidGradleVersionRangeForJavaVersion returns valid Gradle version range for Java version', () {
      final Logger testLogger = BufferLogger.test();
      // Java version too high.
      expect(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: oneMajorVersionHigherJavaVersion), isNull);
      // Maximum known Java version.
      // *The test case that follows needs to be updated* when higher versions of Java are supported:
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '20'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '20.0.2')),
          isNull));
      // Known supported Java versions.
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '19'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '19.0.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '19',
              javaMax: '20',
              gradleMin: '7.6',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '18'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '18.0.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '18',
              javaMax: '19',
              gradleMin: '7.5',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '17'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '17.0.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '17',
              javaMax: '18',
              gradleMin: '7.3',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '16'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '16.0.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '16',
              javaMax: '17',
              gradleMin: '7.0',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '15'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '15.0.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '15',
              javaMax: '16',
              gradleMin: '6.7',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '14'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '14.0.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '14',
              javaMax: '15',
              gradleMin: '6.3',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '13'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '13.0.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '13',
              javaMax: '14',
              gradleMin: '6.0',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '12'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '12.0.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '12',
              javaMax: '13',
              gradleMin: '5.4',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '11'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '11.0.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '11',
              javaMax: '12',
              gradleMin: '5.0',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.10'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.10.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '1.10',
              javaMax: '1.11',
              gradleMin: '4.7',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.9'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.9.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '1.9',
              javaMax: '1.10',
              gradleMin: '4.3',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      // Java 1.8 -- return oldest documented compatibility info
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.8'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.8.2')),
          equals(
            const JavaGradleCompat(
              javaMin: '1.8',
              javaMax: '1.9',
              gradleMin: '2.0',
              gradleMax: maxKnownAndSupportedGradleVersion))));
      // Java version too low.
      expect(
        getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.7'),
        allOf(
          equals(getValidGradleVersionRangeForJavaVersion(testLogger, javaV: '1.7.2')),
          isNull));
    });

    testWithoutContext('getMinimumAgpVersionForJavaVersion returns minimum AGP version for Java version', () {
      final Logger testLogger = BufferLogger.test();
      // Maximum known Java version.
      // *The test case that follows needs to be updated* as higher versions of AGP are supported:
      expect(
        getMinimumAgpVersionForJavaVersion(testLogger, javaV: oneMajorVersionHigherJavaVersion),
        equals(
          const JavaAgpCompat(
            javaMin: '17',
            javaDefault: '17',
            agpMin: '8.0',
            agpMax: '8.1')));
      // Known Java versions.
      expect(
        getMinimumAgpVersionForJavaVersion(testLogger, javaV: '17'),
        allOf(
          equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '17.0.2')),
          equals(
            const JavaAgpCompat(
              javaMin: '17',
              javaDefault: '17',
              agpMin: '8.0',
              agpMax: '8.1'))));
      expect(
        getMinimumAgpVersionForJavaVersion(testLogger, javaV: '15'),
        allOf(
          equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '15.0.2')),
          equals(
            const JavaAgpCompat(
              javaMin: '11',
              javaDefault: '11',
              agpMin: '7.0',
              agpMax: '7.4'))));
      expect(
        getMinimumAgpVersionForJavaVersion(testLogger, javaV: '11'),
        allOf(
          equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '11.0.2')),
          equals(
            const JavaAgpCompat(
              javaMin: '11',
              javaDefault: '11',
              agpMin: '7.0',
              agpMax: '7.4'))));
      expect(
        getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.9'),
        allOf(
          equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.9.2')),
          equals(
            const JavaAgpCompat(
              javaMin: '1.8',
              javaDefault: '1.8',
              agpMin: '4.2',
              agpMax: '4.2'))));
      expect(
        getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.8'),
        allOf(
          equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.8.2')),
          equals(
            const JavaAgpCompat(
              javaMin: '1.8',
              javaDefault: '1.8',
              agpMin: '4.2',
              agpMax: '4.2'))));
      // Java version too low.
      expect(
        getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.7'),
        allOf(
          equals(getMinimumAgpVersionForJavaVersion(testLogger, javaV: '1.7.2')),
          isNull));
    });

    testWithoutContext('getJavaVersionFor returns expected Java version range', () {
      // Strictly too old Gradle and AGP versions.
      expect(getJavaVersionFor(gradleV: '1.9', agpV: '4.1'), equals(const VersionRange(null, null)));
      // Strictly too old Gradle or AGP version.
      expect(getJavaVersionFor(gradleV: '1.9', agpV: '4.2'), equals(const VersionRange('1.8', null)));
      expect(getJavaVersionFor(gradleV: '2.0', agpV: '4.1'), equals(const VersionRange(null, '1.9')));
      // Strictly too new Gradle and AGP versions.
      expect(getJavaVersionFor(gradleV: '8.1', agpV: '8.2'), equals(const VersionRange(null, null)));
      // Strictly too new Gradle version and maximum version of AGP.
      //*This test case will need its expected Java range updated when a new version of AGP is supported.*
      expect(getJavaVersionFor(gradleV: '8.1', agpV: maxKnownAndSupportedAgpVersion), equals(const VersionRange('17', null)));
      // Strictly too new AGP version and maximum version of Gradle.
      //*This test case will need its expected Java range updated when a new version of Gradle is supported.*
      expect(getJavaVersionFor(gradleV: maxKnownAndSupportedGradleVersion, agpV: '8.2'), equals(const VersionRange(null, '20')));
      // Tests with a known compatible Gradle/AGP version pair.
      expect(getJavaVersionFor(gradleV: '7.0', agpV: '7.2'), equals(const VersionRange('11', '17')));
      expect(getJavaVersionFor(gradleV: '7.1', agpV: '7.2'), equals(const VersionRange('11', '17')));
      expect(getJavaVersionFor(gradleV: '7.2.2', agpV: '7.2'), equals(const VersionRange('11', '17')));
      expect(getJavaVersionFor(gradleV: '7.1', agpV: '7.0'), equals(const VersionRange('11', '17')));
      expect(getJavaVersionFor(gradleV: '7.1', agpV: '7.2'), equals(const VersionRange('11', '17')));
      expect(getJavaVersionFor(gradleV: '7.1', agpV: '7.4'), equals(const VersionRange('11', '17')));
    });
  });
}

class GradleAgpTestData {
  GradleAgpTestData(this.validPair, {this.gradleVersion, this.agpVersion});
  final String? gradleVersion;
  final String? agpVersion;
  final bool validPair;
}

class JavaGradleTestData {
  JavaGradleTestData(this.validPair, {this.javaVersion, this.gradleVersion});
  final String? gradleVersion;
  final String? javaVersion;
  final bool validPair;
}

class JavaAgpTestData {
  JavaAgpTestData(this.validPair, {this.javaVersion, this.agpVersion});
  final String? agpVersion;
  final String? javaVersion;
  final bool validPair;
}

final Platform windowsPlatform = FakePlatform(
  operatingSystem: 'windows',
  environment: <String, String>{
    'PROGRAMFILES(X86)':  r'C:\Program Files (x86)\',
    'FLUTTER_ROOT': r'C:\flutter',
    'USERPROFILE': '/',
  }
);
final Platform notWindowsPlatform = FakePlatform(
  environment: <String, String>{
    'FLUTTER_ROOT': r'/users/someuser/flutter',
  }
);