"""POST /payments/initiate and /payments/verify from the Public Portal: a
portal user may use exactly the payment controls on pages their portal role
grants, and nothing else (app/routers/payments.py::_check_payment_access)."""

import uuid

from app.core.auth import create_access_token
from tests.conftest import AsyncSessionLocal
from app.models.identity import Role, User, UserRole
from app.routers import payments as payments_router

CONNECTOR = str(uuid.uuid4())
OTHER_CONNECTOR = str(uuid.uuid4())


async def _portal_headers(tenant_id: uuid.UUID, page_ids: list[str]) -> dict[str, str]:
    unique = uuid.uuid4().hex[:8]
    async with AsyncSessionLocal() as db:
        async with db.begin():
            role = Role(tenant_id=tenant_id, name=f"Citizen {unique}", is_system_role=False, scope="portal", action_matrix={"pages": page_ids})
            db.add(role)
            await db.flush()
            user = User(tenant_id=tenant_id, name=f"Citizen {unique}", email=f"citizen-{unique}@example.com", status="active")
            db.add(user)
            await db.flush()
            db.add(UserRole(user_id=user.id, role_id=role.id))
            user_id = user.id
    token = create_access_token(tenant_id=tenant_id, user_id=user_id, vendor_id=uuid.uuid4())
    return {"Authorization": f"Bearer {token}"}


async def _payment_page(client, auth_headers, name: str) -> str:
    blocks = [
        {
            "id": "panel",
            "type": "panel",
            # Nested inside a container, like most real layouts.
            "children": [
                {"id": "pay", "type": "payment_initiator", "label": "Pay Now", "payment_connector_id": CONNECTOR, "payment_amount": "1000", "payment_checkout_path": "/checkout"},
                {"id": "cb", "type": "payment_callback", "payment_connector_id": CONNECTOR, "payment_status_path": "/status"},
            ],
        }
    ]
    resp = await client.post("/pages", headers=auth_headers, json={"name": name, "blocks": blocks})
    assert resp.status_code == 201, resp.text
    return resp.json()["id"]


def _stub_gateway(monkeypatch):
    async def fake_initiate(db, tenant_id, connector_id, amount, **kwargs):
        return {"checkout_url": "https://pay.example.com/c/1", "payment_reference": "REF-1", "transaction_id": str(uuid.uuid4())}

    async def fake_verify(db, tenant_id, connector_id, reference, status_path=None):
        return {"status": "paid", "reference": reference}

    monkeypatch.setattr(payments_router, "initiate_payment", fake_initiate)
    monkeypatch.setattr(payments_router, "verify_payment", fake_verify)


async def test_portal_user_can_pay_from_a_granted_page(client, auth_headers, test_tenant, monkeypatch):
    _stub_gateway(monkeypatch)
    page_id = await _payment_page(client, auth_headers, "Pay fees")
    portal = await _portal_headers(test_tenant["tenant_id"], [page_id])

    resp = await client.post("/payments/initiate", headers=portal, json={"connector_id": CONNECTOR, "amount": 1000, "checkout_path": "/checkout"})
    assert resp.status_code == 200, resp.text
    assert resp.json()["checkout_url"] == "https://pay.example.com/c/1"

    resp = await client.post("/payments/verify", headers=portal, json={"connector_id": CONNECTOR, "reference": "REF-1", "status_path": "/status"})
    assert resp.status_code == 200, resp.text
    assert resp.json()["status"] == "paid"


async def test_portal_user_cannot_use_other_connectors_or_paths(client, auth_headers, test_tenant, monkeypatch):
    _stub_gateway(monkeypatch)
    page_id = await _payment_page(client, auth_headers, "Pay fees 2")
    portal = await _portal_headers(test_tenant["tenant_id"], [page_id])

    # A connector no granted page uses.
    resp = await client.post("/payments/initiate", headers=portal, json={"connector_id": OTHER_CONNECTOR, "amount": 1000})
    assert resp.status_code == 403
    assert "portal" in resp.json()["detail"]
    # The right connector, but a path the control doesn't use.
    resp = await client.post("/payments/initiate", headers=portal, json={"connector_id": CONNECTOR, "amount": 1000, "checkout_path": "/admin/refund"})
    assert resp.status_code == 403
    resp = await client.post("/payments/verify", headers=portal, json={"connector_id": CONNECTOR, "reference": "R", "status_path": "/other"})
    assert resp.status_code == 403


async def test_portal_user_needs_the_page_granted(client, auth_headers, test_tenant, monkeypatch):
    _stub_gateway(monkeypatch)
    await _payment_page(client, auth_headers, "Not granted")
    portal = await _portal_headers(test_tenant["tenant_id"], [])
    resp = await client.post("/payments/initiate", headers=portal, json={"connector_id": CONNECTOR, "amount": 1000})
    assert resp.status_code == 403


async def test_internal_users_still_need_tasks_edit(client, auth_headers, test_tenant, monkeypatch):
    _stub_gateway(monkeypatch)
    # The tenant admin passes as before.
    resp = await client.post("/payments/initiate", headers=auth_headers, json={"connector_id": OTHER_CONNECTOR, "amount": 1000})
    assert resp.status_code == 200, resp.text

    unique = uuid.uuid4().hex[:8]
    async with AsyncSessionLocal() as db:
        async with db.begin():
            role = Role(tenant_id=test_tenant["tenant_id"], name=f"Viewer {unique}", is_system_role=False, action_matrix={"tasks": ["view"]})
            db.add(role)
            await db.flush()
            user = User(tenant_id=test_tenant["tenant_id"], name=f"Viewer {unique}", email=f"viewer-{unique}@example.com", status="active")
            db.add(user)
            await db.flush()
            db.add(UserRole(user_id=user.id, role_id=role.id))
            user_id = user.id
    viewer = {"Authorization": f"Bearer {create_access_token(tenant_id=test_tenant['tenant_id'], user_id=user_id, vendor_id=None)}"}
    resp = await client.post("/payments/initiate", headers=viewer, json={"connector_id": CONNECTOR, "amount": 1000})
    assert resp.status_code == 403


async def test_portal_users_pay_from_the_published_page_not_a_draft(client, auth_headers, test_tenant, monkeypatch):
    """Portal users see a page's published version, so that's what decides —
    a Pay Now still being edited in the draft neither breaks nor adds one."""
    _stub_gateway(monkeypatch)
    page_id = await _payment_page(client, auth_headers, "Pay fees published")
    published = await client.post(f"/pages/{page_id}/publish", headers=auth_headers, json={})
    assert published.status_code == 200, published.text
    # The draft drops the Pay Now while it's being reworked…
    draft = await client.patch(f"/pages/{page_id}", headers=auth_headers, json={"blocks": [{"id": "panel", "type": "panel", "children": []}]})
    assert draft.status_code == 200, draft.text
    portal = await _portal_headers(test_tenant["tenant_id"], [page_id])
    # …and the published one still works.
    resp = await client.post("/payments/initiate", headers=portal, json={"connector_id": CONNECTOR, "amount": 1000, "checkout_path": "/checkout"})
    assert resp.status_code == 200, resp.text
