"""
Integration and unit tests for Multi-tender Payment Recording & Real-time Settlement (Step 4.3).
"""

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, BillStatus
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 (
    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 payment_lab(db_session: AsyncSession) -> Lab:
    """Create primary test branch lab."""
    lab = Lab(name="Main Payment 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 Payment Lab", address="Pokhara")
    db_session.add(lab)
    await db_session.commit()
    await db_session.refresh(lab)
    return lab


@pytest_asyncio.fixture
async def cashier_token(db_session: AsyncSession, payment_lab: Lab) -> str:
    """Create receptionist/cashier user token with PAYMENT_READ, PAYMENT_WRITE, BILL_READ, BILL_WRITE."""
    cashier = User(
        lab_id=payment_lab.id,
        name="Frontdesk Cashier",
        phone="9819000001",
        role=UserRole.RECEPTIONIST,
        hashed_password=hash_password("CashierPass123!"),
        is_active=True,
    )
    db_session.add(cashier)
    await db_session.commit()
    await db_session.refresh(cashier)
    return create_access_token(
        subject=str(cashier.id),
        extra_claims={
            "phone": cashier.phone,
            "role": cashier.role.value,
            "lab_id": str(payment_lab.id),
        },
    )


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


@pytest_asyncio.fixture
async def unauthorized_user_token(db_session: AsyncSession, payment_lab: Lab) -> str:
    """User without PAYMENT permissions (e.g. General User)."""
    user = User(
        lab_id=payment_lab.id,
        name="Unauthorized User",
        phone="9819000003",
        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(payment_lab.id),
        },
    )


@pytest_asyncio.fixture
async def test_order_with_bill(db_session: AsyncSession, payment_lab: Lab) -> tuple[VisitOrder, Bill]:
    """Create test patient, test, order, and an unpaid bill of Rs. 1500."""
    patient = Patient(
        lab_id=payment_lab.id,
        patient_code="PAT-PAY-001",
        name="John Doe",
        age=35,
        gender=Gender.MALE,
        phone="9841000001",
    )
    db_session.add(patient)

    cat = LabCategory(lab_id=payment_lab.id, name="Biochemistry")
    db_session.add(cat)
    await db_session.flush()

    test = LabTest(
        lab_id=payment_lab.id,
        category_id=cat.id,
        name="Lipid Profile",
        code="LP-PAY",
        price=Decimal("1500.00"),
        result_type=LabResultType.NUMERIC,
    )
    db_session.add(test)
    await db_session.flush()

    order = VisitOrder(
        lab_id=payment_lab.id,
        patient_id=patient.id,
        order_number="ORD/26/009001",
        status=OrderStatus.CONFIRMED,
    )
    db_session.add(order)
    await db_session.flush()

    ot = OrderTest(
        order_id=order.id,
        test_id=test.id,
        price_at_order=Decimal("1500.00"),
    )
    db_session.add(ot)

    bill = Bill(
        lab_id=payment_lab.id,
        order_id=order.id,
        patient_id=patient.id,
        bill_number="INV/26/009001",
        status=BillStatus.UNPAID,
        subtotal=Decimal("1500.00"),
        discount_amount=Decimal("0.00"),
        tax_amount=Decimal("0.00"),
        total_amount=Decimal("1500.00"),
        paid_amount=Decimal("0.00"),
        due_amount=Decimal("1500.00"),
    )
    db_session.add(bill)
    await db_session.commit()
    await db_session.refresh(order)
    await db_session.refresh(bill)
    return order, bill


@pytest.mark.asyncio
async def test_record_partial_payment(
    client: AsyncClient,
    cashier_token: str,
    test_order_with_bill: tuple[VisitOrder, Bill],
) -> None:
    """Record a partial payment (Rs 500 of 1500) and verify ledger updates."""
    _, bill = test_order_with_bill

    payload = {
        "amount": "500.00",
        "payment_mode": "cash",
        "notes": "Advance payment in cash",
    }
    response = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json=payload,
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert response.status_code == 201
    data = response.json()["data"]
    assert Decimal(str(data["amount"])) == Decimal("500.00")
    assert data["payment_mode"] == "cash"
    assert data["bill_id"] == str(bill.id)

    # Verify Bill detail has updated ledger
    bill_resp = await client.get(
        f"/api/v1/bills/{bill.id}",
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert bill_resp.status_code == 200
    bill_data = bill_resp.json()["data"]
    assert bill_data["status"] == "partial"
    assert Decimal(str(bill_data["paid_amount"])) == Decimal("500.00")
    assert Decimal(str(bill_data["due_amount"])) == Decimal("1000.00")
    assert len(bill_data["payments"]) == 1


@pytest.mark.asyncio
async def test_record_full_payment_settlement(
    client: AsyncClient,
    cashier_token: str,
    test_order_with_bill: tuple[VisitOrder, Bill],
) -> None:
    """Record exact full payment (Rs 1500 of 1500) and verify bill status becomes PAID."""
    _, bill = test_order_with_bill

    payload = {
        "amount": "1500.00",
        "payment_mode": "mobile_banking",
        "transaction_reference": "FP-12345678",
        "notes": "QR payment full settlement",
    }
    response = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json=payload,
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert response.status_code == 201

    # Verify Bill status is PAID and due_amount is 0.00
    bill_resp = await client.get(
        f"/api/v1/bills/{bill.id}",
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert bill_resp.status_code == 200
    bill_data = bill_resp.json()["data"]
    assert bill_data["status"] == "paid"
    assert Decimal(str(bill_data["paid_amount"])) == Decimal("1500.00")
    assert Decimal(str(bill_data["due_amount"])) == Decimal("0.00")


@pytest.mark.asyncio
async def test_split_tender_payments(
    client: AsyncClient,
    cashier_token: str,
    test_order_with_bill: tuple[VisitOrder, Bill],
) -> None:
    """Split payment: Rs 500 Cash + Rs 1000 Card settles the entire bill."""
    _, bill = test_order_with_bill

    # Payment 1: Rs. 500 Cash
    p1 = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json={"amount": "500.00", "payment_mode": "cash"},
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert p1.status_code == 201

    # Payment 2: Rs. 1000 Card
    p2 = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json={"amount": "1000.00", "payment_mode": "card", "transaction_reference": "POS-TXN-9988"},
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert p2.status_code == 201

    # Check list of payments for bill
    list_resp = await client.get(
        f"/api/v1/bills/{bill.id}/payments",
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert list_resp.status_code == 200
    payments = list_resp.json()["data"]
    assert len(payments) == 2
    assert payments[0]["payment_mode"] == "cash"
    assert payments[1]["payment_mode"] == "card"
    assert payments[1]["transaction_reference"] == "POS-TXN-9988"

    # Bill is now fully paid
    bill_resp = await client.get(
        f"/api/v1/bills/{bill.id}",
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    bill_data = bill_resp.json()["data"]
    assert bill_data["status"] == "paid"
    assert Decimal(str(bill_data["due_amount"])) == Decimal("0.00")


@pytest.mark.asyncio
async def test_overpayment_rejected(
    client: AsyncClient,
    cashier_token: str,
    test_order_with_bill: tuple[VisitOrder, Bill],
) -> None:
    """Attempting to pay more than due_amount must be rejected with 422."""
    _, bill = test_order_with_bill

    # Bill total is 1500, attempt to pay 1500.01
    payload = {"amount": "1500.01", "payment_mode": "cash"}
    response = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json=payload,
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert response.status_code == 422
    assert "exceeds outstanding" in response.text.lower()


@pytest.mark.asyncio
async def test_payment_on_already_paid_bill_rejected(
    client: AsyncClient,
    cashier_token: str,
    test_order_with_bill: tuple[VisitOrder, Bill],
) -> None:
    """Attempting to pay an already settled bill must be rejected."""
    _, bill = test_order_with_bill

    # Pay full
    p1 = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json={"amount": "1500.00", "payment_mode": "cash"},
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert p1.status_code == 201

    # Attempt second payment
    p2 = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json={"amount": "100.00", "payment_mode": "cash"},
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert p2.status_code == 422
    assert "already fully settled" in p2.text.lower()


@pytest.mark.asyncio
async def test_payment_on_void_bill_rejected(
    client: AsyncClient,
    cashier_token: str,
    test_order_with_bill: tuple[VisitOrder, Bill],
) -> None:
    """Attempting to record payment on a voided bill must fail."""
    _, bill = test_order_with_bill

    # Void the unpaid bill
    void_res = await client.post(
        f"/api/v1/bills/{bill.id}/void",
        json={"reason": "Customer cancelled order before tests"},
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert void_res.status_code == 200

    # Attempt to pay
    pay_res = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json={"amount": "500.00", "payment_mode": "cash"},
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert pay_res.status_code == 422
    assert "voided bill" in pay_res.text.lower()


@pytest.mark.asyncio
async def test_void_bill_with_payment_rejected(
    client: AsyncClient,
    cashier_token: str,
    test_order_with_bill: tuple[VisitOrder, Bill],
) -> None:
    """Cannot void a bill once any payment has been recorded."""
    _, bill = test_order_with_bill

    # Pay 100
    pay_res = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json={"amount": "100.00", "payment_mode": "cash"},
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert pay_res.status_code == 201

    # Attempt to void
    void_res = await client.post(
        f"/api/v1/bills/{bill.id}/void",
        json={"reason": "Void attempt on partially paid bill"},
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert void_res.status_code == 422
    assert "have been recorded" in void_res.text.lower()


@pytest.mark.asyncio
async def test_list_payments_and_filters(
    client: AsyncClient,
    cashier_token: str,
    test_order_with_bill: tuple[VisitOrder, Bill],
) -> None:
    """Test GET /payments listing, pagination, and filter by payment_mode."""
    _, bill = test_order_with_bill

    # Record 1 cash, 1 mobile_banking
    await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json={"amount": "300.00", "payment_mode": "cash"},
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json={"amount": "400.00", "payment_mode": "mobile_banking", "transaction_reference": "FP-999"},
        headers={"Authorization": f"Bearer {cashier_token}"},
    )

    # List all
    all_res = await client.get(
        "/api/v1/payments",
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert all_res.status_code == 200
    assert len(all_res.json()["data"]) == 2
    assert all_res.json()["meta"]["total"] == 2

    # Filter by mode=cash
    cash_res = await client.get(
        "/api/v1/payments?payment_mode=cash",
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert cash_res.status_code == 200
    assert len(cash_res.json()["data"]) == 1
    assert cash_res.json()["data"][0]["payment_mode"] == "cash"


@pytest.mark.asyncio
async def test_get_payment_by_id(
    client: AsyncClient,
    cashier_token: str,
    test_order_with_bill: tuple[VisitOrder, Bill],
) -> None:
    """Test retrieving a single payment transaction by ID."""
    _, bill = test_order_with_bill

    pay_res = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json={"amount": "750.00", "payment_mode": "mobile_banking", "transaction_reference": "ES-1234"},
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    payment_id = pay_res.json()["data"]["id"]

    get_res = await client.get(
        f"/api/v1/payments/{payment_id}",
        headers={"Authorization": f"Bearer {cashier_token}"},
    )
    assert get_res.status_code == 200
    pdata = get_res.json()["data"]
    assert pdata["id"] == payment_id
    assert pdata["payment_mode"] == "mobile_banking"
    assert pdata["transaction_reference"] == "ES-1234"
    assert Decimal(str(pdata["amount"])) == Decimal("750.00")
    assert pdata["received_by_name"] == "Frontdesk Cashier"


@pytest.mark.asyncio
async def test_cross_branch_payment_isolation(
    client: AsyncClient,
    secondary_cashier_token: str,
    test_order_with_bill: tuple[VisitOrder, Bill],
) -> None:
    """Cashier in Branch B cannot record payment on Branch A's bill."""
    _, bill = test_order_with_bill

    response = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json={"amount": "500.00", "payment_mode": "cash"},
        headers={"Authorization": f"Bearer {secondary_cashier_token}"},
    )
    assert response.status_code == 404


@pytest.mark.asyncio
async def test_rbac_payment_read_write(
    client: AsyncClient,
    unauthorized_user_token: str,
    test_order_with_bill: tuple[VisitOrder, Bill],
) -> None:
    """General User without PAYMENT_WRITE or PAYMENT_READ receives 403."""
    _, bill = test_order_with_bill

    # Attempt to record payment
    write_res = await client.post(
        f"/api/v1/bills/{bill.id}/payments",
        json={"amount": "100.00", "payment_mode": "cash"},
        headers={"Authorization": f"Bearer {unauthorized_user_token}"},
    )
    assert write_res.status_code == 403

    # Attempt to read payments
    read_res = await client.get(
        f"/api/v1/bills/{bill.id}/payments",
        headers={"Authorization": f"Bearer {unauthorized_user_token}"},
    )
    assert read_res.status_code == 403
