// 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:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_builder.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart'
    show templateAndroidGradlePluginVersion;
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build_appbundle.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';

import '../../src/android_common.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart' show FakeFlutterVersion;
import '../../src/test_flutter_command_runner.dart';

void main() {
  Cache.disableLocking();

  group('analytics', () {
    late Directory tempDir;
    late FakeAnalytics fakeAnalytics;
    late FakeProcessInfo processInfo;

    setUp(() {
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
      fakeAnalytics = getInitializedFakeAnalyticsInstance(
        fs: MemoryFileSystem.test(),
        fakeFlutterVersion: FakeFlutterVersion(),
      );
      processInfo = FakeProcessInfo();
    });

    tearDown(() {
      tryToDelete(tempDir);
    });

    testUsingContext(
      'indicate the default target platforms',
      () async {
        final String projectPath = await createProject(
          tempDir,
          arguments: <String>['--no-pub', '--template=app'],
        );

        await runBuildAppBundleCommand(projectPath);

        expect(
          fakeAnalytics.sentEvents,
          contains(
            Event.commandUsageValues(
              workflow: 'appbundle',
              commandHasTerminal: false,
              buildAppBundleTargetPlatform: 'android-arm,android-arm64,android-x64',
              buildAppBundleBuildMode: 'release',
            ),
          ),
        );
      },
      overrides: <Type, Generator>{
        AndroidBuilder: () => FakeAndroidBuilder(),
        Analytics: () => fakeAnalytics,
      },
    );

    testUsingContext('alias aab', () async {
      final command = BuildAppBundleCommand(logger: BufferLogger.test());
      expect(command.aliases, contains('aab'));
    });

    testUsingContext(
      'build type',
      () async {
        final String projectPath = await createProject(
          tempDir,
          arguments: <String>['--no-pub', '--template=app'],
        );

        await runBuildAppBundleCommand(projectPath);
        expect(
          fakeAnalytics.sentEvents,
          contains(
            Event.commandUsageValues(
              workflow: 'appbundle',
              commandHasTerminal: false,
              buildAppBundleTargetPlatform: 'android-arm,android-arm64,android-x64',
              buildAppBundleBuildMode: 'release',
            ),
          ),
        );

        fakeAnalytics.sentEvents.clear();
        await runBuildAppBundleCommand(projectPath, arguments: <String>['--release']);
        expect(
          fakeAnalytics.sentEvents,
          contains(
            Event.commandUsageValues(
              workflow: 'appbundle',
              commandHasTerminal: false,
              buildAppBundleTargetPlatform: 'android-arm,android-arm64,android-x64',
              buildAppBundleBuildMode: 'release',
            ),
          ),
        );

        fakeAnalytics.sentEvents.clear();
        await runBuildAppBundleCommand(projectPath, arguments: <String>['--debug']);
        expect(
          fakeAnalytics.sentEvents,
          contains(
            Event.commandUsageValues(
              workflow: 'appbundle',
              commandHasTerminal: false,
              buildAppBundleTargetPlatform: 'android-arm,android-arm64,android-x64',
              buildAppBundleBuildMode: 'debug',
            ),
          ),
        );

        fakeAnalytics.sentEvents.clear();
        await runBuildAppBundleCommand(projectPath, arguments: <String>['--profile']);

        expect(
          fakeAnalytics.sentEvents,
          contains(
            Event.commandUsageValues(
              workflow: 'appbundle',
              commandHasTerminal: false,
              buildAppBundleTargetPlatform: 'android-arm,android-arm64,android-x64',
              buildAppBundleBuildMode: 'profile',
            ),
          ),
        );
      },
      overrides: <Type, Generator>{
        AndroidBuilder: () => FakeAndroidBuilder(),
        Analytics: () => fakeAnalytics,
      },
    );

    testUsingContext(
      'logs success',
      () async {
        final String projectPath = await createProject(
          tempDir,
          arguments: <String>['--no-pub', '--template=app'],
        );

        await runBuildAppBundleCommand(projectPath);

        expect(
          fakeAnalytics.sentEvents,
          contains(
            Event.flutterCommandResult(
              commandPath: 'create',
              result: 'success',
              commandHasTerminal: false,
              maxRss: processInfo.maxRss,
            ),
          ),
        );
      },
      overrides: <Type, Generator>{
        AndroidBuilder: () => FakeAndroidBuilder(),
        Analytics: () => fakeAnalytics,
        ProcessInfo: () => processInfo,
      },
    );

    testUsingContext(
      'use of the deferred components feature sends a build info event indicating so',
      () async {
        final String projectPath = await createProject(
          tempDir,
          arguments: <String>['--empty', '--no-pub', '--template=app'],
        );

        // Add deferred manifest.
        final File pubspec = globals.localFileSystem
            .directory(projectPath)
            .childFile('pubspec.yaml');
        final String modifiedContents = pubspec.readAsStringSync().replaceAll(
          'flutter:',
          'flutter:\n  deferred-components:',
        );
        pubspec.writeAsStringSync(modifiedContents);
        printOnFailure(pubspec.readAsStringSync());

        final Directory oldCwd = globals.localFileSystem.currentDirectory;
        try {
          globals.localFileSystem.currentDirectory = globals.localFileSystem.directory(projectPath);
          await runBuildAppBundleCommand(projectPath);
        } finally {
          globals.localFileSystem.currentDirectory = oldCwd;
        }

        expect(
          fakeAnalytics.sentEvents,
          contains(
            Event.flutterBuildInfo(
              label: 'build-appbundle-deferred-components',
              buildType: 'android',
            ),
          ),
        );
      },
      overrides: <Type, Generator>{
        AndroidBuilder: () => FakeAndroidBuilder(),
        Analytics: () => fakeAnalytics,
        ProcessInfo: () => processInfo,
      },
    );
  });

  group('Gradle', () {
    late Directory tempDir;
    late FakeProcessManager processManager;
    late FakeAndroidSdk androidSdk;
    late FakeAnalytics analytics;

    setUp(() {
      analytics = getInitializedFakeAnalyticsInstance(
        fs: MemoryFileSystem.test(),
        fakeFlutterVersion: FakeFlutterVersion(),
      );
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
      processManager = FakeProcessManager.any();
      androidSdk = FakeAndroidSdk(globals.fs.directory('irrelevant'));
    });

    tearDown(() {
      tryToDelete(tempDir);
    });

    group('AndroidSdk', () {
      testUsingContext(
        'throws throwsToolExit if AndroidSdk is null',
        () async {
          final String projectPath = await createProject(
            tempDir,
            arguments: <String>['--no-pub', '--template=app'],
          );

          await expectLater(
            () async {
              await runBuildAppBundleCommand(projectPath, arguments: <String>['--no-pub']);
            },
            throwsToolExit(
              message: 'No Android SDK found. Try setting the ANDROID_HOME environment variable',
            ),
          );
        },
        overrides: <Type, Generator>{
          AndroidSdk: () => null,
          FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
          ProcessManager: () => processManager,
        },
      );
    });

    testUsingContext(
      "reports when the app isn't using AndroidX",
      () async {
        final String projectPath = await createProject(
          tempDir,
          arguments: <String>['--no-pub', '--template=app'],
        );
        // Simulate a non-androidx project.
        tempDir
            .childDirectory('flutter_project')
            .childDirectory('android')
            .childFile('gradle.properties')
            .writeAsStringSync('android.useAndroidX=false');

        // The command throws a [ToolExit] because it expects an AAB in the file system.
        await expectLater(() async {
          await runBuildAppBundleCommand(projectPath);
        }, throwsToolExit());

        expect(testLogger.statusText, containsIgnoringWhitespace("Your app isn't using AndroidX"));
        expect(
          testLogger.statusText,
          containsIgnoringWhitespace(
            'To avoid potential build failures, you can quickly migrate your app by '
            'following the steps on https://goo.gl/CP92wY',
          ),
        );

        expect(
          analytics.sentEvents,
          contains(
            Event.flutterBuildInfo(
              label: 'app-not-using-android-x',
              buildType: 'gradle',
              settings: 'androidGradlePluginVersion: $templateAndroidGradlePluginVersion',
            ),
          ),
        );
      },
      overrides: <Type, Generator>{
        AndroidSdk: () => androidSdk,
        FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
        ProcessManager: () => processManager,
        Analytics: () => analytics,
      },
    );

    testUsingContext(
      'reports when the app is using AndroidX',
      () async {
        final String projectPath = await createProject(
          tempDir,
          arguments: <String>['--no-pub', '--template=app'],
        );

        // The command throws a [ToolExit] because it expects an AAB in the file system.
        await expectLater(() async {
          await runBuildAppBundleCommand(projectPath);
        }, throwsToolExit());

        expect(
          testLogger.statusText,
          isNot(containsIgnoringWhitespace("Your app isn't using AndroidX")),
        );
        expect(
          testLogger.statusText,
          isNot(
            containsIgnoringWhitespace(
              'To avoid potential build failures, you can quickly migrate your app by '
              'following the steps on https://goo.gl/CP92wY',
            ),
          ),
        );

        expect(
          analytics.sentEvents,
          contains(
            Event.flutterBuildInfo(
              label: 'app-using-android-x',
              buildType: 'gradle',
              settings: 'androidGradlePluginVersion: $templateAndroidGradlePluginVersion',
            ),
          ),
        );
      },
      overrides: <Type, Generator>{
        AndroidSdk: () => androidSdk,
        FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
        ProcessManager: () => processManager,
        Analytics: () => analytics,
      },
    );
  });
}

Future<BuildAppBundleCommand> runBuildAppBundleCommand(
  String target, {
  List<String>? arguments,
}) async {
  final command = BuildAppBundleCommand(logger: BufferLogger.test());
  final CommandRunner<void> runner = createTestCommandRunner(command);
  await runner.run(<String>[
    'appbundle',
    ...?arguments,
    '--no-pub',
    globals.fs.path.join(target, 'lib', 'main.dart'),
  ]);
  return command;
}

class FakeAndroidSdk extends Fake implements AndroidSdk {
  FakeAndroidSdk(this.directory);

  @override
  final Directory directory;
}

class FakeProcessInfo extends Fake implements ProcessInfo {
  @override
  var maxRss = 123456789;
}
