"""/api/invoices + the bulk apply/detach endpoints over the migrated
fixture graph (rpt scratch DB). Every test restores the state it
touches — this database is shared with the read-API tests.

Fixture facts (tests/migrate/fixtures/legacy_fixture.sql, migrated):
  invoices: 'PlusROI 2026-05' (paid, 1 line = te-1, manual 5000),
            'Legacy transfers' (paid, te-6), 'March work' (open, empty)
  pending entries: te-2, te-3, te-7
"""

import uuid

TE = {n: f"0a000000-0000-0000-0000-00000000000{n}" for n in range(1, 8)}


def _invoice_id(client, name: str) -> str:
    rows = client.get("/api/invoices").json()["rows"]
    return next(r["id"] for r in rows if r["name"] == name)


def test_list_requires_auth(client):
    assert client.get("/api/invoices").status_code == 401


def test_member_can_read_owner_gates_enforced(as_adi):
    """B15/B38: any member reads list + detail; every mutation refuses
    a manager with the owner-only message (T-INV-013 at the API)."""
    listed = as_adi.get("/api/invoices")
    assert listed.status_code == 200
    names = {r["name"] for r in listed.json()["rows"]}
    assert {"PlusROI 2026-05", "Legacy transfers", "March work"} <= names

    iid = _invoice_id(as_adi, "March work")
    assert as_adi.get(f"/api/invoices/{iid}").status_code == 200
    assert as_adi.get(f"/api/invoices/{iid}/paste-block").status_code == 200

    created = as_adi.post("/api/invoices", json={"name": "adi invoice"})
    assert created.status_code == 403
    assert created.json()["summary"] == "Only an owner can create invoices."
    patched = as_adi.patch(f"/api/invoices/{iid}", json={"name": "x"})
    assert patched.status_code == 403
    deleted = as_adi.delete(f"/api/invoices/{iid}")
    assert deleted.status_code == 403
    applied = as_adi.post(
        "/api/entries/bulk/apply-to-invoice",
        json={"expected_count": 1, "ids": [TE[2]], "invoice_id": iid},
    )
    assert applied.status_code == 403
    detached = as_adi.post(
        "/api/entries/bulk/detach", json={"expected_count": 1, "ids": [TE[1]]}
    )
    assert detached.status_code == 403


def test_crud_roundtrip_and_duplicate_name(as_rian):
    created = as_rian.post(
        "/api/invoices",
        json={"name": "API roundtrip", "status": "sent", "notes": " hi "},
    )
    assert created.status_code == 200
    row = created.json()["row"]
    assert row["status"] == "sent"
    assert row["sent_at"] is not None
    assert row["notes"] == "hi"
    assert row["entry_count"] == 0
    assert row["invoice_date"] is not None  # server-side default (B4)

    dup = as_rian.post("/api/invoices", json={"name": "api ROUNDTRIP"})
    assert dup.status_code == 409
    assert dup.json()["error_code"] == "DUPLICATE_INVOICE_NAME"

    patched = as_rian.patch(
        f"/api/invoices/{row['id']}",
        json={"manual_total": "123.4", "invoice_date": ""},
    )
    assert patched.status_code == 200
    body = patched.json()["row"]
    assert body["manual_total"] == "123.40"
    assert body["invoice_date"] is None  # B66: empty date writes NULL
    assert body["billout"]["amount"] == "123.40"  # override drives billout

    assert as_rian.delete(f"/api/invoices/{row['id']}").status_code == 200
    assert as_rian.get(f"/api/invoices/{row['id']}").status_code == 404


def test_apply_detach_cycle_via_api(as_rian):
    """The bulk endpoints end-to-end: apply two pending fixture rows,
    see them in the detail + ?invoice= filter, detach, delete — leaves
    the fixture DB exactly as found."""
    created = as_rian.post("/api/invoices", json={"name": "API apply cycle"})
    iid = created.json()["row"]["id"]

    applied = as_rian.post(
        "/api/entries/bulk/apply-to-invoice",
        json={"expected_count": 2, "ids": [TE[2], TE[3]], "invoice_id": iid},
    )
    assert applied.status_code == 200
    body = applied.json()
    assert body["applied"] == 2
    assert sorted(body["applied_ids"]) == sorted([TE[2], TE[3]])

    detail = as_rian.get(f"/api/invoices/{iid}").json()
    assert detail["invoice"]["entry_count"] == 2
    assert detail["lines"]["total"] == 2
    assert {r["client"] for r in detail["breakdown"]} == {"Brentwood", "PlusROI"}

    scoped = as_rian.get(f"/api/entries?status=applied&invoice={iid}").json()
    assert scoped["total_count"] == 2

    block = as_rian.get(f"/api/invoices/{iid}/paste-block").json()
    assert block["line_count"] == 2
    assert all(len(line.split("\t")) == 7 for line in block["text"].split("\n"))

    # guarded delete refuses while lines exist (ADR #007)
    refused = as_rian.delete(f"/api/invoices/{iid}")
    assert refused.status_code == 409
    assert refused.json()["error_code"] == "INVOICE_HAS_LINES"

    detached = as_rian.post(
        "/api/entries/bulk/detach",
        json={"expected_count": 2, "ids": [TE[2], TE[3]], "invoice_id": iid},
    )
    assert detached.status_code == 200
    assert detached.json()["detached"] == 2
    assert detached.json()["invoice_id"] == iid

    assert as_rian.delete(f"/api/invoices/{iid}").status_code == 200
    # fixture restored: the rows are pending again
    pending = as_rian.get("/api/entries?status=pending").json()
    assert {TE[2], TE[3]} <= {r["id"] for r in pending["rows"]}


def test_apply_expected_count_safety_stop(as_rian):
    created = as_rian.post("/api/invoices", json={"name": "API safety stop"})
    iid = created.json()["row"]["id"]
    try:
        stopped = as_rian.post(
            "/api/entries/bulk/apply-to-invoice",
            json={"expected_count": 5, "ids": [TE[2]], "invoice_id": iid},
        )
        assert stopped.status_code == 409
        assert stopped.json()["error_code"] == "SAFETY_STOP"
        assert stopped.json()["summary"].startswith("Safety stop:")
    finally:
        as_rian.delete(f"/api/invoices/{iid}")


def test_apply_to_paid_invoice_refused_at_api(as_rian):
    iid = _invoice_id(as_rian, "PlusROI 2026-05")  # paid fixture invoice
    refused = as_rian.post(
        "/api/entries/bulk/apply-to-invoice",
        json={"expected_count": 1, "ids": [TE[2]], "invoice_id": iid},
    )
    assert refused.status_code == 409
    assert refused.json()["error_code"] == "INVOICE_PAID"


def test_detach_is_explicit_selection_only(as_rian):
    """T-INV-015: the endpoint refuses all_matching outright — there is
    no filter-driven detach path."""
    refused = as_rian.post(
        "/api/entries/bulk/detach",
        json={"expected_count": 1, "ids": [], "all_matching": True},
    )
    assert refused.status_code == 422
    assert refused.json()["error_code"] == "DETACH_EXPLICIT_ONLY"
    assert refused.json()["summary"] == "Detach only works on an explicit selection."

    empty = as_rian.post(
        "/api/entries/bulk/detach", json={"expected_count": 0, "ids": []}
    )
    assert empty.status_code == 422
    assert empty.json()["error_code"] == "NO_SELECTION"


def test_detach_skips_rows_of_other_invoices(as_rian):
    """T-INV-016 at the API: already-detached / other-invoice rows are
    counted no-ops, and the locked fixture row stays locked."""
    result = as_rian.post(
        "/api/entries/bulk/detach",
        json={
            "expected_count": 2,
            "ids": [TE[1], TE[2]],  # te-1 locked on PlusROI 2026-05, te-2 pending
            "invoice_id": _invoice_id(as_rian, "March work"),  # neither is on it
        },
    )
    assert result.status_code == 409
    assert result.json()["error_code"] == "NOTHING_TO_DETACH"
    row = as_rian.get("/api/entries?status=applied&q=March work A").json()
    assert any(r["id"] == TE[1] and r["locked"] for r in row["rows"])


def test_invoice_not_found_is_structured(as_rian):
    ghost = uuid.uuid4()
    missing = as_rian.get(f"/api/invoices/{ghost}")
    assert missing.status_code == 404
    assert missing.json()["error_code"] == "INVOICE_NOT_FOUND"
    assert missing.json()["summary"] == "Invoice not found."
