"""Notification recipients (roles/users, internal or Public Portal): a
targeted notification fans out one row per resolved active user, and each
caller's bell (GET /notifications) shows only what was addressed to them —
a vendor never sees the tenant-wide broadcast rows internal users do."""

import uuid

from sqlalchemy import select

from app.core.auth import create_access_token, hash_password
from tests.conftest import AsyncSessionLocal
from app.crud.notifications import create_task_notification
from app.crud.tasks import _exec_create_notification
from app.models.identity import Role, User, UserRole, Vendor
from app.models.notification import Notification


async def _make_portal_users(tenant_id: uuid.UUID) -> dict:
    """Two vendor users sharing one portal role, one vendor user with no
    role, and one inactive vendor user holding the role (must be skipped)."""
    unique = uuid.uuid4().hex[:6]
    async with AsyncSessionLocal() as db:
        async with db.begin():
            vendor = Vendor(tenant_id=tenant_id, organization_name=f"Acme {unique}")
            db.add(vendor)
            await db.flush()
            role = Role(tenant_id=tenant_id, name="Portal Suppliers", scope="portal", action_matrix={})
            db.add(role)
            await db.flush()

            def user(name: str, status: str = "active") -> User:
                u = User(
                    tenant_id=tenant_id,
                    vendor_id=vendor.id,
                    name=name,
                    email=f"{name.lower()}-{unique}@example.com",
                    password_hash=hash_password("x"),
                    status=status,
                )
                db.add(u)
                return u

            alice, bob, carol, dave = user("Alice"), user("Bob"), user("Carol"), user("Dave", status="inactive")
            await db.flush()
            for u in (alice, bob, dave):
                db.add(UserRole(user_id=u.id, role_id=role.id))
            await db.flush()
            return {
                "vendor_id": vendor.id,
                "role_id": role.id,
                "alice": alice.id,
                "bob": bob.id,
                "carol": carol.id,
                "dave": dave.id,
            }


def _headers(tenant_id, user_id, vendor_id=None) -> dict[str, str]:
    token = create_access_token(tenant_id=tenant_id, user_id=user_id, vendor_id=vendor_id)
    return {"Authorization": f"Bearer {token}"}


async def test_role_and_user_targets_fan_out_one_row_per_active_user(test_tenant):
    tenant_id = test_tenant["tenant_id"]
    p = await _make_portal_users(tenant_id)
    async with AsyncSessionLocal() as db:
        async with db.begin():
            # Carol named directly, Alice/Bob via the role, Alice named twice
            # (role + user) must still get exactly one row; Dave is inactive.
            count = await create_task_notification(
                db, tenant_id, "Hi", "Msg", role_ids=[p["role_id"]], user_ids=[p["carol"], p["alice"]]
            )
    assert count == 3
    async with AsyncSessionLocal() as db:
        rows = (await db.execute(select(Notification).where(Notification.tenant_id == tenant_id))).scalars().all()
    assert sorted(r.recipient_user_id for r in rows) == sorted([p["alice"], p["bob"], p["carol"]])


async def test_untargeted_notification_is_a_single_broadcast_row(test_tenant):
    tenant_id = test_tenant["tenant_id"]
    async with AsyncSessionLocal() as db:
        async with db.begin():
            count = await create_task_notification(db, tenant_id, "Hi", "Msg")
    assert count == 1
    async with AsyncSessionLocal() as db:
        rows = (await db.execute(select(Notification).where(Notification.tenant_id == tenant_id))).scalars().all()
    assert len(rows) == 1 and rows[0].recipient_user_id is None


async def test_bell_visibility_and_per_user_read_state(client, test_tenant):
    tenant_id, admin_id = test_tenant["tenant_id"], test_tenant["admin_id"]
    p = await _make_portal_users(tenant_id)
    async with AsyncSessionLocal() as db:
        async with db.begin():
            await create_task_notification(db, tenant_id, "Broadcast", "to internal users")
            await create_task_notification(db, tenant_id, "Suppliers", "to the portal role", role_ids=[p["role_id"]])

    alice = _headers(tenant_id, p["alice"], p["vendor_id"])
    bob = _headers(tenant_id, p["bob"], p["vendor_id"])
    carol = _headers(tenant_id, p["carol"], p["vendor_id"])
    admin = _headers(tenant_id, admin_id)

    alice_list = (await client.get("/notifications", headers=alice)).json()
    assert [n["title"] for n in alice_list] == ["Suppliers"]
    assert (await client.get("/notifications", headers=carol)).json() == []
    assert [n["title"] for n in (await client.get("/notifications", headers=admin)).json()] == ["Broadcast"]

    # Alice reading hers doesn't touch Bob's copy.
    read = await client.post(f"/notifications/{alice_list[0]['id']}/read", headers=alice)
    assert read.status_code == 200
    assert (await client.get("/notifications/unread-count", headers=alice)).json() == {"count": 0}
    assert (await client.get("/notifications/unread-count", headers=bob)).json() == {"count": 1}

    # Nobody can mark someone else's row read, nor the broadcast as a vendor.
    bob_row = (await client.get("/notifications", headers=bob)).json()[0]
    assert (await client.post(f"/notifications/{bob_row['id']}/read", headers=carol)).status_code == 404
    broadcast_id = (await client.get("/notifications", headers=admin)).json()[0]["id"]
    assert (await client.post(f"/notifications/{broadcast_id}/read", headers=bob)).status_code == 404

    # read-all is scoped to the caller too.
    assert (await client.post("/notifications/read-all", headers=admin)).json() == {"count": 1}
    assert (await client.get("/notifications/unread-count", headers=bob)).json() == {"count": 1}


async def test_task_node_notifies_recipient_user_id_input(test_tenant):
    tenant_id = test_tenant["tenant_id"]
    p = await _make_portal_users(tenant_id)
    node = {"subject": "Received {{ref}}", "body": "Thanks", "notify_role_ids": [], "notify_user_ids": []}
    async with AsyncSessionLocal() as db:
        async with db.begin():
            ok, detail, _ = await _exec_create_notification(
                db, tenant_id, node, {"ref": "A-1", "recipient_user_id": str(p["carol"])}
            )
            assert ok and "1 user" in detail
            # Declared but empty dynamic recipient: nothing sent, NOT a broadcast.
            ok, detail, _ = await _exec_create_notification(db, tenant_id, node, {"ref": "A-2", "recipient_user_id": None})
            assert ok and "nothing sent" in detail
    async with AsyncSessionLocal() as db:
        rows = (await db.execute(select(Notification).where(Notification.tenant_id == tenant_id))).scalars().all()
    assert len(rows) == 1
    assert rows[0].recipient_user_id == p["carol"] and rows[0].title == "Received A-1"
