"""End-to-end tests for the LLMHosts proxy security and request flow.

These tests exercise the full middleware stack (auth → rate limiting → CORS)
using ASGI transport. A ``LLMHostsConfig`` with ``require_auth=True`` is
passed to ``create_app()`` to enable the security middleware.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest
from httpx import ASGITransport, AsyncClient

from llmhosts.config import LLMHostsConfig, RateLimitConfig, SecurityConfig, ServerConfig
from llmhosts.proxy.app import create_app
from llmhosts.proxy.dispatcher import BackendDispatcher

if TYPE_CHECKING:
    from llmhosts.proxy.auth import ProxyKeyStore

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_app(
    tmp_path,
    *,
    require_auth: bool = True,
    cors_restrict: bool = False,
    cors_origins: list[str] | None = None,
    rate_limit_rpm: int = 60,
    rate_limit_burst: int = 10,
):
    """Create a FastAPI app with security middleware using a temp data dir."""
    import os

    # Point llmhosts_dir() to a temp directory for test isolation
    original = os.environ.get("HOME")
    os.environ["HOME"] = str(tmp_path)
    try:
        config = LLMHostsConfig(
            server=ServerConfig(cors_origins=cors_origins or ["*"]),
            security=SecurityConfig(
                require_auth=require_auth,
                rate_limit_rpm=rate_limit_rpm,
                rate_limit_burst=rate_limit_burst,
                cors_restrict=cors_restrict,
            ),
        )
        app = create_app(config=config)
    finally:
        if original is not None:
            os.environ["HOME"] = original
        else:
            os.environ.pop("HOME", None)

    return app


async def _setup_client(app):
    """Attach a BackendDispatcher and return an async test client."""
    dispatcher = BackendDispatcher()
    await dispatcher.startup()
    app.state.dispatcher = dispatcher
    transport = ASGITransport(app=app, raise_app_exceptions=False)
    return AsyncClient(transport=transport, base_url="http://test"), dispatcher


# ---------------------------------------------------------------------------
# Tests: Authentication
# ---------------------------------------------------------------------------


@pytest.mark.e2e
async def test_e2e_unauthenticated_rejected(tmp_path):
    """Requests without a valid key are rejected when auth is required."""
    app = _make_app(tmp_path, require_auth=True)
    ac, dispatcher = await _setup_client(app)
    async with ac:
        resp = await ac.get("/v1/models")
    await dispatcher.shutdown()

    assert resp.status_code == 401
    body = resp.json()
    assert body["error"]["type"] == "authentication_error"


@pytest.mark.e2e
async def test_e2e_health_no_auth(tmp_path):
    """/health is accessible without auth even when require_auth=True."""
    app = _make_app(tmp_path, require_auth=True)
    ac, dispatcher = await _setup_client(app)
    async with ac:
        resp = await ac.get("/health")
    await dispatcher.shutdown()

    assert resp.status_code == 200
    assert resp.json()["status"] == "ok"


@pytest.mark.e2e
async def test_e2e_full_request_flow(tmp_path):
    """Create a key → authenticate → request /v1/models → verify headers."""
    app = _make_app(tmp_path, require_auth=True)
    ac, dispatcher = await _setup_client(app)

    # Create a key via the key store attached to the app
    key_store: ProxyKeyStore = app.state.key_store
    _key_id, raw_key = key_store.create_key("test-e2e")

    async with ac:
        resp = await ac.get(
            "/v1/models",
            headers={"Authorization": f"Bearer {raw_key}"},
        )
    await dispatcher.shutdown()

    assert resp.status_code == 200
    # Verify security response headers are present
    assert "X-Request-ID" in resp.headers
    assert "X-RateLimit-Limit" in resp.headers
    assert "X-RateLimit-Remaining" in resp.headers
    assert "X-RateLimit-Reset" in resp.headers


@pytest.mark.e2e
async def test_e2e_models_authenticated(tmp_path):
    """/v1/models returns a valid model list with a valid key."""
    app = _make_app(tmp_path, require_auth=True)
    ac, dispatcher = await _setup_client(app)

    key_store: ProxyKeyStore = app.state.key_store
    _key_id, raw_key = key_store.create_key("test-models")

    async with ac:
        resp = await ac.get(
            "/v1/models",
            headers={"Authorization": f"Bearer {raw_key}"},
        )
    await dispatcher.shutdown()

    assert resp.status_code == 200
    body = resp.json()
    assert "data" in body
    assert isinstance(body["data"], list)


# ---------------------------------------------------------------------------
# Tests: Rate Limiting
# ---------------------------------------------------------------------------


@pytest.mark.e2e
async def test_e2e_rate_limit_enforcement(tmp_path):
    """Exceeding the weighted budget results in 429."""
    # Use AdaptiveShield with a very small budget (3 units) for fast test.
    import os

    original = os.environ.get("HOME")
    os.environ["HOME"] = str(tmp_path)
    try:
        config = LLMHostsConfig(
            security=SecurityConfig(
                require_auth=False,
                rate_limit=RateLimitConfig(enabled=True, window_seconds=60),
            ),
        )
        app = create_app(config=config)
    finally:
        if original is not None:
            os.environ["HOME"] = original
        else:
            os.environ.pop("HOME", None)

    ac, dispatcher = await _setup_client(app)

    async with ac:
        # Exhaust the free plan budget (30 weighted units)
        for _ in range(30):
            resp = await ac.get("/v1/models")
            assert resp.status_code == 200

        # Next request should be rate limited
        resp = await ac.get("/v1/models")
    await dispatcher.shutdown()

    assert resp.status_code == 429
    assert "Retry-After" in resp.headers
    body = resp.json()
    assert body["error"] == "rate_limit_exceeded"


@pytest.mark.e2e
async def test_e2e_rate_limit_headers_present(tmp_path):
    """Successful responses include X-RateLimit-* headers."""
    app = _make_app(tmp_path, require_auth=False)
    ac, dispatcher = await _setup_client(app)

    async with ac:
        resp = await ac.get("/v1/models")
    await dispatcher.shutdown()

    assert resp.status_code == 200
    # AdaptiveShield: free plan = 30 weighted units per window
    assert resp.headers["X-RateLimit-Limit"] == "30"
    assert int(resp.headers["X-RateLimit-Remaining"]) >= 0
    assert int(resp.headers["X-RateLimit-Reset"]) > 0


# ---------------------------------------------------------------------------
# Tests: CORS
# ---------------------------------------------------------------------------


@pytest.mark.e2e
async def test_e2e_cors_restricted(tmp_path):
    """When cors_restrict=True, preflight from unauthorized origin is blocked."""
    app = _make_app(
        tmp_path,
        require_auth=False,
        cors_restrict=True,
        cors_origins=["https://allowed.example.com"],
    )
    ac, dispatcher = await _setup_client(app)

    async with ac:
        resp = await ac.options(
            "/v1/models",
            headers={
                "Origin": "https://evil.example.com",
                "Access-Control-Request-Method": "GET",
            },
        )
    await dispatcher.shutdown()

    # Unauthorized origin should not get CORS headers
    assert resp.headers.get("access-control-allow-origin") != "https://evil.example.com"


@pytest.mark.e2e
async def test_e2e_cors_allowed(tmp_path):
    """When cors_restrict=True, preflight from allowed origin succeeds."""
    allowed = "https://allowed.example.com"
    app = _make_app(
        tmp_path,
        require_auth=False,
        cors_restrict=True,
        cors_origins=[allowed],
    )
    ac, dispatcher = await _setup_client(app)

    async with ac:
        resp = await ac.options(
            "/v1/models",
            headers={
                "Origin": allowed,
                "Access-Control-Request-Method": "GET",
            },
        )
    await dispatcher.shutdown()

    assert resp.headers.get("access-control-allow-origin") == allowed
