"""CSRF defenses for cookie-authenticated dashboard mutations."""

from __future__ import annotations

import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from starlette.requests import Request

from agent.dashboard import oauth, routes, thread_api


def _request(
    *,
    method: str = "POST",
    path: str = "/dashboard/api/threads/tid/commands",
    origin: str | None = None,
    referer: str | None = None,
) -> Request:
    headers: list[tuple[bytes, bytes]] = []
    if origin is not None:
        headers.append((b"origin", origin.encode()))
    if referer is not None:
        headers.append((b"referer", referer.encode()))
    scope = {
        "type": "http",
        "method": method,
        "path": path,
        "headers": headers,
    }
    return Request(scope)


@pytest.fixture
def dashboard_client(monkeypatch: pytest.MonkeyPatch) -> TestClient:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "http://testserver")
    app = FastAPI()
    app.include_router(routes.router)
    return TestClient(app)


@pytest.mark.asyncio
async def test_require_same_origin_noop_when_unconfigured(monkeypatch) -> None:
    monkeypatch.delenv("DASHBOARD_BASE_URL", raising=False)
    monkeypatch.delenv("DASHBOARD_ALLOWED_ORIGINS", raising=False)

    oauth.require_same_origin(_request(origin="https://evil.example"))


@pytest.mark.asyncio
async def test_require_same_origin_allows_configured_origin(monkeypatch) -> None:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "https://dashboard.example")
    monkeypatch.setenv("DASHBOARD_ALLOWED_ORIGINS", "https://preview.example")

    oauth.require_same_origin(_request(origin="https://dashboard.example"))
    oauth.require_same_origin(_request(origin="https://preview.example"))


@pytest.mark.asyncio
async def test_require_same_origin_normalizes_case_and_trailing_slash(monkeypatch) -> None:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "HTTPS://Dashboard.Example/")
    monkeypatch.setenv("DASHBOARD_ALLOWED_ORIGINS", "https://Preview.Example/")

    oauth.require_same_origin(_request(origin="https://dashboard.example"))
    oauth.require_same_origin(_request(origin="https://preview.example"))


@pytest.mark.asyncio
async def test_require_same_origin_accepts_referer(monkeypatch) -> None:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "https://dashboard.example")

    oauth.require_same_origin(_request(referer="https://dashboard.example/agents/thread-id"))


@pytest.mark.asyncio
async def test_require_same_origin_rejects_missing_origin_and_referer(monkeypatch) -> None:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "https://dashboard.example")

    with pytest.raises(HTTPException) as exc:
        oauth.require_same_origin(_request())

    assert exc.value.status_code == 403


@pytest.mark.asyncio
async def test_require_same_origin_rejects_null_origin(monkeypatch) -> None:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "https://dashboard.example")

    with pytest.raises(HTTPException) as exc:
        oauth.require_same_origin(_request(origin="null"))

    assert exc.value.status_code == 403


@pytest.mark.asyncio
async def test_require_same_origin_rejects_malformed_port(monkeypatch) -> None:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "https://dashboard.example")

    with pytest.raises(HTTPException) as exc:
        oauth.require_same_origin(_request(origin="https://dashboard.example:notaport"))

    assert exc.value.status_code == 403


@pytest.mark.asyncio
async def test_require_same_origin_rejects_unknown_origin(monkeypatch) -> None:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "https://dashboard.example")

    with pytest.raises(HTTPException) as exc:
        oauth.require_same_origin(_request(origin="https://evil.example"))

    assert exc.value.status_code == 403


@pytest.mark.asyncio
async def test_require_same_origin_rejects_prefix_bypass(monkeypatch) -> None:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "https://dashboard.example")

    with pytest.raises(HTTPException) as exc:
        oauth.require_same_origin(_request(origin="https://dashboard.example.evil"))

    assert exc.value.status_code == 403


@pytest.mark.asyncio
async def test_require_same_origin_does_not_fallback_when_origin_invalid(monkeypatch) -> None:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "https://dashboard.example")

    with pytest.raises(HTTPException) as exc:
        oauth.require_same_origin(
            _request(origin="null", referer="https://dashboard.example/agents")
        )

    assert exc.value.status_code == 403


@pytest.mark.asyncio
async def test_require_same_origin_for_mutations_skips_get(monkeypatch) -> None:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "https://dashboard.example")

    oauth.require_same_origin_for_mutations(
        _request(method="GET", path="/dashboard/api/me", origin="https://evil.example")
    )


@pytest.mark.asyncio
async def test_require_same_origin_for_mutations_enforces_post(monkeypatch) -> None:
    monkeypatch.setenv("DASHBOARD_BASE_URL", "https://dashboard.example")

    with pytest.raises(HTTPException) as exc:
        oauth.require_same_origin_for_mutations(_request(origin="https://evil.example"))

    assert exc.value.status_code == 403


def test_router_rejects_cross_site_text_plain_post(dashboard_client: TestClient) -> None:
    response = dashboard_client.post(
        "/dashboard/api/auth/logout",
        headers={"Origin": "https://evil.example", "Content-Type": "text/plain"},
        content='{"method": "run.start"}',
    )

    assert response.status_code == 403
    assert response.json()["detail"] == "CSRF check failed"


def test_router_rejects_post_without_origin(dashboard_client: TestClient) -> None:
    response = dashboard_client.post(
        "/dashboard/api/auth/logout",
        headers={"Content-Type": "application/json"},
    )

    assert response.status_code == 403


def test_router_allows_allowed_origin_post(dashboard_client: TestClient) -> None:
    response = dashboard_client.post(
        "/dashboard/api/auth/logout",
        headers={"Origin": "http://testserver", "Content-Type": "application/json"},
    )

    assert response.status_code == 204


async def test_proxy_commands_rejects_non_json_content_type() -> None:
    for content_type in ("text/plain", "application/x-www-form-urlencoded", "multipart/form-data"):
        with pytest.raises(HTTPException) as exc:
            await thread_api.proxy_dashboard_thread_commands(
                "tid",
                "octocat",
                b'{"method": "run.start"}',
                content_type=content_type,
            )

        assert exc.value.status_code == 415


async def test_proxy_commands_accepts_json_content_type_with_charset() -> None:
    with pytest.raises(HTTPException) as exc:
        await thread_api.proxy_dashboard_thread_commands(
            "tid",
            "octocat",
            b"not-json",
            content_type="application/json; charset=utf-8",
        )

    assert exc.value.status_code == 400
