"""E2E: addon versioning — migrate, rollback, path validation.

Full lifecycle test for addon version migration on a real K8s cluster.
Covers postgres (sequential migration paths) and redis (any-to-any).

Marked @slow — skipped by default, run with ``--runslow``.

PostgreSQL migration uses file-based dump (pg_dump → file → base64 transfer →
psql -f restore) to safely handle major version changes (different on-disk formats).

Redis upgrade (7→8) is a simple image swap (forward compatible).
Redis downgrade (8→7) wipes the PVC (RDB v12 incompatible with v7).

Requires:
- K8s cluster with postgres:15, postgres:16, postgres:17, redis:7, redis:8
  images pullable
- StorageClass with dynamic provisioning

Run with: pytest tests/e2e/test_addon_versioning.py -v -s
"""

from __future__ import annotations

from typing import Any

import pytest
from click.testing import CliRunner

from kuberoku.cli.addons import addons as addons_group
from kuberoku.exceptions import KuberokuError
from kuberoku.factory import KuberokuFactory
from kuberoku.k8s.resources import safe_name
from tests.e2e.helpers import (
    cleanup_namespace_fixture,
    make_real_factory,
    skip_if_no_cluster,
    wait_for_pg_ready,
    wait_for_redis_ready,
    wait_for_sts_ready,
)

pytestmark = pytest.mark.slow

APP = "ver-test"


def _get_sts_image(factory: KuberokuFactory, sts_name: str) -> str:
    sts = factory.k8s.get_statefulset(factory.namespace, sts_name)
    return str(sts["spec"]["template"]["spec"]["containers"][0]["image"])


def _get_metadata_field(
    factory: KuberokuFactory,
    app: str,
    instance: str,
    field: str,
) -> str:
    meta_cm_name = safe_name(factory.prefix, "addon-meta", f"{app}-{instance}")
    cm = factory.k8s.get_configmap(factory.namespace, meta_cm_name)
    return str(cm.get("data", {}).get(field, ""))


# ── Fixtures ─────────────────────────────────────────────────────


@pytest.fixture(scope="module")
def real_factory() -> KuberokuFactory:
    return make_real_factory("e2e-ver")


@pytest.fixture(scope="module", autouse=True)
def cleanup_namespace(real_factory: KuberokuFactory) -> Any:
    yield
    cleanup_namespace_fixture(real_factory)


# ── Tests ────────────────────────────────────────────────────────


def test_00_skip_if_no_cluster(real_factory: KuberokuFactory) -> None:
    skip_if_no_cluster()


def test_01_create_app(real_factory: KuberokuFactory) -> None:
    app = real_factory.apps.create(APP)
    assert app.name == APP
    print(f"\n  App created: {app.name}")
    print(f"  Namespace: {real_factory.namespace}")


def test_02_deploy_app(real_factory: KuberokuFactory) -> None:
    """Deploy nginx — provides a running pod in the namespace."""
    release = real_factory.deploy.deploy(
        APP,
        image="nginx:1.27-alpine",
        ports=[{"containerPort": 80, "protocol": "TCP"}],
        wait=False,
    )
    assert release.version >= 1
    print(f"\n  Deployed v{release.version}")


# ── Postgres: default version creates v16 ────────────────────────


def test_03_create_postgres_default(real_factory: KuberokuFactory) -> None:
    """Create postgres with default version (16)."""
    addon = real_factory.addons.create(APP, "postgres")
    assert addon.addon_type == "postgres"
    assert addon.image == "postgres:16"
    print(f"\n  Postgres created: image={addon.image}")

    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-postgres")
    assert _get_sts_image(real_factory, sts_name) == "postgres:16"

    version = _get_metadata_field(real_factory, APP, "postgres", "version")
    assert version == "16"

    wait_for_sts_ready(real_factory, sts_name)
    wait_for_pg_ready(real_factory, sts_name)
    print(f"  pg_isready: OK (version {version})")


def test_04_write_and_read_data(real_factory: KuberokuFactory) -> None:
    """Write and read data on v16 to verify addon is working."""
    pod_name, cmd = real_factory.addons.exec_command(APP, "postgres")

    real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        cmd + ["-c", "CREATE TABLE IF NOT EXISTS e2e_test (id int, val text);"],
    )
    real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        cmd + ["-c", "INSERT INTO e2e_test VALUES (1, 'hello-v16');"],
    )

    output = real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        cmd + ["-c", "SELECT val FROM e2e_test WHERE id = 1;"],
    )
    assert "hello-v16" in output
    print(f"\n  Data written and verified: hello-v16")


def test_05_backup_works(real_factory: KuberokuFactory) -> None:
    """Verify pg_dump backup succeeds on live addon."""
    output = real_factory.addons.backup(APP, "postgres")
    assert len(output) > 0
    assert "CREATE TABLE" in output or "e2e_test" in output
    print(f"\n  Backup: {len(output)} bytes, contains schema")


# ── Postgres: path validation (no image swap needed) ─────────────


def test_06_invalid_version_rejects(real_factory: KuberokuFactory) -> None:
    """Migrating to a nonexistent version is rejected."""
    with pytest.raises(KuberokuError, match="not available"):
        real_factory.addons.migrate(APP, "postgres", target_version="99")
    print(f"\n  Invalid version '99' correctly rejected")


def test_07_same_version_rejects(real_factory: KuberokuFactory) -> None:
    """Migrating to the current version is rejected."""
    with pytest.raises(KuberokuError, match="already at version"):
        real_factory.addons.migrate(APP, "postgres", target_version="16")
    print(f"\n  Same-version migration correctly rejected")


def test_08_skip_path_rejects(real_factory: KuberokuFactory) -> None:
    """16→15 is allowed, but 16→... let's test the real forbidden one.

    Destroy v16 and create v15 to test 15→17 (skip not allowed).
    """
    real_factory.addons.destroy(APP, "postgres", delete_data=True)
    real_factory.addons.create(APP, "postgres", version="15")
    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-postgres")
    wait_for_sts_ready(real_factory, sts_name)
    wait_for_pg_ready(real_factory, sts_name)

    with pytest.raises(KuberokuError, match="15 -> 16 -> 17"):
        real_factory.addons.migrate(APP, "postgres", target_version="17")
    print(f"\n  15→17 correctly rejected with path suggestion")


def test_09_force_migrate_with_data(real_factory: KuberokuFactory) -> None:
    """Force migrate postgres 15→17 with file-based binary dump + restore.

    Write data on v15, force migrate to v17. The migrate does:
    freeze service → pg_dump -Fc to file → base64 read → scale down →
    delete PVC → swap to v17 → scale up → chunked base64 write →
    pg_restore from file → restore service.

    Verify: pod starts, data survives.
    """
    # Write data on v15
    pod_name, cmd = real_factory.addons.exec_command(APP, "postgres")
    real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        cmd + ["-c", "CREATE TABLE IF NOT EXISTS e2e_test (id int, val text);"],
    )
    real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        cmd + ["-c", "INSERT INTO e2e_test VALUES (2, 'hello-v15');"],
    )

    # Force migrate 15→17
    addon = real_factory.addons.migrate(
        APP,
        "postgres",
        target_version="17",
        force=True,
    )
    assert addon.image == "postgres:17"

    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-postgres")
    assert _get_sts_image(real_factory, sts_name) == "postgres:17"

    version = _get_metadata_field(real_factory, APP, "postgres", "version")
    assert version == "17"

    # SDK migrate() already waited for pod ready — just verify postgres accepts connections
    wait_for_pg_ready(real_factory, sts_name)

    # Data should survive via backup+restore
    pod_name, cmd = real_factory.addons.exec_command(APP, "postgres")
    output = real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        cmd + ["-c", "SELECT val FROM e2e_test WHERE id = 2;"],
    )
    assert "hello-v15" in output
    print(f"\n  Force migrate 15→17: pod running, data survived!")


def test_10_rollback_with_data(real_factory: KuberokuFactory) -> None:
    """Rollback postgres 17→15 with backup + PVC wipe + restore.

    Verify: pod starts, data survives.
    """
    # Write more data on v17 before rollback
    pod_name, cmd = real_factory.addons.exec_command(APP, "postgres")
    real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        cmd + ["-c", "INSERT INTO e2e_test VALUES (3, 'hello-v17');"],
    )

    addon = real_factory.addons.migrate_rollback(APP, "postgres")
    assert addon.image == "postgres:15"

    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-postgres")
    assert _get_sts_image(real_factory, sts_name) == "postgres:15"

    version = _get_metadata_field(real_factory, APP, "postgres", "version")
    assert version == "15"

    # SDK migrate_rollback() already waited — verify postgres accepts connections
    wait_for_pg_ready(real_factory, sts_name)

    # Data from v17 should survive
    pod_name, cmd = real_factory.addons.exec_command(APP, "postgres")
    output = real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        cmd + ["-c", "SELECT val FROM e2e_test WHERE id = 3;"],
    )
    assert "hello-v17" in output
    print(f"\n  Rollback 17→15: pod running, data survived!")


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


def test_11_destroy_postgres_for_fresh(real_factory: KuberokuFactory) -> None:
    real_factory.addons.destroy(APP, "postgres", delete_data=True)
    addons = real_factory.addons.list(APP)
    assert not any(a.instance_name == "postgres" for a in addons)
    print(f"\n  Postgres destroyed for fresh create")


def test_12_create_postgres_v15(real_factory: KuberokuFactory) -> None:
    """Create with explicit --version 15."""
    addon = real_factory.addons.create(APP, "postgres", version="15")
    assert addon.image == "postgres:15"

    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-postgres")
    assert _get_sts_image(real_factory, sts_name) == "postgres:15"

    wait_for_sts_ready(real_factory, sts_name)
    wait_for_pg_ready(real_factory, sts_name)
    print(f"\n  Created postgres v15: image={addon.image}")


# ── Redis: full migration with data ─────────────────────────────


def test_13_create_redis_default(real_factory: KuberokuFactory) -> None:
    addon = real_factory.addons.create(APP, "redis")
    assert addon.addon_type == "redis"
    assert addon.image == "redis:7"

    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-redis")
    assert _get_sts_image(real_factory, sts_name) == "redis:7"

    wait_for_sts_ready(real_factory, sts_name)
    wait_for_redis_ready(real_factory, sts_name, APP)
    print(f"\n  Redis created: image={addon.image}")


def test_14_redis_write_data(real_factory: KuberokuFactory) -> None:
    creds = real_factory.addons.credentials(APP, "redis")
    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-redis")
    pod_name = f"{sts_name}-0"

    real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        ["redis-cli", "-a", creds["password"], "SET", "e2e-key", "e2e-value"],
    )

    output = real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        ["redis-cli", "-a", creds["password"], "GET", "e2e-key"],
    )
    assert "e2e-value" in output
    print(f"\n  Redis data written: e2e-key=e2e-value")


def test_15_migrate_redis_7_to_8(real_factory: KuberokuFactory) -> None:
    """Redis migration_paths=None → any transition allowed."""
    addon = real_factory.addons.migrate(APP, "redis", target_version="8")
    assert addon.image == "redis:8"

    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-redis")
    assert _get_sts_image(real_factory, sts_name) == "redis:8"

    version = _get_metadata_field(real_factory, APP, "redis", "version")
    assert version == "8"
    prev = _get_metadata_field(real_factory, APP, "redis", "previous_version")
    assert prev == "7"

    # SDK migrate() already waited for pod ready — just verify redis accepts connections
    wait_for_redis_ready(real_factory, sts_name, APP)
    print(f"\n  Redis migrated 7→8: image={addon.image}")


def test_16_redis_operational_after_migration(real_factory: KuberokuFactory) -> None:
    """Verify redis is fully operational after migration."""
    creds = real_factory.addons.credentials(APP, "redis")
    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-redis")
    pod_name = f"{sts_name}-0"

    real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        ["redis-cli", "-a", creds["password"], "SET", "post-migrate", "ok"],
    )
    output = real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        ["redis-cli", "-a", creds["password"], "GET", "post-migrate"],
    )
    assert "ok" in output
    print(f"\n  Redis operational after migration: post-migrate=ok")


def test_17_redis_rollback_with_pvc_wipe(real_factory: KuberokuFactory) -> None:
    """Redis rollback 8→7: PVC wipe ensures pod starts cleanly.

    Redis 8 writes RDB format v12 which Redis 7 cannot read. The migrate
    now wipes the PVC on downgrades, so the pod starts with a fresh
    data directory. Data is lost (documented — redis has no backup_command).
    """
    addon = real_factory.addons.migrate_rollback(APP, "redis")
    assert addon.image == "redis:7"

    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-redis")
    assert _get_sts_image(real_factory, sts_name) == "redis:7"

    version = _get_metadata_field(real_factory, APP, "redis", "version")
    assert version == "7"

    # SDK migrate() already waited for pod ready — just verify redis accepts connections
    wait_for_redis_ready(real_factory, sts_name, APP)

    # Verify data_wiped marker in metadata
    data_wiped = _get_metadata_field(real_factory, APP, "redis", "data_wiped")
    assert data_wiped == "true"
    print(f"\n  Redis rollback 8→7: pod running after PVC wipe, data_wiped={data_wiped}")


def test_18_redis_operational_after_rollback(real_factory: KuberokuFactory) -> None:
    """Verify redis is fully operational after rollback (SET/GET works)."""
    creds = real_factory.addons.credentials(APP, "redis")
    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-redis")
    pod_name = f"{sts_name}-0"

    real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        ["redis-cli", "-a", creds["password"], "SET", "post-rollback", "works"],
    )
    output = real_factory.k8s.exec_in_pod(
        real_factory.namespace,
        pod_name,
        ["redis-cli", "-a", creds["password"], "GET", "post-rollback"],
    )
    assert "works" in output
    print(f"\n  Redis operational after rollback: post-rollback=works")


# ── Addon info shows version ─────────────────────────────────────


def test_19_addon_info_shows_version(real_factory: KuberokuFactory) -> None:
    addon = real_factory.addons.info(APP, "postgres")
    assert "postgres:15" in addon.image
    print(f"\n  Addon info postgres: image={addon.image}")

    addon_redis = real_factory.addons.info(APP, "redis")
    assert "redis:7" in addon_redis.image
    print(f"  Addon info redis: image={addon_redis.image}")


# ── Scale (resource change, not version) ─────────────────────────


def test_20_scale_postgres_memory(real_factory: KuberokuFactory) -> None:
    addon = real_factory.addons.scale(APP, "postgres", memory="512Mi")
    assert addon.memory == "512Mi"
    print(f"\n  Scaled postgres memory: {addon.memory}")

    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-postgres")
    wait_for_sts_ready(real_factory, sts_name)

    sts = real_factory.k8s.get_statefulset(real_factory.namespace, sts_name)
    container = sts["spec"]["template"]["spec"]["containers"][0]
    assert container["resources"]["requests"]["memory"] == "512Mi"

    version = _get_metadata_field(real_factory, APP, "postgres", "version")
    assert version == "15"
    print(f"  Version unchanged: {version}")


# ── CLI commands (use redis which handles migration cleanly) ─────


def test_21_cli_addons_migrate_redis(real_factory: KuberokuFactory) -> None:
    """CLI: addons migrate redis --version 8.

    Redis is at v7 from test_17 (rollback).  Forward migration
    (v7→v8) works because Redis 8 reads Redis 7 data format.
    """
    runner = CliRunner()
    result = runner.invoke(
        addons_group,
        ["migrate", "redis", "-a", APP, "--version", "8"],
        obj={"factory": real_factory},
    )
    assert result.exit_code == 0, f"CLI failed: {result.output}"
    assert "Migrated" in result.output
    print(f"\n  CLI migrate redis: {result.output.strip()}")

    # SDK migrate() already waited for pod ready — just verify redis accepts connections
    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-redis")
    wait_for_redis_ready(real_factory, sts_name, APP)


def test_22_cli_rollback_redis_pod_starts(real_factory: KuberokuFactory) -> None:
    """CLI: addons migrate-rollback redis — pod starts after PVC wipe.

    Verifies the full CLI flow: rollback → PVC wipe → pod ready.
    """
    runner = CliRunner()
    result = runner.invoke(
        addons_group,
        ["migrate-rollback", "redis", "-a", APP],
        obj={"factory": real_factory},
    )
    assert result.exit_code == 0, f"CLI failed: {result.output}"
    assert "Rolled back" in result.output
    print(f"\n  CLI rollback redis: {result.output.strip()}")

    sts_name = safe_name(real_factory.prefix, "addon", f"{APP}-redis")
    assert _get_sts_image(real_factory, sts_name) == "redis:7"

    # SDK migrate() already waited for pod ready — just verify redis accepts connections
    wait_for_redis_ready(real_factory, sts_name, APP)
    print(f"  CLI rollback: pod running, redis operational")


# ── Cleanup ──────────────────────────────────────────────────────


def test_23_destroy_all(real_factory: KuberokuFactory) -> None:
    real_factory.addons.destroy(APP, "redis", delete_data=True)
    real_factory.addons.destroy(APP, "postgres", delete_data=True)

    addons = real_factory.addons.list(APP)
    assert len(addons) == 0
    print(f"\n  All addons destroyed")


def test_24_destroy_app(real_factory: KuberokuFactory) -> None:
    real_factory.apps.destroy(APP)
    app_list = real_factory.apps.list()
    assert not any(a.name == APP for a in app_list)
    print(f"\n  App {APP} destroyed")


def test_25_verify_clean(real_factory: KuberokuFactory) -> None:
    """Verify no resources remain."""
    app_list = real_factory.apps.list()
    assert not any(a.name == APP for a in app_list)
    print(f"\n  Clean: no app or addons remain")
