from __future__ import annotations

import httpx
import pytest
import respx

from rungate import Admin, AsyncAdmin
from rungate._exceptions import AuthenticationError, NotFoundError, PermissionDeniedError, RungateError

MOCK_TOKEN = "rg_adm_test_token"
MOCK_BASE_URL = "http://test-rungate:8080"


@pytest.fixture
def admin_router():
    with respx.mock(assert_all_called=False) as router:
        yield router


@pytest.fixture
def admin():
    c = Admin(token=MOCK_TOKEN, base_url=MOCK_BASE_URL)
    yield c
    c.close()


@pytest.fixture
async def async_admin():
    c = AsyncAdmin(token=MOCK_TOKEN, base_url=MOCK_BASE_URL)
    yield c
    await c.close()


def _ok(data: dict | list | None = None, total: int | None = None) -> httpx.Response:
    body: dict = {}
    if data is not None:
        body["data"] = data
    if total is not None:
        body["total"] = total
    return httpx.Response(200, json=body)


def _created(data: dict | None = None, **extra: object) -> httpx.Response:
    body: dict = {"data": data or {}}
    body.update(extra)
    return httpx.Response(201, json=body)


# ─── Client Init ─────────────────────────────────────────


class TestAdminInit:
    def test_init_with_token(self):
        admin = Admin(token=MOCK_TOKEN, base_url=MOCK_BASE_URL)
        assert admin.token == MOCK_TOKEN
        admin.close()

    def test_reads_admin_token_from_env(self, monkeypatch: pytest.MonkeyPatch):
        monkeypatch.setenv("RUNGATE_ADMIN_TOKEN", MOCK_TOKEN)
        admin = Admin(base_url=MOCK_BASE_URL)
        assert admin.token == MOCK_TOKEN
        admin.close()

    def test_reads_dashboard_password_as_fallback(self, monkeypatch: pytest.MonkeyPatch):
        monkeypatch.delenv("RUNGATE_ADMIN_TOKEN", raising=False)
        monkeypatch.setenv("RUNGATE_DASHBOARD_PASSWORD", "my-password")
        admin = Admin(base_url=MOCK_BASE_URL)
        assert admin.token == "my-password"
        admin.close()

    def test_raises_when_no_token(self, monkeypatch: pytest.MonkeyPatch):
        monkeypatch.delenv("RUNGATE_ADMIN_TOKEN", raising=False)
        monkeypatch.delenv("RUNGATE_DASHBOARD_PASSWORD", raising=False)
        with pytest.raises(RungateError, match="Admin token is required"):
            Admin(base_url=MOCK_BASE_URL)

    def test_context_manager(self):
        with Admin(token=MOCK_TOKEN, base_url=MOCK_BASE_URL) as admin:
            assert admin.token == MOCK_TOKEN

    def test_has_all_resources(self):
        admin = Admin(token=MOCK_TOKEN, base_url=MOCK_BASE_URL)
        assert hasattr(admin, "provider_keys")
        assert hasattr(admin, "policies")
        assert hasattr(admin, "agents")
        assert hasattr(admin, "runs")
        assert hasattr(admin, "webhooks")
        assert hasattr(admin, "tokens")
        assert hasattr(admin, "approvals")
        assert hasattr(admin, "alert_rules")
        assert hasattr(admin, "analytics")
        admin.close()


# ─── Error Handling ──────────────────────────────────────


class TestAdminErrors:
    def test_401_raises_authentication_error(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/agents").mock(
            return_value=httpx.Response(401, json={"error": "Authentication required"})
        )
        with pytest.raises(AuthenticationError):
            admin.agents.list()

    def test_403_raises_permission_denied(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/analytics/cleanup").mock(
            return_value=httpx.Response(403, json={"error": {"type": "forbidden", "message": "Insufficient scope"}})
        )
        with pytest.raises(PermissionDeniedError):
            admin.analytics.cleanup(days=30)

    def test_404_raises_not_found(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/agents/nonexistent").mock(
            return_value=httpx.Response(404, json={"error": {"type": "not_found", "message": "Not found"}})
        )
        with pytest.raises(NotFoundError):
            admin.agents.get("nonexistent")

    def test_409_raises_api_error(self, admin: Admin, admin_router: respx.Router):
        from rungate._exceptions import APIError

        admin_router.post(f"{MOCK_BASE_URL}/api/alert-rules").mock(
            return_value=httpx.Response(
                409, json={"error": {"type": "conflict", "message": "Alert rule already exists"}}
            )
        )
        with pytest.raises(APIError) as exc_info:
            admin.alert_rules.create(
                name="duplicate",
                condition_type="cost_threshold",
                condition_config={"threshold_usd": 10, "window_seconds": 3600},
            )
        assert exc_info.value.status_code == 409

    def test_400_raises_api_error(self, admin: Admin, admin_router: respx.Router):
        from rungate._exceptions import APIError

        admin_router.post(f"{MOCK_BASE_URL}/api/admin-tokens").mock(
            return_value=httpx.Response(
                400, json={"error": {"type": "validation_error", "message": "scopes is required"}}
            )
        )
        with pytest.raises(APIError) as exc_info:
            admin.tokens.create(name="bad-token", scopes=[])
        assert exc_info.value.status_code == 400

    def test_503_raises_internal_server_error(self, admin: Admin, admin_router: respx.Router):
        from rungate._exceptions import InternalServerError

        admin_router.post(f"{MOCK_BASE_URL}/api/webhooks/wh-1/test").mock(
            return_value=httpx.Response(
                503, json={"error": {"type": "service_unavailable", "message": "Delivery engine not available"}}
            )
        )
        with pytest.raises(InternalServerError):
            admin.webhooks.test("wh-1")


# ─── Provider Keys ───────────────────────────────────────


class TestProviderKeys:
    def test_list(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/provider-keys").mock(
            return_value=_ok(data=[{"id": "pk-1", "provider": "openai"}], total=1)
        )
        result = admin.provider_keys.list()
        assert len(result["data"]) == 1

    def test_create(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/provider-keys").mock(
            return_value=_created({"id": "pk-1", "provider": "openai"})
        )
        result = admin.provider_keys.create(provider="openai", name="Test", api_key="sk-test")
        assert result["data"]["provider"] == "openai"

    def test_get(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/provider-keys/pk-1").mock(return_value=_ok(data={"id": "pk-1"}))
        result = admin.provider_keys.get("pk-1")
        assert result["data"]["id"] == "pk-1"

    def test_delete(self, admin: Admin, admin_router: respx.Router):
        admin_router.delete(f"{MOCK_BASE_URL}/api/provider-keys/pk-1").mock(
            return_value=_ok(data={"message": "Deleted"})
        )
        result = admin.provider_keys.delete("pk-1")
        assert "message" in result["data"]


# ─── Policies ────────────────────────────────────────────


class TestPolicies:
    def test_list(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/policies").mock(
            return_value=_ok(data=[{"id": "pol-1", "name": "default"}], total=1)
        )
        result = admin.policies.list()
        assert result["data"][0]["name"] == "default"

    def test_create(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/policies").mock(
            return_value=_created({"id": "pol-1", "name": "new-policy"})
        )
        result = admin.policies.create(name="new-policy")
        assert result["data"]["name"] == "new-policy"

    def test_versions(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/policies/pol-1/versions").mock(
            return_value=_ok(data=[{"version_number": 1}, {"version_number": 2}], total=2)
        )
        result = admin.policies.versions("pol-1")
        assert len(result["data"]) == 2

    def test_version_detail(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/policies/pol-1/versions/1").mock(
            return_value=_ok(data={"version_number": 1, "snapshot": {}})
        )
        result = admin.policies.version("pol-1", 1)
        assert result["data"]["version_number"] == 1


# ─── Agents ──────────────────────────────────────────────


class TestAgents:
    def test_list(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/agents").mock(
            return_value=_ok(data=[{"id": "ag-1", "name": "agent-1"}], total=1)
        )
        result = admin.agents.list()
        assert result["data"][0]["name"] == "agent-1"

    def test_create(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/agents").mock(
            return_value=_created({"id": "ag-1", "name": "new-agent", "api_key": "rg_sk_shown_once"})
        )
        result = admin.agents.create(name="new-agent", policy_id="pol-1")
        assert "api_key" in result["data"]

    def test_rotate_key(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/agents/ag-1/rotate-key").mock(
            return_value=_ok(data={"message": "Key rotated", "api_key": "rg_sk_new"})
        )
        result = admin.agents.rotate_key("ag-1")
        assert "api_key" in result["data"]

    def test_stats(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/agents/stats/all").mock(
            return_value=_ok(data={"ag-1": {"total_spend": 1.5, "total_runs": 10}})
        )
        result = admin.agents.stats()
        assert "data" in result


# ─── Runs ────────────────────────────────────────────────


class TestRuns:
    def test_list_with_filters(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.get(f"{MOCK_BASE_URL}/api/runs").mock(return_value=_ok(data=[], total=0))
        admin.runs.list(agent_id="ag-1", status="running", limit=10)
        request = route.calls[0].request
        assert "agent_id=ag-1" in str(request.url)
        assert "status=running" in str(request.url)

    def test_get(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/runs/run-1").mock(
            return_value=_ok(data={"id": "run-1", "status": "running"})
        )
        result = admin.runs.get("run-1")
        assert result["data"]["status"] == "running"

    def test_steps(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/runs/run-1/steps").mock(
            return_value=_ok(data=[{"id": "step-1"}], total=1)
        )
        result = admin.runs.steps("run-1")
        assert len(result["data"]) == 1

    def test_stop(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/runs/run-1/stop").mock(
            return_value=_ok(data={"message": "Run stopped"})
        )
        result = admin.runs.stop("run-1")
        assert "message" in result["data"]


# ─── Webhooks ────────────────────────────────────────────


class TestWebhooks:
    def test_list(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/webhooks").mock(return_value=_ok(data=[], total=0))
        result = admin.webhooks.list()
        assert result["total"] == 0

    def test_create(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/webhooks").mock(
            return_value=_created({"id": "wh-1", "secret": "plaintext-shown-once"})
        )
        result = admin.webhooks.create(name="slack", url="https://hooks.slack.com/...", events=["run.completed"])
        assert "secret" in result["data"]

    def test_test(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/webhooks/wh-1/test").mock(
            return_value=_ok(data={"message": "Test sent", "delivery_id": "del-1"})
        )
        result = admin.webhooks.test("wh-1")
        assert "delivery_id" in result["data"]

    def test_enable_disable(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/webhooks/wh-1/enable").mock(
            return_value=_ok(data={"id": "wh-1", "status": "active"})
        )
        admin_router.post(f"{MOCK_BASE_URL}/api/webhooks/wh-1/disable").mock(
            return_value=_ok(data={"id": "wh-1", "status": "paused"})
        )
        admin.webhooks.enable("wh-1")
        result = admin.webhooks.disable("wh-1")
        assert result["data"]["status"] == "paused"


# ─── Tokens ──────────────────────────────────────────────


class TestTokens:
    def test_list(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/admin-tokens").mock(
            return_value=_ok(data=[{"id": "tok-1", "name": "ci-token"}], total=1)
        )
        result = admin.tokens.list()
        assert result["data"][0]["name"] == "ci-token"

    def test_create(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/admin-tokens").mock(
            return_value=httpx.Response(201, json={"data": {"id": "tok-1"}, "token": "rg_adm_shown_once"})
        )
        result = admin.tokens.create(name="new-token", scopes=["read", "write"])
        assert "token" in result

    def test_revoke(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/admin-tokens/tok-1/revoke").mock(
            return_value=_ok(data={"message": "Revoked"})
        )
        result = admin.tokens.revoke("tok-1")
        assert "message" in result["data"]


# ─── Approvals ───────────────────────────────────────────


class TestApprovals:
    def test_list_with_filters(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.get(f"{MOCK_BASE_URL}/api/approval-requests").mock(
            return_value=_ok(data=[{"id": "apr-1", "status": "pending"}], total=1)
        )
        result = admin.approvals.list(status="pending")
        assert result["data"][0]["status"] == "pending"
        assert "status=pending" in str(route.calls[0].request.url)

    def test_approve(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/approval-requests/apr-1/approve").mock(
            return_value=_ok(data={"id": "apr-1", "status": "approved"})
        )
        result = admin.approvals.approve("apr-1")
        assert result["data"]["status"] == "approved"

    def test_reject_with_reason(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.post(f"{MOCK_BASE_URL}/api/approval-requests/apr-1/reject").mock(
            return_value=_ok(data={"id": "apr-1", "status": "rejected", "reason": "Too risky"})
        )
        result = admin.approvals.reject("apr-1", reason="Too risky")
        assert result["data"]["reason"] == "Too risky"
        import json

        body = json.loads(route.calls[0].request.read())
        assert body["reason"] == "Too risky"

    def test_reject_without_reason(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.post(f"{MOCK_BASE_URL}/api/approval-requests/apr-1/reject").mock(
            return_value=_ok(data={"id": "apr-1", "status": "rejected", "reason": None})
        )
        result = admin.approvals.reject("apr-1")
        assert result["data"]["status"] == "rejected"
        import json

        body = json.loads(route.calls[0].request.read())
        assert "reason" not in body  # No reason key sent when not provided


# ─── Alert Rules ─────────────────────────────────────────


class TestAlertRules:
    def test_create(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/alert-rules").mock(
            return_value=_created({"id": "ar-1", "name": "high-cost", "condition_type": "cost_threshold"})
        )
        result = admin.alert_rules.create(
            name="high-cost",
            condition_type="cost_threshold",
            condition_config={"threshold_usd": 100, "window_seconds": 3600},
        )
        assert result["data"]["condition_type"] == "cost_threshold"

    def test_enable_disable(self, admin: Admin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/alert-rules/ar-1/enable").mock(
            return_value=_ok(data={"id": "ar-1", "status": "active"})
        )
        admin_router.post(f"{MOCK_BASE_URL}/api/alert-rules/ar-1/disable").mock(
            return_value=_ok(data={"id": "ar-1", "status": "disabled"})
        )
        admin.alert_rules.enable("ar-1")
        result = admin.alert_rules.disable("ar-1")
        assert result["data"]["status"] == "disabled"

    def test_delete(self, admin: Admin, admin_router: respx.Router):
        admin_router.delete(f"{MOCK_BASE_URL}/api/alert-rules/ar-1").mock(return_value=_ok(data={"message": "Deleted"}))
        admin.alert_rules.delete("ar-1")


# ─── Analytics ───────────────────────────────────────────


class TestAnalytics:
    def test_costs(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.get(f"{MOCK_BASE_URL}/api/analytics/costs").mock(
            return_value=_ok(data=[{"agent_id": "ag-1", "total_cost": 5.0}])
        )
        admin.analytics.costs(group_by="agent")
        assert "group_by=agent" in str(route.calls[0].request.url)

    def test_overview(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/overview/stats").mock(
            return_value=_ok(data={"total_runs": 100, "total_cost": 50.0})
        )
        result = admin.analytics.overview()
        assert "data" in result

    def test_costs_enhanced_params(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.get(f"{MOCK_BASE_URL}/api/analytics/costs").mock(return_value=_ok(data=[], total=0))
        admin.analytics.costs(group_by="provider", provider="openai", granularity="day", limit=10)
        url = str(route.calls[0].request.url)
        assert "group_by=provider" in url
        assert "provider=openai" in url
        assert "granularity=day" in url
        assert "limit=10" in url

    def test_latency(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.get(f"{MOCK_BASE_URL}/api/analytics/latency").mock(
            return_value=_ok(data=[{"group": "openai", "p50_ms": 100}], total=1)
        )
        admin.analytics.latency(group_by="provider")
        assert "group_by=provider" in str(route.calls[0].request.url)

    def test_errors(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.get(f"{MOCK_BASE_URL}/api/analytics/errors").mock(
            return_value=_ok(data=[{"group": "openai", "error_rate_pct": 5.0}], total=1)
        )
        admin.analytics.errors(group_by="provider")
        assert "group_by=provider" in str(route.calls[0].request.url)

    def test_throughput(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.get(f"{MOCK_BASE_URL}/api/analytics/throughput").mock(
            return_value=_ok(data=[{"group": "openai", "request_count": 100}], total=1)
        )
        admin.analytics.throughput(group_by="provider", provider="openai")
        url = str(route.calls[0].request.url)
        assert "group_by=provider" in url
        assert "provider=openai" in url

    def test_provider_reliability(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.get(f"{MOCK_BASE_URL}/api/analytics/provider-reliability").mock(
            return_value=_ok(data=[{"provider": "openai", "success_rate_pct": 99.5}], total=1)
        )
        admin.analytics.provider_reliability(agent_id="ag-1")
        assert "agent_id=ag-1" in str(route.calls[0].request.url)

    def test_runs(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.get(f"{MOCK_BASE_URL}/api/analytics/runs").mock(
            return_value=_ok(data=[{"total_runs": 50, "completion_rate_pct": 90}], total=1)
        )
        admin.analytics.runs(granularity="day")
        assert "granularity=day" in str(route.calls[0].request.url)

    def test_top(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.get(f"{MOCK_BASE_URL}/api/analytics/top").mock(
            return_value=_ok(data=[{"rank": 1, "entity": "ag-1", "value": 45.0}], total=1)
        )
        admin.analytics.top(metric="spend", entity="agent")
        url = str(route.calls[0].request.url)
        assert "metric=spend" in url
        assert "entity=agent" in url

    def test_cleanup(self, admin: Admin, admin_router: respx.Router):
        route = admin_router.post(f"{MOCK_BASE_URL}/api/analytics/cleanup").mock(
            return_value=_ok(data={"runs": 5, "steps": 20, "ledger_entries": 20})
        )
        admin.analytics.cleanup(days=30)
        import json

        body = json.loads(route.calls[0].request.read())
        assert body["days"] == 30


class TestProviders:
    def test_health(self, admin: Admin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/providers/health").mock(return_value=_ok(data=[], total=0))
        result = admin.providers.health()
        assert "data" in result


# ─── Async Admin ─────────────────────────────────────────


class TestAsyncAdmin:
    @pytest.mark.asyncio
    async def test_async_agents_list(self, async_admin: AsyncAdmin, admin_router: respx.Router):
        admin_router.get(f"{MOCK_BASE_URL}/api/agents").mock(return_value=_ok(data=[{"id": "ag-1"}], total=1))
        result = await async_admin.agents.list()
        assert len(result["data"]) == 1

    @pytest.mark.asyncio
    async def test_async_approvals_approve(self, async_admin: AsyncAdmin, admin_router: respx.Router):
        admin_router.post(f"{MOCK_BASE_URL}/api/approval-requests/apr-1/approve").mock(
            return_value=_ok(data={"id": "apr-1", "status": "approved"})
        )
        result = await async_admin.approvals.approve("apr-1")
        assert result["data"]["status"] == "approved"

    @pytest.mark.asyncio
    async def test_async_context_manager(self):
        async with AsyncAdmin(token=MOCK_TOKEN, base_url=MOCK_BASE_URL) as admin:
            assert admin.token == MOCK_TOKEN
