"""
Regression tests for paisa-accurate billing (no stray .50 dues).

Covers: HALF_UP quantization, repeating-decimal discounts, the exact reported
case (2200 − 125.50 → pay in full → PAID with 0.00 due), float-artifact tender
amounts, overpay guards, and the paid-bill void block.
"""

from decimal import Decimal
from uuid import uuid4

import pytest
import pytest_asyncio
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.money import CENT, npr
from app.core.security import create_access_token, hash_password
from app.models.billing import DiscountRule, DiscountType
from app.models.lab import Lab
from app.models.order import OrderStatus, OrderTest, VisitOrder
from app.models.patient import Gender, Patient
from app.models.test import (
    Test as LabTest,
)
from app.models.test import (
    TestCategory as LabCategory,
)
from app.models.test import (
    TestParameter as LabParameter,
)
from app.models.test import (
    TestResultType as LabResultType,
)
from app.models.user import User, UserRole
from app.repositories.permission_repository import PermissionRepository
from app.services.discount_service import DiscountService


@pytest_asyncio.fixture(autouse=True)
async def seed_perms(db_session: AsyncSession) -> None:
    """Seed permissions for RBAC checks."""
    repo = PermissionRepository()
    await repo.seed_permissions(db_session)


@pytest_asyncio.fixture
async def money_lab(db_session: AsyncSession) -> Lab:
    """Create a primary test lab."""
    lab = Lab(name="Money Lab", address="Kathmandu")
    db_session.add(lab)
    await db_session.commit()
    await db_session.refresh(lab)
    return lab


@pytest_asyncio.fixture
async def money_token(db_session: AsyncSession, money_lab: Lab) -> str:
    """Create a reception cashier token with order and billing permissions."""
    user = User(
        lab_id=money_lab.id,
        name="Money Cashier",
        phone="9819000001",
        role=UserRole.RECEPTIONIST,
        hashed_password=hash_password("CashierPass123!"),
        is_active=True,
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)
    return create_access_token(
        subject=str(user.id),
        extra_claims={"phone": user.phone, "role": user.role.value, "lab_id": str(money_lab.id)},
    )


@pytest_asyncio.fixture
async def money_patient(db_session: AsyncSession, money_lab: Lab) -> Patient:
    """Create a test patient."""
    patient = Patient(
        lab_id=money_lab.id,
        patient_code=f"P26-{uuid4().hex[:6].upper()}",
        name="Paisa Patient",
        gender=Gender.MALE,
        phone="9819000002",
    )
    db_session.add(patient)
    await db_session.commit()
    await db_session.refresh(patient)
    return patient


async def _make_order(
    db_session: AsyncSession, lab: Lab, patient: Patient, price: Decimal, tag: str
) -> VisitOrder:
    cat = LabCategory(lab_id=lab.id, name=f"Money Cat {tag}")
    db_session.add(cat)
    await db_session.flush()
    test = LabTest(
        lab_id=lab.id,
        category_id=cat.id,
        name=f"Money Panel {tag}",
        code=f"MNY{tag}",
        result_type=LabResultType.NUMERIC,
        price=price,
    )
    db_session.add(test)
    await db_session.flush()
    order = VisitOrder(
        lab_id=lab.id, patient_id=patient.id, order_number=f"ORD26{tag}", status=OrderStatus.PENDING
    )
    db_session.add(order)
    await db_session.flush()
    db_session.add(OrderTest(order_id=order.id, test_id=test.id, price_at_order=price))
    await db_session.commit()
    await db_session.refresh(order)
    return order


def _headers(token: str) -> dict[str, str]:
    return {"Authorization": f"Bearer {token}"}


@pytest.mark.asyncio
class TestMoneyRounding:
    """Paisa quantization across discounts, bills, and payments."""

    async def test_npr_quantizes_half_up(self) -> None:
        assert npr(Decimal("10.005")) == Decimal("10.01")
        assert npr(Decimal("2074.5500000000002")) == Decimal("2074.55")
        assert npr(Decimal("0.004")) == CENT

    async def test_percentage_discount_repeating_decimal(
        self, db_session: AsyncSession, money_lab: Lab
    ) -> None:
        rule = DiscountRule(
            lab_id=money_lab.id,
            name="Third Off",
            discount_type=DiscountType.PERCENTAGE,
            discount_value=Decimal("33.333"),
            is_active=True,
        )
        assert DiscountService().calculate_discount(Decimal("100.00"), rule) == Decimal("33.33")

    async def test_reported_case_full_payment_settles_to_zero(
        self,
        client: AsyncClient,
        money_token: str,
        money_lab: Lab,
        money_patient: Patient,
        db_session: AsyncSession,
    ) -> None:
        """2200 − 125.50 fixed discount → pay 2074.50 → PAID, due exactly 0.00."""
        order = await _make_order(db_session, money_lab, money_patient, Decimal("2200.00"), "0001")
        bill_res = await client.post(
            "/api/v1/bills",
            json={"order_id": str(order.id), "manual_discount_amount": "125.50"},
            headers=_headers(money_token),
        )
        assert bill_res.status_code == 201
        bill = bill_res.json()["data"]
        assert Decimal(str(bill["discount_amount"])) == Decimal("125.50")
        assert Decimal(str(bill["total_amount"])) == Decimal("2074.50")
        assert Decimal(str(bill["due_amount"])) == Decimal("2074.50")

        pay_res = await client.post(
            f"/api/v1/bills/{bill['id']}/payments",
            json={"amount": "2074.50", "payment_mode": "cash"},
            headers=_headers(money_token),
        )
        assert pay_res.status_code == 201

        get_res = await client.get(f"/api/v1/bills/{bill['id']}", headers=_headers(money_token))
        assert get_res.status_code == 200
        settled = get_res.json()["data"]
        assert settled["status"] == "paid"
        assert Decimal(str(settled["due_amount"])) == Decimal("0.00")

    async def test_float_artifact_tender_still_settles(
        self,
        client: AsyncClient,
        money_token: str,
        money_lab: Lab,
        money_patient: Patient,
        db_session: AsyncSession,
    ) -> None:
        """A binary float artifact amount quantizes to paisa instead of lingering due."""
        order = await _make_order(db_session, money_lab, money_patient, Decimal("2074.55"), "0002")
        bill_res = await client.post(
            "/api/v1/bills", json={"order_id": str(order.id)}, headers=_headers(money_token)
        )
        assert bill_res.status_code == 201
        bill = bill_res.json()["data"]

        pay_res = await client.post(
            f"/api/v1/bills/{bill['id']}/payments",
            json={"amount": "2074.5500000000002", "payment_mode": "cash"},
            headers=_headers(money_token),
        )
        assert pay_res.status_code == 201
        get_res = await client.get(f"/api/v1/bills/{bill['id']}", headers=_headers(money_token))
        assert get_res.json()["data"]["status"] == "paid"
        assert Decimal(str(get_res.json()["data"]["due_amount"])) == Decimal("0.00")

    async def test_overpay_still_rejected(
        self,
        client: AsyncClient,
        money_token: str,
        money_lab: Lab,
        money_patient: Patient,
        db_session: AsyncSession,
    ) -> None:
        order = await _make_order(db_session, money_lab, money_patient, Decimal("500.00"), "0003")
        bill_res = await client.post(
            "/api/v1/bills", json={"order_id": str(order.id)}, headers=_headers(money_token)
        )
        bill = bill_res.json()["data"]
        pay_res = await client.post(
            f"/api/v1/bills/{bill['id']}/payments",
            json={"amount": "500.01", "payment_mode": "cash"},
            headers=_headers(money_token),
        )
        assert pay_res.status_code == 422

    async def test_paid_bill_cannot_be_voided_unpaid_can(
        self,
        client: AsyncClient,
        money_token: str,
        money_lab: Lab,
        money_patient: Patient,
        db_session: AsyncSession,
    ) -> None:
        order = await _make_order(db_session, money_lab, money_patient, Decimal("500.00"), "0004")
        bill_res = await client.post(
            "/api/v1/bills", json={"order_id": str(order.id)}, headers=_headers(money_token)
        )
        bill = bill_res.json()["data"]
        pay_res = await client.post(
            f"/api/v1/bills/{bill['id']}/payments",
            json={"amount": "500.00", "payment_mode": "cash"},
            headers=_headers(money_token),
        )
        assert pay_res.status_code == 201

        void_paid = await client.post(
            f"/api/v1/bills/{bill['id']}/void", json={"reason": "oops"}, headers=_headers(money_token)
        )
        assert void_paid.status_code == 422

        order2 = await _make_order(db_session, money_lab, money_patient, Decimal("300.00"), "0005")
        bill2_res = await client.post(
            "/api/v1/bills", json={"order_id": str(order2.id)}, headers=_headers(money_token)
        )
        bill2 = bill2_res.json()["data"]
        void_unpaid = await client.post(
            f"/api/v1/bills/{bill2['id']}/void",
            json={"reason": "duplicate invoice"},
            headers=_headers(money_token),
        )
        assert void_unpaid.status_code == 200
        assert void_unpaid.json()["data"]["status"] == "void"


@pytest.mark.asyncio
class TestScopedAnalytePricing:
    """Partial-panel orders bill the picked analytes, never the whole panel."""

    async def test_partial_analytes_bill_sum_not_panel(
        self,
        client: AsyncClient,
        money_token: str,
        money_lab: Lab,
        money_patient: Patient,
        db_session: AsyncSession,
    ) -> None:
        cat = LabCategory(lab_id=money_lab.id, name="Scope Category")
        db_session.add(cat)
        await db_session.flush()
        panel = LabTest(
            lab_id=money_lab.id,
            category_id=cat.id,
            name="Scope CBC",
            code="SCBC",
            result_type=LabResultType.NUMERIC,
            price=Decimal("450.00"),
        )
        db_session.add(panel)
        await db_session.flush()
        params = [
            LabParameter(test_id=panel.id, name="Hemoglobin", price=Decimal("30.00")),
            LabParameter(test_id=panel.id, name="Total RBC", price=Decimal("30.00")),
            LabParameter(test_id=panel.id, name="Platelets", price=Decimal("390.00")),
        ]
        db_session.add_all(params)
        await db_session.commit()
        for p in params:
            await db_session.refresh(p)

        order_res = await client.post(
            "/api/v1/orders",
            json={
                "lab_id": str(money_lab.id),
                "patient_id": str(money_patient.id),
                "test_ids": [str(panel.id)],
                "test_parameters": [
                    {"test_id": str(panel.id), "parameter_ids": [str(params[0].id), str(params[1].id)]}
                ],
            },
            headers=_headers(money_token),
        )
        assert order_res.status_code == 201
        order = order_res.json()["data"]
        assert Decimal(str(order["total_amount"])) == Decimal("60.00")

        bill_res = await client.post(
            "/api/v1/bills", json={"order_id": order["id"]}, headers=_headers(money_token)
        )
        assert bill_res.status_code == 201
        bill = bill_res.json()["data"]
        assert Decimal(str(bill["total_amount"])) == Decimal("60.00")

        pay_res = await client.post(
            f"/api/v1/bills/{bill['id']}/payments",
            json={"amount": "60.00", "payment_mode": "cash"},
            headers=_headers(money_token),
        )
        assert pay_res.status_code == 201
        get_res = await client.get(f"/api/v1/bills/{bill['id']}", headers=_headers(money_token))
        assert get_res.json()["data"]["status"] == "paid"

    async def test_whole_panel_keeps_panel_price(
        self,
        client: AsyncClient,
        money_token: str,
        money_lab: Lab,
        money_patient: Patient,
        db_session: AsyncSession,
    ) -> None:
        cat = LabCategory(lab_id=money_lab.id, name="Whole Category")
        db_session.add(cat)
        await db_session.flush()
        panel = LabTest(
            lab_id=money_lab.id,
            category_id=cat.id,
            name="Whole Panel",
            code="WPAN",
            result_type=LabResultType.NUMERIC,
            price=Decimal("450.00"),
        )
        db_session.add(panel)
        await db_session.commit()

        order_res = await client.post(
            "/api/v1/orders",
            json={
                "lab_id": str(money_lab.id),
                "patient_id": str(money_patient.id),
                "test_ids": [str(panel.id)],
            },
            headers=_headers(money_token),
        )
        assert order_res.status_code == 201
        assert Decimal(str(order_res.json()["data"]["total_amount"])) == Decimal("450.00")
