// Copyright 2017 The Chromium 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 'dart:async';
import 'dart:io';

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.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
import '../../src/pubspec_schema.dart';

void main() {
  Cache.flutterRoot = getFlutterRoot();
  group('gradle build', () {
    test('do not crash if there is no Android SDK', () async {
      Exception shouldBeToolExit;
      try {
        // We'd like to always set androidSdk to null and test updateLocalProperties. But that's
        // currently impossible as the test is not hermetic. Luckily, our bots don't have Android
        // SDKs yet so androidSdk should be null by default.
        //
        // This test is written to fail if our bots get Android SDKs in the future: shouldBeToolExit
        // will be null and our expectation would fail. That would remind us to make these tests
        // hermetic before adding Android SDKs to the bots.
        updateLocalProperties(project: FlutterProject.current());
      } on Exception catch (e) {
        shouldBeToolExit = e;
      }
      // Ensure that we throw a meaningful ToolExit instead of a general crash.
      expect(shouldBeToolExit, isToolExit);
    });

    // Regression test for https://github.com/flutter/flutter/issues/34700
    testUsingContext('Does not return nulls in apk list', () {
      final GradleProject gradleProject = MockGradleProject();
      const AndroidBuildInfo buildInfo = AndroidBuildInfo(BuildInfo.debug);
      when(gradleProject.apkFilesFor(buildInfo)).thenReturn(<String>['not_real']);
      when(gradleProject.apkDirectory).thenReturn(fs.currentDirectory);

      expect(findApkFiles(gradleProject, buildInfo), <File>[]);
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    test('androidXFailureRegex should match lines with likely AndroidX errors', () {
      final List<String> nonMatchingLines = <String>[
        ':app:preBuild UP-TO-DATE',
        'BUILD SUCCESSFUL in 0s',
        '',
      ];
      final List<String> matchingLines = <String>[
        'AAPT: error: resource android:attr/fontVariationSettings not found.',
        'AAPT: error: resource android:attr/ttcIndex not found.',
        'error: package android.support.annotation does not exist',
        'import android.support.annotation.NonNull;',
        'import androidx.annotation.NonNull;',
        'Daemon:  AAPT2 aapt2-3.2.1-4818971-linux Daemon #0',
      ];
      for (String m in nonMatchingLines) {
        expect(androidXFailureRegex.hasMatch(m), isFalse);
      }
      for (String m in matchingLines) {
        expect(androidXFailureRegex.hasMatch(m), isTrue);
      }
    });

    test('androidXPluginWarningRegex should match lines with the AndroidX plugin warnings', () {
      final List<String> nonMatchingLines = <String>[
        ':app:preBuild UP-TO-DATE',
        'BUILD SUCCESSFUL in 0s',
        'Generic plugin AndroidX text',
        '',
      ];
      final List<String> matchingLines = <String>[
        '*********************************************************************************************************************************',
        "WARNING: This version of image_picker will break your Android build if it or its dependencies aren't compatible with AndroidX.",
        'See https://goo.gl/CP92wY for more information on the problem and how to fix it.',
        'This warning prints for all Android build failures. The real root cause of the error may be unrelated.',
      ];
      for (String m in nonMatchingLines) {
        expect(androidXPluginWarningRegex.hasMatch(m), isFalse);
      }
      for (String m in matchingLines) {
        expect(androidXPluginWarningRegex.hasMatch(m), isTrue);
      }
    });

    test('ndkMessageFilter should only match lines without the error message', () {
      final List<String> nonMatchingLines = <String>[
        'NDK is missing a "platforms" directory.',
        'If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to /usr/local/company/home/username/Android/Sdk/ndk-bundle.',
        'If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning.',
      ];
      final List<String> matchingLines = <String>[
        ':app:preBuild UP-TO-DATE',
        'BUILD SUCCESSFUL in 0s',
        '',
        'Something NDK related mentioning ANDROID_NDK_HOME',
      ];
      for (String m in nonMatchingLines) {
        expect(ndkMessageFilter.hasMatch(m), isFalse);
      }
      for (String m in matchingLines) {
        expect(ndkMessageFilter.hasMatch(m), isTrue);
      }
    });

    testUsingContext('Finds app bundle when flavor contains underscores in release mode', () {
      final GradleProject gradleProject = generateFakeAppBundle('foo_barRelease', 'app.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo_bar'));
      expect(bundle, isNotNull);
      expect(bundle.path, '/foo_barRelease/app.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in release mode', () {
      final GradleProject gradleProject = generateFakeAppBundle('fooRelease', 'app.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo'));
      expect(bundle, isNotNull);
      expect(bundle.path, '/fooRelease/app.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle when no flavor is used in release mode', () {
      final GradleProject gradleProject = generateFakeAppBundle('release', 'app.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, null));
      expect(bundle, isNotNull);
      expect(bundle.path, '/release/app.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle when flavor contains underscores in debug mode', () {
      final GradleProject gradleProject = generateFakeAppBundle('foo_barDebug', 'app.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo_bar'));
      expect(bundle, isNotNull);
      expect(bundle.path, '/foo_barDebug/app.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in debug mode', () {
      final GradleProject gradleProject = generateFakeAppBundle('fooDebug', 'app.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo'));
      expect(bundle, isNotNull);
      expect(bundle.path, '/fooDebug/app.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle when no flavor is used in debug mode', () {
      final GradleProject gradleProject = generateFakeAppBundle('debug', 'app.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, null));
      expect(bundle, isNotNull);
      expect(bundle.path, '/debug/app.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle when flavor contains underscores in profile mode', () {
      final GradleProject gradleProject = generateFakeAppBundle('foo_barProfile', 'app.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo_bar'));
      expect(bundle, isNotNull);
      expect(bundle.path, '/foo_barProfile/app.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in profile mode', () {
      final GradleProject gradleProject = generateFakeAppBundle('fooProfile', 'app.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo'));
      expect(bundle, isNotNull);
      expect(bundle.path, '/fooProfile/app.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle when no flavor is used in profile mode', () {
      final GradleProject gradleProject = generateFakeAppBundle('profile', 'app.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, null));
      expect(bundle, isNotNull);
      expect(bundle.path, '/profile/app.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle in release mode - Gradle 3.5', () {
      final GradleProject gradleProject = generateFakeAppBundle('release', 'app-release.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, null));
      expect(bundle, isNotNull);
      expect(bundle.path, '/release/app-release.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle in profile mode - Gradle 3.5', () {
      final GradleProject gradleProject = generateFakeAppBundle('profile', 'app-profile.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, null));
      expect(bundle, isNotNull);
      expect(bundle.path, '/profile/app-profile.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle in debug mode - Gradle 3.5', () {
      final GradleProject gradleProject = generateFakeAppBundle('debug', 'app-debug.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, null));
      expect(bundle, isNotNull);
      expect(bundle.path, '/debug/app-debug.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle when flavor contains underscores in release mode - Gradle 3.5', () {
      final GradleProject gradleProject = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo_bar'));
      expect(bundle, isNotNull);
      expect(bundle.path, '/foo_barRelease/app-foo_bar-release.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle when flavor contains underscores in profile mode - Gradle 3.5', () {
      final GradleProject gradleProject = generateFakeAppBundle('foo_barProfile', 'app-foo_bar-profile.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo_bar'));
      expect(bundle, isNotNull);
      expect(bundle.path, '/foo_barProfile/app-foo_bar-profile.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });

    testUsingContext('Finds app bundle when flavor contains underscores in debug mode - Gradle 3.5', () {
      final GradleProject gradleProject = generateFakeAppBundle('foo_barDebug', 'app-foo_bar-debug.aab');
      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo_bar'));
      expect(bundle, isNotNull);
      expect(bundle.path, '/foo_barDebug/app-foo_bar-debug.aab');
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
    });
  });

  group('gradle project', () {
    GradleProject projectFrom(String properties, String tasks) => GradleProject.fromAppProperties(properties, tasks);

    test('should extract build directory from app properties', () {
      final GradleProject project = projectFrom('''
someProperty: someValue
buildDir: /Users/some/apps/hello/build/app
someOtherProperty: someOtherValue
      ''', '');
      expect(
        fs.path.normalize(project.apkDirectory.path),
        fs.path.normalize('/Users/some/apps/hello/build/app/outputs/apk'),
      );
    });
    test('should extract default build variants from app properties', () {
      final GradleProject project = projectFrom('buildDir: /Users/some/apps/hello/build/app', '''
someTask
assemble
assembleAndroidTest
assembleDebug
assembleProfile
assembleRelease
someOtherTask
      ''');
      expect(project.buildTypes, <String>['debug', 'profile', 'release']);
      expect(project.productFlavors, isEmpty);
    });
    test('should extract custom build variants from app properties', () {
      final GradleProject project = projectFrom('buildDir: /Users/some/apps/hello/build/app', '''
someTask
assemble
assembleAndroidTest
assembleDebug
assembleFree
assembleFreeAndroidTest
assembleFreeDebug
assembleFreeProfile
assembleFreeRelease
assemblePaid
assemblePaidAndroidTest
assemblePaidDebug
assemblePaidProfile
assemblePaidRelease
assembleProfile
assembleRelease
someOtherTask
      ''');
      expect(project.buildTypes, <String>['debug', 'profile', 'release']);
      expect(project.productFlavors, <String>['free', 'paid']);
    });
    test('should provide apk file name for default build types', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.debug)).first, 'app-debug.apk');
      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.profile)).first, 'app-profile.apk');
      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.release)).first, 'app-release.apk');
      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'unknown'))).isEmpty, isTrue);
    });
    test('should provide apk file name for flavored build types', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.debug, 'free'))).first, 'app-free-debug.apk');
      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'paid'))).first, 'app-paid-release.apk');
      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'unknown'))).isEmpty, isTrue);
    });
    test('should provide apks for default build types and each ABI', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
      expect(project.apkFilesFor(
        const AndroidBuildInfo(
          BuildInfo.debug,
            splitPerAbi: true,
            targetArchs: <AndroidArch>[
                AndroidArch.armeabi_v7a,
                AndroidArch.arm64_v8a,
              ]
            )
          ),
        <String>[
          'app-armeabi-v7a-debug.apk',
          'app-arm64-v8a-debug.apk',
        ]);

      expect(project.apkFilesFor(
        const AndroidBuildInfo(
          BuildInfo.release,
            splitPerAbi: true,
            targetArchs: <AndroidArch>[
                AndroidArch.armeabi_v7a,
                AndroidArch.arm64_v8a,
              ]
            )
          ),
        <String>[
          'app-armeabi-v7a-release.apk',
          'app-arm64-v8a-release.apk',
        ]);

      expect(project.apkFilesFor(
        const AndroidBuildInfo(
          BuildInfo(BuildMode.release, 'unknown'),
            splitPerAbi: true,
            targetArchs: <AndroidArch>[
                AndroidArch.armeabi_v7a,
                AndroidArch.arm64_v8a,
              ]
            )
          ).isEmpty, isTrue);
    });
    test('should provide apks for each ABI and flavored build types', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
      expect(project.apkFilesFor(
        const AndroidBuildInfo(
          BuildInfo(BuildMode.debug, 'free'),
            splitPerAbi: true,
            targetArchs: <AndroidArch>[
                AndroidArch.armeabi_v7a,
                AndroidArch.arm64_v8a,
              ]
            )
          ),
        <String>[
          'app-free-armeabi-v7a-debug.apk',
          'app-free-arm64-v8a-debug.apk',
        ]);

      expect(project.apkFilesFor(
        const AndroidBuildInfo(
          BuildInfo(BuildMode.release, 'paid'),
            splitPerAbi: true,
            targetArchs: <AndroidArch>[
                AndroidArch.armeabi_v7a,
                AndroidArch.arm64_v8a,
              ]
            )
          ),
        <String>[
          'app-paid-armeabi-v7a-release.apk',
          'app-paid-arm64-v8a-release.apk',
        ]);

      expect(project.apkFilesFor(
        const AndroidBuildInfo(
          BuildInfo(BuildMode.release, 'unknown'),
            splitPerAbi: true,
            targetArchs: <AndroidArch>[
                AndroidArch.armeabi_v7a,
                AndroidArch.arm64_v8a,
              ]
            )
          ).isEmpty, isTrue);
    });
    test('should provide assemble task name for default build types', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
      expect(project.assembleTaskFor(BuildInfo.debug), 'assembleDebug');
      expect(project.assembleTaskFor(BuildInfo.profile), 'assembleProfile');
      expect(project.assembleTaskFor(BuildInfo.release), 'assembleRelease');
      expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
    });
    test('should provide assemble task name for flavored build types', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
      expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'assembleFreeDebug');
      expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'assemblePaidRelease');
      expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
    });
    test('should respect format of the flavored build types', () {
      final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], '/some/dir');
      expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'assembleRandomFlavorDebug');
    });
    test('bundle should provide assemble task name for default build types', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
      expect(project.bundleTaskFor(BuildInfo.debug), 'bundleDebug');
      expect(project.bundleTaskFor(BuildInfo.profile), 'bundleProfile');
      expect(project.bundleTaskFor(BuildInfo.release), 'bundleRelease');
      expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
    });
    test('bundle should provide assemble task name for flavored build types', () {
      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
      expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'bundleFreeDebug');
      expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'bundlePaidRelease');
      expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
    });
    test('bundle should respect format of the flavored build types', () {
      final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], '/some/dir');
      expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'bundleRandomFlavorDebug');
    });
  });

  group('Config files', () {
    BufferLogger mockLogger;
    Directory tempDir;

    setUp(() {
      mockLogger = BufferLogger();
      tempDir = fs.systemTempDirectory.createTempSync('settings_aar_test.');

    });

    testUsingContext('create settings_aar.gradle when current settings.gradle loads plugins', () {
      const String currentSettingsGradle = '''
include ':app'

def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()

def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}

plugins.each { name, path ->
    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
    include ":\$name"
    project(":\$name").projectDir = pluginDirectory
}
''';

      const String settingsAarFile = '''
include ':app'
''';

      tempDir.childFile('settings.gradle').writeAsStringSync(currentSettingsGradle);

      final String toolGradlePath = fs.path.join(
          fs.path.absolute(Cache.flutterRoot),
          'packages',
          'flutter_tools',
          'gradle');
      fs.directory(toolGradlePath).createSync(recursive: true);
      fs.file(fs.path.join(toolGradlePath, 'deprecated_settings.gradle'))
          .writeAsStringSync(currentSettingsGradle);

      fs.file(fs.path.join(toolGradlePath, 'settings_aar.gradle.tmpl'))
          .writeAsStringSync(settingsAarFile);

      createSettingsAarGradle(tempDir);

      expect(mockLogger.statusText, contains('created successfully'));
      expect(tempDir.childFile('settings_aar.gradle').existsSync(), isTrue);

    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
      Logger: () => mockLogger,
    });

    testUsingContext('create settings_aar.gradle when current settings.gradle doesn\'t load plugins', () {
      const String currentSettingsGradle = '''
include ':app'
''';

      const String settingsAarFile = '''
include ':app'
''';

      tempDir.childFile('settings.gradle').writeAsStringSync(currentSettingsGradle);

      final String toolGradlePath = fs.path.join(
          fs.path.absolute(Cache.flutterRoot),
          'packages',
          'flutter_tools',
          'gradle');
      fs.directory(toolGradlePath).createSync(recursive: true);
      fs.file(fs.path.join(toolGradlePath, 'deprecated_settings.gradle'))
          .writeAsStringSync(currentSettingsGradle);

      fs.file(fs.path.join(toolGradlePath, 'settings_aar.gradle.tmpl'))
          .writeAsStringSync(settingsAarFile);

      createSettingsAarGradle(tempDir);

      expect(mockLogger.statusText, contains('created successfully'));
      expect(tempDir.childFile('settings_aar.gradle').existsSync(), isTrue);

    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
      Logger: () => mockLogger,
    });
  });

  group('Undefined task', () {
    BufferLogger mockLogger;

    setUp(() {
      mockLogger = BufferLogger();
    });

    testUsingContext('print undefined build type', () {
      final GradleProject project = GradleProject(<String>['debug', 'release'],
          const <String>['free', 'paid'], '/some/dir');

      printUndefinedTask(project, const BuildInfo(BuildMode.profile, 'unknown'));
      expect(mockLogger.errorText, contains('The Gradle project does not define a task suitable for the requested build'));
      expect(mockLogger.errorText, contains('Review the android/app/build.gradle file and ensure it defines a profile build type'));
    }, overrides: <Type, Generator>{
      Logger: () => mockLogger,
    });

    testUsingContext('print no flavors', () {
      final GradleProject project = GradleProject(<String>['debug', 'release'],
          const <String>[], '/some/dir');

      printUndefinedTask(project, const BuildInfo(BuildMode.debug, 'unknown'));
      expect(mockLogger.errorText, contains('The Gradle project does not define a task suitable for the requested build'));
      expect(mockLogger.errorText, contains('The android/app/build.gradle file does not define any custom product flavors'));
      expect(mockLogger.errorText, contains('You cannot use the --flavor option'));
    }, overrides: <Type, Generator>{
      Logger: () => mockLogger,
    });

    testUsingContext('print flavors', () {
      final GradleProject project = GradleProject(<String>['debug', 'release'],
          const <String>['free', 'paid'], '/some/dir');

      printUndefinedTask(project, const BuildInfo(BuildMode.debug, 'unknown'));
      expect(mockLogger.errorText, contains('The Gradle project does not define a task suitable for the requested build'));
      expect(mockLogger.errorText, contains('The android/app/build.gradle file defines product flavors: free, paid'));
    }, overrides: <Type, Generator>{
      Logger: () => mockLogger,
    });
  });

  group('Gradle local.properties', () {
    MockLocalEngineArtifacts mockArtifacts;
    MockProcessManager mockProcessManager;
    FakePlatform android;
    FileSystem fs;

    setUp(() {
      fs = MemoryFileSystem();
      mockArtifacts = MockLocalEngineArtifacts();
      mockProcessManager = MockProcessManager();
      android = fakePlatform('android');
    });

    void testUsingAndroidContext(String description, dynamic testMethod()) {
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        ProcessManager: () => mockProcessManager,
        Platform: () => android,
        FileSystem: () => fs,
      });
    }

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

    Future<void> checkBuildVersion({
      String manifest,
      BuildInfo buildInfo,
      String expectedBuildName,
      String expectedBuildNumber,
    }) async {
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
          platform: TargetPlatform.android_arm, mode: anyNamed('mode'))).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'android_arm'));

      final File manifestFile = fs.file('path/to/project/pubspec.yaml');
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync(manifest);

      // write schemaData otherwise pubspec.yaml file can't be loaded
      writeEmptySchemaFile(fs);

      updateLocalProperties(
        project: FlutterProject.fromPath('path/to/project'),
        buildInfo: buildInfo,
        requireAndroidSdk: false,
      );

      final File localPropertiesFile = fs.file('path/to/project/android/local.properties');
      expect(propertyFor('flutter.versionName', localPropertiesFile), expectedBuildName);
      expect(propertyFor('flutter.versionCode', localPropertiesFile), expectedBuildNumber);
    }

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

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

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

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

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

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

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

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

    testUsingAndroidContext('allow build info to unset build name and number', () async {
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: null, buildNumber: null),
        expectedBuildName: null,
        expectedBuildNumber: null,
      );
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3'),
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.3', buildNumber: '4'),
        expectedBuildName: '1.0.3',
        expectedBuildNumber: '4',
      );
      // Values don't get unset.
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: null,
        expectedBuildName: '1.0.3',
        expectedBuildNumber: '4',
      );
      // Values get unset.
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: null, buildNumber: null),
        expectedBuildName: null,
        expectedBuildNumber: null,
      );
    });
  });

  group('gradle version', () {
    test('should be compatible with the Android plugin version', () {
      // Granular versions.
      expect(getGradleVersionFor('1.0.0'), '2.3');
      expect(getGradleVersionFor('1.0.1'), '2.3');
      expect(getGradleVersionFor('1.0.2'), '2.3');
      expect(getGradleVersionFor('1.0.4'), '2.3');
      expect(getGradleVersionFor('1.0.8'), '2.3');
      expect(getGradleVersionFor('1.1.0'), '2.3');
      expect(getGradleVersionFor('1.1.2'), '2.3');
      expect(getGradleVersionFor('1.1.2'), '2.3');
      expect(getGradleVersionFor('1.1.3'), '2.3');
      // Version Ranges.
      expect(getGradleVersionFor('1.2.0'), '2.9');
      expect(getGradleVersionFor('1.3.1'), '2.9');

      expect(getGradleVersionFor('1.5.0'), '2.2.1');

      expect(getGradleVersionFor('2.0.0'), '2.13');
      expect(getGradleVersionFor('2.1.2'), '2.13');

      expect(getGradleVersionFor('2.1.3'), '2.14.1');
      expect(getGradleVersionFor('2.2.3'), '2.14.1');

      expect(getGradleVersionFor('2.3.0'), '3.3');

      expect(getGradleVersionFor('3.0.0'), '4.1');

      expect(getGradleVersionFor('3.1.0'), '4.4');

      expect(getGradleVersionFor('3.2.0'), '4.6');
      expect(getGradleVersionFor('3.2.1'), '4.6');

      expect(getGradleVersionFor('3.3.0'), '4.10.2');
      expect(getGradleVersionFor('3.3.2'), '4.10.2');

      expect(getGradleVersionFor('3.4.0'), '5.1.1');
      expect(getGradleVersionFor('3.5.0'), '5.1.1');
    });

    test('throws on unsupported versions', () {
      expect(() => getGradleVersionFor('3.6.0'),
          throwsA(predicate<Exception>((Exception e) => e is ToolExit)));
    });
  });

  group('injectGradleWrapperIfNeeded', () {
    MemoryFileSystem memoryFileSystem;
    Directory tempDir;
    Directory gradleWrapperDirectory;

    setUp(() {
      memoryFileSystem = MemoryFileSystem();
      tempDir = memoryFileSystem.systemTempDirectory.createTempSync('artifacts_test.');
      gradleWrapperDirectory = memoryFileSystem.directory(
          memoryFileSystem.path.join(tempDir.path, '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');
    });

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

      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-4.10.2-all.zip\n');
    }, overrides: <Type, Generator>{
      Cache: () => Cache(rootOverride: tempDir),
      FileSystem: () => memoryFileSystem,
    });

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

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

      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-4.10.2-all.zip\n');
    }, overrides: <Type, Generator>{
      Cache: () => Cache(rootOverride: tempDir),
      FileSystem: () => memoryFileSystem,
    });

    testUsingContext('Gives executable permission to gradle', () {
      final Directory sampleAppAndroid = fs.directory('/sample-app/android');
      sampleAppAndroid.createSync(recursive: true);

      // Make gradlew in the wrapper executable.
      os.makeExecutable(gradleWrapperDirectory.childFile('gradlew'));

      injectGradleWrapperIfNeeded(sampleAppAndroid);

      final File gradlew = sampleAppAndroid.childFile('gradlew');
      expect(gradlew.existsSync(), isTrue);
      expect(gradlew.statSync().modeString().contains('x'), isTrue);
    }, overrides: <Type, Generator>{
      Cache: () => Cache(rootOverride: tempDir),
      FileSystem: () => memoryFileSystem,
      OperatingSystemUtils: () => OperatingSystemUtils(),
    });
  });

  group('gradle build', () {
    MockAndroidSdk mockAndroidSdk;
    MockAndroidStudio mockAndroidStudio;
    MockLocalEngineArtifacts mockArtifacts;
    MockProcessManager mockProcessManager;
    FakePlatform android;
    FileSystem fs;
    Cache cache;

    setUp(() {
      fs = MemoryFileSystem();
      mockAndroidSdk = MockAndroidSdk();
      mockAndroidStudio = MockAndroidStudio();
      mockArtifacts = MockLocalEngineArtifacts();
      mockProcessManager = MockProcessManager();
      android = fakePlatform('android');

      final Directory tempDir = fs.systemTempDirectory.createTempSync('artifacts_test.');
      cache = Cache(rootOverride: tempDir);

      final Directory gradleWrapperDirectory = tempDir
          .childDirectory('bin')
          .childDirectory('cache')
          .childDirectory('artifacts')
          .childDirectory('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');
    });

    testUsingContext('build aar uses selected local engine', () async {
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
          platform: TargetPlatform.android_arm, mode: anyNamed('mode'))).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'android_arm'));

      final File manifestFile = fs.file('path/to/project/pubspec.yaml');
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync('''
        name: test
        version: 1.0.0+1
        dependencies:
          flutter:
            sdk: flutter
        flutter:
          module:
            androidX: false
            androidPackage: com.example.test
            iosBundleIdentifier: com.example.test
        '''
      );

      final File gradlew = fs.file('path/to/project/.android/gradlew');
      gradlew.createSync(recursive: true);

      when(mockProcessManager.run(
          <String> ['/path/to/project/.android/gradlew', '-v'],
          workingDirectory: anyNamed('workingDirectory'),
          environment: anyNamed('environment'),
      )).thenAnswer(
          (_) async => ProcessResult(1, 0, '5.1.1', ''),
      );

      // write schemaData otherwise pubspec.yaml file can't be loaded
      writeEmptySchemaFile(fs);
      fs.currentDirectory = 'path/to/project';

      // Let any process start. Assert after.
      when(mockProcessManager.start(
        any,
        environment: anyNamed('environment'),
        workingDirectory: anyNamed('workingDirectory'))
      ).thenAnswer((Invocation invocation) => Future<Process>.value(MockProcess()));
      fs.directory('build/outputs/repo').createSync(recursive: true);

      await buildGradleAar(
        androidBuildInfo: const AndroidBuildInfo(BuildInfo(BuildMode.release, null)),
        project: FlutterProject.current(),
        outputDir: 'build/',
        target: ''
      );

      final List<String> actualGradlewCall = verify(mockProcessManager.start(
        captureAny,
        environment: anyNamed('environment'),
        workingDirectory: anyNamed('workingDirectory')),
      ).captured.single;

      expect(actualGradlewCall, contains('/path/to/project/.android/gradlew'));
      expect(actualGradlewCall, contains('-PlocalEngineOut=out/android_arm'));
    }, overrides: <Type, Generator>{
        AndroidSdk: () => mockAndroidSdk,
        AndroidStudio: () => mockAndroidStudio,
        Artifacts: () => mockArtifacts,
        Cache: () => cache,
        ProcessManager: () => mockProcessManager,
        Platform: () => android,
        FileSystem: () => fs,
      });
  });
}

/// Generates a fake app bundle at the location [directoryName]/[fileName].
GradleProject generateFakeAppBundle(String directoryName, String fileName) {
  final GradleProject gradleProject = MockGradleProject();
  when(gradleProject.bundleDirectory).thenReturn(fs.currentDirectory);

  final Directory aabDirectory = gradleProject.bundleDirectory.childDirectory(directoryName);
  fs.directory(aabDirectory).createSync(recursive: true);
  fs.file(fs.path.join(aabDirectory.path, fileName)).writeAsStringSync('irrelevant');
  return gradleProject;
}

Platform fakePlatform(String name) {
  return FakePlatform.fromPlatform(const LocalPlatform())..operatingSystem = name;
}

class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
class MockProcessManager extends Mock implements ProcessManager {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
class MockGradleProject extends Mock implements GradleProject {}
class MockitoAndroidSdk extends Mock implements AndroidSdk {}
class MockAndroidStudio extends Mock implements AndroidStudio {}