#!/usr/bin/env python3
"""Tests for raising a Flutter app's Gradle wrapper to the SDK's floor."""

from __future__ import annotations

import tempfile
import unittest
from pathlib import Path

import gradle_wrapper as gw

CHECKER_SOURCE = """
package com.flutter.gradle
object DependencyVersionChecker {
    internal val warnGradleVersion: Version = Version(8, 7, 0)
    internal val errorGradleVersion: Version = Version(8, 14, 0)
}
"""

WRAPPER = (
    "distributionBase=GRADLE_USER_HOME\n"
    "distributionPath=wrapper/dists\n"
    "distributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-all.zip\n"
)


def never(_name: str) -> bool:
    """Probe stub for tests: the network is never consulted."""
    return False


class RenderTest(unittest.TestCase):
    def test_eight_x_drops_a_trailing_zero_patch(self):
        self.assertEqual(gw.candidates((8, 14, 0))[0], "8.14")

    def test_nine_x_keeps_a_trailing_zero_patch(self):
        # gradle-9.0-all.zip is a 404; gradle-9.0.0-all.zip is the real name.
        self.assertEqual(gw.candidates((9, 0, 0))[0], "9.0.0")

    def test_a_real_patch_is_never_stripped(self):
        self.assertEqual(gw.candidates((8, 14, 3)), ["8.14.3"])

    def test_the_other_spelling_stays_available_as_a_fallback(self):
        self.assertIn("8.14.0", gw.candidates((8, 14, 0)))

    def test_falls_back_to_the_first_candidate_without_network(self):
        self.assertEqual(gw.normalize((8, 14, 0), probe=never), "8.14")

    def test_a_successful_probe_wins_over_the_ordering(self):
        self.assertEqual(
            gw.normalize((8, 14, 0), probe=lambda name: name == "8.14.0"), "8.14.0"
        )


class WrapperFileTest(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.workspace = Path(self.tmp.name)
        self.properties = gw.wrapper_properties(self.workspace)
        self.properties.parent.mkdir(parents=True)
        self.properties.write_text(WRAPPER, encoding="utf-8")
        self.flutter = self.workspace / "flutter"
        checker = self.flutter / gw._CHECKER
        checker.parent.mkdir(parents=True)
        checker.write_text(CHECKER_SOURCE, encoding="utf-8")
        self.addCleanup(self.tmp.cleanup)

    def test_reads_the_declared_version(self):
        self.assertEqual(gw.read_wrapper_version(self.properties), (8, 11, 1))

    def test_reads_the_sdk_floor(self):
        self.assertEqual(gw.flutter_minimum_gradle(self.flutter), (8, 14, 0))

    def test_raises_a_wrapper_below_the_floor(self):
        gw.set_wrapper_version(self.properties, (8, 14, 0))
        self.assertEqual(gw.read_wrapper_version(self.properties), (8, 14,))
        self.assertIn("gradle-8.14-all.zip", self.properties.read_text())

    def test_preserves_a_bin_distribution(self):
        self.properties.write_text(
            WRAPPER.replace("-all.zip", "-bin.zip"), encoding="utf-8"
        )
        gw.set_wrapper_version(self.properties, (8, 14, 0))
        self.assertIn("gradle-8.14-bin.zip", self.properties.read_text())

    def test_leaves_a_wrapper_already_at_the_floor_alone(self):
        self.properties.write_text(
            WRAPPER.replace("8.11.1", "8.14.3"), encoding="utf-8"
        )
        message = gw.ensure_minimum(self.workspace, self.flutter)
        self.assertIn("meets Flutter's minimum", message)
        self.assertIn("gradle-8.14.3-all.zip", self.properties.read_text())

    def test_warns_and_raises_when_below_the_floor(self):
        message = gw.ensure_minimum(self.workspace, self.flutter)
        self.assertTrue(message.startswith("::warning::"))
        self.assertIn("8.11.1", message)
        self.assertIn("gradle-8.14", self.properties.read_text())

    def test_no_wrapper_is_not_an_error(self):
        self.properties.unlink()
        self.assertEqual(gw.ensure_minimum(self.workspace, self.flutter), "")

    def test_unreadable_sdk_leaves_the_wrapper_untouched(self):
        message = gw.ensure_minimum(self.workspace, self.workspace / "nope")
        self.assertTrue(message.startswith("::warning::"))
        self.assertIn("gradle-8.11.1-all.zip", self.properties.read_text())

    def test_a_wrapper_without_a_distribution_url_is_left_alone(self):
        self.properties.write_text("distributionBase=GRADLE_USER_HOME\n", encoding="utf-8")
        message = gw.ensure_minimum(self.workspace, self.flutter)
        self.assertIn("No Gradle distributionUrl", message)

    def test_honours_an_explicit_gradle_root(self):
        root = self.workspace / "nested"
        properties = gw.wrapper_properties(self.workspace, root)
        properties.parent.mkdir(parents=True)
        properties.write_text(WRAPPER, encoding="utf-8")
        gw.ensure_minimum(self.workspace, self.flutter, gradle_root=root)
        self.assertIn("gradle-8.14", properties.read_text())


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


SETTINGS = """
pluginManagement { includeBuild("../flutter/packages/flutter_tools/gradle") }
plugins {
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
    id "com.android.application" version "8.9.1" apply false
    id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
include ":app"
"""

LEGACY_BUILD = """
buildscript {
    ext.kotlin_version = '1.7.10'
    dependencies { classpath 'com.android.tools.build:gradle:7.4.2' }
}
"""

CHECKER_WITH_ALL_FLOORS = """
    internal val errorGradleVersion: Version = Version(8, 14, 0)
    internal val errorAGPVersion: AndroidPluginVersion = AndroidPluginVersion(8, 11, 1)
    internal val errorKGPVersion: Version = Version(2, 0, 0)
"""


class ToolchainTest(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.workspace = Path(self.tmp.name)
        android = self.workspace / "android"
        android.mkdir()
        (android / "settings.gradle").write_text(SETTINGS, encoding="utf-8")
        properties = gw.wrapper_properties(self.workspace)
        properties.parent.mkdir(parents=True)
        properties.write_text(WRAPPER, encoding="utf-8")
        self.settings = android / "settings.gradle"
        self.flutter = self.workspace / "flutter"
        checker = self.flutter / gw._CHECKER
        checker.parent.mkdir(parents=True)
        checker.write_text(CHECKER_WITH_ALL_FLOORS, encoding="utf-8")
        self.addCleanup(self.tmp.cleanup)

    def test_reads_each_floor_from_the_sdk(self):
        self.assertEqual(gw.flutter_minimum(self.flutter, "gradle"), (8, 14, 0))
        self.assertEqual(gw.flutter_minimum(self.flutter, "agp"), (8, 11, 1))
        self.assertEqual(gw.flutter_minimum(self.flutter, "kgp"), (2, 0, 0))

    def test_reads_the_plugins_block(self):
        files = gw.toolchain_files(self.workspace)
        self.assertEqual(gw.read_declared(files, "agp")[1], (8, 9, 1))
        self.assertEqual(gw.read_declared(files, "kgp")[1], (2, 1, 0))

    def test_raises_agp_but_leaves_a_compliant_kgp(self):
        messages = gw.ensure_toolchain(self.workspace, self.flutter)
        text = self.settings.read_text()
        self.assertIn('"com.android.application" version "8.11.1"', text)
        self.assertIn('"org.jetbrains.kotlin.android" version "2.1.0"', text)
        self.assertTrue(any("Android Gradle Plugin 8.9.1 is below" in m for m in messages))
        self.assertTrue(any("Kotlin Gradle Plugin 2.1.0 meets" in m for m in messages))

    def test_raises_the_wrapper_in_the_same_pass(self):
        gw.ensure_toolchain(self.workspace, self.flutter)
        self.assertIn("gradle-8.14-all.zip", gw.wrapper_properties(self.workspace).read_text())

    def test_handles_the_legacy_buildscript_form(self):
        self.settings.unlink()
        (self.workspace / "android" / "build.gradle").write_text(LEGACY_BUILD, encoding="utf-8")
        gw.ensure_toolchain(self.workspace, self.flutter)
        text = (self.workspace / "android" / "build.gradle").read_text()
        self.assertIn("com.android.tools.build:gradle:8.11.1", text)
        self.assertIn("ext.kotlin_version = '2.0.0'", text)

    def test_a_project_declaring_neither_is_left_alone(self):
        self.settings.write_text("include ':app'\n", encoding="utf-8")
        messages = gw.ensure_toolchain(self.workspace, self.flutter)
        self.assertFalse(any("Android Gradle Plugin" in m for m in messages))

    def test_an_unreadable_sdk_warns_per_tool(self):
        messages = gw.ensure_toolchain(self.workspace, self.workspace / "nope")
        self.assertEqual(sum("::warning::" in m for m in messages), 3)


SETTINGS_KTS = """
plugins {
    id("dev.flutter.flutter-plugin-loader") version "1.0.0"
    id("com.android.application") version "8.9.1" apply false
    id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")
"""


class KotlinDslTest(unittest.TestCase):
    """The Kotlin DSL spells a plugin id `id("x") version "y"`, Groovy spells it
    `id "x" version "y"`. A matcher written against only one silently no-ops on
    the other — and a no-op here means shipping an app whose AGP is still below
    Flutter's floor while the log claims it was raised."""

    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.workspace = Path(self.tmp.name)
        android = self.workspace / "android"
        android.mkdir()
        self.settings = android / "settings.gradle.kts"
        self.settings.write_text(SETTINGS_KTS, encoding="utf-8")
        properties = gw.wrapper_properties(self.workspace)
        properties.parent.mkdir(parents=True)
        properties.write_text(WRAPPER, encoding="utf-8")
        self.flutter = self.workspace / "flutter"
        checker = self.flutter / gw._CHECKER
        checker.parent.mkdir(parents=True)
        checker.write_text(CHECKER_WITH_ALL_FLOORS, encoding="utf-8")
        self.addCleanup(self.tmp.cleanup)

    def test_finds_settings_gradle_kts(self):
        self.assertIn("settings.gradle.kts", [f.name for f in gw.toolchain_files(self.workspace)])

    def test_reads_both_plugin_ids_from_the_kotlin_dsl(self):
        files = gw.toolchain_files(self.workspace)
        self.assertEqual(gw.read_declared(files, "agp")[1], (8, 9, 1))
        self.assertEqual(gw.read_declared(files, "kgp")[1], (2, 1, 0))

    def test_actually_rewrites_the_kotlin_dsl(self):
        gw.ensure_toolchain(self.workspace, self.flutter)
        text = self.settings.read_text()
        self.assertIn('id("com.android.application") version "8.11.1"', text)
        # Untouched: 2.1.0 already meets the 2.0.0 floor.
        self.assertIn('id("org.jetbrains.kotlin.android") version "2.1.0"', text)
        # The loader's own version must not be caught by the AGP matcher.
        self.assertIn('id("dev.flutter.flutter-plugin-loader") version "1.0.0"', text)

    def test_reports_the_raise_only_when_the_file_really_changed(self):
        messages = gw.ensure_toolchain(self.workspace, self.flutter)
        raised = [m for m in messages if "Android Gradle Plugin 8.9.1 is below" in m]
        self.assertEqual(len(raised), 1)
        self.assertIn("8.11.1", self.settings.read_text())
