// 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_studio.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart';
import 'package:flutter_tools/src/android/migrations/android_studio_java_gradle_conflict_migration.dart';
import 'package:flutter_tools/src/android/migrations/min_sdk_version_migration.dart';
import 'package:flutter_tools/src/android/migrations/top_level_gradle_build_file_migration.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';

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

const String otherGradleVersionWrapper = r'''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
''';

const String gradleWrapperToMigrate = r'''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
''';

const String gradleWrapperToMigrateTo = r'''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
''';

String sampleModuleGradleBuildFile(String minSdkVersionString) {
  return r'''
plugins {
    id "com.android.application"
    id "kotlin-android"
    id "dev.flutter.flutter-gradle-plugin"
}

def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

android {
    namespace "com.example.asset_sample"
    compileSdkVersion flutter.compileSdkVersion
    ndkVersion flutter.ndkVersion

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.asset_sample"
        // You can update the following values to match your application needs.
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
        ''' + minSdkVersionString + r'''

        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }
}

flutter {
    source '../..'
}

dependencies {}
''';
}

final Version androidStudioDolphin = Version(2021, 3, 1);

const Version _javaVersion17 = Version.withText(17, 0, 2, 'openjdk 17.0.2');
const Version _javaVersion16 = Version.withText(16, 0, 2, 'openjdk 16.0.2');

void main() {
  group('Android migration', () {
    group('migrate the Gradle "clean" task to lazy declaration', () {
      late MemoryFileSystem memoryFileSystem;
      late BufferLogger bufferLogger;
      late FakeAndroidProject project;
      late File topLevelGradleBuildFile;

      setUp(() {
        memoryFileSystem = MemoryFileSystem.test();
        bufferLogger = BufferLogger.test();
        project = FakeAndroidProject(
          root: memoryFileSystem.currentDirectory.childDirectory('android')..createSync(),
        );
        topLevelGradleBuildFile = project.hostAppGradleRoot.childFile('build.gradle');
      });

      testUsingContext('skipped if files are missing', () {
        final TopLevelGradleBuildFileMigration androidProjectMigration = TopLevelGradleBuildFileMigration(
          project,
          bufferLogger,
        );
        androidProjectMigration.migrate();
        expect(topLevelGradleBuildFile.existsSync(), isFalse);
        expect(bufferLogger.traceText, contains('Top-level Gradle build file not found, skipping migration of task "clean".'));
      });

      testUsingContext('skipped if nothing to upgrade', () {
        topLevelGradleBuildFile.writeAsStringSync('''
tasks.register("clean", Delete) {
  delete rootProject.buildDir
}
        ''');

        final TopLevelGradleBuildFileMigration androidProjectMigration = TopLevelGradleBuildFileMigration(
          project,
          bufferLogger,
        );
        final DateTime previousLastModified = topLevelGradleBuildFile.lastModifiedSync();
        androidProjectMigration.migrate();

        expect(topLevelGradleBuildFile.lastModifiedSync(), previousLastModified);
      });

      testUsingContext('top-level build.gradle is migrated', () {
        topLevelGradleBuildFile.writeAsStringSync('''
task clean(type: Delete) {
    delete rootProject.buildDir
}
''');

        final TopLevelGradleBuildFileMigration androidProjectMigration = TopLevelGradleBuildFileMigration(
          project,
          bufferLogger,
        );
        androidProjectMigration.migrate();

        expect(bufferLogger.traceText, contains('Migrating "clean" Gradle task to lazy declaration style.'));
        expect(topLevelGradleBuildFile.readAsStringSync(), equals('''
tasks.register("clean", Delete) {
    delete rootProject.buildDir
}
'''));
      });
    });

    group('migrate the gradle version to one that does not conflict with the '
        'Android Studio-provided java version', () {
      late MemoryFileSystem memoryFileSystem;
      late BufferLogger bufferLogger;
      late FakeAndroidProject project;
      late File gradleWrapperPropertiesFile;

      setUp(() {
        memoryFileSystem = MemoryFileSystem.test();
        bufferLogger = BufferLogger.test();
        project = FakeAndroidProject(
          root: memoryFileSystem.currentDirectory.childDirectory('android')..createSync(),
        );
        project.hostAppGradleRoot.childDirectory(gradleDirectoryName)
            .childDirectory(gradleWrapperDirectoryName)
            .createSync(recursive: true);
        gradleWrapperPropertiesFile = project.hostAppGradleRoot
            .childDirectory(gradleDirectoryName)
            .childDirectory(gradleWrapperDirectoryName)
            .childFile(gradleWrapperPropertiesFilename);
      });

      testWithoutContext('skipped if files are missing', () {
        final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration(
          java: FakeJava(version: _javaVersion17),
          bufferLogger,
          project: project,
          androidStudio: FakeAndroidStudio(version: androidStudioDolphin),
        );
        migration.migrate();
        expect(gradleWrapperPropertiesFile.existsSync(), isFalse);
        expect(bufferLogger.traceText, contains(gradleWrapperNotFound));
      });


      testWithoutContext('skipped if android studio is null', () {
        final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration(
          java: FakeJava(version: _javaVersion17),
          bufferLogger,
          project: project,
        );
        gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate);
        migration.migrate();
        expect(bufferLogger.traceText, contains(androidStudioNotFound));
        expect(gradleWrapperPropertiesFile.readAsStringSync(),
            gradleWrapperToMigrate);
      });

      testWithoutContext('skipped if android studio version is null', () {
        final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration(
          java: FakeJava(version: _javaVersion17),
          bufferLogger,
          project: project,
          androidStudio: FakeAndroidStudio(version: null),
        );
        gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate);
        migration.migrate();
        expect(bufferLogger.traceText, contains(androidStudioNotFound));
        expect(gradleWrapperPropertiesFile.readAsStringSync(),
            gradleWrapperToMigrate);
      });

      testWithoutContext('skipped if error is encountered in migrate()', () {
        final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration(
          java: FakeErroringJava(),
          bufferLogger,
          project: project,
          androidStudio: FakeAndroidStudio(version: androidStudioFlamingo),
        );
        gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate);
        migration.migrate();
        expect(bufferLogger.traceText, contains(errorWhileMigrating));
        expect(gradleWrapperPropertiesFile.readAsStringSync(),
            gradleWrapperToMigrate);
      });

      testWithoutContext('skipped if android studio version is less than flamingo', () {
        final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration(
          java: FakeJava(),
          bufferLogger,
          project: project,
          androidStudio: FakeAndroidStudio(version: androidStudioDolphin),
        );
        gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate);
        migration.migrate();
        expect(gradleWrapperPropertiesFile.readAsStringSync(), gradleWrapperToMigrate);
        expect(bufferLogger.traceText, contains(androidStudioVersionBelowFlamingo));
      });

      testWithoutContext('skipped if bundled java version is less than 17', () {
        final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration(
          java: FakeJava(version: _javaVersion16),
          bufferLogger,
          project: project,
          androidStudio: FakeAndroidStudio(version: androidStudioFlamingo),
        );
        gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate);
        migration.migrate();
        expect(gradleWrapperPropertiesFile.readAsStringSync(), gradleWrapperToMigrate);
        expect(bufferLogger.traceText, contains(javaVersionNot17));
      });

      testWithoutContext('nothing is changed if gradle version not one that was '
          'used by flutter create', () {
        final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration(
          java: FakeJava(version: _javaVersion17),
          bufferLogger,
          project: project,
          androidStudio: FakeAndroidStudio(version: androidStudioFlamingo),
        );
        gradleWrapperPropertiesFile.writeAsStringSync(otherGradleVersionWrapper);
        migration.migrate();
        expect(gradleWrapperPropertiesFile.readAsStringSync(), otherGradleVersionWrapper);
        expect(bufferLogger.traceText, isEmpty);
      });

      testWithoutContext('change is made with one of the specific gradle versions'
          ' we migrate for', () {
        final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration(
          java: FakeJava(version: _javaVersion17),
          bufferLogger,
          project: project,
          androidStudio: FakeAndroidStudio(version: androidStudioFlamingo),
        );
        gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate);
        migration.migrate();
        expect(gradleWrapperPropertiesFile.readAsStringSync(), gradleWrapperToMigrateTo);
        expect(bufferLogger.statusText, contains('Conflict detected between '
            'Android Studio Java version and Gradle version, upgrading Gradle '
            'version from 6.7 to $gradleVersion7_6_1.'));
      });

      testWithoutContext('change is not made when opt out flag is set', () {
        final AndroidStudioJavaGradleConflictMigration migration = AndroidStudioJavaGradleConflictMigration(
          java: FakeJava(version: _javaVersion17),
          bufferLogger,
          project: project,
          androidStudio: FakeAndroidStudio(version: androidStudioFlamingo),
        );
        gradleWrapperPropertiesFile.writeAsStringSync(gradleWrapperToMigrate + optOutFlag);
        migration.migrate();
        expect(gradleWrapperPropertiesFile.readAsStringSync(), gradleWrapperToMigrate + optOutFlag);
        expect(bufferLogger.traceText, contains(optOutFlagEnabled));
      });
    });

    group('migrate min sdk versions less than 19 to flutter.minSdkVersion '
        'when in a FlutterProject that is an app', ()
    {
      late MemoryFileSystem memoryFileSystem;
      late BufferLogger bufferLogger;
      late FakeAndroidProject project;
      late MinSdkVersionMigration migration;

      setUp(() {
        memoryFileSystem = MemoryFileSystem.test();
        memoryFileSystem.currentDirectory.childDirectory('android').createSync();
        bufferLogger = BufferLogger.test();
        project = FakeAndroidProject(
          root: memoryFileSystem.currentDirectory.childDirectory('android'),
        );
        project.appGradleFile.parent.createSync(recursive: true);
        migration = MinSdkVersionMigration(
            project,
            bufferLogger
        );
      });

      testWithoutContext('do nothing when files missing', () {
        migration.migrate();
        expect(bufferLogger.traceText, contains(appGradleNotFoundWarning));
      });

      testWithoutContext('replace when api 16', () {
        const String minSdkVersion16 = 'minSdkVersion 16';
        project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion16));
        migration.migrate();
        expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText));
      });

      testWithoutContext('replace when api 17', () {
        const String minSdkVersion17 = 'minSdkVersion 17';
        project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion17));
        migration.migrate();
        expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText));
      });

      testWithoutContext('replace when api 18', () {
        const String minSdkVersion18 = 'minSdkVersion 18';
        project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion18));
        migration.migrate();
        expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText));
      });

      testWithoutContext('do nothing when >=api 19', () {
        const String minSdkVersion19 = 'minSdkVersion 19';
        project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion19));
        migration.migrate();
        expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersion19));
      });

      testWithoutContext('do nothing when already using '
          'flutter.minSdkVersion', () {
        project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(replacementMinSdkText));
        migration.migrate();
        expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText));
      });

      testWithoutContext('avoid rewriting comments', () {
        const String code = '// minSdkVersion 16  // old default\n'
            '        minSdkVersion 23  // new version';
        project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(code));
        migration.migrate();
        expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(code));
      });

      testWithoutContext('do nothing when project is a module', () {
        project = FakeAndroidProject(
          root: memoryFileSystem.currentDirectory.childDirectory('android'),
          module: true,
        );
        migration = MinSdkVersionMigration(
            project,
            bufferLogger
        );
        const String minSdkVersion16 = 'minSdkVersion 16';
        project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion16));
        migration.migrate();
        expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersion16));
      });

      testWithoutContext('do nothing when minSdkVersion is set '
          'to a constant', () {
        const String minSdkVersionConstant = 'minSdkVersion kMinSdkversion';
        project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersionConstant));
        migration.migrate();
        expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersionConstant));
      });

      testWithoutContext('do nothing when minSdkVersion is set '
          'using = syntax', () {
        const String equalsSyntaxMinSdkVersion16 = 'minSdkVersion = 16';
        project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(equalsSyntaxMinSdkVersion16));
        migration.migrate();
        expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(equalsSyntaxMinSdkVersion16));
      });
    });
  });
}

class FakeAndroidProject extends Fake implements AndroidProject {
  FakeAndroidProject({required Directory root, this.module, this.plugin}) : hostAppGradleRoot = root;

  @override
  Directory hostAppGradleRoot;

  final bool? module;
  final bool? plugin;

  @override
  bool get isPlugin => plugin ?? false;

  @override
  bool get isModule => module ?? false;

  @override
  File get appGradleFile => hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
}

class FakeAndroidStudio extends Fake implements AndroidStudio {
  FakeAndroidStudio({required Version? version}) {
    _version = version;
  }

  late Version? _version;

  @override
  Version? get version => _version;
}

class FakeErroringJava extends FakeJava {
  @override
  Version get version {
    throw Exception('How did this happen?');
  }
}