"""Tests for the write-proxy middleware (2026-05-12).

The middleware forwards POST/PUT/PATCH/DELETE to an upstream URL when
RECOIL_WRITE_UPSTREAM is set. GET/HEAD/OPTIONS always hit local handlers
regardless. See `write_proxy.py` for the rationale (Console v2 host-arch).
"""
from __future__ import annotations

import json
from unittest.mock import AsyncMock, patch

import httpx
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from recoil.api.write_proxy import WriteProxyMiddleware, install_write_proxy


# ── Fixtures ────────────────────────────────────────────────────────────────


def _make_app(upstream: str | None) -> FastAPI:
    """Build a tiny app with a local echo route + the middleware installed."""
    app = FastAPI()

    @app.get("/api/health")
    def get_health():
        return {"local": True, "method": "GET"}

    @app.post("/api/echo")
    def post_echo():
        return {"local": True, "method": "POST"}

    @app.put("/api/echo")
    def put_echo():
        return {"local": True, "method": "PUT"}

    @app.delete("/api/echo")
    def delete_echo():
        return {"local": True, "method": "DELETE"}

    if upstream:
        app.add_middleware(WriteProxyMiddleware, upstream=upstream)
    return app


# ── Unset → pass-through ────────────────────────────────────────────────────


def test_no_upstream_passes_through_all_methods():
    """When upstream is None, every request hits local handlers."""
    app = _make_app(upstream=None)
    with TestClient(app) as c:
        for method, expected in [
            ("GET", "/api/health"),
            ("POST", "/api/echo"),
            ("PUT", "/api/echo"),
            ("DELETE", "/api/echo"),
        ]:
            r = c.request(method, expected)
            assert r.status_code == 200
            assert r.json()["local"] is True
            assert r.json()["method"] == method


def test_install_write_proxy_noop_when_env_unset(monkeypatch):
    """install_write_proxy() with no env var set adds no middleware."""
    monkeypatch.delenv("RECOIL_WRITE_UPSTREAM", raising=False)
    app = FastAPI()
    install_write_proxy(app)
    # Sanity check: app's middleware stack should not contain WriteProxy.
    classes = [m.cls for m in app.user_middleware]
    assert WriteProxyMiddleware not in classes


def test_install_write_proxy_installs_when_env_set(monkeypatch):
    monkeypatch.setenv("RECOIL_WRITE_UPSTREAM", "http://elsewhere:8431")
    app = FastAPI()
    install_write_proxy(app)
    classes = [m.cls for m in app.user_middleware]
    assert WriteProxyMiddleware in classes


# ── Upstream set → forwarding ───────────────────────────────────────────────


@pytest.fixture
def _patched_client():
    """Patch httpx.AsyncClient.request used by the middleware.

    Returns a mock so tests can assert the upstream call args + control the
    upstream response shape.
    """
    mock_request = AsyncMock()
    mock_request.return_value = httpx.Response(
        200,
        content=b'{"upstream": true}',
        headers={"content-type": "application/json"},
    )
    with patch("recoil.api.write_proxy.httpx.AsyncClient") as MockClient:
        instance = MockClient.return_value
        instance.request = mock_request
        yield mock_request


def test_get_with_upstream_still_passes_through(_patched_client):
    """GETs ALWAYS hit local handlers — that's the whole point of the arch."""
    app = _make_app(upstream="http://elsewhere:8431")
    with TestClient(app) as c:
        r = c.get("/api/health")
    assert r.status_code == 200
    assert r.json()["local"] is True
    _patched_client.assert_not_called()


def test_post_with_upstream_forwards(_patched_client):
    app = _make_app(upstream="http://elsewhere:8431")
    with TestClient(app) as c:
        r = c.post("/api/echo", json={"key": "value"})
    assert r.status_code == 200
    assert r.json() == {"upstream": True}
    # Local handler was NOT invoked — middleware short-circuited.
    _patched_client.assert_called_once()
    call_kwargs = _patched_client.call_args.kwargs
    assert call_kwargs["method"] == "POST"
    assert call_kwargs["url"] == "/api/echo"
    # Body preserved through the proxy.
    assert json.loads(call_kwargs["content"]) == {"key": "value"}


def test_put_with_upstream_forwards(_patched_client):
    app = _make_app(upstream="http://elsewhere:8431")
    with TestClient(app) as c:
        r = c.put("/api/echo", json={"k": 1})
    assert r.status_code == 200
    assert r.json() == {"upstream": True}
    _patched_client.assert_called_once()
    assert _patched_client.call_args.kwargs["method"] == "PUT"


def test_delete_with_upstream_forwards(_patched_client):
    app = _make_app(upstream="http://elsewhere:8431")
    with TestClient(app) as c:
        r = c.delete("/api/echo")
    assert r.status_code == 200
    _patched_client.assert_called_once()
    assert _patched_client.call_args.kwargs["method"] == "DELETE"


def test_query_string_preserved_in_proxy(_patched_client):
    app = _make_app(upstream="http://elsewhere:8431")
    with TestClient(app) as c:
        c.post("/api/echo?retries=3&dry_run=true", json={})
    assert "?retries=3&dry_run=true" in _patched_client.call_args.kwargs["url"]


def test_upstream_error_response_passed_through(_patched_client):
    """4xx/5xx from upstream comes back to caller unchanged."""
    _patched_client.return_value = httpx.Response(
        422,
        content=b'{"detail": "invalid payload"}',
        headers={"content-type": "application/json"},
    )
    app = _make_app(upstream="http://elsewhere:8431")
    with TestClient(app) as c:
        r = c.post("/api/echo", json={})
    assert r.status_code == 422
    assert r.json() == {"detail": "invalid payload"}


def test_upstream_unreachable_returns_502(_patched_client):
    """Connection failure to upstream → 502 with descriptive body."""
    _patched_client.side_effect = httpx.ConnectError("nope")
    app = _make_app(upstream="http://elsewhere:8431")
    with TestClient(app) as c:
        r = c.post("/api/echo", json={})
    assert r.status_code == 502
    body = r.json()
    assert "write upstream unreachable" in body["detail"]


def test_trailing_slash_in_upstream_url_stripped():
    """Configuration robustness — trailing slash on upstream shouldn't double up."""
    app = _make_app(upstream="http://elsewhere:8431/")
    # Internal: middleware should have stripped the slash.
    middleware = app.user_middleware[0]
    # Build the middleware to inspect (won't actually run it).
    proxy = WriteProxyMiddleware(app, upstream="http://elsewhere:8431/")
    assert proxy.upstream == "http://elsewhere:8431"


def test_hop_by_hop_headers_not_forwarded(_patched_client):
    """Connection / Transfer-Encoding / etc. should be stripped per RFC 7230."""
    app = _make_app(upstream="http://elsewhere:8431")
    with TestClient(app) as c:
        c.post(
            "/api/echo",
            json={},
            headers={
                "Connection": "keep-alive",
                "Transfer-Encoding": "chunked",
                "X-Custom": "kept",
            },
        )
    forwarded = _patched_client.call_args.kwargs["headers"]
    forwarded_lower = {k.lower() for k in forwarded}
    assert "connection" not in forwarded_lower
    assert "transfer-encoding" not in forwarded_lower
    # Custom header passes through.
    assert any(k.lower() == "x-custom" for k in forwarded)
