// 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/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/android_workflow.dart';
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/base/platform.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/doctor_validator.dart';
import 'package:test/fake.dart';

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

void main() {
  late FakeAndroidSdk sdk;
  late Logger logger;
  late MemoryFileSystem fileSystem;
  late FakeProcessManager processManager;
  late FakeStdio stdio;

  setUp(() {
    sdk = FakeAndroidSdk();
    fileSystem = MemoryFileSystem.test();
    fileSystem.directory('/home/me').createSync(recursive: true);
    logger = BufferLogger.test();
    processManager = FakeProcessManager.empty();
    stdio = FakeStdio();
  });

  testWithoutContext('AndroidWorkflow handles a null AndroidSDK', () {
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
      androidSdk: null,
    );

    expect(androidWorkflow.canLaunchDevices, false);
    expect(androidWorkflow.canListDevices, false);
    expect(androidWorkflow.canListEmulators, false);
  });

  testWithoutContext('AndroidWorkflow handles a null adb', () {
    final FakeAndroidSdk androidSdk = FakeAndroidSdk();
    androidSdk.adbPath = null;
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
      androidSdk: androidSdk,
    );

    expect(androidWorkflow.canLaunchDevices, false);
    expect(androidWorkflow.canListDevices, false);
    expect(androidWorkflow.canListEmulators, false);
  });

  // Android SDK is actually supported on Linux Arm64 hosts.
  testWithoutContext('Support for Android SDK on Linux Arm Hosts', () {
    final FakeAndroidSdk androidSdk = FakeAndroidSdk();
    androidSdk.adbPath = null;
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
      androidSdk: androidSdk,
    );

    expect(androidWorkflow.appliesToHostPlatform, isTrue);
    expect(androidWorkflow.canLaunchDevices, isFalse);
    expect(androidWorkflow.canListDevices, isFalse);
    expect(androidWorkflow.canListEmulators, isFalse);
  });

  testWithoutContext('AndroidWorkflow is disabled if feature is disabled', () {
    final FakeAndroidSdk androidSdk = FakeAndroidSdk();
    androidSdk.adbPath = 'path/to/adb';
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(isAndroidEnabled: false),
      androidSdk: androidSdk,
    );

    expect(androidWorkflow.appliesToHostPlatform, false);
    expect(androidWorkflow.canLaunchDevices, false);
    expect(androidWorkflow.canListDevices, false);
    expect(androidWorkflow.canListEmulators, false);
  });

  testWithoutContext('AndroidWorkflow cannot list emulators if emulatorPath is null', () {
    final FakeAndroidSdk androidSdk = FakeAndroidSdk();
    androidSdk.adbPath = 'path/to/adb';
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
      androidSdk: androidSdk,
    );

    expect(androidWorkflow.appliesToHostPlatform, true);
    expect(androidWorkflow.canLaunchDevices, true);
    expect(androidWorkflow.canListDevices, true);
    expect(androidWorkflow.canListEmulators, false);
  });

  testWithoutContext('AndroidWorkflow can list emulators', () {
    final FakeAndroidSdk androidSdk = FakeAndroidSdk();
    androidSdk.adbPath = 'path/to/adb';
    androidSdk.emulatorPath = 'path/to/emulator';
    final AndroidWorkflow androidWorkflow = AndroidWorkflow(
      featureFlags: TestFeatureFlags(),
      androidSdk: androidSdk,
    );

    expect(androidWorkflow.appliesToHostPlatform, true);
    expect(androidWorkflow.canLaunchDevices, true);
    expect(androidWorkflow.canListDevices, true);
    expect(androidWorkflow.canListEmulators, true);
  });

  testWithoutContext('licensesAccepted returns LicensesAccepted.unknown if cannot find sdkmanager', () async {
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.excludedExecutables.add('/foo/bar/sdkmanager');
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
    final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted;

    expect(licenseStatus, LicensesAccepted.unknown);
  });

  testWithoutContext('licensesAccepted returns LicensesAccepted.unknown if cannot run sdkmanager', () async {
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.excludedExecutables.add('/foo/bar/sdkmanager');
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
    final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted;

    expect(licenseStatus, LicensesAccepted.unknown);
  });

  testWithoutContext('licensesAccepted handles garbage/no output', () async {
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        '/foo/bar/sdkmanager',
        '--licenses',
      ], stdout: 'asdasassad',
    ));
    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
    final LicensesAccepted result = await licenseValidator.licensesAccepted;

    expect(result, LicensesAccepted.unknown);
  });

  testWithoutContext('licensesAccepted works for all licenses accepted', () async {
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    const String output = '''
[=======================================] 100% Computing updates...
All SDK package licenses accepted.
''';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        '/foo/bar/sdkmanager',
        '--licenses',
      ], stdout: output,
    ));

    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
    final LicensesAccepted result = await licenseValidator.licensesAccepted;

    expect(result, LicensesAccepted.all);
  });

  testWithoutContext('licensesAccepted works for some licenses accepted', () async {
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    const String output = '''
[=======================================] 100% Computing updates...
2 of 5 SDK package licenses not accepted.
Review licenses that have not been accepted (y/N)?
''';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        '/foo/bar/sdkmanager',
        '--licenses',
      ], stdout: output,
    ));

    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
    final LicensesAccepted result = await licenseValidator.licensesAccepted;

    expect(result, LicensesAccepted.some);
  });

  testWithoutContext('licensesAccepted works for no licenses accepted', () async {
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    const String output = '''
[=======================================] 100% Computing updates...
5 of 5 SDK package licenses not accepted.
Review licenses that have not been accepted (y/N)?
''';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        '/foo/bar/sdkmanager',
        '--licenses',
      ], stdout: output,
    ));

    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );
    final LicensesAccepted result = await licenseValidator.licensesAccepted;

    expect(result, LicensesAccepted.none);
  });

  testWithoutContext('runLicenseManager succeeds for version >= 26', () async {
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    sdk.sdkManagerVersion = '26.0.0';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        '/foo/bar/sdkmanager',
        '--licenses',
      ],
    ));

    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );

    expect(await licenseValidator.runLicenseManager(), isTrue);
  });

  testWithoutContext('runLicenseManager errors when sdkmanager is not found', () async {
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.excludedExecutables.add('/foo/bar/sdkmanager');

    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );

    expect(licenseValidator.runLicenseManager(), throwsToolExit());
  });

  testWithoutContext('runLicenseManager handles broken pipe without ArgumentError', () async {
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    const String exceptionMessage = 'Write failed (OS Error: Broken pipe, errno = 32), port = 0';
    const SocketException exception = SocketException(exceptionMessage);
    // By using a `Socket` generic parameter, the stdin.addStream will return a `Future<Socket>`
    // We are testing that our error handling properly handles futures of this type
    final ThrowingStdin<Socket> fakeStdin = ThrowingStdin<Socket>(exception);
    final FakeCommand licenseCommand = FakeCommand(
      command: <String>[sdk.sdkManagerPath!, '--licenses'],
      stdin: fakeStdin,
    );
    processManager.addCommand(licenseCommand);
    final BufferLogger logger = BufferLogger.test();

    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: logger,
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );

    await licenseValidator.runLicenseManager();
    expect(logger.traceText, contains(exceptionMessage));
    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext('runLicenseManager errors when sdkmanager fails to run', () async {
    sdk.sdkManagerPath = '/foo/bar/sdkmanager';
    processManager.excludedExecutables.add('/foo/bar/sdkmanager');

    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );

    expect(licenseValidator.runLicenseManager(), throwsToolExit());
  });

  testWithoutContext('runLicenseManager errors when sdkmanager exits non-zero', () async {
    const String sdkManagerPath = '/foo/bar/sdkmanager';
    sdk.sdkManagerPath = sdkManagerPath;
    final BufferLogger logger = BufferLogger.test();
    processManager.addCommand(
      const FakeCommand(
        command: <String>[sdkManagerPath, '--licenses'],
        exitCode: 1,
        stderr: 'sdkmanager crash',
      ),
    );

    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(
      androidSdk: sdk,
      fileSystem: fileSystem,
      processManager: processManager,
      platform: FakePlatform(environment: <String, String>{'HOME': '/home/me'}),
      stdio: stdio,
      logger: logger,
      userMessages: UserMessages(),
      androidStudio: FakeAndroidStudio(),
      operatingSystemUtils: FakeOperatingSystemUtils(),
    );

    await expectLater(
      licenseValidator.runLicenseManager(),
      throwsToolExit(
        message: 'Android sdkmanager tool was found, but failed to run ($sdkManagerPath): "exited code 1"',
      ),
    );
    expect(processManager, hasNoRemainingExpectations);
    expect(logger.traceText, isEmpty);
    expect(stdio.writtenToStdout, isEmpty);
    expect(stdio.writtenToStderr, contains('sdkmanager crash'));
  });

  testWithoutContext('detects license-only SDK installation with cmdline-tools', () async {
    sdk
      ..licensesAvailable = true
      ..platformToolsAvailable = false
      ..cmdlineToolsAvailable = true
      ..directory = fileSystem.directory('/foo/bar');
    final ValidationResult validationResult = await AndroidValidator(
      androidStudio: FakeAndroidStudio(),
      androidSdk: sdk,
      fileSystem: fileSystem,
      logger: logger,
      processManager: processManager,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
      userMessages: UserMessages(),
    ).validate();

    expect(validationResult.type, ValidationType.partial);

    final ValidationMessage sdkMessage = validationResult.messages.first;
    expect(sdkMessage.type, ValidationMessageType.information);
    expect(sdkMessage.message, 'Android SDK at /foo/bar');

    final ValidationMessage licenseMessage = validationResult.messages.last;
    expect(licenseMessage.type, ValidationMessageType.hint);
    expect(licenseMessage.message, UserMessages().androidSdkLicenseOnly(kAndroidHome));
  });

  testWithoutContext('detects minimum required SDK and buildtools', () async {
    processManager.addCommand(const FakeCommand(
      command: <String>[
        'which',
        'java',
      ], exitCode: 1,
    ));
    final FakeAndroidSdkVersion sdkVersion = FakeAndroidSdkVersion()
      ..sdkLevel = 28
      ..buildToolsVersion = Version(26, 0, 3);

    sdk
      ..licensesAvailable = true
      ..platformToolsAvailable = true
      ..cmdlineToolsAvailable = true
    // Test with invalid SDK and build tools
      ..directory = fileSystem.directory('/foo/bar')
      ..sdkManagerPath = '/foo/bar/sdkmanager'
      ..latestVersion = sdkVersion;

    final String errorMessage = UserMessages().androidSdkBuildToolsOutdated(
      kAndroidSdkMinVersion,
      kAndroidSdkBuildToolsMinVersion.toString(),
      FakePlatform(),
    );

    final AndroidValidator androidValidator = AndroidValidator(
      androidStudio: null,
      androidSdk: sdk,
      fileSystem: fileSystem,
      logger: logger,
      processManager: processManager,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
      userMessages: UserMessages(),
    );

    ValidationResult validationResult = await androidValidator.validate();
    expect(validationResult.type, ValidationType.missing);
    expect(
      validationResult.messages.last.message,
      errorMessage,
    );

    // Test with valid SDK but invalid build tools
    sdkVersion.sdkLevel = 29;
    sdkVersion.buildToolsVersion = Version(28, 0, 2);

    validationResult = await androidValidator.validate();
    expect(validationResult.type, ValidationType.missing);
    expect(
      validationResult.messages.last.message,
      errorMessage,
    );

    // Test with valid SDK and valid build tools
    // Will still be partial because AndroidSdk.findJavaBinary is static :(
    sdkVersion.sdkLevel = kAndroidSdkMinVersion;
    sdkVersion.buildToolsVersion = kAndroidSdkBuildToolsMinVersion;

    validationResult = await androidValidator.validate();
    expect(validationResult.type, ValidationType.partial); // No Java binary
    expect(
      validationResult.messages.any((ValidationMessage message) => message.message == errorMessage),
      isFalse,
    );
  });

  testWithoutContext('detects missing cmdline tools', () async {
    sdk
      ..licensesAvailable = true
      ..platformToolsAvailable = true
      ..cmdlineToolsAvailable = false
      ..directory = fileSystem.directory('/foo/bar');

    final AndroidValidator androidValidator = AndroidValidator(
      androidStudio: null,
      androidSdk: sdk,
      fileSystem: fileSystem,
      logger: logger,
      processManager: processManager,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
      userMessages: UserMessages(),
    );

    final String errorMessage = UserMessages().androidMissingCmdTools;

    final ValidationResult validationResult = await androidValidator.validate();
    expect(validationResult.type, ValidationType.missing);

    final ValidationMessage sdkMessage = validationResult.messages.first;
    expect(sdkMessage.type, ValidationMessageType.information);
    expect(sdkMessage.message, 'Android SDK at /foo/bar');

    final ValidationMessage cmdlineMessage = validationResult.messages.last;
    expect(cmdlineMessage.type, ValidationMessageType.error);
    expect(cmdlineMessage.message, errorMessage);
  });

  testWithoutContext('detects minimum required java version', () async {
    // Test with older version of JDK
    const String javaVersionText = 'openjdk version "1.7.0_212"';
    processManager.addCommand(const FakeCommand(
      command: <String>[
        'home/java/bin/java',
        '-version',
      ], stderr: javaVersionText,
    ));
    final FakeAndroidSdkVersion sdkVersion = FakeAndroidSdkVersion()
      ..sdkLevel = 29
      ..buildToolsVersion = Version(28, 0, 3);

    // Mock a pass through scenario to reach _checkJavaVersion()
    sdk
      ..licensesAvailable = true
      ..platformToolsAvailable = true
      ..cmdlineToolsAvailable = true
      ..directory = fileSystem.directory('/foo/bar')
      ..sdkManagerPath = '/foo/bar/sdkmanager';
    sdk.latestVersion = sdkVersion;

    final String errorMessage = UserMessages().androidJavaMinimumVersion(javaVersionText);

    final ValidationResult validationResult = await AndroidValidator(
      androidSdk: sdk,
      androidStudio: null,
      fileSystem: fileSystem,
      logger: logger,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me', 'JAVA_HOME': 'home/java'},
      processManager: processManager,
      userMessages: UserMessages(),
    ).validate();
    expect(validationResult.type, ValidationType.partial);
    expect(
      validationResult.messages.last.message,
      errorMessage,
    );
    expect(
      validationResult.messages.any(
        (ValidationMessage message) => message.message.contains('Unable to locate Android SDK')
      ),
      false,
    );
  });

  testWithoutContext('Mentions `flutter config --android-sdk if user has no AndroidSdk`', () async {
    final ValidationResult validationResult = await AndroidValidator(
      androidSdk: null,
      androidStudio: null,
      fileSystem: fileSystem,
      logger: logger,
      platform: FakePlatform()..environment = <String, String>{'HOME': '/home/me', 'JAVA_HOME': 'home/java'},
      processManager: processManager,
      userMessages: UserMessages(),
    ).validate();

    expect(
      validationResult.messages.any(
        (ValidationMessage message) => message.message.contains('flutter config --android-sdk')
      ),
      true,
    );
  });
}

class FakeAndroidSdk extends Fake implements AndroidSdk {
  @override
  String? sdkManagerPath;

  @override
  String? sdkManagerVersion;

  @override
  String? adbPath;

  @override
  bool licensesAvailable = false;

  @override
  bool platformToolsAvailable = false;

  @override
  bool cmdlineToolsAvailable = false;

  @override
  Directory directory = MemoryFileSystem.test().directory('/foo/bar');

  @override
  AndroidSdkVersion? latestVersion;

  @override
  String? emulatorPath;

  @override
  List<String> validateSdkWellFormed() => <String>[];

  @override
  Map<String, String> get sdkManagerEnv => <String, String>{};
}

class FakeAndroidSdkVersion extends Fake implements AndroidSdkVersion {
  @override
  int sdkLevel = 0;

  @override
  Version buildToolsVersion = Version(0, 0, 0);

  @override
  String get buildToolsVersionName => '';

  @override
  String get platformName => '';
}

class FakeAndroidStudio extends Fake implements AndroidStudio {
  @override
  String get javaPath => 'java';
}

class ThrowingStdin<T> extends Fake implements IOSink {
  ThrowingStdin(this.exception);

  final Exception exception;

  @override
  Future<dynamic> addStream(Stream<List<int>> stream) {
    return Future<T>.error(exception);
  }
}
