#!/usr/bin/env python3

from __future__ import annotations

import tempfile
import unittest
from pathlib import Path

from select_jdk import (
    kotlin_plugin_version,
    parse_version,
    requests_java_21_or_newer,
    select_jdk,
)


class SelectJdkTest(unittest.TestCase):
    def setUp(self) -> None:
        self.temp = tempfile.TemporaryDirectory()
        self.workspace = Path(self.temp.name)

    def tearDown(self) -> None:
        self.temp.cleanup()

    def write(self, relative: str, text: str) -> Path:
        path = self.workspace / relative
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(text)
        return path

    def version_of(self, flutter: bool = False, override: str = "") -> str:
        return select_jdk(self.workspace, override, flutter)[0]

    # --- the explicit override ------------------------------------------
    def test_repo_variable_beats_everything(self) -> None:
        self.write("build.gradle", "ext.kotlin_version = '1.6.21'")

        for flutter in (True, False):
            self.assertEqual(self.version_of(flutter=flutter, override="11"), "11")

    def test_override_is_trimmed_and_blank_is_ignored(self) -> None:
        self.assertEqual(self.version_of(override=" 23 "), "23")
        self.assertEqual(self.version_of(flutter=True, override="   "), "17")

    def test_malformed_override_is_refused_not_passed_through(self) -> None:
        # setup-java wants a bare major version. A multi-line value would append
        # extra `key=value` lines to $GITHUB_OUTPUT and invent step outputs, so a
        # value that is not all digits must fall through to the normal choice
        # rather than reach the workflow.
        self.write("build.gradle", "ext.kotlin_version = '2.0.21'")

        for bad in ("17\nEVIL=1", "seventeen", "17;rm -rf /", "17.0.2"):
            with self.subTest(override=bad):
                self.assertEqual(self.version_of(override=bad), "21")

    def test_override_reason_names_the_variable(self) -> None:
        _, why = select_jdk(self.workspace, "17", flutter=False)

        self.assertIn("JAVA_VERSION", why)

    # --- today's defaults, which must not move --------------------------
    def test_flutter_still_gets_17(self) -> None:
        self.assertEqual(self.version_of(flutter=True), "17")

    def test_native_gradle_still_gets_21_by_default(self) -> None:
        self.assertEqual(self.version_of(), "21")

    def test_modern_kotlin_still_gets_21(self) -> None:
        self.write("build.gradle", "ext.kotlin_version = '2.0.21'")

        self.assertEqual(self.version_of(), "21")

    def test_kotlin_exactly_at_the_floor_gets_21(self) -> None:
        self.write("build.gradle", "ext.kotlin_version = '1.9.20'")

        self.assertEqual(self.version_of(), "21")

    def test_unresolvable_kotlin_version_keeps_21(self) -> None:
        # The overwhelmingly common Groovy idiom: the classpath interpolates a
        # property, so the coordinate itself carries no digits.
        self.write(
            "build.gradle",
            'classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"',
        )

        self.assertEqual(self.version_of(), "21")

    def test_kotlinx_dependencies_do_not_masquerade_as_the_plugin(self) -> None:
        self.write(
            "app/build.gradle",
            'implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"\n'
            'implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"\n',
        )

        self.assertIsNone(kotlin_plugin_version(self.workspace))
        self.assertEqual(self.version_of(), "21")

    # --- the repair -----------------------------------------------------
    def test_old_kotlin_drops_to_17(self) -> None:
        # gowalk-public/android-taxi-deals: kapt dies with an "Internal compiler
        # error" on JDK 21 and builds fine on 17.
        self.write("build.gradle", "ext.kotlin_version = '1.6.21'")

        version, why = select_jdk(self.workspace, "", flutter=False)
        self.assertEqual(version, "17")
        self.assertIn("1.6.21", why)

    def test_old_kotlin_detected_in_the_android_subdirectory(self) -> None:
        self.write("android/build.gradle", "ext.kotlin_version = '1.7.10'")

        self.assertEqual(self.version_of(), "17")

    def test_old_kotlin_in_a_module_wins_over_a_modern_root(self) -> None:
        self.write("build.gradle", "ext.kotlin_version = '2.0.21'")
        self.write("legacy/build.gradle", "ext.kotlin_version = '1.5.31'")

        self.assertEqual(kotlin_plugin_version(self.workspace), (1, 5, 31))
        self.assertEqual(self.version_of(), "17")

    def test_a_project_that_asks_for_java_21_keeps_21(self) -> None:
        # Downgrading this one would break a build that works today.
        self.write("build.gradle", "ext.kotlin_version = '1.8.22'")
        self.write("app/build.gradle", "kotlin { jvmToolchain(21) }")

        version, why = select_jdk(self.workspace, "", flutter=False)
        self.assertEqual(version, "21")
        self.assertIn("explicitly targets Java 21+", why)

    def test_java_17_target_does_not_block_the_downgrade(self) -> None:
        self.write("build.gradle", "ext.kotlin_version = '1.8.22'")
        self.write(
            "app/build.gradle",
            "compileOptions {\n"
            "  sourceCompatibility JavaVersion.VERSION_17\n"
            "  targetCompatibility JavaVersion.VERSION_17\n"
            "}\n",
        )

        self.assertFalse(requests_java_21_or_newer(self.workspace))
        self.assertEqual(self.version_of(), "17")

    def test_legacy_java_8_target_does_not_block_the_downgrade(self) -> None:
        self.write("build.gradle", "ext.kotlin_version = '1.6.21'")
        self.write(
            "app/build.gradle",
            "compileOptions {\n"
            "  sourceCompatibility JavaVersion.VERSION_1_8\n"
            "}\n"
            'kotlinOptions { jvmTarget = "1.8" }\n',
        )

        self.assertFalse(requests_java_21_or_newer(self.workspace))
        self.assertEqual(self.version_of(), "17")

    # --- where the version can be declared ------------------------------
    def test_reads_the_version_catalog(self) -> None:
        self.write(
            "gradle/libs.versions.toml",
            "[versions]\nkotlin = \"1.7.22\"\nagp = \"8.1.0\"\n",
        )

        self.assertEqual(kotlin_plugin_version(self.workspace), (1, 7, 22))

    def test_reads_the_plugins_dsl(self) -> None:
        self.write(
            "build.gradle",
            "plugins { id 'org.jetbrains.kotlin.android' version '1.8.10' apply false }",
        )

        self.assertEqual(kotlin_plugin_version(self.workspace), (1, 8, 10))

    def test_reads_the_kotlin_dsl_accessor(self) -> None:
        self.write("build.gradle.kts", 'plugins { kotlin("android") version "1.7.20" }')

        self.assertEqual(kotlin_plugin_version(self.workspace), (1, 7, 20))

    def test_reads_a_buildsrc_version_object(self) -> None:
        # gowalk-public/dating-android keeps its versions in Kotlin source, so
        # every build file interpolates `Config.Versions.kotlin` and declares
        # no digits at all.
        self.write(
            "build.gradle.kts",
            'classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Config.Versions.kotlin}")',
        )
        self.write(
            "buildSrc/src/main/java/Config.kt",
            "object Config {\n"
            "    object Versions {\n"
            '        val kotlin = "1.3.61"\n'
            '        val kotlinx = "1.3.3"\n'
            "    }\n"
            "}\n",
        )

        # 1.3.3 is kotlinx, not the compiler, and must not be mistaken for it.
        self.assertEqual(kotlin_plugin_version(self.workspace), (1, 3, 61))
        self.assertEqual(self.version_of(), "17")

    def test_reads_a_literal_classpath_coordinate(self) -> None:
        self.write(
            "build.gradle",
            'classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"',
        )

        self.assertEqual(kotlin_plugin_version(self.workspace), (1, 6, 10))

    def test_two_component_versions_normalise(self) -> None:
        self.assertEqual(parse_version("1.9"), (1, 9, 0))
        self.assertEqual(parse_version("1.9.20"), (1, 9, 20))


if __name__ == "__main__":
    unittest.main()
