"""Tests for cube_cluster_login server-side tool (clusters.py)."""

from __future__ import annotations

import asyncio
import json
import os
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import yaml

from cube_cloud.tools.clusters import cube_cluster_login


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

SAMPLE_KUBECONFIG = {
    "apiVersion": "v1",
    "kind": "Config",
    "clusters": [{
        "name": "staging-int",
        "cluster": {
            "server": "https://teleport-proxy.example.com:443",
            "certificate-authority-data": "LS0tLS1CRUdJTi...",
            "tls-server-name": "kube-teleport-proxy-alpn.teleport-proxy.example.com",
        },
    }],
    "users": [{
        "name": "bot-user",
        "user": {
            "exec": {
                "command": "tbot",
                "args": ["kube", "credentials", "--destination-dir=/tbot-data/kube/staging-int"],
            },
        },
    }],
    "contexts": [{
        "name": "staging-int",
        "context": {"cluster": "staging-int", "user": "bot-user"},
    }],
    "current-context": "staging-int",
}

SAMPLE_EXEC_CREDENTIAL = {
    "kind": "ExecCredential",
    "apiVersion": "client.authentication.k8s.io/v1",
    "status": {
        "clientCertificateData": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0t...",
        "clientKeyData": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURS0t...",
        "expirationTimestamp": "2026-02-28T12:00:00Z",
    },
}


@pytest.fixture
def fake_tbot_dir(tmp_path):
    """Create a fake tbot data directory with a kubeconfig for staging-int."""
    kube_dir = tmp_path / "kube" / "staging-int"
    kube_dir.mkdir(parents=True)
    kc_path = kube_dir / "kubeconfig.yaml"
    kc_path.write_text(yaml.dump(SAMPLE_KUBECONFIG))
    return tmp_path


@pytest.fixture
def mock_tbot(fake_tbot_dir):
    """Patch the tbot singleton so tests don't need real /tbot-data."""
    with patch("cube_cloud.tools.clusters.tbot") as m:
        m.is_ready = True
        m.kube_dir = fake_tbot_dir / "kube"
        m.get_kubeconfig.return_value = str(
            fake_tbot_dir / "kube" / "staging-int" / "kubeconfig.yaml"
        )
        m.list_kube_clusters.return_value = ["staging-int", "staging-pre-prod"]
        yield m


def _make_proc_mock(stdout: bytes, stderr: bytes = b"", returncode: int = 0):
    """Create a mock asyncio.Process."""
    proc = MagicMock()
    proc.returncode = returncode
    proc.communicate = AsyncMock(return_value=(stdout, stderr))
    proc.kill = MagicMock()
    return proc


# ---------------------------------------------------------------------------
# Happy path
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_success(mock_tbot):
    """cube_cluster_login returns a valid self-contained kubeconfig JSON."""
    stdout = json.dumps(SAMPLE_EXEC_CREDENTIAL).encode()
    proc = _make_proc_mock(stdout)

    with patch("cube_cloud.tools.clusters.asyncio") as mock_aio:
        mock_aio.create_subprocess_exec = AsyncMock(return_value=proc)
        mock_aio.subprocess = asyncio.subprocess
        mock_aio.wait_for = AsyncMock(return_value=(stdout, b""))
        # Directly mock wait_for to return the communicate result
        mock_aio.TimeoutError = asyncio.TimeoutError

        # Instead of mocking asyncio entirely, mock just subprocess_exec
        with patch("asyncio.create_subprocess_exec", new=AsyncMock(return_value=proc)):
            with patch("asyncio.wait_for", new=AsyncMock(return_value=(stdout, b""))):
                result = await cube_cluster_login("staging-int")

    # Parse the JSON result
    data = json.loads(result)
    assert "kubeconfig" in data
    assert "expires_at" in data
    assert data["cluster"] == "staging-int"
    assert data["expires_at"] == "2026-02-28T12:00:00Z"

    # Parse the kubeconfig YAML inside the result
    kc = yaml.safe_load(data["kubeconfig"])
    assert kc["apiVersion"] == "v1"
    assert kc["kind"] == "Config"
    assert kc["current-context"] == "staging-int"

    # Should have embedded client certs, NOT an exec plugin
    user = kc["users"][0]["user"]
    assert "client-certificate-data" in user
    assert "client-key-data" in user
    assert "exec" not in user

    # Cluster info should be carried over
    cluster = kc["clusters"][0]["cluster"]
    assert cluster["server"] == "https://teleport-proxy.example.com:443"
    assert cluster["certificate-authority-data"] == "LS0tLS1CRUdJTi..."
    assert cluster["tls-server-name"] == "kube-teleport-proxy-alpn.teleport-proxy.example.com"


@pytest.mark.asyncio
async def test_kube_login_no_tls_server_name(mock_tbot, fake_tbot_dir):
    """When tls-server-name is absent, it's omitted from output kubeconfig."""
    kc_no_tls = dict(SAMPLE_KUBECONFIG)
    kc_no_tls["clusters"] = [{
        "name": "staging-int",
        "cluster": {
            "server": "https://example.com:443",
            "certificate-authority-data": "FAKECA",
            # no tls-server-name
        },
    }]
    kc_path = fake_tbot_dir / "kube" / "staging-int" / "kubeconfig.yaml"
    kc_path.write_text(yaml.dump(kc_no_tls))

    stdout = json.dumps(SAMPLE_EXEC_CREDENTIAL).encode()
    proc = _make_proc_mock(stdout)

    with patch("asyncio.create_subprocess_exec", new=AsyncMock(return_value=proc)):
        with patch("asyncio.wait_for", new=AsyncMock(return_value=(stdout, b""))):
            result = await cube_cluster_login("staging-int")

    data = json.loads(result)
    kc = yaml.safe_load(data["kubeconfig"])
    cluster_cfg = kc["clusters"][0]["cluster"]
    assert "tls-server-name" not in cluster_cfg


# ---------------------------------------------------------------------------
# Error: tbot not ready
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_tbot_not_ready(mock_tbot):
    mock_tbot.is_ready = False
    result = await cube_cluster_login("staging-int")
    assert "initializing" in result.lower()
    assert result.startswith("Error:")


# ---------------------------------------------------------------------------
# Error: empty cluster
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_empty_cluster(mock_tbot):
    result = await cube_cluster_login("")
    assert "required" in result.lower()


# ---------------------------------------------------------------------------
# Error: cluster not found
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_cluster_not_found(mock_tbot):
    mock_tbot.get_kubeconfig.return_value = None
    result = await cube_cluster_login("nonexistent")
    assert "not found" in result.lower()
    assert "staging-int" in result  # lists available clusters


# ---------------------------------------------------------------------------
# Error: tbot kube credentials fails (non-zero exit)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_tbot_fails(mock_tbot):
    proc = _make_proc_mock(b"", stderr=b"something went wrong", returncode=1)

    with patch("asyncio.create_subprocess_exec", new=AsyncMock(return_value=proc)):
        with patch("asyncio.wait_for", new=AsyncMock(return_value=(b"", b"something went wrong"))):
            result = await cube_cluster_login("staging-int")

    assert result.startswith("Error:")
    assert "Failed to generate credentials" in result
    assert "Contact admin" in result


# ---------------------------------------------------------------------------
# Error: tbot returns invalid JSON
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_invalid_json(mock_tbot):
    proc = _make_proc_mock(b"not-json")

    with patch("asyncio.create_subprocess_exec", new=AsyncMock(return_value=proc)):
        with patch("asyncio.wait_for", new=AsyncMock(return_value=(b"not-json", b""))):
            result = await cube_cluster_login("staging-int")

    assert result.startswith("Error:")
    assert "Failed to generate credentials" in result


# ---------------------------------------------------------------------------
# Error: tbot returns empty creds
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_empty_certs(mock_tbot):
    empty_cred = {
        "kind": "ExecCredential",
        "status": {
            "clientCertificateData": "",
            "clientKeyData": "",
        },
    }
    stdout = json.dumps(empty_cred).encode()
    proc = _make_proc_mock(stdout)

    with patch("asyncio.create_subprocess_exec", new=AsyncMock(return_value=proc)):
        with patch("asyncio.wait_for", new=AsyncMock(return_value=(stdout, b""))):
            result = await cube_cluster_login("staging-int")

    assert result.startswith("Error:")
    assert "empty credentials" in result.lower()


# ---------------------------------------------------------------------------
# Error: timeout
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_timeout(mock_tbot):
    with patch("asyncio.create_subprocess_exec", new=AsyncMock()):
        with patch("asyncio.wait_for", new=AsyncMock(side_effect=asyncio.TimeoutError)):
            result = await cube_cluster_login("staging-int")

    assert result.startswith("Error:")
    assert "timed out" in result.lower()


# ---------------------------------------------------------------------------
# Error: tbot binary not found
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_tbot_not_found(mock_tbot):
    with patch("asyncio.create_subprocess_exec", new=AsyncMock(side_effect=FileNotFoundError)):
        result = await cube_cluster_login("staging-int")

    assert result.startswith("Error:")
    assert "tbot not found" in result.lower()
