"""Tests for addon versioning — create with version, migrate, rollback."""

from __future__ import annotations

from typing import Any

import pytest
from click.testing import CliRunner

from kuberoku.addons import get_addon_def
from kuberoku.addons.postgres import POSTGRES
from kuberoku.addons.redis import REDIS
from kuberoku.cli.main import cli
from kuberoku.exceptions import KuberokuError
from kuberoku.factory import KuberokuFactory
from kuberoku.k8s.resources import addon_pvc_name, safe_name
from tests.backends.fake import FakeK8sClient, FakeNotFoundError


def _inject_addon_pod(factory: KuberokuFactory, app: str, instance: str) -> None:
    """Inject exec output and PVC for an addon so migrate/backup works.

    Does NOT create a fake Pod — FakeK8sClient returns injected exec output
    without requiring the pod to exist. This avoids _wait_for_pod_gone
    blocking for 60s on a pod that the fake STS controller can't remove.
    """
    k8s: Any = factory.k8s
    assert isinstance(k8s, FakeK8sClient)
    addon_type = instance  # instance name defaults to addon type
    addon_def = get_addon_def(addon_type)
    sts_name = safe_name(factory.prefix, "addon", f"{app}-{instance}")
    pod_name = f"{sts_name}-0"
    k8s.inject_exec_output(pod_name, "-- backup data --")
    # Create PVC so migrate can delete it
    pvc_name = addon_pvc_name(factory.prefix, app, instance, addon_def.volume_name)
    k8s.create_pvc(
        factory.namespace,
        {
            "apiVersion": "v1",
            "kind": "PersistentVolumeClaim",
            "metadata": {"name": pvc_name},
            "spec": {"resources": {"requests": {"storage": "1Gi"}}},
        },
    )


# ── AddonDef version registries ─────────────────────────────────


class TestAddonDefVersions:
    def test_postgres_has_versions_15_16_17(self) -> None:
        assert POSTGRES.versions is not None
        assert set(POSTGRES.versions.keys()) == {"15", "16", "17"}

    def test_postgres_default_version(self) -> None:
        assert POSTGRES.default_version == "16"

    def test_postgres_migration_paths(self) -> None:
        assert POSTGRES.migration_paths is not None
        assert POSTGRES.migration_paths["15"] == ("16",)
        assert POSTGRES.migration_paths["16"] == ("15", "17")
        assert POSTGRES.migration_paths["17"] == ("16",)

    def test_redis_has_versions_7_8(self) -> None:
        assert REDIS.versions is not None
        assert set(REDIS.versions.keys()) == {"7", "8"}

    def test_redis_default_version(self) -> None:
        assert REDIS.default_version == "7"

    def test_redis_migration_paths_none(self) -> None:
        assert REDIS.migration_paths is None


# ── Create with version ─────────────────────────────────────────


class TestCreateWithVersion:
    def test_create_with_default_version(self, app_factory: KuberokuFactory) -> None:
        addon = app_factory.addons.create("myapp", "postgres")
        assert addon.image == "postgres:16"
        sts_name = safe_name(app_factory.prefix, "addon", "myapp-postgres")
        sts = app_factory.k8s.get_statefulset(app_factory.namespace, sts_name)
        assert sts["spec"]["template"]["spec"]["containers"][0]["image"] == "postgres:16"

    def test_create_with_explicit_version(self, app_factory: KuberokuFactory) -> None:
        addon = app_factory.addons.create("myapp", "postgres", version="17")
        assert addon.image == "postgres:17"
        sts_name = safe_name(app_factory.prefix, "addon", "myapp-postgres")
        sts = app_factory.k8s.get_statefulset(app_factory.namespace, sts_name)
        assert sts["spec"]["template"]["spec"]["containers"][0]["image"] == "postgres:17"

    def test_create_with_invalid_version_raises(self, app_factory: KuberokuFactory) -> None:
        with pytest.raises(KuberokuError, match="not available"):
            app_factory.addons.create("myapp", "postgres", version="99")

    def test_version_stored_in_metadata(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="15")
        meta_cm_name = safe_name(app_factory.prefix, "addon-meta", "myapp-postgres")
        cm = app_factory.k8s.get_configmap(app_factory.namespace, meta_cm_name)
        assert cm["data"]["version"] == "15"

    def test_info_returns_correct_image_for_version(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="15")
        addon = app_factory.addons.info("myapp", "postgres")
        assert addon.image == "postgres:15"

    def test_redis_create_with_version(self, app_factory: KuberokuFactory) -> None:
        addon = app_factory.addons.create("myapp", "redis", version="8")
        assert addon.image == "redis:8"


# ── Migration ───────────────────────────────────────────────────


class TestMigrate:
    def test_migrate_updates_image(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        addon = app_factory.addons.migrate("myapp", "postgres", target_version="17")
        assert addon.image == "postgres:17"

    def test_migrate_updates_sts(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        app_factory.addons.migrate("myapp", "postgres", target_version="17")
        sts_name = safe_name(app_factory.prefix, "addon", "myapp-postgres")
        sts = app_factory.k8s.get_statefulset(app_factory.namespace, sts_name)
        assert sts["spec"]["template"]["spec"]["containers"][0]["image"] == "postgres:17"

    def test_migrate_updates_metadata(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        app_factory.addons.migrate("myapp", "postgres", target_version="17")
        meta_cm_name = safe_name(app_factory.prefix, "addon-meta", "myapp-postgres")
        cm = app_factory.k8s.get_configmap(app_factory.namespace, meta_cm_name)
        assert cm["data"]["version"] == "17"
        assert cm["data"]["previous_version"] == "16"
        assert cm["data"]["previous_image"] == "postgres:16"
        assert "migration_date" in cm["data"]

    def test_migrate_same_version_raises(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="16")
        with pytest.raises(KuberokuError, match="already at version"):
            app_factory.addons.migrate("myapp", "postgres", target_version="16")

    def test_migrate_invalid_version_raises(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="16")
        with pytest.raises(KuberokuError, match="not available"):
            app_factory.addons.migrate("myapp", "postgres", target_version="99")

    def test_migrate_external_addon_raises(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", external_url="postgres://ext:5432/db")
        with pytest.raises(KuberokuError, match="Cannot migrate external"):
            app_factory.addons.migrate("myapp", "postgres", target_version="17")

    def test_migrate_calls_on_step(self, app_factory: KuberokuFactory) -> None:
        steps: list[str] = []
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        app_factory.addons.migrate("myapp", "postgres", target_version="17", on_step=steps.append)
        assert len(steps) > 0
        assert any("backup" in s.lower() or "migrat" in s.lower() for s in steps)


# ── Migration path validation ───────────────────────────────────


class TestMigrationPaths:
    def test_valid_upgrade_path_allowed(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="15")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        addon = app_factory.addons.migrate("myapp", "postgres", target_version="16")
        assert addon.image == "postgres:16"

    def test_invalid_upgrade_path_rejected(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="15")
        with pytest.raises(KuberokuError, match="Cannot migrate"):
            app_factory.addons.migrate("myapp", "postgres", target_version="17")

    def test_invalid_path_suggests_correct_path(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="15")
        with pytest.raises(KuberokuError, match="15 -> 16 -> 17"):
            app_factory.addons.migrate("myapp", "postgres", target_version="17")

    def test_none_migration_paths_allows_any(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "redis", version="7")
        # Redis has no backup_command, so no pod needed
        addon = app_factory.addons.migrate("myapp", "redis", target_version="8")
        assert addon.image == "redis:8"

    def test_force_flag_bypasses_path_validation(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="15")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        addon = app_factory.addons.migrate("myapp", "postgres", target_version="17", force=True)
        assert addon.image == "postgres:17"

    def test_downgrade_allowed(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="17")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        addon = app_factory.addons.migrate("myapp", "postgres", target_version="16")
        assert addon.image == "postgres:16"


# ── Rollback ────────────────────────────────────────────────────


class TestMigrateRollback:
    def test_rollback_to_previous_version(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        app_factory.addons.migrate("myapp", "postgres", target_version="17")
        addon = app_factory.addons.migrate_rollback("myapp", "postgres")
        assert addon.image == "postgres:16"

    def test_rollback_without_previous_version_raises(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="16")
        with pytest.raises(KuberokuError, match="No previous version"):
            app_factory.addons.migrate_rollback("myapp", "postgres")

    def test_rollback_updates_metadata(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        app_factory.addons.migrate("myapp", "postgres", target_version="17")
        app_factory.addons.migrate_rollback("myapp", "postgres")
        meta_cm_name = safe_name(app_factory.prefix, "addon-meta", "myapp-postgres")
        cm = app_factory.k8s.get_configmap(app_factory.namespace, meta_cm_name)
        assert cm["data"]["version"] == "16"

    def test_rollback_calls_on_step(self, app_factory: KuberokuFactory) -> None:
        steps: list[str] = []
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        app_factory.addons.migrate("myapp", "postgres", target_version="17")
        app_factory.addons.migrate_rollback("myapp", "postgres", on_step=steps.append)
        assert len(steps) > 0


# ── CLI: addons migrate / addons migrate-rollback ───────────────


class TestCliMigrate:
    def test_cli_migrate(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        runner = CliRunner()
        result = runner.invoke(
            cli,
            ["addons", "migrate", "postgres", "-a", "myapp", "--version", "17"],
            obj={"factory": app_factory},
        )
        assert result.exit_code == 0
        assert "Migrated" in result.output

    def test_cli_migrate_rollback(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        app_factory.addons.migrate("myapp", "postgres", target_version="17")
        runner = CliRunner()
        result = runner.invoke(
            cli,
            ["addons", "migrate-rollback", "postgres", "-a", "myapp"],
            obj={"factory": app_factory},
        )
        assert result.exit_code == 0
        assert "Rolled back" in result.output

    def test_cli_migrate_colon_syntax(self, app_factory: KuberokuFactory) -> None:
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        runner = CliRunner()
        result = runner.invoke(
            cli,
            ["addons:migrate", "postgres", "-a", "myapp", "--version", "17"],
            obj={"factory": app_factory},
        )
        assert result.exit_code == 0
        assert "Migrated" in result.output

    def test_cli_migrate_rollback_colon_syntax(self, app_factory: KuberokuFactory) -> None:
        """addons:migrate:rollback should resolve to migrate-rollback."""
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        app_factory.addons.migrate("myapp", "postgres", target_version="17")
        runner = CliRunner()
        result = runner.invoke(
            cli,
            ["addons:migrate:rollback", "postgres", "-a", "myapp"],
            obj={"factory": app_factory},
        )
        assert result.exit_code == 0
        assert "Rolled back" in result.output

    def test_cli_create_with_version(self, app_factory: KuberokuFactory) -> None:
        runner = CliRunner()
        result = runner.invoke(
            cli,
            ["addons", "create", "postgres", "-a", "myapp", "--version", "17"],
            obj={"factory": app_factory},
        )
        assert result.exit_code == 0
        assert "postgres" in result.output


# ── PVC wipe + restore behavior ─────────────────────────────────


class TestMigratePvcWipe:
    def test_migrate_postgres_deletes_pvc(self, app_factory: KuberokuFactory) -> None:
        """Postgres migrate should delete the PVC (has migrate_dump_command)."""
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        pvc_name = addon_pvc_name(app_factory.prefix, "myapp", "postgres", "pgdata")

        # Verify PVC exists before migrate
        app_factory.k8s.get_pvc(app_factory.namespace, pvc_name)

        app_factory.addons.migrate("myapp", "postgres", target_version="17")

        # PVC should be deleted after migrate
        k8s: Any = app_factory.k8s
        with pytest.raises(FakeNotFoundError):
            k8s.get_pvc(app_factory.namespace, pvc_name)

    def test_migrate_postgres_calls_file_based_restore(self, app_factory: KuberokuFactory) -> None:
        """Postgres migrate should dump to file, transfer via base64, restore from file."""
        app_factory.addons.create("myapp", "postgres", version="16")
        _inject_addon_pod(app_factory, "myapp", "postgres")
        k8s: Any = app_factory.k8s
        assert isinstance(k8s, FakeK8sClient)
        k8s.exec_calls.clear()

        app_factory.addons.migrate("myapp", "postgres", target_version="17")

        commands = [c["command"] for c in k8s.exec_calls]

        # Should call pg_dump (text dump to file)
        dump_calls = [c for c in commands if c[0] == "pg_dump" and "-f" in c]
        assert len(dump_calls) == 1

        # Should call base64 to read the dump
        b64_read_calls = [c for c in commands if c[0] == "base64"]
        assert len(b64_read_calls) == 1

        # Should call psql -f (restore from file, no stdin_data)
        restore_calls = [c for c in commands if c[0] == "psql" and "-f" in c]
        assert len(restore_calls) == 1
        assert "/tmp/backup.sql" in restore_calls[0]

        # No calls should use stdin_data
        stdin_calls = [c for c in k8s.exec_calls if c.get("stdin_data")]
        assert len(stdin_calls) == 0

    def test_redis_upgrade_no_pvc_wipe(self, app_factory: KuberokuFactory) -> None:
        """Redis upgrade (7→8) should NOT delete PVC (forward compatible)."""
        app_factory.addons.create("myapp", "redis", version="7")
        _inject_addon_pod(app_factory, "myapp", "redis")
        pvc_name = addon_pvc_name(app_factory.prefix, "myapp", "redis", "redis-data")

        app_factory.addons.migrate("myapp", "redis", target_version="8")

        # PVC should still exist after upgrade
        app_factory.k8s.get_pvc(app_factory.namespace, pvc_name)

    def test_redis_downgrade_wipes_pvc(self, app_factory: KuberokuFactory) -> None:
        """Redis downgrade (8→7) should delete PVC (RDB format incompatible)."""
        app_factory.addons.create("myapp", "redis", version="8")
        _inject_addon_pod(app_factory, "myapp", "redis")
        pvc_name = addon_pvc_name(app_factory.prefix, "myapp", "redis", "redis-data")

        app_factory.addons.migrate("myapp", "redis", target_version="7")

        # PVC should be deleted after downgrade
        k8s: Any = app_factory.k8s
        with pytest.raises(FakeNotFoundError):
            k8s.get_pvc(app_factory.namespace, pvc_name)

    def test_redis_downgrade_sets_data_wiped(self, app_factory: KuberokuFactory) -> None:
        """Redis downgrade should set data_wiped=true in metadata."""
        app_factory.addons.create("myapp", "redis", version="8")
        _inject_addon_pod(app_factory, "myapp", "redis")

        app_factory.addons.migrate("myapp", "redis", target_version="7")

        meta_cm_name = safe_name(app_factory.prefix, "addon-meta", "myapp-redis")
        cm = app_factory.k8s.get_configmap(app_factory.namespace, meta_cm_name)
        assert cm["data"].get("data_wiped") == "true"

    def test_force_migrate_tolerates_backup_failure(self, app_factory: KuberokuFactory) -> None:
        """force=True should tolerate backup failure (crashing pod)."""
        app_factory.addons.create("myapp", "postgres", version="16")
        # Inject pod WITHOUT exec output → backup will get empty string
        # but we don't inject a pod at all to simulate crash
        # Actually, need to create PVC but no pod so backup raises
        k8s: Any = app_factory.k8s
        assert isinstance(k8s, FakeK8sClient)
        pvc_name = addon_pvc_name(app_factory.prefix, "myapp", "postgres", "pgdata")
        k8s.create_pvc(
            app_factory.namespace,
            {
                "apiVersion": "v1",
                "kind": "PersistentVolumeClaim",
                "metadata": {"name": pvc_name},
                "spec": {"resources": {"requests": {"storage": "1Gi"}}},
            },
        )
        # No pod → backup will fail, but force=True should tolerate it
        addon = app_factory.addons.migrate("myapp", "postgres", target_version="17", force=True)
        assert addon.image == "postgres:17"
