"""SP41 Task 11 — HTTP endpoints for the in-window rebill pipeline.

Three tests:

  1. ``test_post_rebill_requires_auth_or_returns_404_or_422_when_no_input`` —
     bare POST with no body. With ``AUTH_DISABLED = True`` (the autouse
     conftest default) the request reaches the handler; without a body,
     Pydantic returns 422. If AUTH_DISABLED is ever flipped off, the
     matrix_gate returns 401/403 first. Either outcome proves the route
     is wired.

  2. ``test_post_rebill_runs_and_returns_summary_path`` — happy path.
     Builds a 1-row visits CSV + zero-SVC 835 fixture under tmp_path,
     POSTs with the window pinned + paths pointed at the fixtures,
     asserts 200 + ``summary_path`` + a ``counts`` dict that includes
     the REBILLED_B key (the in-window visit lands as NOT_IN_835 →
     REBILLED_B because the 835 has no SVCs and the visit is fresh
     enough to clear the 120-day gate).

  3. ``test_get_rebill_status_returns_recent_runs`` — seeds a stub
     ``summary.csv`` under ``tmp_path / "rebills_root"`` and
     monkey-patches :data:`cyclone.api_routers.rebill.REBILLS_DIR` to
     point there, so the GET handler's filesystem scan finds it
     without the test having to chdir the whole process.
"""
from __future__ import annotations

import csv
from datetime import date
from decimal import Decimal
from pathlib import Path

import pytest
from fastapi.testclient import TestClient


# --------------------------------------------------------------------------- #
# Fixtures
# --------------------------------------------------------------------------- #


@pytest.fixture
def client() -> TestClient:
    """Standard TestClient; conftest.py autouse handles DB + auth gate.

    AUTH_DISABLED is True so the matrix_gate short-circuits and the
    request reaches the handler (no login dance required). The seeded
    clearhouse isn't needed — the rebill pipeline doesn't touch SFTP.
    """
    from cyclone.api import app
    return TestClient(app)


def _write_visits_csv(path: Path, rows: list[tuple[str, str, str, str]]) -> Path:
    """rows: list of (dos_mmddyyyy, member, procedure, billed_str)."""
    with path.open("w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["Visit Date", "Member ID", "Procedure Code", "Billable Amount"])
        for r in rows:
            w.writerow(r)
    return path


def _stub_835(ingest_dir: Path, name: str = "x.835") -> Path:
    """An *.835 file parseable enough to not crash the walker.

    Content is just an empty 835 envelope; the rebill pipeline's
    ``parse_835_svc`` reparser emits zero SVCs, which is what we want
    for the NOT_IN_835 → REBILLED_B classification.
    """
    ingest_dir.mkdir(exist_ok=True)
    p = ingest_dir / name
    p.write_text("ST*835*0001~SE*0*0001~")
    return p


# --------------------------------------------------------------------------- #
# Tests
# --------------------------------------------------------------------------- #


def test_post_rebill_requires_auth_or_returns_404_or_422_when_no_input(client):
    """Bare POST with no body — should NOT 500.

    With AUTH_DISABLED the matrix_gate short-circuits and Pydantic
    validation fires on the empty body (422). With auth enabled the
    matrix_gate returns 401/403 first. Either outcome proves the route
    is wired and the request reached the handler.
    """
    resp = client.post("/api/admin/rebill-from-835")
    assert resp.status_code in (401, 403, 422), (
        f"unexpected status {resp.status_code}: {resp.text}"
    )


def test_post_rebill_runs_and_returns_summary_path(client, tmp_path):
    """Happy path: 1 in-window visit, 0 SVCs → REBILLED_B + summary.csv."""
    visits_path = _write_visits_csv(tmp_path / "visits.csv", [
        ("06/27/2026", "J813715", "T1019", "$2.32"),
    ])
    ingest_dir = _stub_835(tmp_path / "ingest")
    out_dir = tmp_path / "out"

    resp = client.post(
        "/api/admin/rebill-from-835",
        json={
            "window": "2026-01-01..2026-06-27",
            "override_filing": False,
            "visits_csv_path": str(visits_path),
            "ingest_dir": str(ingest_dir),
            "out_dir": str(out_dir),
        },
    )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert "summary_path" in body, body
    assert "counts" in body, body
    assert "pipeline_a_files" in body, body
    assert "pipeline_b_files" in body, body

    # summary_path is real and points at the out_dir we passed in.
    assert Path(body["summary_path"]).exists(), body["summary_path"]
    assert str(out_dir) in body["summary_path"]

    # The counts dict surfaces REBILLED_B for the in-window visit (the
    # 835 has no SVCs so the visit falls into NOT_IN_835 → REBILLED_B
    # — it's well within the 120-day gate).
    counts = body["counts"]
    assert "REBILLED_B" in counts, counts
    assert counts["REBILLED_B"] >= 1, counts

    # Pipeline B emitted exactly one file for the one REBILLED_B visit.
    assert len(body["pipeline_b_files"]) == 1, body["pipeline_b_files"]

    # The summary CSV header matches the writer's contract so the GET
    # /status handler can parse it.
    with open(body["summary_path"], newline="") as f:
        reader = csv.DictReader(f)
        rows = list(reader)
    assert len(rows) == 1, rows
    assert rows[0]["disposition"] == "REBILLED_B"
    assert rows[0]["member_id"] == "J813715"


def test_get_rebill_status_returns_recent_runs(client, tmp_path, monkeypatch):
    """GET /status scans the configured REBILLS_DIR and tallies per-disposition counts.

    The handler reads :data:`cyclone.api_routers.rebill.REBILLS_DIR`
    (module-level so tests can monkeypatch it). We point that at a
    tmp_path-relative tree, seed one dated subdir with a 2-row
    summary.csv, and assert the response surfaces that directory's
    name + per-disposition tally.
    """
    from cyclone.api_routers import rebill

    # Build a date-named subdir with a real summary.csv shape so the
    # handler's csv.DictReader finds a `disposition` column.
    rebills_root = tmp_path / "rebills_root"
    dated_dir = rebills_root / "2026-07-07"
    dated_dir.mkdir(parents=True)
    summary = dated_dir / "summary.csv"
    with summary.open("w", newline="") as f:
        w = csv.writer(f)
        w.writerow([
            "dos", "member_id", "procedure", "billed",
            "disposition", "unpaid", "cas_reasons", "file_path",
        ])
        w.writerow([
            "2026-06-27", "MEM-A", "T1019", "2.32",
            "REBILLED_B", "2.32", "", "pipeline-b/",
        ])
        w.writerow([
            "2020-01-01", "MEM-B", "T1019", "2.32",
            "EXCLUDED_TIMELY_FILING", "2.32", "", "",
        ])

    # Repoint the handler's filesystem root at our tmp_path tree.
    monkeypatch.setattr(rebill, "REBILLS_DIR", rebills_root)

    resp = client.get("/api/admin/rebill-from-835/status")
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert "recent_runs" in body, body
    assert len(body["recent_runs"]) == 1, body["recent_runs"]

    run = body["recent_runs"][0]
    assert run["as_of"] == "2026-07-07", run
    assert "summary_path" in run, run
    assert Path(run["summary_path"]).exists(), run["summary_path"]
    counts = run["counts"]
    # Each disposition surfaced exactly once — matches the 2-row CSV.
    assert counts.get("REBILLED_B") == 1, counts
    assert counts.get("EXCLUDED_TIMELY_FILING") == 1, counts


def test_post_rebill_rejects_malformed_window(client):
    """Pydantic window validator should 422 on bad shapes (no `..`, non-dates, reversed)."""
    bad_windows = [
        "2026-01-01",                        # no `..` separator
        "foo..bar",                          # non-ISO dates
        "2026-12-01..2026-01-01",            # start after end
    ]
    for w in bad_windows:
        resp = client.post(
            "/api/admin/rebill-from-835",
            json={"window": w},
        )
        assert resp.status_code == 422, (
            f"expected 422 for window={w!r}, got {resp.status_code}: {resp.text}"
        )


def test_get_rebill_status_returns_empty_when_no_runs(client, tmp_path, monkeypatch):
    """GET /status on an empty rebills dir must return {"recent_runs": []}, not 500.

    Monkeypatches REBILLS_DIR to a fresh tmp_path subdir so the test
    doesn't depend on (or pollute) any pre-existing dev/rebills/ tree.
    """
    from cyclone.api_routers import rebill

    monkeypatch.setattr(rebill, "REBILLS_DIR", tmp_path / "rebills_empty")

    resp = client.get("/api/admin/rebill-from-835/status")
    assert resp.status_code == 200, resp.text
    assert resp.json() == {"recent_runs": []}

