"""Tests for the FastAPI surface in ``cyclone.api`` for the 277CA endpoint.

SP10 T3. Mirrors ``test_api_999.py``:
- 400 on empty / undecodable / malformed EDI (never 500).
- 200 on success with the parsed envelope + counts.
- After parse, ``apply_277ca_rejections`` stamps matching claim rows.
"""
from __future__ import annotations

from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from cyclone import db
from cyclone.api import app
from cyclone.db import Claim, init_db

ACCEPTED_FIXTURE = Path(__file__).parent / "fixtures" / "minimal_277ca.txt"
REJECTED_FIXTURE = Path(__file__).parent / "fixtures" / "minimal_277ca_rejected_only.txt"


@pytest.fixture(autouse=True)
def _fresh_db():
    """Each test gets a fresh DB and a clean 277CA ack list."""
    init_db()
    yield


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


def _seed_claim(claim_id: str, pcn: str) -> None:
    with db.SessionLocal()() as s:
        s.add(Claim(
            id=claim_id, batch_id="BATCH-1",
            patient_control_number=pcn, charge_amount=100.00,
        ))
        s.commit()


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


class TestParse277CAEndpointHappyPath:
    def test_upload_minimal_277ca_returns_200(self, client: TestClient):
        text = ACCEPTED_FIXTURE.read_text()
        resp = client.post(
            "/api/parse-277ca",
            files={"file": ("minimal_277ca.txt", text, "text/plain")},
            headers={"Accept": "application/json"},
        )
        assert resp.status_code == 200, resp.text
        body = resp.json()
        assert "ack" in body
        assert "parsed" in body
        ack = body["ack"]
        assert ack["accepted_count"] == 1
        assert ack["rejected_count"] == 1
        assert ack["pended_count"] == 1
        assert ack["control_number"] == "000000123"

    def test_persists_two77ca_row(self, client: TestClient):
        text = ACCEPTED_FIXTURE.read_text()
        client.post(
            "/api/parse-277ca",
            files={"file": ("minimal_277ca.txt", text, "text/plain")},
        )
        rows_resp = client.get("/api/277ca-acks")
        assert rows_resp.status_code == 200
        rows = rows_resp.json()
        assert rows["total"] == 1
        assert rows["items"][0]["control_number"] == "000000123"

    def test_get_277ca_ack_by_id(self, client: TestClient):
        text = ACCEPTED_FIXTURE.read_text()
        post_resp = client.post(
            "/api/parse-277ca",
            files={"file": ("minimal_277ca.txt", text, "text/plain")},
        )
        ack_id = post_resp.json()["ack"]["id"]
        detail = client.get(f"/api/277ca-acks/{ack_id}")
        assert detail.status_code == 200
        assert detail.json()["control_number"] == "000000123"

    def test_stamps_matching_claim(self, client: TestClient):
        """A rejected 277CA claim with REF*1K=CLAIM002 stamps claim c2."""
        # Seed two claims matching the fixture's PCNs.
        _seed_claim("c1", "CLAIM001")
        _seed_claim("c2", "CLAIM002")
        text = ACCEPTED_FIXTURE.read_text()
        client.post(
            "/api/parse-277ca",
            files={"file": ("minimal_277ca.txt", text, "text/plain")},
        )
        with db.SessionLocal()() as s:
            # Migration 0014: composite PK (batch_id, id). The _seed_claim
            # helper uses batch_id="BATCH-1".
            c1 = s.get(Claim, ("BATCH-1", "c1"))
            c2 = s.get(Claim, ("BATCH-1", "c2"))
        assert c1.payer_rejected_at is None
        assert c2.payer_rejected_at is not None
        assert c2.payer_rejected_status_code == "A6"
        assert "A6" in c2.payer_rejected_reason


# --------------------------------------------------------------------------- #
# Error paths
# --------------------------------------------------------------------------- #


class TestParse277CAEndpointErrors:
    def test_empty_file_raises_400(self, client: TestClient):
        resp = client.post(
            "/api/parse-277ca",
            files={"file": ("empty.txt", "", "text/plain")},
        )
        assert resp.status_code == 400, resp.text
        assert "error" in resp.json()

    def test_garbage_raises_400(self, client: TestClient):
        resp = client.post(
            "/api/parse-277ca",
            files={"file": ("garbage.txt", "this is not EDI", "text/plain")},
        )
        assert resp.status_code == 400, resp.text

    def test_wrong_transaction_set_raises_400(self, client: TestClient):
        """A 999 must NOT be accepted as a 277CA — different transaction set id."""
        text = (
            "ISA*00*          *00*          *ZZ*AAAAAAAAAAAAAAA*ZZ*BBBBBBBBBBBBBBB*240620*1200*^*00501*000000001*0*P*:~"
            "GS*HN*A*B*20240620*1200*1*X*005010X231A1~"
            "ST*999*0001*005010X231A1~"
            "AK1*HC*0001~"
            "AK9*A*0*0*0~"
            "SE*4*0001~"
            "GE*1*1~"
            "IEA*1*000000001~"
        )
        resp = client.post(
            "/api/parse-277ca",
            files={"file": ("bad.txt", text, "text/plain")},
        )
        assert resp.status_code == 400, resp.text


# --------------------------------------------------------------------------- #
# Inbox lane
# --------------------------------------------------------------------------- #


class TestInboxPayerRejectedLane:
    def test_payer_rejected_claim_appears_in_lane(self, client: TestClient):
        """A claim with payer_rejected_at set must appear in the payer_rejected lane."""
        _seed_claim("c1", "CLAIM099")
        text = REJECTED_FIXTURE.read_text()  # single A7 for CLAIM099
        client.post(
            "/api/parse-277ca",
            files={"file": ("rejected.txt", text, "text/plain")},
        )
        lanes = client.get("/api/inbox/lanes").json()
        assert "payer_rejected" in lanes
        ids = [c["id"] for c in lanes["payer_rejected"]]
        assert "c1" in ids
        # The rejected lane (999 envelope) must be empty — we haven't
        # uploaded a 999, so this claim isn't there.
        assert "c1" not in [c["id"] for c in lanes["rejected"]]
