// 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_studio.dart';
import 'package:flutter_tools/src/android/java.dart';
import 'package:flutter_tools/src/base/config.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:test/fake.dart';
import 'package:webdriver/async_io.dart';

import '../../integration.shard/test_utils.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';

void main() {

  late Config config;
  late Logger logger;
  late FileSystem fs;
  late Platform platform;
  late FakeProcessManager processManager;

  setUp(() {
    config = Config.test();
    logger = BufferLogger.test();
    fs = MemoryFileSystem.test();
    platform = FakePlatform(environment: <String, String>{
      'PATH': '',
    });
    processManager = FakeProcessManager.empty();
  });

  group(Java, () {

    group('find', () {
      testWithoutContext('finds the JDK bundled with Android Studio, if it exists', () {
        final AndroidStudio androidStudio = _FakeAndroidStudioWithJdk();
        final String androidStudioBundledJdkHome = androidStudio.javaPath!;
        final String expectedJavaBinaryPath = fs.path.join(androidStudioBundledJdkHome, 'bin', 'java');

        processManager.addCommand(FakeCommand(
          command: <String>[
            expectedJavaBinaryPath,
            '--version',
          ],
          stdout: '''
openjdk 19.0.2 2023-01-17
OpenJDK Runtime Environment Zulu19.32+15-CA (build 19.0.2+7)
OpenJDK 64-Bit Server VM Zulu19.32+15-CA (build 19.0.2+7, mixed mode, sharing)
'''
        ));
        final Java java = Java.find(
          config: config,
          androidStudio: androidStudio,
          logger: logger,
          fileSystem: fs,
          platform: platform,
          processManager: processManager,
        )!;

        expect(java.javaHome, androidStudioBundledJdkHome);
        expect(java.binaryPath, expectedJavaBinaryPath);

        expect(java.version!.toString(), 'OpenJDK Runtime Environment Zulu19.32+15-CA (build 19.0.2+7)');
        expect(java.version, equals(Version(19, 0, 2)));
      });

      testWithoutContext('finds JAVA_HOME if it is set and the JDK bundled with Android Studio could not be found', () {
        final AndroidStudio androidStudio = _FakeAndroidStudioWithoutJdk();
        const String javaHome = '/java/home';
        final String expectedJavaBinaryPath = fs.path.join(javaHome, 'bin', 'java');

        final Java java = Java.find(
          config: config,
          androidStudio: androidStudio,
          logger: logger,
          fileSystem: fs,
          platform: FakePlatform(environment: <String, String>{
            Java.javaHomeEnvironmentVariable: javaHome,
          }),
          processManager: processManager,
        )!;

        expect(java.javaHome, javaHome);
        expect(java.binaryPath, expectedJavaBinaryPath);
      });

      testWithoutContext('returns the java binary found on PATH if no other can be found', () {
        final AndroidStudio androidStudio = _FakeAndroidStudioWithoutJdk();
        final OperatingSystemUtils os = _FakeOperatingSystemUtilsWithJava(fileSystem);

        processManager.addCommand(
          const FakeCommand(
            command: <String>['which', 'java'],
            stdout: '/fake/which/java/path',
          ),
        );

        final Java java = Java.find(
          config: config,
          androidStudio: androidStudio,
          logger: logger,
          fileSystem: fs,
          platform: platform,
          processManager: processManager,
        )!;

        expect(java.javaHome, isNull);
        expect(java.binaryPath, os.which('java')!.path);
      });

      testWithoutContext('returns null if no java could be found', () {
        final AndroidStudio androidStudio = _FakeAndroidStudioWithoutJdk();
        processManager.addCommand(
          const FakeCommand(
            command: <String>['which', 'java'],
            exitCode: 1,
          ),
        );
        final Java? java = Java.find(
          config: config,
          androidStudio: androidStudio,
          logger: logger,
          fileSystem: fs,
          platform: platform,
          processManager: processManager,
        );
        expect(java, isNull);
      });

      testWithoutContext('finds and prefers JDK found at config item "jdk-dir" if it is set', () {
        const String configuredJdkPath = '/jdk';
        config.setValue('jdk-dir', configuredJdkPath);

        processManager.addCommand(
          const FakeCommand(
            command: <String>['which', 'java'],
            stdout: '/fake/which/java/path',
          ),
        );

        final _FakeAndroidStudioWithJdk androidStudio = _FakeAndroidStudioWithJdk();
        final FakePlatform platformWithJavaHome = FakePlatform(
          environment: <String, String>{
            'JAVA_HOME': '/old/jdk'
          },
        );
        Java? java = Java.find(
          config: config,
          androidStudio: androidStudio,
          logger: logger,
          fileSystem: fs,
          platform: platformWithJavaHome,
          processManager: processManager,
        );

        expect(java, isNotNull);
        expect(java!.javaHome, configuredJdkPath);
        expect(java.binaryPath, fs.path.join(configuredJdkPath, 'bin', 'java'));

        config.removeValue('jdk-dir');

        java = Java.find(
          config: config,
          androidStudio: androidStudio,
          logger: logger,
          fileSystem: fs,
          platform: platformWithJavaHome,
          processManager: processManager,
        );

        expect(java, isNotNull);
        assert(androidStudio.javaPath != configuredJdkPath);
        expect(java!.javaHome, androidStudio.javaPath);
        expect(java.binaryPath, fs.path.join(androidStudio.javaPath!, 'bin', 'java'));
      });
    });

    group('version', () {
      late Java java;

      setUp(() {
        processManager = FakeProcessManager.empty();
        java = Java(
          fileSystem: fs,
          logger: logger,
          os: FakeOperatingSystemUtils(),
          platform: platform,
          processManager: processManager,
          binaryPath: 'javaHome/bin/java',
          javaHome: 'javaHome',
        );
      });

      void addJavaVersionCommand(String output) {
        processManager.addCommand(
          FakeCommand(
            command: <String>[java.binaryPath, '--version'],
            stdout: output,
          ),
        );
      }

      testWithoutContext('is null when java binary cannot be run', () async {
        addJavaVersionCommand('');
        processManager.excludedExecutables.add('java');

        expect(java.version, null);
      });

      testWithoutContext('is null when java --version returns a non-zero exit code', () async {
        processManager.addCommand(
          FakeCommand(
            command: <String>[java.binaryPath, '--version'],
            exitCode: 1,
          ),
        );
        expect(java.version, null);
      });

      testWithoutContext('parses jdk 8', () {
        addJavaVersionCommand('''
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b10, mixed mode)
''');
        final Version version = java.version!;
        expect(version.toString(), 'Java(TM) SE Runtime Environment (build 1.8.0_202-b10)');
        expect(version, equals(Version(1, 8, 0)));
      });
      testWithoutContext('parses jdk 11 windows', () {
        addJavaVersionCommand('''
java version "11.0.14"
Java(TM) SE Runtime Environment (build 11.0.14+10-b13)
Java HotSpot(TM) 64-Bit Server VM (build 11.0.14+10-b13, mixed mode)
''');
        final Version version = java.version!;
        expect(version.toString(), 'Java(TM) SE Runtime Environment (build 11.0.14+10-b13)');
        expect(version, equals(Version(11, 0, 14)));
      });

      testWithoutContext('parses jdk 11 mac/linux', () {
        addJavaVersionCommand('''
openjdk version "11.0.18" 2023-01-17 LTS
OpenJDK Runtime Environment Zulu11.62+17-CA (build 11.0.18+10-LTS)
OpenJDK 64-Bit Server VM Zulu11.62+17-CA (build 11.0.18+10-LTS, mixed mode)
''');
        final Version version = java.version!;
        expect(version.toString(), 'OpenJDK Runtime Environment Zulu11.62+17-CA (build 11.0.18+10-LTS)');
        expect(version, equals(Version(11, 0, 18)));
      });

      testWithoutContext('parses jdk 17', () {
        addJavaVersionCommand('''
openjdk 17.0.6 2023-01-17
OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694)
OpenJDK 64-Bit Server VM (build 17.0.6+0-17.0.6b802.4-9586694, mixed mode)
''');
        final Version version = java.version!;
        expect(version.toString(), 'OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694)');
        expect(version, equals(Version(17, 0, 6)));
      });

      testWithoutContext('parses jdk 19', () {
        addJavaVersionCommand('''
openjdk 19.0.2 2023-01-17
OpenJDK Runtime Environment Homebrew (build 19.0.2)
OpenJDK 64-Bit Server VM Homebrew (build 19.0.2, mixed mode, sharing)
''');
        final Version version = java.version!;
        expect(version.toString(), 'OpenJDK Runtime Environment Homebrew (build 19.0.2)');
        expect(version, equals(Version(19, 0, 2)));
      });

      // https://chrome-infra-packages.appspot.com/p/flutter/java/openjdk/
      testWithoutContext('parses jdk output from ci', () {
        addJavaVersionCommand('''
openjdk 11.0.2 2019-01-15
OpenJDK Runtime Environment 18.9 (build 11.0.2+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode)
''');
        final Version version = java.version!;
        expect(version.toString(), 'OpenJDK Runtime Environment 18.9 (build 11.0.2+9)');
        expect(version, equals(Version(11, 0, 2)));
      });

      testWithoutContext('parses jdk two number versions', () {
        addJavaVersionCommand('openjdk 19.0 2023-01-17');
        final Version version = java.version!;
        expect(version.toString(), 'openjdk 19.0 2023-01-17');
        expect(version, equals(Version(19, 0, null)));
      });

      testWithoutContext('parses jdk 21 with patch numbers', () {
        addJavaVersionCommand('''
java 21.0.1 2023-09-19 LTS
Java(TM) SE Runtime Environment (build 21+35-LTS-2513)
Java HotSpot(TM) 64-Bit Server VM (build 21+35-LTS-2513, mixed mode, sharing)
''');
        final Version? version = java.version;
        expect(version, equals(Version(21, 0, 1)));
      });

      testWithoutContext('parses jdk 21 with no patch numbers', () {
        addJavaVersionCommand('''
java 21 2023-09-19 LTS
Java(TM) SE Runtime Environment (build 21+35-LTS-2513)
Java HotSpot(TM) 64-Bit Server VM (build 21+35-LTS-2513, mixed mode, sharing)
''');
        final Version? version = java.version;
        expect(version, equals(Version(21, 0, 0)));
      });
      testWithoutContext('parses openjdk 21 with no patch numbers', () {
        addJavaVersionCommand('''
openjdk version "21" 2023-09-19
OpenJDK Runtime Environment (build 21+35)
OpenJDK 64-Bit Server VM (build 21+35, mixed mode, sharing)
''');
        final Version? version = java.version;
        expect(version, equals(Version(21, 0, 0)));
      });
    });
  });
}

class _FakeAndroidStudioWithJdk extends Fake implements AndroidStudio {
  @override
  String? get javaPath => '/fake/android_studio/java/path/';
}

class _FakeAndroidStudioWithoutJdk extends Fake implements AndroidStudio {
  @override
  String? get javaPath => null;
}

class _FakeOperatingSystemUtilsWithJava extends FakeOperatingSystemUtils {
  _FakeOperatingSystemUtilsWithJava(this._fileSystem);

  final FileSystem _fileSystem;
  @override
  File? which(String execName) {
    if (execName == 'java') {
      return _fileSystem.file('/fake/which/java/path');
    }
    throw const InvalidArgumentException(null, null);
  }
}
