"""End-to-end git-deploy test on a real K8s cluster.

Creates a git repository with a Dockerfile, commits changes, builds images,
and deploys to K8s. Tests deploying from HEAD, a tag, and a raw commit SHA.

Registry behaviour:
- CI (k3d):  KUBEROKU_REGISTRY=k3d-registry.localhost:5000 (set by workflow)
             → images are pushed to the k3d in-cluster registry.
- Local dev: No env var needed — images are loaded directly into the
             cluster's containerd via ``local_load_command()``
             (k3d, kind, minikube, Colima all supported).

Requires:
- K8s cluster running (colima, k3d, kind, etc.)
- Docker daemon running

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

from __future__ import annotations

import os
import subprocess
import tempfile
from collections.abc import Generator
from pathlib import Path
from typing import Any

import pytest

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_pods_ready,
    wait_for_port_open,
)

# Registry to use — CI sets KUBEROKU_REGISTRY for k3d's built-in registry.
# On local clusters (Colima/kind/minikube), images are loaded directly
# into the cluster's containerd — no registry needed.
_REGISTRY = os.environ.get("KUBEROKU_REGISTRY", "")


def _expected_image(app: str, commit_sha: str) -> str:
    """Return the expected image name for the given app and commit."""
    short = commit_sha[:7]
    return f"{_REGISTRY}/{app}:{short}" if _REGISTRY else f"{app}:{short}"


def _detect_local_load_cmd() -> str:
    """Auto-detect the KUBEROKU_LOCAL_LOAD command for the current cluster.

    Only needed when KUBEROKU_REGISTRY is not set (local cluster, no push).
    Returns empty string if no load command is needed (e.g. Docker Desktop
    shares the daemon, or auto-detect handles it via local_load_command()).
    """
    if _REGISTRY:
        return ""  # Using a registry — no local load needed

    # Check kubectl context for known local distributions
    try:
        ctx = (
            subprocess.run(
                ["kubectl", "config", "current-context"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            .stdout.strip()
            .lower()
        )
    except Exception:
        return ""

    if "colima" in ctx:
        return "sh -c 'docker save \"$1\" | colima ssh -- ctr -n k8s.io images import -' --"
    # k3d/kind/minikube are handled by local_load_command() auto-detect
    return ""


# ── Git repo fixture ────────────────────────────────────────────


@pytest.fixture(scope="module")
def git_repo() -> Generator[Path, None, None]:
    """Create a temp git repo with a Dockerfile and two commits.

    Commit 1 (tagged v1.0): nginx:1.27-alpine
    Commit 2 (HEAD):        nginx:1.27
    """
    with tempfile.TemporaryDirectory(prefix="kuberoku-git-e2e-") as tmpdir:
        repo = Path(tmpdir)

        # Init repo
        subprocess.run(["git", "init"], cwd=repo, capture_output=True, check=True)
        subprocess.run(
            ["git", "config", "user.email", "test@kuberoku.dev"],
            cwd=repo,
            capture_output=True,
            check=True,
        )
        subprocess.run(
            ["git", "config", "user.name", "E2E Test"],
            cwd=repo,
            capture_output=True,
            check=True,
        )

        # Commit 1: simple alpine web server
        # NOTE: Use alpine base with RUN layer — Docker 28/29 legacy builder
        # can't push multi-platform base images (nginx:alpine) to registries.
        # Building FROM alpine with a RUN layer creates a single-platform image.
        dockerfile = repo / "Dockerfile"
        dockerfile.write_text(
            "FROM alpine:3.21\n"
            "RUN apk add --no-cache nginx "
            "&& mkdir -p /run/nginx /var/lib/nginx/html "
            '&& echo "v1" > /var/lib/nginx/html/index.html '
            '&& printf "server {\\n  listen 80;\\n  location / '
            '{\\n    root /var/lib/nginx/html;\\n  }\\n}\\n" '
            "> /etc/nginx/http.d/default.conf\n"
            "EXPOSE 80\n"
            'CMD ["nginx", "-g", "daemon off;"]\n'
        )
        subprocess.run(["git", "add", "."], cwd=repo, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "v1: alpine"],
            cwd=repo,
            capture_output=True,
            check=True,
        )
        subprocess.run(
            ["git", "tag", "v1.0"],
            cwd=repo,
            capture_output=True,
            check=True,
        )

        # Commit 2: updated content
        dockerfile.write_text(
            "FROM alpine:3.21\n"
            "RUN apk add --no-cache nginx "
            "&& mkdir -p /run/nginx /var/lib/nginx/html "
            '&& echo "v2" > /var/lib/nginx/html/index.html '
            '&& printf "server {\\n  listen 80;\\n  location / '
            '{\\n    root /var/lib/nginx/html;\\n  }\\n}\\n" '
            "> /etc/nginx/http.d/default.conf\n"
            "EXPOSE 80\n"
            'CMD ["nginx", "-g", "daemon off;"]\n'
        )
        subprocess.run(["git", "add", "."], cwd=repo, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "v2: full"],
            cwd=repo,
            capture_output=True,
            check=True,
        )

        yield repo


@pytest.fixture(scope="module")
def commit_shas(git_repo: Path) -> dict[str, str]:
    """Get commit SHAs for both commits."""
    result = subprocess.run(
        ["git", "log", "--format=%H", "--reverse"],
        cwd=git_repo,
        capture_output=True,
        text=True,
        check=True,
    )
    shas = result.stdout.strip().split("\n")
    return {"v1": shas[0], "v2": shas[1]}


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


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


# ── Module-level skip ────────────────────────────────────────────


@pytest.fixture(scope="module", autouse=True)
def _skip_if_no_infra() -> Generator[None, None, None]:
    """Skip the entire module if no cluster or Docker is available.

    Also sets KUBEROKU_LOCAL_LOAD for local clusters that aren't auto-detected
    by ``local_load_command()`` (e.g. Colima).
    """
    skip_if_no_cluster()

    # Check Docker is running
    try:
        result = subprocess.run(
            ["docker", "info"],
            capture_output=True,
            timeout=10,
        )
        if result.returncode != 0:
            pytest.skip("Docker not running")
    except Exception:
        pytest.skip("Docker not available")

    # Set up env vars for the module
    local_load = _detect_local_load_cmd()
    if _REGISTRY:
        os.environ["KUBEROKU_REGISTRY"] = _REGISTRY
    if local_load:
        os.environ["KUBEROKU_LOCAL_LOAD"] = local_load

    yield

    os.environ.pop("KUBEROKU_REGISTRY", None)
    os.environ.pop("KUBEROKU_LOCAL_LOAD", None)


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


def test_01_create_app(real_factory: KuberokuFactory) -> None:
    """Create the git-deploy test app."""
    app = real_factory.apps.create("gitapp", on_step=lambda s: print(f"  {s}"))
    assert app.name == "gitapp"
    print(f"\n  App created: {app.name} in {real_factory.namespace}")


def test_02_deploy_head(
    real_factory: KuberokuFactory,
    git_repo: Path,
    commit_shas: dict[str, str],
) -> None:
    """Deploy from HEAD (commit 2)."""
    old_cwd = os.getcwd()
    os.chdir(git_repo)
    try:
        steps: list[str] = []
        release = real_factory.deploy.deploy(
            "gitapp",
            ref="HEAD",
            ports=[{"containerPort": 80, "protocol": "TCP"}],
            wait=False,
            on_step=steps.append,
        )
        assert release.version == 1
        assert release.commit == commit_shas["v2"]
        assert release.images["web"] == _expected_image("gitapp", commit_shas["v2"])
        print(f"\n  Release v{release.version}: {release.images}")
        print(f"  Commit: {release.commit}")
        print(f"  Steps: {steps}")
    finally:
        os.chdir(old_cwd)


def test_03_pods_up_after_head(real_factory: KuberokuFactory) -> None:
    """Verify pods come up after HEAD deploy."""
    pods = wait_for_pods_ready(real_factory, "gitapp", expected=1)
    assert len(pods) >= 1
    image = pods[0]["spec"]["containers"][0].get("image", "")
    print(f"\n  Pod running with image: {image}")
    assert "gitapp:" in image


def test_04_deploy_tag(
    real_factory: KuberokuFactory,
    git_repo: Path,
    commit_shas: dict[str, str],
) -> None:
    """Deploy from tag v1.0 (commit 1)."""
    old_cwd = os.getcwd()
    os.chdir(git_repo)
    try:
        release = real_factory.deploy.deploy(
            "gitapp",
            ref="v1.0",
            ports=[{"containerPort": 80, "protocol": "TCP"}],
            wait=False,
            on_step=lambda s: print(f"  {s}"),
        )
        assert release.version == 2
        assert release.commit == commit_shas["v1"]
        assert release.images["web"] == _expected_image("gitapp", commit_shas["v1"])
        print(f"\n  Release v{release.version}: {release.images}")
        print(f"  Commit: {release.commit}")
    finally:
        os.chdir(old_cwd)


def test_05_pods_up_after_tag(real_factory: KuberokuFactory) -> None:
    """Verify pods update after tag deploy."""
    # Get the expected image from the tag deploy
    releases = real_factory.releases.list("gitapp")
    latest = max(releases, key=lambda r: r.version)
    expected_image = latest.images["web"]

    pods = wait_for_pods_ready(real_factory, "gitapp", expected=1, image=expected_image)
    assert len(pods) >= 1
    image = pods[0]["spec"]["containers"][0].get("image", "")
    print(f"\n  Pod running with image: {image}")


def test_06_deploy_sha(
    real_factory: KuberokuFactory,
    git_repo: Path,
    commit_shas: dict[str, str],
) -> None:
    """Deploy from raw commit SHA (commit 1)."""
    old_cwd = os.getcwd()
    os.chdir(git_repo)
    try:
        sha = commit_shas["v1"]
        release = real_factory.deploy.deploy(
            "gitapp",
            ref=sha,
            ports=[{"containerPort": 80, "protocol": "TCP"}],
            wait=False,
            on_step=lambda s: print(f"  {s}"),
        )
        assert release.version == 3
        assert release.commit == sha
        assert release.images["web"] == _expected_image("gitapp", sha)
        print(f"\n  Release v{release.version}: {release.images}")
        print(f"  Commit: {release.commit}")
    finally:
        os.chdir(old_cwd)


def test_07_pods_up_after_sha(real_factory: KuberokuFactory) -> None:
    """Verify pods update after SHA deploy."""
    releases = real_factory.releases.list("gitapp")
    latest = max(releases, key=lambda r: r.version)
    expected_image = latest.images["web"]

    pods = wait_for_pods_ready(real_factory, "gitapp", expected=1, image=expected_image)
    assert len(pods) >= 1
    print(f"\n  Pod running with image: {pods[0]['spec']['containers'][0].get('image', '')}")


def test_08_releases_list(real_factory: KuberokuFactory) -> None:
    """Verify all 3 releases exist."""
    releases = real_factory.releases.list("gitapp")
    assert len(releases) >= 3
    for r in releases:
        print(f"  v{r.version}: {r.description} (commit={r.commit})")


def test_09_port_forward_and_curl(real_factory: KuberokuFactory) -> None:
    """Port-forward to the service and verify HTTP response."""
    svc_name = safe_name(real_factory.prefix, "gitapp", "web")
    wait_for_pods_ready(real_factory, "gitapp", expected=1)

    try:
        pf = subprocess.Popen(
            [
                "kubectl",
                "port-forward",
                f"svc/{svc_name}",
                "9998:80",
                "-n",
                real_factory.namespace,
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        try:
            wait_for_port_open("127.0.0.1", 9998, timeout=10)
            result = subprocess.run(
                ["curl", "-s", "-m", "10", "http://127.0.0.1:9998/"],
                capture_output=True,
                text=True,
                timeout=15,
            )
            body = result.stdout
            print(f"\n  Response length: {len(body)}")
            print(f"  Response preview: {body[:200]}")
            assert len(body) > 0
        finally:
            pf.terminate()
            pf.wait(timeout=5)
    except Exception as e:
        print(f"  Port-forward/curl failed: {e}")
        pytest.skip(f"Port-forward not available: {e}")


def test_10_destroy_app(real_factory: KuberokuFactory) -> None:
    """Destroy the app and verify cleanup."""
    real_factory.apps.destroy("gitapp")
    app_list = real_factory.apps.list()
    assert not any(a.name == "gitapp" for a in app_list)
    print("\n  App destroyed successfully")
