"""Tests for the FastAPI surface in ``cyclone.api``.

All tests use ``fastapi.testclient.TestClient`` — no real network or uvicorn
process is started. The fixture file is the same one used by the parser's
own end-to-end test (``co_medicaid_837p.txt`` → 2 claims, both pass).
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from cyclone.api import app
from cyclone import __version__


FIXTURE = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt"


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


# --------------------------------------------------------------------------- #
# Health
# --------------------------------------------------------------------------- #


def test_health_endpoint(client: TestClient):
    """SP19: health endpoint now returns a subsystem snapshot."""
    resp = client.get("/api/health")
    assert resp.status_code == 200
    body = resp.json()
    # Old contract (status + version) is preserved.
    assert body["status"] == "ok"
    assert body["version"] == __version__
    # SP19 additions.
    assert "db" in body and body["db"].get("ok") is True
    assert "scheduler" in body
    assert "pubsub" in body
    assert "batch" in body


# --------------------------------------------------------------------------- #
# JSON response path
# --------------------------------------------------------------------------- #


def test_parse_837_endpoint_returns_json(client: TestClient):
    text = FIXTURE.read_text()
    resp = client.post(
        "/api/parse-837",
        files={"file": ("co_medicaid_837p.txt", text, "text/plain")},
        headers={"Accept": "application/json"},
    )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert "envelope" in body
    assert body["envelope"] is not None
    assert "claims" in body and len(body["claims"]) == 2
    assert "summary" in body
    assert body["summary"]["total_claims"] == 2
    assert body["summary"]["passed"] == 2


# --------------------------------------------------------------------------- #
# NDJSON streaming path
# --------------------------------------------------------------------------- #


def test_parse_837_endpoint_streams_ndjson(client: TestClient):
    text = FIXTURE.read_text()
    resp = client.post(
        "/api/parse-837",
        files={"file": ("co_medicaid_837p.txt", text, "text/plain")},
    )
    assert resp.status_code == 200, resp.text
    assert resp.headers["content-type"].startswith("application/x-ndjson")

    # Consume line-by-line (this is exactly what the React frontend will do).
    lines = list(resp.iter_lines())
    # 1 envelope + 2 claims + 1 summary
    assert len(lines) == 4

    parsed = [json.loads(line) for line in lines]
    assert parsed[0]["type"] == "envelope"
    assert parsed[0]["data"] is not None
    assert parsed[1]["type"] == "claim"
    assert parsed[2]["type"] == "claim"
    assert parsed[3]["type"] == "summary"

    # When include_raw_segments defaults to True, each claim carries raw segments.
    for obj in parsed[1:3]:
        assert "raw_segments" in obj["data"]
        assert isinstance(obj["data"]["raw_segments"], list)

    # Summary numbers match the JSON path.
    assert parsed[3]["data"]["total_claims"] == 2
    assert parsed[3]["data"]["passed"] == 2


def test_parse_837_endpoint_streams_ndjson_without_raw_segments(client: TestClient):
    text = FIXTURE.read_text()
    resp = client.post(
        "/api/parse-837?include_raw_segments=false",
        files={"file": ("co_medicaid_837p.txt", text, "text/plain")},
    )
    assert resp.status_code == 200
    assert resp.headers["content-type"].startswith("application/x-ndjson")

    claims = [json.loads(line) for line in resp.iter_lines() if json.loads(line)["type"] == "claim"]
    assert len(claims) == 2
    for c in claims:
        assert c["data"]["raw_segments"] == []


# --------------------------------------------------------------------------- #
# Validation / error paths
# --------------------------------------------------------------------------- #


def test_parse_837_endpoint_rejects_missing_file(client: TestClient):
    # FastAPI's `File(...)` (no default) → 422 Unprocessable Entity.
    resp = client.post("/api/parse-837")
    assert resp.status_code == 422


def test_parse_837_endpoint_handles_payer_query_param(client: TestClient):
    text = FIXTURE.read_text()
    for payer in ("co_medicaid", "generic_837p"):
        resp = client.post(
            f"/api/parse-837?payer={payer}",
            files={"file": ("co_medicaid_837p.txt", text, "text/plain")},
            headers={"Accept": "application/json"},
        )
        assert resp.status_code == 200, (payer, resp.text)
        body = resp.json()
        assert body["summary"]["passed"] == 2
        assert body["summary"]["total_claims"] == 2


# --------------------------------------------------------------------------- #
# CORS
# --------------------------------------------------------------------------- #


def test_cors_headers_present(client: TestClient):
    # Simulate a preflight from the Vite dev origin.
    resp = client.options(
        "/api/parse-837",
        headers={
            "Origin": "http://localhost:5173",
            "Access-Control-Request-Method": "POST",
            "Access-Control-Request-Headers": "content-type",
        },
    )
    assert resp.headers.get("access-control-allow-origin") == "http://localhost:5173"
    assert "POST" in resp.headers.get("access-control-allow-methods", "").upper()


def test_cors_headers_present_for_loopback_ip(client: TestClient):
    # ``http://127.0.0.1:5173`` is a distinct origin from
    # ``http://localhost:5173`` per the CORS spec, even though both resolve
    # to the same Vite dev server. Both must be allow-listed or tabs opened
    # via the IP form silently break.
    resp = client.options(
        "/api/parse-837",
        headers={
            "Origin": "http://127.0.0.1:5173",
            "Access-Control-Request-Method": "POST",
            "Access-Control-Request-Headers": "content-type",
        },
    )
    assert resp.headers.get("access-control-allow-origin") == "http://127.0.0.1:5173"


def test_cors_extra_origins_via_env(client: TestClient, monkeypatch):
    # LAN / staging hosts opt in via CYCLONE_ALLOWED_ORIGINS. The env var
    # is a comma-separated list; the middleware must reflect each entry.
    # The allow-list is built at module import, so we re-execute the
    # module under the env var and build a TestClient against the
    # reloaded app.
    monkeypatch.setenv(
        "CYCLONE_ALLOWED_ORIGINS", "http://192.168.1.42:5173,https://staging.example.com"
    )
    import importlib
    from cyclone import api as api_module
    from fastapi.testclient import TestClient as _TC
    importlib.reload(api_module)
    try:
        with _TC(api_module.app) as tc:
            for origin in ("http://192.168.1.42:5173", "https://staging.example.com"):
                resp = tc.options(
                    "/api/parse-837",
                    headers={
                        "Origin": origin,
                        "Access-Control-Request-Method": "POST",
                        "Access-Control-Request-Headers": "content-type",
                    },
                )
                assert resp.headers.get("access-control-allow-origin") == origin
    finally:
        monkeypatch.delenv("CYCLONE_ALLOWED_ORIGINS", raising=False)
        # Reload once more so the module-level allow-list returns to its
        # default for any test that imports `cyclone.api` after this one.
        importlib.reload(api_module)
