"""Tests for the read-only statement HTTP API.

These exercise the API layer (routing, status codes, JSON shape) against
fixtures built directly through the ORM -- they do not exercise intake or
resolution, which `tests/test_visible.py` already covers.
"""

from __future__ import annotations

from django.test import TestCase

from adjustments.models import Account, Invoice, LineItem


def _find(node: dict, role: str) -> dict | None:
    """Depth-first search for the first node of `role` in a serialised tree."""
    if node["role"] == role:
        return node
    for child in node["children"]:
        found = _find(child, role)
        if found is not None:
            return found
    return None


class StatementApiTests(TestCase):
    def setUp(self) -> None:
        # `amount_due` is seeded to match the invoice as currently corrected,
        # which is what docs/STATEMENT.md defines it as: "the invoice total as
        # currently corrected; a correction takes effect on the statement as
        # soon as it is received." An earlier fixture seeded a bare -10.00 with
        # no relationship to the invoice, which quietly made this suite assert
        # that the screen should show `Account.amount_due` whatever it happens
        # to hold -- so a submission that fixed the statement to follow the
        # contract failed a VISIBLE test for doing so. A visible test that
        # punishes contract-correct work is worse than no visible test.
        self.account = Account.objects.create(
            account_id="acct-api-1", amount_due=24.00
        )
        self.invoice = Invoice.objects.create(
            invoice_id="INV-acct-api-1-2026-06",
            account=self.account,
            period_key="2026-06",
            total=24.00,
            issued_total=24.00,
            issued=True,
        )
        LineItem.objects.create(
            invoice=self.invoice,
            usage_key="base-plan",
            description="Base plan",
            quantity=1,
            base=24.00,
            net=24.00,
            tax=0.00,
            total=24.00,
        )

    def test_statement_tree_serialises(self) -> None:
        response = self.client.get(
            f"/api/accounts/{self.account.account_id}/statement/2026-06"
        )
        self.assertEqual(response.status_code, 200)
        data = response.json()

        self.assertEqual(data["role"], "statement")
        self.assertEqual(data["label"], f"Statement {self.invoice.invoice_id}")
        self.assertIsInstance(data["children"], list)

        header = _find(data, "header")
        self.assertIsNotNone(header)
        self.assertEqual(header["label"], "Billing period 2026-06")

        line = _find(data, "line")
        self.assertIsNotNone(line)
        self.assertEqual(line["label"], "Base plan (1.0)")
        self.assertEqual(line["value"], "24.00")

    def test_unknown_viewport_is_400(self) -> None:
        response = self.client.get(
            f"/api/accounts/{self.account.account_id}/statement/2026-06"
            "?viewport=desktop"
        )
        self.assertEqual(response.status_code, 400)

    def test_unknown_account_or_period_is_404(self) -> None:
        missing_account = self.client.get(
            "/api/accounts/does-not-exist/statement/2026-06"
        )
        self.assertEqual(missing_account.status_code, 404)

        missing_period = self.client.get(
            f"/api/accounts/{self.account.account_id}/statement/2099-01"
        )
        self.assertEqual(missing_period.status_code, 404)

    def test_money_is_a_decimal_string_not_a_float(self) -> None:
        response = self.client.get(
            f"/api/accounts/{self.account.account_id}/statement/2026-06"
        )
        data = response.json()

        amount_due = _find(data, "amount_due")
        self.assertIsNotNone(amount_due)
        self.assertIsInstance(amount_due["value"], str)
        # The SHAPE is what this test is about, not the amount. Asserting a
        # specific figure here pinned the suite to whatever the fixture happened
        # to hold, so changing the statement to follow docs/STATEMENT.md's
        # definition of `amount_due` broke a visible test -- which is backwards.
        self.assertRegex(amount_due["value"], r"^-?\d+\.\d{2}$")

        # Also check the wire bytes directly, independent of how .json()
        # happens to parse them: a quoted string and a bare number are
        # unambiguous in the raw JSON text.
        raw_body = response.content.decode()
        self.assertIn(f'"value": "{amount_due["value"]}"', raw_body)
        self.assertNotIn('"value": -10.0', raw_body)

    def test_periods_lists_newest_first(self) -> None:
        Invoice.objects.create(
            invoice_id="INV-acct-api-1-2026-05",
            account=self.account,
            period_key="2026-05",
            total=24.00,
            issued_total=24.00,
            issued=True,
        )
        # A revision row for 2026-06 must not create a duplicate period key.
        Invoice.objects.create(
            invoice_id="INV-acct-api-1-2026-06-rev1",
            account=self.account,
            period_key="2026-06",
            total=24.00,
            revision_of=self.invoice.invoice_id,
        )

        response = self.client.get(
            f"/api/accounts/{self.account.account_id}/periods"
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), ["2026-06", "2026-05"])
