from __future__ import annotations

import httpx
import pytest
import respx

from rungate import AsyncRungate, ChatCompletion, Rungate
from rungate._exceptions import (
    APIError,
    ApprovalRequiredError,
    AuthenticationError,
    BudgetExceededError,
    GovernanceError,
    HourlyCostExceededError,
    InternalServerError,
    ModelNotAllowedError,
    PermissionDeniedError,
    ProviderError,
    RateLimitedError,
    RunawayPreventionError,
    ToolBlockedError,
)
from tests.conftest import MOCK_BASE_URL, make_openai_response, make_rungate_headers


def _gov_response(status: int, code: str, message: str, details: dict | None = None) -> httpx.Response:
    """Helper to create a governance block response."""
    return httpx.Response(
        status,
        json={
            "error": {
                "type": "governance_block",
                "code": code,
                "message": message,
                "run_id": "run-123",
                "details": details or {},
            }
        },
        headers={"x-rungate-governance": "blocked"},
    )


class TestChatCompletions:
    def test_basic_create(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(200, json=make_openai_response(), headers=make_rungate_headers())
        )

        response = client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": "Hello"}],
        )

        assert isinstance(response, ChatCompletion)
        assert response.id == "chatcmpl-test-123"
        assert response.model == "gpt-4o"
        assert len(response.choices) == 1

    def test_response_includes_rungate_metadata(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(
                200,
                json=make_openai_response(),
                headers=make_rungate_headers(
                    cost_usd="0.001234",
                    budget_remaining_usd="4.998766",
                    run_id="run-abc-123",
                ),
            )
        )

        response = client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert response.rungate.run_id == "run-abc-123"
        assert response.rungate.cost_usd == 0.001234
        assert response.rungate.budget_remaining_usd == 4.998766
        assert response.rungate.provider == "openai"
        assert response.rungate.tokens_used == 15

    def test_passes_parameters_in_request_body(self, client: Rungate, mock_router: respx.Router):
        route = mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(200, json=make_openai_response(), headers=make_rungate_headers())
        )

        client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": "Hi"}],
            temperature=0.7,
            max_tokens=100,
            top_p=0.9,
        )

        request = route.calls[0].request
        body = request.read()
        import json

        parsed = json.loads(body)
        assert parsed["model"] == "gpt-4o"
        assert parsed["temperature"] == 0.7
        assert parsed["max_tokens"] == 100
        assert parsed["top_p"] == 0.9

    def test_passes_tools_in_request(self, client: Rungate, mock_router: respx.Router):
        route = mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(200, json=make_openai_response(), headers=make_rungate_headers())
        )

        tools = [{"type": "function", "function": {"name": "search", "description": "Search the web"}}]
        client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}], tools=tools)

        import json

        parsed = json.loads(route.calls[0].request.read())
        assert parsed["tools"] == tools

    def test_passes_extra_body(self, client: Rungate, mock_router: respx.Router):
        route = mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(200, json=make_openai_response(), headers=make_rungate_headers())
        )

        client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": "Hi"}],
            extra_body={"response_format": {"type": "json_object"}},
        )

        import json

        parsed = json.loads(route.calls[0].request.read())
        assert parsed["response_format"] == {"type": "json_object"}

    def test_passes_run_id_header(self, client: Rungate, mock_router: respx.Router):
        route = mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(200, json=make_openai_response(), headers=make_rungate_headers())
        )

        client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": "Hi"}],
            run_id="my-explicit-run-id",
        )

        request = route.calls[0].request
        assert request.headers.get("x-rungate-run-id") == "my-explicit-run-id"

    def test_handles_missing_optional_headers(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(
                200,
                json=make_openai_response(),
                headers={"x-rungate-run-id": "run-123"},
            )
        )

        response = client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert response.rungate.run_id == "run-123"
        assert response.rungate.cost_usd is None
        assert response.rungate.budget_remaining_usd is None
        assert response.rungate.failover_count is None

    # ─── Authentication Errors ────────────────────────────

    def test_raises_authentication_error_on_401(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(
                401,
                json={"error": {"type": "unauthorized", "message": "Invalid API key"}},
            )
        )

        with pytest.raises(AuthenticationError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.status_code == 401

    # ─── Governance Errors ────────────────────────────────

    def test_budget_exceeded_raises_specific_error(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=_gov_response(
                402, "budget_exceeded", "Run budget exceeded", {"cost_usd": 10.0, "hard_limit_usd": 10.0}
            )
        )

        with pytest.raises(BudgetExceededError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.status_code == 402
        assert exc_info.value.governance_code == "budget_exceeded"
        assert exc_info.value.run_id == "run-123"
        assert exc_info.value.details["cost_usd"] == 10.0

    def test_hourly_cost_exceeded_raises_specific_error(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=_gov_response(402, "hourly_cost_exceeded", "Hourly limit hit", {"hourly_cost_usd": 5.0})
        )

        with pytest.raises(HourlyCostExceededError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.governance_code == "hourly_cost_exceeded"

    def test_tool_blocked_raises_specific_error(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=_gov_response(403, "tool_blocked", 'Tool "dangerous" is blocked', {"tool": "dangerous"})
        )

        with pytest.raises(ToolBlockedError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.details["tool"] == "dangerous"

    def test_model_not_allowed_raises_specific_error(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=_gov_response(403, "model_not_allowed", "Model blocked", {"model": "gpt-4o"})
        )

        with pytest.raises(ModelNotAllowedError):
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

    def test_rate_limited_raises_specific_error(self, client: Rungate, mock_router: respx.Router):
        """429 from Rungate is a governance rate limit — SDK does NOT retry."""
        route = mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=_gov_response(429, "rate_limited", "Rate limit exceeded", {"max_requests_per_minute": 10})
        )

        with pytest.raises(RateLimitedError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.governance_code == "rate_limited"
        assert len(route.calls) == 1  # NOT retried

    def test_budget_alert_exceeded_raises_specific_error(self, client: Rungate, mock_router: respx.Router):
        from rungate._exceptions import BudgetAlertExceededError

        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=_gov_response(402, "budget_alert_exceeded", "Alert threshold exceeded")
        )

        with pytest.raises(BudgetAlertExceededError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.governance_code == "budget_alert_exceeded"

    def test_agent_disabled_raises_specific_error(self, client: Rungate, mock_router: respx.Router):
        from rungate._exceptions import AgentDisabledError

        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=_gov_response(403, "agent_disabled", 'Agent "test" is disabled')
        )

        with pytest.raises(AgentDisabledError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.governance_code == "agent_disabled"

    def test_run_force_stopped_raises_specific_error(self, client: Rungate, mock_router: respx.Router):
        from rungate._exceptions import RunForceStoppedError

        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=_gov_response(403, "run_force_stopped", "Run has been force-stopped")
        )

        with pytest.raises(RunForceStoppedError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.governance_code == "run_force_stopped"

    def test_runaway_prevention_raises_specific_error(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=_gov_response(
                403, "runaway_prevention", "Max steps exceeded", {"step_count": 100, "max_steps": 100}
            )
        )

        with pytest.raises(RunawayPreventionError):
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

    def test_unknown_governance_code_raises_base_governance_error(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=_gov_response(403, "some_future_code", "New governance rule")
        )

        with pytest.raises(GovernanceError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.governance_code == "some_future_code"

    def test_governance_errors_caught_by_except_api_error(self, client: Rungate, mock_router: respx.Router):
        """Users who catch APIError broadly still catch governance errors."""
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=_gov_response(402, "budget_exceeded", "Over budget")
        )

        with pytest.raises(APIError):
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

    # ─── Approval Required ────────────────────────────────

    def test_approval_required_raises_specific_error(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(
                202,
                json={
                    "status": "awaiting_approval",
                    "approval_requests": [{"id": "apr-123", "tool": "execute_code"}],
                    "run_id": "run-123",
                    "message": "Tools require approval.",
                },
                headers={"x-rungate-governance": "awaiting_approval", "x-rungate-run-id": "run-123"},
            )
        )

        with pytest.raises(ApprovalRequiredError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.status_code == 202
        assert exc_info.value.run_id == "run-123"
        assert len(exc_info.value.approval_requests) == 1
        assert exc_info.value.approval_requests[0]["tool"] == "execute_code"

    # ─── Provider Errors ──────────────────────────────────

    def test_provider_error_raises_specific_error(self, client: Rungate, mock_router: respx.Router):
        route = mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(
                502,
                json={
                    "error": {
                        "type": "provider_error",
                        "message": "All providers failed",
                        "details": [{"provider": "openai", "status": 500, "message": "Internal error"}],
                    }
                },
            )
        )

        with pytest.raises(ProviderError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.status_code == 502
        assert len(exc_info.value.provider_errors) == 1
        assert exc_info.value.provider_errors[0]["provider"] == "openai"
        assert len(route.calls) == 1  # NOT retried

    # ─── Non-Governance Errors ────────────────────────────

    def test_raises_permission_denied_on_403_non_governance(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(
                403,
                json={"error": {"type": "forbidden", "message": "Insufficient scope. Required: write"}},
            )
        )

        with pytest.raises(PermissionDeniedError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.status_code == 403

    def test_non_json_error_response_handled(self, client: Rungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(500, text="<html>Internal Server Error</html>")
        )

        with pytest.raises(InternalServerError):
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

    # ─── Connection Retries ───────────────────────────────

    def test_connection_retry_on_connect_error(self, client: Rungate, mock_router: respx.Router):
        call_count = 0

        def side_effect(_request: httpx.Request) -> httpx.Response:
            nonlocal call_count
            call_count += 1
            if call_count <= 2:
                raise httpx.ConnectError("Connection refused")
            return httpx.Response(200, json=make_openai_response(), headers=make_rungate_headers())

        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(side_effect=side_effect)

        response = client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])
        assert response.model == "gpt-4o"
        assert call_count == 3


class TestEdgeCases:
    """Edge cases for error handling paths."""

    def test_202_without_awaiting_approval_treated_as_success(self, client: Rungate, mock_router: respx.Router):
        """A 202 without status=awaiting_approval is a normal success."""
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(202, json=make_openai_response(), headers=make_rungate_headers())
        )

        # Should NOT raise — 202 without awaiting_approval is a valid success
        response = client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])
        assert response.model == "gpt-4o"

    def test_governance_response_with_missing_code(self, client: Rungate, mock_router: respx.Router):
        """Governance block with missing code falls back to base GovernanceError."""
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(
                403,
                json={
                    "error": {
                        "type": "governance_block",
                        "message": "Something blocked",
                        "run_id": "run-123",
                    }
                },
            )
        )

        with pytest.raises(GovernanceError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.governance_code == ""

    def test_governance_response_with_null_details(self, client: Rungate, mock_router: respx.Router):
        """Governance block with null details gets empty dict."""
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(
                402,
                json={
                    "error": {
                        "type": "governance_block",
                        "code": "budget_exceeded",
                        "message": "Over budget",
                        "run_id": "run-123",
                        "details": None,
                    }
                },
            )
        )

        with pytest.raises(BudgetExceededError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.details == {}

    def test_402_without_governance_type_raises_api_error(self, client: Rungate, mock_router: respx.Router):
        """402 without governance_block type raises generic APIError, not GovernanceError."""
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(
                402,
                json={"error": {"type": "payment_required", "message": "Payment required"}},
            )
        )

        with pytest.raises(APIError) as exc_info:
            client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "Hi"}])

        assert exc_info.value.status_code == 402
        assert not isinstance(exc_info.value, GovernanceError)


class TestAsyncChatCompletions:
    @pytest.mark.asyncio
    async def test_async_create(self, async_client: AsyncRungate, mock_router: respx.Router):
        mock_router.post(f"{MOCK_BASE_URL}/v1/chat/completions").mock(
            return_value=httpx.Response(200, json=make_openai_response(), headers=make_rungate_headers())
        )

        response = await async_client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": "Hello"}],
        )

        assert isinstance(response, ChatCompletion)
        assert response.model == "gpt-4o"
        assert response.rungate.run_id == "run-test-123"
