"""Tests for cube_cluster_login agent-side tool (kube_login.py)."""

from __future__ import annotations

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

import pytest
import yaml

from cube_agent.local.kube_login import kube_login, _merge_kubeconfig, DEFAULT_KUBECONFIG


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

SAMPLE_KUBECONFIG_YAML = yaml.dump({
    "apiVersion": "v1",
    "kind": "Config",
    "clusters": [{
        "name": "staging-int",
        "cluster": {
            "server": "https://example.com:443",
            "certificate-authority-data": "FAKECA",
        },
    }],
    "users": [{
        "name": "cube-mcp-staging-int",
        "user": {
            "client-certificate-data": "FAKECERT",
            "client-key-data": "FAKEKEY",
        },
    }],
    "contexts": [{
        "name": "staging-int",
        "context": {"cluster": "staging-int", "user": "cube-mcp-staging-int"},
    }],
    "current-context": "staging-int",
})


def _cloud_response(cluster: str = "staging-int", expires: str = "2026-02-28T12:00:00Z") -> str:
    """Build a mock cube-cloud response for cube_cluster_login."""
    return json.dumps({
        "kubeconfig": SAMPLE_KUBECONFIG_YAML,
        "cluster": cluster,
        "expires_at": expires,
    })


@pytest.fixture
def kubeconfig_path(tmp_path, monkeypatch):
    """Override DEFAULT_KUBECONFIG to a temp path."""
    fake_path = tmp_path / ".kube" / "config"
    monkeypatch.setattr("cube_agent.local.kube_login.DEFAULT_KUBECONFIG", fake_path)
    return fake_path


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


@pytest.mark.asyncio
async def test_kube_login_success(kubeconfig_path):
    """Successful login merges kubeconfig and returns instructions."""
    mock_cloud = AsyncMock()
    mock_cloud.call_tool = AsyncMock(return_value=_cloud_response())
    mock_cloud.__aenter__ = AsyncMock(return_value=mock_cloud)
    mock_cloud.__aexit__ = AsyncMock(return_value=False)

    with patch("cube_agent.local.kube_login.CloudClient", return_value=mock_cloud):
        result = await kube_login("staging-int")

    # Check the kubeconfig file was written
    assert kubeconfig_path.exists()

    # Check file permissions (owner read/write only)
    mode = oct(kubeconfig_path.stat().st_mode & 0o777)
    assert mode == "0o600"

    # Check the kubeconfig content
    kc = yaml.safe_load(kubeconfig_path.read_text())
    assert kc["current-context"] == "staging-int"
    assert kc["clusters"][0]["name"] == "staging-int"
    assert kc["users"][0]["name"] == "cube-mcp-staging-int"

    # Check the response message — should mention merge, not standalone file
    assert "merged into" in result.lower()
    assert "export KUBECONFIG" not in result
    assert "kubectl get namespaces" in result
    assert "2026-02-28T12:00:00Z" in result
    assert "cube_cluster_login" in result  # refresh instructions


@pytest.mark.asyncio
async def test_kube_login_creates_directory(kubeconfig_path):
    """The ~/.kube/ directory is created if it doesn't exist."""
    assert not kubeconfig_path.parent.exists()

    mock_cloud = AsyncMock()
    mock_cloud.call_tool = AsyncMock(return_value=_cloud_response())
    mock_cloud.__aenter__ = AsyncMock(return_value=mock_cloud)
    mock_cloud.__aexit__ = AsyncMock(return_value=False)

    with patch("cube_agent.local.kube_login.CloudClient", return_value=mock_cloud):
        await kube_login("staging-int")

    assert kubeconfig_path.parent.exists()
    assert kubeconfig_path.exists()


# ---------------------------------------------------------------------------
# Merge behaviour
# ---------------------------------------------------------------------------


def test_merge_preserves_existing_contexts(tmp_path):
    """Existing contexts (e.g. minikube) survive a merge."""
    target = tmp_path / "config"
    existing = {
        "apiVersion": "v1",
        "kind": "Config",
        "clusters": [{"name": "minikube", "cluster": {"server": "https://127.0.0.1:8443"}}],
        "users": [{"name": "minikube", "user": {"client-certificate": "/path/cert"}}],
        "contexts": [{"name": "minikube", "context": {"cluster": "minikube", "user": "minikube"}}],
        "current-context": "minikube",
    }
    target.write_text(yaml.dump(existing))

    new_kc = yaml.safe_load(SAMPLE_KUBECONFIG_YAML)
    _merge_kubeconfig(new_kc, target)

    merged = yaml.safe_load(target.read_text())

    # Both clusters present
    cluster_names = [c["name"] for c in merged["clusters"]]
    assert "minikube" in cluster_names
    assert "staging-int" in cluster_names

    # Both users present
    user_names = [u["name"] for u in merged["users"]]
    assert "minikube" in user_names
    assert "cube-mcp-staging-int" in user_names

    # Both contexts present
    ctx_names = [c["name"] for c in merged["contexts"]]
    assert "minikube" in ctx_names
    assert "staging-int" in ctx_names

    # Current context set to the new cluster
    assert merged["current-context"] == "staging-int"


def test_merge_overwrites_stale_cube_entry(tmp_path):
    """Old cube certs are updated in-place, no duplicates."""
    target = tmp_path / "config"
    existing = {
        "apiVersion": "v1",
        "kind": "Config",
        "clusters": [{"name": "staging-int", "cluster": {"server": "https://old-server:443"}}],
        "users": [{"name": "cube-mcp-staging-int", "user": {"client-certificate-data": "OLDCERT"}}],
        "contexts": [{"name": "staging-int", "context": {"cluster": "staging-int", "user": "cube-mcp-staging-int"}}],
        "current-context": "staging-int",
    }
    target.write_text(yaml.dump(existing))

    new_kc = yaml.safe_load(SAMPLE_KUBECONFIG_YAML)
    _merge_kubeconfig(new_kc, target)

    merged = yaml.safe_load(target.read_text())

    # No duplicates
    assert len(merged["clusters"]) == 1
    assert len(merged["users"]) == 1
    assert len(merged["contexts"]) == 1

    # Values updated
    assert merged["clusters"][0]["cluster"]["server"] == "https://example.com:443"
    assert merged["users"][0]["user"]["client-certificate-data"] == "FAKECERT"


def test_merge_creates_file_from_scratch(tmp_path):
    """Merge into a non-existent file creates a valid kubeconfig."""
    target = tmp_path / ".kube" / "config"
    assert not target.exists()

    new_kc = yaml.safe_load(SAMPLE_KUBECONFIG_YAML)
    _merge_kubeconfig(new_kc, target)

    assert target.exists()
    merged = yaml.safe_load(target.read_text())
    assert merged["apiVersion"] == "v1"
    assert merged["current-context"] == "staging-int"
    assert len(merged["clusters"]) == 1


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


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


# ---------------------------------------------------------------------------
# Error: auth failure
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_auth_failure(kubeconfig_path):
    """Authentication errors should give a clear message."""
    from cube_agent.remote.client import CloudClientError

    mock_cloud = AsyncMock()
    mock_cloud.call_tool = AsyncMock(side_effect=CloudClientError("Authentication failed. Check your API key."))
    mock_cloud.__aenter__ = AsyncMock(return_value=mock_cloud)
    mock_cloud.__aexit__ = AsyncMock(return_value=False)

    with patch("cube_agent.local.kube_login.CloudClient", return_value=mock_cloud):
        result = await kube_login("staging-int")

    assert "agent_login" in result.lower() or "authentication" in result.lower()


# ---------------------------------------------------------------------------
# Error: network failure
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_network_failure(kubeconfig_path):
    """Network errors should mention connectivity."""
    from cube_agent.remote.client import CloudClientError

    mock_cloud = AsyncMock()
    mock_cloud.call_tool = AsyncMock(side_effect=CloudClientError("Cannot connect to cube-cloud"))
    mock_cloud.__aenter__ = AsyncMock(return_value=mock_cloud)
    mock_cloud.__aexit__ = AsyncMock(return_value=False)

    with patch("cube_agent.local.kube_login.CloudClient", return_value=mock_cloud):
        result = await kube_login("staging-int")

    assert "connectivity" in result.lower() or "network" in result.lower() or "reach" in result.lower()


# ---------------------------------------------------------------------------
# Error: server returns error string
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_server_error(kubeconfig_path):
    """Server error strings should be passed through."""
    mock_cloud = AsyncMock()
    mock_cloud.call_tool = AsyncMock(return_value="Error: Cluster 'bad' not found. Available: staging-int")
    mock_cloud.__aenter__ = AsyncMock(return_value=mock_cloud)
    mock_cloud.__aexit__ = AsyncMock(return_value=False)

    with patch("cube_agent.local.kube_login.CloudClient", return_value=mock_cloud):
        result = await kube_login("bad")

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


# ---------------------------------------------------------------------------
# Error: server returns invalid JSON
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_invalid_response(kubeconfig_path):
    """Non-JSON, non-Error response should be handled gracefully."""
    mock_cloud = AsyncMock()
    mock_cloud.call_tool = AsyncMock(return_value="some unexpected text")
    mock_cloud.__aenter__ = AsyncMock(return_value=mock_cloud)
    mock_cloud.__aexit__ = AsyncMock(return_value=False)

    with patch("cube_agent.local.kube_login.CloudClient", return_value=mock_cloud):
        result = await kube_login("staging-int")

    assert "error" in result.lower() or "unexpected" in result.lower()


# ---------------------------------------------------------------------------
# Error: empty kubeconfig in response
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_kube_login_empty_kubeconfig(kubeconfig_path):
    """Empty kubeconfig in response should return an error."""
    resp = json.dumps({"kubeconfig": "", "cluster": "staging-int", "expires_at": ""})
    mock_cloud = AsyncMock()
    mock_cloud.call_tool = AsyncMock(return_value=resp)
    mock_cloud.__aenter__ = AsyncMock(return_value=mock_cloud)
    mock_cloud.__aexit__ = AsyncMock(return_value=False)

    with patch("cube_agent.local.kube_login.CloudClient", return_value=mock_cloud):
        result = await kube_login("staging-int")

    assert "error" in result.lower()
    assert "empty" in result.lower()
