"""SP40: tests for ``POST /api/admin/validate-837`` (admin-gated).

Covers:

1. Auth gate — 401 with no session cookie (matrix_gate fires when
   AUTH_DISABLED is flipped off).
2. Happy path — admin-cookie request with a multipart file upload
   returns the Edifabric OperationResult JSON.
3. Missing file part — FastAPI's built-in validation rejects requests
   without the ``file`` form part with 422.
"""
from __future__ import annotations

import httpx
import pytest
from fastapi.testclient import TestClient

from cyclone import edifabric
from cyclone.api import app


_TEST_KEY = "test-edifabric-key-0123456789abcdef"


def _make_client(handler):
    transport = httpx.MockTransport(handler)
    return httpx.Client(transport=transport, timeout=10.0)


def _install(handler):
    edifabric.set_transport_factory(lambda: _make_client(handler))


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


@pytest.fixture(autouse=True)
def _reset_transport():
    yield
    edifabric._reset_transport_factory()
    import cyclone.secrets
    cyclone.secrets.get_secret = _real_get_secret  # type: ignore[assignment]


import cyclone.secrets as _secrets_module  # noqa: E402
_real_get_secret = _secrets_module.get_secret  # noqa: E402


def _mock_secrets(patched_key: str):
    """Install a get_secret mock that returns ``patched_key`` for any
    'edifabric.api_key' lookup. Restores the real function on
    teardown via the autouse ``_reset_transport`` fixture.

    Until SP40 Task 6 lands, ``_ENV_NAME_FOR`` doesn't know about
    'edifabric.api_key', so the env-var name expected is the same —
    but the more robust pattern is to monkeypatch at the function
    boundary. That keeps test behavior independent of the secrets
    table layout.
    """
    def _fake(name: str):
        if name == "edifabric.api_key":
            return patched_key
        return _real_get_secret(name)

    _secrets_module.get_secret = _fake  # type: ignore[assignment]
    return _fake


@pytest.fixture
def mock_edifabric_ok():
    """Mock Edifabric to return a clean OperationResult on /validate.

    Wires both the transport mock and the secrets.get_secret monkeypatch
    so ``cyclone.edifabric.validate_edi(...)`` resolves an API key.
    """
    operation_result = {
        "Status": "success",
        "Details": [],
        "LastIndex": 46,
    }
    x12 = {
        "SegmentDelimiter": "~",
        "DataElementDelimiter": "*",
        "ISA": {"InterchangeControlNumber_13": "000000001"},
        "Groups": [],
        "IEATrailers": [],
    }

    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path.endswith("/read"):
            return httpx.Response(200, json=[x12])
        if request.url.path.endswith("/validate"):
            return httpx.Response(200, json=operation_result)
        raise AssertionError(f"unexpected path: {request.url.path}")

    _mock_secrets(_TEST_KEY)
    _install(handler)


# --------------------------------------------------------------------------- #
# Auth gate
# --------------------------------------------------------------------------- #


def test_validate_837_no_session_returns_401(client, tmp_path, monkeypatch):
    """No session cookie + AUTH_DISABLED off → 401 from matrix_gate.

    Mirrors the test pattern in test_api_clearhouse_patch.py:99 — we
    flip AUTH_DISABLED to False inside the test (monkeypatch restores
    it at teardown) so the gate fires.
    """
    import cyclone.auth.deps as auth_deps

    monkeypatch.setattr(auth_deps, "AUTH_DISABLED", False)

    sample = tmp_path / "ok.x12"
    sample.write_text("ISA*seg~SE*1*0001~IEA*0*000000001~")

    with sample.open("rb") as fh:
        resp = client.post(
            "/api/admin/validate-837",
            files={"file": ("ok.x12", fh, "application/octet-stream")},
        )
    assert resp.status_code == 401, resp.text


# --------------------------------------------------------------------------- #
# Happy path
# --------------------------------------------------------------------------- #


def test_validate_837_returns_operation_result(client, tmp_path, mock_edifabric_ok):
    """Admin request with multipart file returns the OperationResult verbatim.

    AUTH_DISABLED is True (set by conftest's autouse fixture), so the
    admin gate fires-and-passes; the Edifabric client is mocked to
    return a clean Success.
    """
    fixture = tmp_path / "ok.x12"
    fixture.write_text("ISA*00*...~IEA*0*000000001~")

    with fixture.open("rb") as fh:
        resp = client.post(
            "/api/admin/validate-837",
            files={"file": ("ok.x12", fh, "application/octet-stream")},
        )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert body["Status"] == "success"
    assert body["Details"] == []
    assert body["LastIndex"] == 46


def test_validate_837_returns_error_details(client, tmp_path):
    """A 200 response with Status='error' + Details passes through verbatim.

    The OperationResult is data; the HTTP response is 200 (the
    endpoint did its job of talking to Edifabric). Callers inspect
    Status in the body to decide whether to fail-closed.
    """
    error_payload = {
        "Status": "error",
        "Details": [
            {"SegmentId": "PER", "Message": "PER-04 is required", "Status": "error"},
            {"SegmentId": "SBR", "Message": "SBR-09 is required", "Status": "error"},
        ],
        "LastIndex": 5,
    }
    x12 = {
        "SegmentDelimiter": "~",
        "DataElementDelimiter": "*",
        "ISA": {"InterchangeControlNumber_13": "000000001"},
        "Groups": [],
        "IEATrailers": [],
    }

    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path.endswith("/read"):
            return httpx.Response(200, json=[x12])
        if request.url.path.endswith("/validate"):
            return httpx.Response(200, json=error_payload)
        raise AssertionError(f"unexpected path: {request.url.path}")

    _mock_secrets(_TEST_KEY)
    _install(handler)

    fixture = tmp_path / "bad.x12"
    fixture.write_text("ISA*bad~")

    with fixture.open("rb") as fh:
        resp = client.post(
            "/api/admin/validate-837",
            files={"file": ("bad.x12", fh, "application/octet-stream")},
        )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert body["Status"] == "error"
    assert len(body["Details"]) == 2
    assert body["Details"][0]["Message"] == "PER-04 is required"


def test_validate_837_missing_file_returns_422(client):
    """No ``file`` form part → FastAPI returns 422 (built-in validation).

    This is the FastAPI-default behavior for ``UploadFile = File(...)``
    when the field is absent. The endpoint body itself never runs.
    """
    resp = client.post("/api/admin/validate-837")
    assert resp.status_code == 422, resp.text


def test_validate_837_missing_api_key_returns_503(client, tmp_path, monkeypatch):
    """With no API key configured, the endpoint surfaces a 503 with the
    'API key not configured' detail (status_code 0 → 503, not 502).

    Upstream 4xx/5xx map to 502; a 0-status_code EdifabricError means
    the client-side config is missing and the caller can fix it.
    """
    monkeypatch.delenv("CYCLONE_EDIFABRIC_API_KEY", raising=False)

    fixture = tmp_path / "ok.x12"
    fixture.write_text("ISA*seg~")

    with fixture.open("rb") as fh:
        resp = client.post(
            "/api/admin/validate-837",
            files={"file": ("ok.x12", fh, "application/octet-stream")},
        )
    assert resp.status_code == 503, resp.text
    body = resp.json()
    assert "API key not configured" in str(body)
