"""SP41 — tests for the `cyclone rebill-from-835` CLI.

Three smoke tests:
  1. --help renders cleanly and surfaces --window and --override-filing.
  2. --status (with no other args) renders cleanly and exits 0 — the
     option is recognized even when no prior summary.csv exists.
  3. End-to-end run against a 2-row visits CSV (one in-window, one
     ancient) and a zero-SVC 835 fixture produces a summary.csv with
     the in-window visit classified as REBILLED_B.

Plus five disposition-coverage tests added during the Task 10 review:
  4. EXCLUDED_CARC: CO-45 on a denied visit → not rebilled.
  5. REBILLED_A: CO-97 on a denied visit → pipeline-a freq-7.
  6. PAID visits are filtered out of the summary.
  7. EXCLUDED_TIMELY_FILING vs REBILLED_B via --override-filing (two runs).
  8. --status on a real summary.csv prints the per-disposition tally.
"""
from __future__ import annotations

import csv
from datetime import date as _date
from decimal import Decimal
from pathlib import Path
from unittest import mock

from click.testing import CliRunner

from cyclone.cli import main
from cyclone.rebill.parse_835_svc import SvcRow
from cyclone.rebill.reconcile import VisitRow


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 _read_summary(out_dir: Path) -> list[dict[str, str]]:
    with (out_dir / "summary.csv").open(newline="") as f:
        return list(csv.DictReader(f))


def _stub_svc(monkeypatch, svcs: list[SvcRow]) -> None:
    """Replace cli.parse_835_svc's underlying cyclonic call with a fixed list."""
    # cli.rebill_from_835 imports parse_835_svc locally, so patch the
    # source module's symbol — the local `from ... import parse_835_svc`
    # binds the symbol at call time on each invocation.
    monkeypatch.setattr(
        "cyclone.rebill.parse_835_svc.parse_835_svc",
        lambda path, _svcs=svcs: iter(_svcs),
    )


def _make_835_placeholder(ingest_dir: Path, name: str = "x.835") -> Path:
    """A real *.835 file present in the glob; parse_835_svc is mocked so content doesn't matter."""
    ingest_dir.mkdir(exist_ok=True)
    p = ingest_dir / name
    p.write_text("ST*835*0001~SE*0*0001~")
    return p


def test_rebill_from_835_help():
    runner = CliRunner()
    result = runner.invoke(main, ["rebill-from-835", "--help"])
    assert result.exit_code == 0, result.output
    assert "--window" in result.output
    assert "--override-filing" in result.output


def test_rebill_from_835_status_help():
    """Either bare --status or --status --help should exit 0.

    The spec note: the help-test ensures the option is recognized, so
    even a "no prior runs" 0-exit path is acceptable. We try both
    invocations because click short-circuits --help ahead of any
    required-option check; the bare --status path is the one that
    proves --status works without --visits / --ingest.
    """
    runner = CliRunner()
    result_help = runner.invoke(main, ["rebill-from-835", "--status", "--help"])
    assert result_help.exit_code == 0, result_help.output

    result_bare = runner.invoke(main, ["rebill-from-835", "--status"])
    assert result_bare.exit_code == 0, result_bare.output


def test_rebill_from_835_runs(tmp_path: Path):
    """End-to-end: 1 in-window visit + 1 ancient visit + zero-SVC 835 → REBILLED_B for the in-window row."""
    visits_path = tmp_path / "visits.csv"
    visits_path.write_text(
        "Visit Date,Member ID,Procedure Code,Billable Amount\n"
        "06/27/2026,J813715,T1019,$2.32\n"
        "01/01/2020,ANCIENT,T1019,$2.32\n"
    )

    ingest_dir = tmp_path / "ingest"
    ingest_dir.mkdir()
    (ingest_dir / "x.835").write_text("ST*835*0001~SE*0*0001~")

    out_dir = tmp_path / "out"

    runner = CliRunner()
    result = runner.invoke(main, [
        "rebill-from-835",
        "--visits", str(visits_path),
        "--ingest", str(ingest_dir),
        "--out", str(out_dir),
        "--as-of", "2026-07-07",
    ])
    assert result.exit_code == 0, (
        f"CLI exited {result.exit_code}, output={result.output!r}, "
        f"exception={result.exception!r}"
    )

    summary_path = out_dir / "summary.csv"
    assert summary_path.exists(), summary_path

    rows = _read_summary(out_dir)
    # Two visits in, two summary rows out (one REBILLED_B, one
    # EXCLUDED_TIMELY_FILING). PAID outcomes would be dropped, but
    # neither of these is PAID — the 835 has no SVCs.
    assert len(rows) == 2, rows

    by_member = {r["member_id"]: r for r in rows}
    in_window = by_member["J813715"]
    assert in_window["disposition"] == "REBILLED_B", in_window
    assert in_window["dos"] == "2026-06-27", in_window

    ancient = by_member["ANCIENT"]
    assert ancient["disposition"] == "EXCLUDED_TIMELY_FILING", ancient
    assert ancient["dos"] == "2020-01-01", ancient


# ---------------------------------------------------------------------------
# Disposition coverage (Task 10 review)
# ---------------------------------------------------------------------------

def test_rebill_carc_excluded_visit_landed_as_excluded_carc(tmp_path: Path, monkeypatch):
    """A denied visit whose CAS list contains CO-45 (excluded) must land as EXCLUDED_CARC, not REBILLED_A."""
    visits = _write_visits_csv(tmp_path / "visits.csv", [
        ("06/27/2026", "J813715", "T1019", "$16.24"),
    ])
    ingest = tmp_path / "ingest"
    _make_835_placeholder(ingest)
    out_dir = tmp_path / "out"

    # A denied SVC for the visit (paid == 0) with CO-45 in CAS.
    svc = SvcRow(
        src_file="x.835",
        claim_id="T1",
        member_id="J813715",
        status="4",  # denied
        procedure="T1019",
        modifiers="",
        charge=Decimal("16.24"),
        paid=Decimal("0"),
        units=Decimal("1"),
        svc_date=_date(2026, 6, 27),
        cas_reasons=("CO-45",),
        pay_date=None,
    )
    _stub_svc(monkeypatch, [svc])

    runner = CliRunner()
    result = runner.invoke(main, [
        "rebill-from-835",
        "--visits", str(visits),
        "--ingest", str(ingest),
        "--out", str(out_dir),
        "--as-of", "2026-07-07",
    ])
    assert result.exit_code == 0, (result.output, result.exception)
    rows = _read_summary(out_dir)
    assert len(rows) == 1, rows
    assert rows[0]["disposition"] == "EXCLUDED_CARC"
    assert rows[0]["member_id"] == "J813715"
    assert "CO-45" in rows[0]["cas_reasons"]


def test_rebill_denied_rebill_visit_landed_as_rebilled_a(tmp_path: Path, monkeypatch):
    """A denied visit whose CAS list contains CO-97 (rebillable) must land as REBILLED_A."""
    visits = _write_visits_csv(tmp_path / "visits.csv", [
        ("06/27/2026", "J813715", "T1019", "$16.24"),
    ])
    ingest = tmp_path / "ingest"
    _make_835_placeholder(ingest)
    out_dir = tmp_path / "out"

    svc = SvcRow(
        src_file="x.835",
        claim_id="T1",
        member_id="J813715",
        status="4",  # denied
        procedure="T1019",
        modifiers="",
        charge=Decimal("16.24"),
        paid=Decimal("0"),
        units=Decimal("1"),
        svc_date=_date(2026, 6, 27),
        cas_reasons=("CO-97",),  # rebillable (not in EXCLUDED_CARCS or REVIEW_CARCS)
        pay_date=None,
    )
    _stub_svc(monkeypatch, [svc])

    runner = CliRunner()
    result = runner.invoke(main, [
        "rebill-from-835",
        "--visits", str(visits),
        "--ingest", str(ingest),
        "--out", str(out_dir),
        "--as-of", "2026-07-07",
    ])
    assert result.exit_code == 0, (result.output, result.exception)
    rows = _read_summary(out_dir)
    assert len(rows) == 1, rows
    assert rows[0]["disposition"] == "REBILLED_A"
    assert rows[0]["file_path"] == "pipeline-a/"
    assert "CO-97" in rows[0]["cas_reasons"]


def test_rebill_paid_visit_excluded_from_summary(tmp_path: Path, monkeypatch):
    """PAID visits (paid >= 95% of billed) must not appear in summary.csv — only NOT_IN_835 should."""
    visits = _write_visits_csv(tmp_path / "visits.csv", [
        ("06/27/2026", "PAID-MEMBER",   "T1019", "$100.00"),
        ("06/27/2026", "MISSING-MEMBER", "T1019", "$2.32"),
    ])
    ingest = tmp_path / "ingest"
    _make_835_placeholder(ingest)
    out_dir = tmp_path / "out"

    # One fully-paid SVC for PAID-MEMBER; nothing matches MISSING-MEMBER.
    paid_svc = SvcRow(
        src_file="x.835",
        claim_id="T1",
        member_id="PAID-MEMBER",
        status="1",
        procedure="T1019",
        modifiers="",
        charge=Decimal("100.00"),
        paid=Decimal("100.00"),
        units=Decimal("1"),
        svc_date=_date(2026, 6, 27),
        cas_reasons=(),
        pay_date=None,
    )
    _stub_svc(monkeypatch, [paid_svc])

    runner = CliRunner()
    result = runner.invoke(main, [
        "rebill-from-835",
        "--visits", str(visits),
        "--ingest", str(ingest),
        "--out", str(out_dir),
        "--as-of", "2026-07-07",
    ])
    assert result.exit_code == 0, (result.output, result.exception)
    rows = _read_summary(out_dir)
    assert len(rows) == 1, rows
    assert rows[0]["member_id"] == "MISSING-MEMBER"
    assert rows[0]["disposition"] == "REBILLED_B"


def test_rebill_override_filing_makes_ancient_visit_rebilled_b(tmp_path: Path, monkeypatch):
    """A 2020 visit without --override-filing → EXCLUDED_TIMELY_FILING; with it → REBILLED_B."""
    # No SVCs at all — both visits fall into NOT_IN_835.
    visits = _write_visits_csv(tmp_path / "visits.csv", [
        ("01/01/2020", "ANCIENT", "T1019", "$2.32"),
    ])
    ingest = tmp_path / "ingest"
    _make_835_placeholder(ingest)
    out_dir = tmp_path / "out"

    # Stub the (local) parse_835_svc import with an empty generator so
    # the CLI sees no SVCs and every visit is NOT_IN_835.
    monkeypatch.setattr(
        "cyclone.rebill.parse_835_svc.parse_835_svc",
        lambda path: iter([]),
    )

    runner = CliRunner()

    # Run 1: as-of 2026-07-07 minus 2020-01-01 = ~6.5 years > 120 days → EXCLUDED.
    r1 = runner.invoke(main, [
        "rebill-from-835",
        "--visits", str(visits),
        "--ingest", str(ingest),
        "--out", str(out_dir),
        "--as-of", "2026-07-07",
    ])
    assert r1.exit_code == 0, (r1.output, r1.exception)
    rows1 = _read_summary(out_dir)
    assert len(rows1) == 1, rows1
    assert rows1[0]["disposition"] == "EXCLUDED_TIMELY_FILING", rows1

    # Run 2: --override-filing bypasses the 120-day gate → REBILLED_B.
    r2 = runner.invoke(main, [
        "rebill-from-835",
        "--visits", str(visits),
        "--ingest", str(ingest),
        "--out", str(out_dir),
        "--as-of", "2026-07-07",
        "--override-filing",
    ])
    assert r2.exit_code == 0, (r2.output, r2.exception)
    rows2 = _read_summary(out_dir)
    assert len(rows2) == 1, rows2
    assert rows2[0]["disposition"] == "REBILLED_B", rows2
    assert rows2[0]["file_path"] == "pipeline-b/"


def test_rebill_status_with_real_summary_prints_tally(tmp_path: Path):
    """--status against a real summary.csv must print both REBILLED_B and EXCLUDED_TIMELY_FILING rows."""
    from cyclone.rebill.reconcile import VisitRow
    from cyclone.rebill.summary import (
        EXCLUDED_TIMELY_FILING,
        REBILLED_B,
        SummaryRow,
        write_summary_csv,
    )

    out_dir = tmp_path / "dev" / "rebills" / "2026-07-07"
    out_dir.mkdir(parents=True, exist_ok=True)
    summary_path = out_dir / "summary.csv"

    rows = [
        SummaryRow(
            visit=VisitRow(
                date=_date(2026, 6, 27), member_id="MEM-A",
                procedure="T1019", billed=Decimal("2.32"),
            ),
            disposition=REBILLED_B,
            unpaid=Decimal("2.32"),
            cas_reasons=(),
            file_path="pipeline-b/",
        ),
        SummaryRow(
            visit=VisitRow(
                date=_date(2026, 6, 27), member_id="MEM-B",
                procedure="T1019", billed=Decimal("2.32"),
            ),
            disposition=EXCLUDED_TIMELY_FILING,
            unpaid=Decimal("2.32"),
            cas_reasons=(),
            file_path="",
        ),
    ]
    write_summary_csv(rows, summary_path)

    runner = CliRunner()
    result = runner.invoke(main, [
        "rebill-from-835",
        "--status",
        "--out", str(out_dir),
    ])
    assert result.exit_code == 0, (result.output, result.exception)
    assert "REBILLED_B" in result.output
    assert "EXCLUDED_TIMELY_FILING" in result.output
    assert "TOTAL" in result.output