"""Tests for security headers middleware (T-10, issue #194).

Verifies that SecurityHeadersMiddleware injects defense-in-depth security
headers on every API response. In production these are also set by Traefik;
these tests cover direct access (dev, CI, bypassed proxy).
"""

from __future__ import annotations

from contextlib import asynccontextmanager
from typing import TYPE_CHECKING
from unittest.mock import MagicMock

import pytest
from httpx import ASGITransport, AsyncClient

from llmhosts.proxy.app import create_app

if TYPE_CHECKING:
    from collections.abc import AsyncIterator

    from fastapi import FastAPI


# ---------------------------------------------------------------------------
# Fake lifespan — avoids real I/O (DB, Ollama discovery, Headscale, etc.)
# ---------------------------------------------------------------------------


@asynccontextmanager
async def _fake_lifespan(app: FastAPI) -> AsyncIterator[None]:  # type: ignore[misc]
    dispatcher = MagicMock()
    dispatcher.list_backends_info.return_value = []
    dispatcher.attach_router = MagicMock()
    dispatcher.attach_hive_karma = MagicMock()
    dispatcher.list_models.return_value = []
    app.state.dispatcher = dispatcher
    app.state.router = MagicMock()
    app.state.key_store = MagicMock()
    yield


@pytest.fixture()
def app_with_lifespan():
    """FastAPI app wired with a fake lifespan that avoids real I/O."""
    import llmhosts.proxy.app as proxy_app

    original = proxy_app._lifespan
    proxy_app._lifespan = _fake_lifespan  # type: ignore[assignment]
    try:
        yield create_app()
    finally:
        proxy_app._lifespan = original


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_security_headers_on_health(app_with_lifespan):
    """Health endpoint must respond with all required security headers."""
    async with AsyncClient(transport=ASGITransport(app=app_with_lifespan), base_url="http://test") as client:
        response = await client.get("/health")

    assert response.status_code == 200
    assert response.headers["x-content-type-options"] == "nosniff"
    assert response.headers["x-frame-options"] == "DENY"
    assert response.headers["referrer-policy"] == "strict-origin-when-cross-origin"
    assert response.headers["cross-origin-opener-policy"] == "same-origin"


@pytest.mark.asyncio
async def test_security_headers_on_not_found(app_with_lifespan):
    """404 responses must also carry security headers (no path is exempt)."""
    async with AsyncClient(transport=ASGITransport(app=app_with_lifespan), base_url="http://test") as client:
        response = await client.get("/nonexistent-path-xyz")

    # 404 is fine — we only care that security headers are present
    assert response.headers.get("x-content-type-options") == "nosniff"
    assert response.headers.get("x-frame-options") == "DENY"


@pytest.mark.asyncio
async def test_security_headers_do_not_override_existing(app_with_lifespan):
    """setdefault() must not duplicate headers already set by downstream handlers."""
    async with AsyncClient(transport=ASGITransport(app=app_with_lifespan), base_url="http://test") as client:
        response = await client.get("/health")

    # Header exists exactly once (not duplicated by the middleware)
    assert response.headers.get("x-content-type-options") == "nosniff"
