"""
Integration and unit tests for Bill Creation & Balance Ledger (Step 4.2).
"""

import asyncio
from decimal import Decimal

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

from app.core.security import create_access_token, hash_password
from app.models.billing import Bill, DiscountRule, DiscountType
from app.models.lab import Lab
from app.models.order import OrderPackage, OrderStatus, OrderTest, VisitOrder
from app.models.package import TestPackage as LabPackage
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 (
    TestResultType as LabResultType,
)
from app.models.user import User, UserRole
from app.repositories.permission_repository import PermissionRepository


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


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


@pytest_asyncio.fixture
async def secondary_lab(db_session: AsyncSession) -> Lab:
    """Create secondary branch lab for tenant isolation testing."""
    lab = Lab(name="Secondary Billing Lab", address="Pokhara")
    db_session.add(lab)
    await db_session.commit()
    await db_session.refresh(lab)
    return lab


@pytest_asyncio.fixture
async def accountant_token(db_session: AsyncSession, bill_lab: Lab) -> str:
    """Create accountant user token with BILL_READ, BILL_WRITE, DISCOUNT_READ."""
    accountant = User(
        lab_id=bill_lab.id,
        name="Billing Accountant",
        phone="9818000001",
        role=UserRole.ACCOUNTANT,
        hashed_password=hash_password("AccountantPass123!"),
        is_active=True,
    )
    db_session.add(accountant)
    await db_session.commit()
    await db_session.refresh(accountant)
    return create_access_token(
        subject=str(accountant.id),
        extra_claims={
            "phone": accountant.phone,
            "role": accountant.role.value,
            "lab_id": str(bill_lab.id),
        },
    )


@pytest_asyncio.fixture
async def secondary_accountant_token(db_session: AsyncSession, secondary_lab: Lab) -> str:
    """Create accountant user token in secondary branch."""
    accountant = User(
        lab_id=secondary_lab.id,
        name="Pokhara Accountant",
        phone="9818000002",
        role=UserRole.ACCOUNTANT,
        hashed_password=hash_password("AccountantPass123!"),
        is_active=True,
    )
    db_session.add(accountant)
    await db_session.commit()
    await db_session.refresh(accountant)
    return create_access_token(
        subject=str(accountant.id),
        extra_claims={
            "phone": accountant.phone,
            "role": accountant.role.value,
            "lab_id": str(secondary_lab.id),
        },
    )


@pytest_asyncio.fixture
async def general_user_token(db_session: AsyncSession, bill_lab: Lab) -> str:
    """Create unprivileged general user token."""
    user = User(
        lab_id=bill_lab.id,
        name="General User",
        phone="9818000003",
        role=UserRole.GENERAL_USER,
        hashed_password=hash_password("UserPass123!"),
        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(bill_lab.id),
        },
    )


@pytest_asyncio.fixture
async def bill_patient(db_session: AsyncSession, bill_lab: Lab) -> Patient:
    """Create test patient in primary lab."""
    patient = Patient(
        lab_id=bill_lab.id,
        patient_code="P/26/0000000999",
        name="Billing Test Patient",
        gender=Gender.MALE,
        phone="9818000999",
    )
    db_session.add(patient)
    await db_session.commit()
    await db_session.refresh(patient)
    return patient


@pytest_asyncio.fixture
async def test_order(db_session: AsyncSession, bill_lab: Lab, bill_patient: Patient) -> VisitOrder:
    """Create test visit order with 2 tests (1200 + 800) and 1 package (2500). Total subtotal = 4500.00."""
    category = LabCategory(lab_id=bill_lab.id, name="Biochemistry")
    db_session.add(category)
    await db_session.flush()

    t1 = LabTest(
        lab_id=bill_lab.id,
        category_id=category.id,
        name="Lipid Profile",
        code="LP01",
        result_type=LabResultType.NUMERIC,
        price=Decimal("1200.00"),
    )
    t2 = LabTest(
        lab_id=bill_lab.id,
        category_id=category.id,
        name="Blood Glucose",
        code="GLU01",
        result_type=LabResultType.NUMERIC,
        price=Decimal("800.00"),
    )
    pkg = LabPackage(
        lab_id=bill_lab.id,
        name="Executive Health Package",
        code="EHP01",
        price=Decimal("2500.00"),
    )
    db_session.add_all([t1, t2, pkg])
    await db_session.flush()

    order = VisitOrder(
        lab_id=bill_lab.id,
        patient_id=bill_patient.id,
        order_number="ORD/26/000099",
        status=OrderStatus.PENDING,
    )
    db_session.add(order)
    await db_session.flush()

    ot1 = OrderTest(order_id=order.id, test_id=t1.id, price_at_order=Decimal("1200.00"))
    ot2 = OrderTest(order_id=order.id, test_id=t2.id, price_at_order=Decimal("800.00"))
    op1 = OrderPackage(order_id=order.id, package_id=pkg.id, price_at_order=Decimal("2500.00"))
    db_session.add_all([ot1, ot2, op1])
    await db_session.commit()
    await db_session.refresh(order)
    return order


@pytest_asyncio.fixture
async def percent_discount(db_session: AsyncSession, bill_lab: Lab) -> DiscountRule:
    """Create 10% discount rule."""
    rule = DiscountRule(
        lab_id=bill_lab.id,
        name="Senior Citizen 10%",
        discount_type=DiscountType.PERCENTAGE,
        discount_value=Decimal("10.00"),
        is_active=True,
    )
    db_session.add(rule)
    await db_session.commit()
    await db_session.refresh(rule)
    return rule


@pytest_asyncio.fixture
async def fixed_discount(db_session: AsyncSession, bill_lab: Lab) -> DiscountRule:
    """Create 500 NPR fixed discount rule."""
    rule = DiscountRule(
        lab_id=bill_lab.id,
        name="Festival Voucher 500",
        discount_type=DiscountType.FIXED,
        discount_value=Decimal("500.00"),
        is_active=True,
    )
    db_session.add(rule)
    await db_session.commit()
    await db_session.refresh(rule)
    return rule


# ── Tests ──────────────────────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_create_bill_success_from_order(
    client: AsyncClient,
    accountant_token: str,
    test_order: VisitOrder,
) -> None:
    """Bill creation correctly aggregates test and package prices into subtotal."""
    payload = {
        "order_id": str(test_order.id),
        "notes": "Walk-in registration billing",
    }
    resp = await client.post(
        "/api/v1/bills",
        json=payload,
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp.status_code == 201
    body = resp.json()
    assert body["success"] is True
    data = body["data"]
    assert data["order_id"] == str(test_order.id)
    assert data["bill_number"].startswith("INV")
    assert "/" not in data["bill_number"]
    assert Decimal(str(data["subtotal"])) == Decimal("4500.00")
    assert Decimal(str(data["discount_amount"])) == Decimal("0.00")
    assert Decimal(str(data["tax_amount"])) == Decimal("0.00")
    assert Decimal(str(data["total_amount"])) == Decimal("4500.00")
    assert Decimal(str(data["paid_amount"])) == Decimal("0.00")
    assert Decimal(str(data["due_amount"])) == Decimal("4500.00")
    assert data["status"] == "unpaid"
    assert data["notes"] == "Walk-in registration billing"


@pytest.mark.asyncio
async def test_bill_number_sequential_format(
    client: AsyncClient,
    accountant_token: str,
    db_session: AsyncSession,
    bill_lab: Lab,
    bill_patient: Patient,
) -> None:
    """Sequential bill numbers increment per branch in INV{YY}{6-digit} format (no separators)."""
    # Create two separate orders
    order1 = VisitOrder(lab_id=bill_lab.id, patient_id=bill_patient.id, order_number="ORD/26/000101")
    order2 = VisitOrder(lab_id=bill_lab.id, patient_id=bill_patient.id, order_number="ORD/26/000102")
    db_session.add_all([order1, order2])
    await db_session.commit()

    resp1 = await client.post(
        "/api/v1/bills",
        json={"order_id": str(order1.id)},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp1.status_code == 201
    num1 = resp1.json()["data"]["bill_number"]

    resp2 = await client.post(
        "/api/v1/bills",
        json={"order_id": str(order2.id)},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp2.status_code == 201
    num2 = resp2.json()["data"]["bill_number"]

    assert num1.startswith("INV")
    assert num2.startswith("INV")
    if "/" in num1:
        parts1 = num1.split("/")
        parts2 = num2.split("/")
        assert int(parts2[2]) == int(parts1[2]) + 1
    else:
        seq1 = int(num1[5:])
        seq2 = int(num2[5:])
        assert seq2 == seq1 + 1


@pytest.mark.asyncio
async def test_concurrent_invoice_numbering(
    client: AsyncClient,
    accountant_token: str,
    db_session: AsyncSession,
    bill_lab: Lab,
    bill_patient: Patient,
) -> None:
    """Concurrent bill creation requests generate strictly unique invoice numbers without collisions."""
    orders = []
    for i in range(5):
        order = VisitOrder(
            lab_id=bill_lab.id,
            patient_id=bill_patient.id,
            order_number=f"ORD/26/CONC{i:04d}",
        )
        db_session.add(order)
        orders.append(order)
    await db_session.commit()

    async def create_single_bill(order_id: str) -> str:
        resp = await client.post(
            "/api/v1/bills",
            json={"order_id": order_id},
            headers={"Authorization": f"Bearer {accountant_token}"},
        )
        assert resp.status_code == 201
        return resp.json()["data"]["bill_number"]

    results = await asyncio.gather(*(create_single_bill(str(o.id)) for o in orders))
    assert len(results) == 5
    assert len(set(results)) == 5, f"Invoice number collision detected: {results}"


@pytest.mark.asyncio
async def test_create_bill_with_percentage_discount(
    client: AsyncClient,
    accountant_token: str,
    test_order: VisitOrder,
    percent_discount: DiscountRule,
) -> None:
    """10% discount on 4500.00 subtotal yields 450.00 discount and 4050.00 total."""
    payload = {
        "order_id": str(test_order.id),
        "discount_rule_id": str(percent_discount.id),
        "tax_amount": "50.00",
    }
    resp = await client.post(
        "/api/v1/bills",
        json=payload,
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp.status_code == 201
    data = resp.json()["data"]
    assert Decimal(str(data["subtotal"])) == Decimal("4500.00")
    assert Decimal(str(data["discount_amount"])) == Decimal("450.00")
    assert Decimal(str(data["tax_amount"])) == Decimal("50.00")
    assert Decimal(str(data["total_amount"])) == Decimal("4100.00")
    assert Decimal(str(data["due_amount"])) == Decimal("4100.00")
    assert data["discount_rule_name"] == "Senior Citizen 10%"


@pytest.mark.asyncio
async def test_create_bill_with_fixed_discount(
    client: AsyncClient,
    accountant_token: str,
    test_order: VisitOrder,
    fixed_discount: DiscountRule,
) -> None:
    """500.00 fixed discount on 4500.00 subtotal yields 4000.00 total."""
    payload = {
        "order_id": str(test_order.id),
        "discount_rule_id": str(fixed_discount.id),
    }
    resp = await client.post(
        "/api/v1/bills",
        json=payload,
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp.status_code == 201
    data = resp.json()["data"]
    assert Decimal(str(data["subtotal"])) == Decimal("4500.00")
    assert Decimal(str(data["discount_amount"])) == Decimal("500.00")
    assert Decimal(str(data["total_amount"])) == Decimal("4000.00")


@pytest.mark.asyncio
async def test_create_bill_with_manual_discount(
    client: AsyncClient,
    accountant_token: str,
    test_order: VisitOrder,
) -> None:
    """Manual discount is applied correctly when no discount rule is provided."""
    payload = {
        "order_id": str(test_order.id),
        "manual_discount_amount": "300.00",
    }
    resp = await client.post(
        "/api/v1/bills",
        json=payload,
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp.status_code == 201
    data = resp.json()["data"]
    assert Decimal(str(data["discount_amount"])) == Decimal("300.00")
    assert Decimal(str(data["total_amount"])) == Decimal("4200.00")


@pytest.mark.asyncio
async def test_discount_exclusivity_validation(
    client: AsyncClient,
    accountant_token: str,
    test_order: VisitOrder,
    percent_discount: DiscountRule,
) -> None:
    """Providing both discount_rule_id and manual_discount_amount fails validation."""
    payload = {
        "order_id": str(test_order.id),
        "discount_rule_id": str(percent_discount.id),
        "manual_discount_amount": "200.00",
    }
    resp = await client.post(
        "/api/v1/bills",
        json=payload,
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_discount_cannot_exceed_subtotal(
    client: AsyncClient,
    accountant_token: str,
    test_order: VisitOrder,
) -> None:
    """Manual discount larger than subtotal is clamped to subtotal (total amount >= 0.00)."""
    payload = {
        "order_id": str(test_order.id),
        "manual_discount_amount": "99999.00",
    }
    resp = await client.post(
        "/api/v1/bills",
        json=payload,
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp.status_code == 201
    data = resp.json()["data"]
    assert Decimal(str(data["discount_amount"])) == Decimal("4500.00")
    assert Decimal(str(data["total_amount"])) == Decimal("0.00")
    assert Decimal(str(data["due_amount"])) == Decimal("0.00")
    assert data["status"] == "paid"


@pytest.mark.asyncio
async def test_zero_total_bill_auto_marked_paid(
    client: AsyncClient,
    accountant_token: str,
    db_session: AsyncSession,
    bill_lab: Lab,
    bill_patient: Patient,
) -> None:
    """Order with 0.00 total is automatically created with PAID status."""
    free_order = VisitOrder(lab_id=bill_lab.id, patient_id=bill_patient.id, order_number="ORD/26/000FREE")
    db_session.add(free_order)
    await db_session.commit()

    resp = await client.post(
        "/api/v1/bills",
        json={"order_id": str(free_order.id)},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp.status_code == 201
    data = resp.json()["data"]
    assert Decimal(str(data["total_amount"])) == Decimal("0.00")
    assert data["status"] == "paid"


@pytest.mark.asyncio
async def test_reject_duplicate_bill_for_same_order(
    client: AsyncClient,
    accountant_token: str,
    test_order: VisitOrder,
) -> None:
    """A second bill for the same order is rejected with 409 Conflict."""
    payload = {"order_id": str(test_order.id)}
    resp1 = await client.post(
        "/api/v1/bills",
        json=payload,
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp1.status_code == 201

    resp2 = await client.post(
        "/api/v1/bills",
        json=payload,
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp2.status_code == 409


@pytest.mark.asyncio
async def test_get_bill_by_id_and_by_order(
    client: AsyncClient,
    accountant_token: str,
    test_order: VisitOrder,
) -> None:
    """Retrieve bill by ID and by order ID."""
    create_resp = await client.post(
        "/api/v1/bills",
        json={"order_id": str(test_order.id)},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    bill_id = create_resp.json()["data"]["id"]

    # Get by bill_id
    get_resp = await client.get(
        f"/api/v1/bills/{bill_id}",
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert get_resp.status_code == 200
    assert get_resp.json()["data"]["id"] == bill_id
    assert get_resp.json()["data"]["order_number"] == test_order.order_number

    # Get by order_id
    order_resp = await client.get(
        f"/api/v1/bills/order/{test_order.id}",
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert order_resp.status_code == 200
    assert order_resp.json()["data"]["id"] == bill_id


@pytest.mark.asyncio
async def test_update_bill_notes(
    client: AsyncClient,
    accountant_token: str,
    test_order: VisitOrder,
) -> None:
    """Update bill notes successfully."""
    create_resp = await client.post(
        "/api/v1/bills",
        json={"order_id": str(test_order.id)},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    bill_id = create_resp.json()["data"]["id"]

    patch_resp = await client.patch(
        f"/api/v1/bills/{bill_id}",
        json={"notes": "Updated cashier remarks"},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert patch_resp.status_code == 200
    assert patch_resp.json()["data"]["notes"] == "Updated cashier remarks"


@pytest.mark.asyncio
async def test_void_unpaid_bill_success(
    client: AsyncClient,
    accountant_token: str,
    test_order: VisitOrder,
) -> None:
    """Voiding an unpaid bill transitions status to VOID with audit log."""
    create_resp = await client.post(
        "/api/v1/bills",
        json={"order_id": str(test_order.id), "notes": "Initial notes"},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    bill_id = create_resp.json()["data"]["id"]

    void_resp = await client.post(
        f"/api/v1/bills/{bill_id}/void",
        json={"reason": "Customer changed mind before testing"},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert void_resp.status_code == 200
    data = void_resp.json()["data"]
    assert data["status"] == "void"
    assert "VOIDED" in data["notes"]
    assert "Customer changed mind before testing" in data["notes"]


@pytest.mark.asyncio
async def test_void_bill_with_payments_rejected(
    client: AsyncClient,
    accountant_token: str,
    db_session: AsyncSession,
    test_order: VisitOrder,
) -> None:
    """A bill with recorded payments cannot be voided directly."""
    create_resp = await client.post(
        "/api/v1/bills",
        json={"order_id": str(test_order.id)},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    bill_id = create_resp.json()["data"]["id"]

    # Artificially simulate a recorded payment on the bill
    from uuid import UUID

    bill = await db_session.get(Bill, UUID(bill_id))
    assert bill is not None
    bill.paid_amount = Decimal("500.00")
    await db_session.commit()

    void_resp = await client.post(
        f"/api/v1/bills/{bill_id}/void",
        json={"reason": "Trying to void paid bill"},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert void_resp.status_code == 422
    assert "payments" in void_resp.json()["error"]["message"].lower()


@pytest.mark.asyncio
async def test_cross_branch_bill_isolation(
    client: AsyncClient,
    accountant_token: str,
    secondary_accountant_token: str,
    test_order: VisitOrder,
) -> None:
    """Accountant from Branch B cannot access or void bill from Branch A."""
    create_resp = await client.post(
        "/api/v1/bills",
        json={"order_id": str(test_order.id)},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    bill_id = create_resp.json()["data"]["id"]

    # Secondary branch accountant tries to read
    get_resp = await client.get(
        f"/api/v1/bills/{bill_id}",
        headers={"Authorization": f"Bearer {secondary_accountant_token}"},
    )
    assert get_resp.status_code == 404

    # Secondary branch accountant tries to void
    void_resp = await client.post(
        f"/api/v1/bills/{bill_id}/void",
        json={"reason": "Hacking across branches"},
        headers={"Authorization": f"Bearer {secondary_accountant_token}"},
    )
    assert void_resp.status_code == 404


@pytest.mark.asyncio
async def test_bill_permissions_enforced(
    client: AsyncClient,
    general_user_token: str,
    test_order: VisitOrder,
) -> None:
    """General user without BILL_READ/BILL_WRITE is denied with 403."""
    # Try create
    post_resp = await client.post(
        "/api/v1/bills",
        json={"order_id": str(test_order.id)},
        headers={"Authorization": f"Bearer {general_user_token}"},
    )
    assert post_resp.status_code == 403

    # Try list
    get_resp = await client.get(
        "/api/v1/bills",
        headers={"Authorization": f"Bearer {general_user_token}"},
    )
    assert get_resp.status_code == 403


@pytest.mark.asyncio
async def test_list_bills_with_filters(
    client: AsyncClient,
    accountant_token: str,
    test_order: VisitOrder,
) -> None:
    """List bills with status filtering and pagination metadata."""
    await client.post(
        "/api/v1/bills",
        json={"order_id": str(test_order.id)},
        headers={"Authorization": f"Bearer {accountant_token}"},
    )

    resp = await client.get(
        "/api/v1/bills?status=unpaid&page=1&limit=10",
        headers={"Authorization": f"Bearer {accountant_token}"},
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["success"] is True
    assert len(body["data"]) >= 1
    assert body["meta"]["page"] == 1
    assert body["meta"]["total"] >= 1
