"""INDL-59 (follow-up) — Staff-seat limit enforcement.

Per PLAN_LIMITS (src/core/constants.py): Starter = 2 staff seats,
Professional = 10, Enterprise = 10. A "seat" is any user with status
`active` or `invited` (not soft-deleted) — the admin (first user) counts
toward the limit too. Inviting past the limit must 403 server-side, not
just be discouraged in the UI.
"""
from uuid import uuid4

import pytest
from httpx import AsyncClient, Response
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.auth.models.user import User
from src.apps.tenants.models.account import Account
from src.core.security import build_token_payload, create_access_token, hash_password

INVITE_URL = "/api/v1/users/invite"


async def _make_account(db: AsyncSession, *, plan: str) -> Account:
    uid = uuid4().hex[:8]
    acc = Account(
        organization_name=f"Staff Limit Cemetery {uid}",
        subdomain=f"stafflimit-{uid}",
        contact_email=f"admin-{uid}@example.com",
        plan=plan,
        status="active",
    )
    db.add(acc)
    await db.flush()
    return acc


async def _make_user(
    db: AsyncSession, account: Account, *, role: str = "administrator", status: str = "active", email: str | None = None
) -> User:
    user = User(
        tenant_id=account.id,
        email=email or f"user-{uuid4().hex[:8]}@example.com",
        password_hash=hash_password("Test1234!"),
        first_name="Test",
        last_name="User",
        role=role,
        status=status,
    )
    db.add(user)
    await db.flush()
    return user


def _headers(token: str, account: Account) -> dict:
    return {"Authorization": f"Bearer {token}", "X-Tenant-ID": str(account.id)}


async def _invite(client: AsyncClient, token: str, account: Account, email: str) -> Response:
    return await client.post(
        INVITE_URL,
        json={"name": "New Hire", "email": email, "role": "staff"},
        headers=_headers(token, account),
    )


@pytest.mark.asyncio
async def test_starter_tenant_blocked_after_2_seats(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session, plan="starter")
    admin = await _make_user(db_session, acc, role="administrator")
    token = create_access_token(build_token_payload(admin, acc))

    # Seat 1 is the admin itself. One invite fills seat 2 (the limit).
    resp = await _invite(client, token, acc, f"staff1-{uuid4().hex[:6]}@example.com")
    assert resp.status_code == 201

    # A 3rd seat must be rejected — Starter's ceiling is 2.
    resp = await _invite(client, token, acc, f"staff2-{uuid4().hex[:6]}@example.com")
    assert resp.status_code == 403
    assert "maximum of 2 staff users" in resp.json()["message"]
    assert "Starter" in resp.json()["message"]
    assert "Upgrade" in resp.json()["message"]


@pytest.mark.asyncio
async def test_professional_tenant_blocked_after_10_seats(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session, plan="professional")
    admin = await _make_user(db_session, acc, role="administrator")
    token = create_access_token(build_token_payload(admin, acc))

    # Admin = seat 1; fill seats 2 through 10 (9 more invites).
    for i in range(9):
        resp = await _invite(client, token, acc, f"staff{i}-{uuid4().hex[:6]}@example.com")
        assert resp.status_code == 201, f"invite {i} should succeed, got {resp.status_code}: {resp.text}"

    resp = await _invite(client, token, acc, f"overflow-{uuid4().hex[:6]}@example.com")
    assert resp.status_code == 403
    assert "maximum of 10 staff users" in resp.json()["message"]
    # Professional and Enterprise share the same ceiling — no upgrade nudge.
    assert "Upgrade" not in resp.json()["message"]


@pytest.mark.asyncio
async def test_enterprise_tenant_blocked_after_10_seats(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session, plan="enterprise")
    admin = await _make_user(db_session, acc, role="administrator")
    token = create_access_token(build_token_payload(admin, acc))

    for i in range(9):
        resp = await _invite(client, token, acc, f"staff{i}-{uuid4().hex[:6]}@example.com")
        assert resp.status_code == 201

    resp = await _invite(client, token, acc, f"overflow-{uuid4().hex[:6]}@example.com")
    assert resp.status_code == 403
    assert "maximum of 10 staff users" in resp.json()["message"]


@pytest.mark.asyncio
async def test_inactive_users_do_not_count_toward_the_limit(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session, plan="starter")
    admin = await _make_user(db_session, acc, role="administrator")
    token = create_access_token(build_token_payload(admin, acc))

    # An inactive (deactivated) user occupies no seat.
    await _make_user(db_session, acc, role="staff", status="inactive")

    # Seat 2 (admin + 1 invite) must still succeed — the inactive user
    # doesn't count against the 2-seat Starter ceiling.
    resp = await _invite(client, token, acc, f"staff-{uuid4().hex[:6]}@example.com")
    assert resp.status_code == 201


@pytest.mark.asyncio
async def test_invited_pending_users_count_toward_the_limit(client: AsyncClient, db_session: AsyncSession):
    acc = await _make_account(db_session, plan="starter")
    admin = await _make_user(db_session, acc, role="administrator")
    token = create_access_token(build_token_payload(admin, acc))

    # A pending (not-yet-accepted) invite still occupies seat 2.
    await _make_user(db_session, acc, role="staff", status="invited")

    resp = await _invite(client, token, acc, f"staff-{uuid4().hex[:6]}@example.com")
    assert resp.status_code == 403
    assert "maximum of 2 staff users" in resp.json()["message"]
