"""End-to-end tests for the AI Proxy HTTP API.

These tests run the FastAPI app with a test client and mock LiteLLM
calls to verify the full request pipeline: auth → analysis → routing →
completion → cost tracking.

Mark with @pytest.mark.e2e for CI filtering.
"""

import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi.testclient import TestClient

# Patch settings before importing app
with patch.dict("os.environ", {
    "AI_PROXY_MASTER_KEY": "sk-test-e2e",
    "ANTHROPIC_API_KEY": "sk-ant-test",
    "OPENAI_API_KEY": "sk-test",
    "DEEPSEEK_API_KEY": "sk-ds-test",
    "GOOGLE_AI_KEY": "AI-test",
    "OPENROUTER_API_KEY": "sk-or-test",
    "GROQ_API_KEY": "gsk-test",
    "REDIS_URL": "redis://localhost:6379/0",
}):
    from proxym.main import app

AUTH = {"Authorization": "Bearer sk-test-e2e"}


@pytest.fixture(autouse=True)
def _reset_global_state():
    """Reset proxy global state between tests."""
    import proxym.main as main_mod
    import proxym.config as config_mod
    import proxym.api._state as state_mod
    main_mod._router = None
    main_mod._delta_buffer = None
    main_mod._context_payload = ""
    state_mod._router = None
    state_mod._delta_buffer = None
    with patch.dict("os.environ", {
        "AI_PROXY_MASTER_KEY": "sk-test-e2e",
        "ANTHROPIC_API_KEY": "sk-ant-test",
        "OPENAI_API_KEY": "sk-test",
        "DEEPSEEK_API_KEY": "sk-ds-test",
        "GOOGLE_AI_KEY": "AI-test",
        "OPENROUTER_API_KEY": "sk-or-test",
        "GROQ_API_KEY": "gsk-test",
        "REDIS_URL": "redis://localhost:6379/0",
    }):
        config_mod._settings = None  # Clear settings cache under patched env
        yield


@pytest.fixture
def client():
    return TestClient(app)


class TestHealthAndInfo:
    """Basic endpoints that don't require mocking."""

    def test_health(self, client):
        r = client.get("/health")
        assert r.status_code == 200
        data = r.json()
        assert data["status"] == "ok"

    def test_list_models(self, client):
        r = client.get("/v1/models", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert data["object"] == "list"
        assert len(data["data"]) >= 4  # at minimum: 3 anthropic + 1 ollama
        # Check structure
        model = data["data"][0]
        assert "id" in model
        assert "input_cost_per_1m" in model
        assert "capabilities" in model

    def test_budget_status(self, client):
        r = client.get("/v1/budget", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert "daily_remaining_usd" in data
        assert "monthly_remaining_usd" in data

    def test_context_stats(self, client):
        r = client.get("/v1/context/stats", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert "version" in data
        assert "files_tracked" in data


class TestAuth:
    """Verify authentication middleware."""

    def test_no_auth_rejected(self, client):
        r = client.get("/v1/models")
        assert r.status_code == 401

    def test_wrong_key_rejected(self, client):
        r = client.get("/v1/models", headers={"Authorization": "Bearer wrong-key"})
        assert r.status_code == 401

    def test_correct_key_accepted(self, client):
        r = client.get("/v1/models", headers=AUTH)
        assert r.status_code == 200

    def test_health_no_auth_needed(self, client):
        r = client.get("/health")
        assert r.status_code == 200


class TestContextDelta:
    """Test the context delta push endpoint."""

    def test_push_delta(self, client):
        payload = {
            "version": 1,
            "files_changed": 2,
            "files_added": 0,
            "files_removed": 0,
            "files_total": 10,
            "is_full_refresh": False,
            "token_estimate": 500,
            "payload": "<context_delta>test content</context_delta>",
        }
        r = client.post("/v1/context/delta", json=payload, headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert data["ok"] is True
        assert data["version"] == 1


@pytest.mark.e2e
class TestChatCompletions:
    """Test the main /v1/chat/completions endpoint with mocked LiteLLM."""

    def _mock_response(self, content="Hello!", model="anthropic/claude-haiku-4-5-20251001"):
        """Create a mock LiteLLM response."""
        mock = MagicMock()
        mock.model_dump.return_value = {
            "id": "chatcmpl-test",
            "object": "chat.completion",
            "model": model,
            "choices": [{
                "index": 0,
                "message": {"role": "assistant", "content": content},
                "finish_reason": "stop",
            }],
            "usage": {
                "prompt_tokens": 100,
                "completion_tokens": 50,
                "total_tokens": 150,
            },
        }
        return mock

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_simple_completion(self, mock_completion, client):
        mock_completion.return_value = self._mock_response()

        r = client.post("/v1/chat/completions", json={
            "model": "cheap",
            "messages": [{"role": "user", "content": "Hello"}],
        }, headers=AUTH)

        assert r.status_code == 200
        data = r.json()
        assert data["choices"][0]["message"]["content"] == "Hello!"
        assert "_proxy" in data
        assert data["_proxy"]["cost_usd"] >= 0

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_tier_header_routing(self, mock_completion, client):
        mock_completion.return_value = self._mock_response()

        r = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content": "Fix a typo"}],
        }, headers={**AUTH, "X-Task-Tier": "trivial"})

        assert r.status_code == 200
        # Should route to a cheap model
        assert r.headers.get("X-Task-Tier") == "trivial"

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_complex_task_routing(self, mock_completion, client):
        mock_completion.return_value = self._mock_response()

        r = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content":
                "Refactor the entire authentication module to use repository pattern "
                "across all 50+ files in the project"}],
        }, headers=AUTH)

        assert r.status_code == 200
        tier = r.headers.get("X-Task-Tier")
        assert tier in ("complex", "deep")

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_model_alias_cheap(self, mock_completion, client):
        mock_completion.return_value = self._mock_response()

        r = client.post("/v1/chat/completions", json={
            "model": "cheap",
            "messages": [{"role": "user", "content": "hello"}],
        }, headers=AUTH)

        assert r.status_code == 200
        model_id = r.headers.get("X-Model-Id")
        assert model_id == "anthropic/haiku-4.5"

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_model_alias_premium(self, mock_completion, client):
        mock_completion.return_value = self._mock_response()

        r = client.post("/v1/chat/completions", json={
            "model": "premium",
            "messages": [{"role": "user", "content": "hello"}],
        }, headers=AUTH)

        assert r.status_code == 200
        model_id = r.headers.get("X-Model-Id")
        assert model_id == "anthropic/opus-4.6"

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_cost_header_present(self, mock_completion, client):
        mock_completion.return_value = self._mock_response()

        r = client.post("/v1/chat/completions", json={
            "model": "cheap",
            "messages": [{"role": "user", "content": "hello"}],
        }, headers=AUTH)

        assert "X-Cost-Usd" in r.headers
        cost = float(r.headers["X-Cost-Usd"])
        assert cost >= 0

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_routing_reason_header(self, mock_completion, client):
        mock_completion.return_value = self._mock_response()

        r = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content": "Implement a function"}],
        }, headers=AUTH)

        assert "X-Routing-Reason" in r.headers

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_context_injection(self, mock_completion, client):
        mock_completion.return_value = self._mock_response()

        # First push some context
        client.post("/v1/context/delta", json={
            "version": 1, "files_changed": 0, "files_added": 1,
            "files_removed": 0, "files_total": 1,
            "is_full_refresh": True, "token_estimate": 100,
            "payload": "<context>def hello(): pass</context>",
        }, headers=AUTH)

        # Then request with injection
        r = client.post("/v1/chat/completions", json={
            "model": "cheap",
            "messages": [{"role": "user", "content": "explain the code"}],
        }, headers={**AUTH, "X-Inject-Context": "true"})

        assert r.status_code == 200
        # Verify context was injected into the messages sent to LiteLLM
        call_args = mock_completion.call_args
        messages = call_args.kwargs.get("messages") or call_args[1].get("messages")
        system_content = messages[0]["content"]
        assert "def hello(): pass" in system_content

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_fallback_on_error(self, mock_completion, client):
        # First call fails, second succeeds
        mock_completion.side_effect = [
            Exception("rate limited"),
            self._mock_response(content="fallback worked"),
        ]

        # Use auto-routing (no model alias) so router picks model + fallbacks
        r = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content": "Implement a function to parse JSON"}],
        }, headers=AUTH)

        # Should succeed via fallback (or 502 if only 1 candidate)
        if r.status_code == 200:
            data = r.json()
            assert data["choices"][0]["message"]["content"] == "fallback worked"
            assert data["_proxy"]["fallback_index"] >= 1
        else:
            # If only 1 model matched the tier+caps, fallback chain is empty
            assert r.status_code == 502

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_all_models_fail_returns_502(self, mock_completion, client):
        mock_completion.side_effect = Exception("all dead")

        r = client.post("/v1/chat/completions", json={
            "model": "local",
            "messages": [{"role": "user", "content": "hello"}],
        }, headers=AUTH)

        assert r.status_code == 502

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_passthrough_params(self, mock_completion, client):
        mock_completion.return_value = self._mock_response()

        r = client.post("/v1/chat/completions", json={
            "model": "cheap",
            "messages": [{"role": "user", "content": "hello"}],
            "temperature": 0.5,
            "max_tokens": 100,
            "top_p": 0.9,
        }, headers=AUTH)

        assert r.status_code == 200
        call_kwargs = mock_completion.call_args.kwargs
        assert call_kwargs.get("temperature") == 0.5
        assert call_kwargs.get("max_tokens") == 100


@pytest.mark.e2e
class TestEndToEndFlow:
    """Full workflow tests simulating real usage patterns."""

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_full_workflow_push_context_then_query(self, mock_completion, client):
        """Simulate: code2llm watcher pushes context → user queries with context."""
        mock_completion.return_value = MagicMock(
            model_dump=MagicMock(return_value={
                "id": "chatcmpl-flow",
                "object": "chat.completion",
                "model": "test",
                "choices": [{"index": 0, "message": {
                    "role": "assistant",
                    "content": "The main function prints hello and returns 0."
                }, "finish_reason": "stop"}],
                "usage": {"prompt_tokens": 200, "completion_tokens": 30, "total_tokens": 230},
            })
        )

        # Step 1: Push context
        r = client.post("/v1/context/delta", json={
            "version": 1,
            "files_changed": 0, "files_added": 2, "files_removed": 0,
            "files_total": 2, "is_full_refresh": True,
            "token_estimate": 500,
            "payload": (
                '<context version="1" files="2" type="full">\n'
                '=== src/main.py ===\ndef main():\n    print("hello")\n    return 0\n\n'
                '=== src/utils.py ===\ndef add(a, b):\n    return a + b\n'
                '</context>'
            ),
        }, headers=AUTH)
        assert r.status_code == 200

        # Step 2: Query with context injection
        r = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content": "What does main() do?"}],
        }, headers={**AUTH, "X-Inject-Context": "true"})

        assert r.status_code == 200
        data = r.json()
        assert "main" in data["choices"][0]["message"]["content"].lower()
        assert data["_proxy"]["cost_usd"] >= 0

        # Step 3: Check budget was recorded
        r = client.get("/v1/budget", headers=AUTH)
        assert r.status_code == 200

    @patch("proxym.main.litellm.acompletion", new_callable=AsyncMock)
    def test_multiple_tiers_in_session(self, mock_completion, client):
        """Simulate a real session with different task types."""
        mock_completion.return_value = MagicMock(
            model_dump=MagicMock(return_value={
                "id": "chatcmpl-multi",
                "object": "chat.completion",
                "model": "test",
                "choices": [{"index": 0, "message": {
                    "role": "assistant", "content": "done"
                }, "finish_reason": "stop"}],
                "usage": {"prompt_tokens": 50, "completion_tokens": 20, "total_tokens": 70},
            })
        )

        # Trivial task
        r1 = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content": "What is a Python list?"}],
        }, headers=AUTH)
        tier1 = r1.headers.get("X-Task-Tier")

        # Standard task
        r2 = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content": "Implement a function to validate emails"}],
        }, headers=AUTH)
        tier2 = r2.headers.get("X-Task-Tier")

        # Complex task
        r3 = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content":
                "Refactor the entire auth module across 20+ files to use dependency injection"}],
        }, headers=AUTH)
        tier3 = r3.headers.get("X-Task-Tier")

        # Tiers should generally escalate
        tier_order = {"trivial": 0, "operational": 1, "standard": 2, "complex": 3, "deep": 4}
        # tier3 should be >= tier1 (complex >= trivial)
        assert tier_order.get(tier3, 0) >= tier_order.get(tier1, 0)
