#!/usr/bin/env python3

from __future__ import annotations

import json
import os
import tempfile
import unittest
from pathlib import Path
from unittest import mock

from android_config import (
    ConfigError,
    detect_bundle,
    detect_package_name,
    find_gradle_project,
    find_play_service_account,
    find_signing_config,
    project_kind,
    set_gradle_version,
)


class AndroidConfigTest(unittest.TestCase):
    def setUp(self) -> None:
        self.temp = tempfile.TemporaryDirectory()
        self.workspace = Path(self.temp.name)
        (self.workspace / "creds").mkdir()

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

    def test_finds_arbitrarily_named_service_account(self) -> None:
        path = self.workspace / "creds" / "Gowalk.json"
        path.write_text(
            json.dumps(
                {
                    "type": "service_account",
                    "client_email": "ci@example.invalid",
                    "private_key": "secret",
                }
            )
        )
        (self.workspace / "creds" / "metadata.json").write_text("{}")

        self.assertEqual(find_play_service_account(self.workspace), path.resolve())

    def test_rejects_multiple_service_accounts(self) -> None:
        payload = json.dumps(
            {
                "type": "service_account",
                "client_email": "ci@example.invalid",
                "private_key": "secret",
            }
        )
        (self.workspace / "creds" / "one.json").write_text(payload)
        (self.workspace / "creds" / "two.json").write_text(payload)

        with self.assertRaisesRegex(ConfigError, "multiple Google Play"):
            find_play_service_account(self.workspace)

    def test_loads_signing_config_next_to_keystore(self) -> None:
        properties = self.workspace / "creds" / "android-signing.properties"
        properties.write_text(
            "storeFile=android-upload-key.jks\n"
            "storePassword=store-secret\n"
            "keyPassword=key-secret\n"
            "keyAlias=upload\n"
        )
        keystore = self.workspace / "creds" / "android-upload-key.jks"
        keystore.write_bytes(b"keystore")

        result = find_signing_config(self.workspace)

        self.assertEqual(result.keystore_path, keystore.resolve())
        self.assertEqual(result.key_alias, "upload")

    def test_detects_kotlin_application_id(self) -> None:
        gradle = self.workspace / "android" / "app" / "build.gradle.kts"
        gradle.parent.mkdir(parents=True)
        gradle.write_text('android { defaultConfig { applicationId = "benz.gowalk.com" } }')

        self.assertEqual(detect_package_name(self.workspace), "benz.gowalk.com")

    def test_detects_only_bundle(self) -> None:
        bundle = (
            self.workspace
            / "build"
            / "app"
            / "outputs"
            / "bundle"
            / "release"
            / "app-release.aab"
        )
        bundle.parent.mkdir(parents=True)
        bundle.write_bytes(b"bundle")

        self.assertEqual(detect_bundle(self.workspace), bundle)


class NativeGradleProjectTest(unittest.TestCase):
    """Native Kotlin/Gradle apps ship through the same action as Flutter ones."""

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

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

    def scaffold(self, base: str = "android", *, alias: bool = False) -> Path:
        root = self.workspace / base if base != "." else self.workspace
        (root / "app").mkdir(parents=True)
        (root / "settings.gradle.kts").write_text('include(":app")')
        (root / "gradlew").write_text("#!/bin/sh\n")
        (root / "build.gradle.kts").write_text(
            "plugins { alias(libs.plugins.android.application) apply false }"
        )
        if alias:
            (root / "gradle").mkdir()
            (root / "gradle" / "libs.versions.toml").write_text(
                '[plugins]\nandroid-application = { id = "com.android.application" }\n'
            )
            plugins = "plugins { alias(libs.plugins.android.application) }"
        else:
            plugins = 'plugins { id("com.android.application") }'
        (root / "app" / "build.gradle.kts").write_text(
            f"{plugins}\n"
            "android {\n"
            "    defaultConfig {\n"
            '        applicationId = "com.gowalk.form"\n'
            "        versionCode = 1\n"
            '        versionName = "1.0.0"\n'
            "    }\n"
            "}\n"
        )
        return root

    def test_flutter_wins_over_a_nested_gradle_build(self) -> None:
        self.scaffold()
        self.assertEqual(project_kind(self.workspace), "gradle")
        (self.workspace / "pubspec.yaml").write_text("name: app\n")
        self.assertEqual(project_kind(self.workspace), "flutter")

    def test_finds_app_module_under_android(self) -> None:
        root = self.scaffold()

        project = find_gradle_project(self.workspace)

        self.assertEqual(project.root, root.resolve())
        self.assertEqual(project.module, ":app")
        self.assertEqual(project.build_file, (root / "app" / "build.gradle.kts").resolve())

    def test_finds_app_module_at_the_repo_root(self) -> None:
        self.scaffold(".")

        self.assertEqual(find_gradle_project(self.workspace).module, ":app")

    def test_resolves_version_catalog_plugin_aliases(self) -> None:
        # `alias(libs.plugins.android.application)` says nothing about the plugin
        # id without the catalog, and the root build file declares the same alias
        # with `apply false`.
        self.scaffold(alias=True)

        self.assertEqual(find_gradle_project(self.workspace).module, ":app")

    def test_ignores_library_modules(self) -> None:
        root = self.scaffold()
        (root / "network").mkdir()
        (root / "network" / "build.gradle.kts").write_text(
            'plugins { id("com.android.library") }'
        )

        self.assertEqual(find_gradle_project(self.workspace).module, ":app")

    def test_prefers_the_app_module_over_a_sibling_application(self) -> None:
        # A wear/automotive companion is also `com.android.application`; the
        # module named `app` is the one that ships to Play.
        root = self.scaffold()
        (root / "wear").mkdir()
        (root / "wear" / "build.gradle.kts").write_text(
            'plugins { id("com.android.application") }'
        )

        self.assertEqual(find_gradle_project(self.workspace).module, ":app")

    def test_rejects_ambiguous_application_modules(self) -> None:
        root = self.scaffold()
        (root / "app").rename(root / "phone")
        (root / "wear").mkdir()
        (root / "wear" / "build.gradle.kts").write_text(
            'plugins { id("com.android.application") }'
        )

        with self.assertRaisesRegex(ConfigError, "multiple Android application"):
            find_gradle_project(self.workspace)

    def test_reports_a_missing_gradle_build(self) -> None:
        with self.assertRaisesRegex(ConfigError, "no Gradle build found"):
            find_gradle_project(self.workspace)

    def test_detects_package_name_from_the_resolved_module(self) -> None:
        project = find_gradle_project(self.scaffold("."))

        self.assertEqual(
            detect_package_name(self.workspace, project.build_file), "com.gowalk.form"
        )

    def test_detects_bundle_in_the_gradle_module_output(self) -> None:
        root = self.scaffold()
        bundle = root / "app" / "build" / "outputs" / "bundle" / "release" / "app.aab"
        bundle.parent.mkdir(parents=True)
        bundle.write_bytes(b"bundle")

        self.assertEqual(
            detect_bundle(self.workspace, root / "app" / "build"), bundle
        )

    def test_pins_version_code_and_name(self) -> None:
        project = find_gradle_project(self.scaffold())

        applied = set_gradle_version(project.build_file, 214, "1.2.3")

        self.assertEqual(applied, {"version_code": True, "version_name": True})
        text = project.build_file.read_text()
        self.assertIn("versionCode = 214", text)
        self.assertIn('versionName = "1.2.3"', text)

    def test_leaves_version_name_alone_when_not_overridden(self) -> None:
        project = find_gradle_project(self.scaffold())

        applied = set_gradle_version(project.build_file, 9)

        self.assertFalse(applied["version_name"])
        self.assertIn('versionName = "1.0.0"', project.build_file.read_text())

    def test_reports_a_non_literal_version_code_instead_of_guessing(self) -> None:
        project = find_gradle_project(self.scaffold())
        project.build_file.write_text(
            "android { defaultConfig { versionCode = computeVersionCode() } }"
        )

        applied = set_gradle_version(project.build_file, 12)

        self.assertFalse(applied["version_code"])
        self.assertIn("computeVersionCode()", project.build_file.read_text())

    def test_groovy_version_code_syntax(self) -> None:
        project = find_gradle_project(self.scaffold())
        project.build_file.write_text(
            "android {\n    defaultConfig {\n        versionCode 7\n    }\n}\n"
        )

        self.assertTrue(set_gradle_version(project.build_file, 88)["version_code"])
        self.assertIn("versionCode 88", project.build_file.read_text())


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


class AutoBuildNumberTest(unittest.TestCase):
    """versionCode must never collide with what Play already holds."""

    def test_uses_play_high_water_mark_when_run_number_is_lower(self) -> None:
        import resolve_android

        with mock.patch.dict(os.environ, {"GITHUB_RUN_NUMBER": "1"}):
            self.assertEqual(resolve_android.auto_build_number(173), 174)

    def test_uses_run_number_when_it_is_already_higher(self) -> None:
        import resolve_android

        with mock.patch.dict(os.environ, {"GITHUB_RUN_NUMBER": "42"}):
            self.assertEqual(resolve_android.auto_build_number(5), 42)

    def test_uses_run_number_for_a_successfully_read_empty_inventory(self) -> None:
        import resolve_android

        with mock.patch.dict(os.environ, {"GITHUB_RUN_NUMBER": "7"}):
            self.assertEqual(resolve_android.auto_build_number(None), 7)
