"""
Tests for INDL-57 — Support Tickets: tenant-scoped module.

Covers `/api/v1/support/tickets` (GET list, POST create), `/api/v1/support/tickets/{id}`
(GET detail), `/api/v1/support/tickets/{id}/messages` (POST reply).

Backend-testable Acceptance Criteria covered here:
  AC-06 — Your Support Requests panel: ticket number/subject/status/date, newest first
  AC-07 — empty state (zero tickets)
  AC-08 — create modal fields (subject/description only; identity from session)
  AC-09 — valid submission creates status=open, appears at top of list
  AC-10 — empty subject/description -> inline validation errors (422)
  AC-11 — Ticket Detail: thread oldest-first, ticket number/subject/status
  AC-12 — tenant follow-up reply appears in thread, bumps last_message_at
  AC-13 — cross-tenant ticket access -> 404 (also see S-02 in test_support_security.py)

Skipped (frontend-only, no backend-testable component):
  AC-01 — sidebar nav item visibility
  AC-02 — page header copy
  AC-03 — "Create Support Request" button placement
  AC-04 — Contact Us card static content / no map rendering
  AC-05 — phone-support plan gating is rendered client-side from tenant/plan
          context already available via existing tenant/auth endpoints; no
          support-module API surface implements or should implement this.
"""
from __future__ import annotations

import uuid

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

from src.apps.auth.models.user import User
from src.apps.support.models.support_ticket import SupportTicket
from src.apps.support.models.support_ticket_message import SupportTicketMessage
from src.apps.tenants.models.account import Account
from src.core.security import build_token_payload, create_access_token, hash_password

SUPPORT_URL = "/api/v1/support/tickets"


# ── Helpers ───────────────────────────────────────────────────────────────────


async def _create_account(db: AsyncSession, *, org: str = "Test Cemetery", plan: str = "starter") -> Account:
    subdomain = f"cem-{uuid.uuid4().hex[:10]}"
    account = Account(
        organization_name=org,
        subdomain=subdomain,
        contact_email=f"contact@{subdomain}.ca",
        plan=plan,
        status="active",
    )
    db.add(account)
    await db.flush()
    return account


async def _create_user(db: AsyncSession, account: Account, *, role: str = "view_only", email: str | None = None) -> User:
    email = email or f"{role}-{uuid.uuid4().hex[:8]}@test.ca"
    user = User(
        tenant_id=account.id,
        email=email,
        password_hash=hash_password("Password123!"),
        first_name="Test",
        last_name=role.title(),
        role=role,
        status="active",
    )
    db.add(user)
    await db.flush()
    return user


def _headers(user: User, account: Account | None = None) -> dict:
    token = create_access_token(build_token_payload(user, account))
    return {"Authorization": f"Bearer {token}"}


async def _make_tenant(db: AsyncSession, *, role: str = "view_only", plan: str = "starter"):
    """Create an account + one user of the given role. Returns (headers, user, account)."""
    account = await _create_account(db, plan=plan)
    user = await _create_user(db, account, role=role)
    return _headers(user, account), user, account


# ── AC-09 / AC-08: Create ticket — happy path ────────────────────────────────


@pytest.mark.asyncio
async def test_create_ticket_valid_returns_open_status(client: AsyncClient, db_session: AsyncSession):
    """AC-09: A valid create request produces a ticket with status='open'."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(
        SUPPORT_URL,
        json={"subject": "Login issue", "description": "I can't log in after the update."},
        headers=headers,
    )
    assert resp.status_code == 201
    body = resp.json()
    assert body["success"] is True
    data = body["data"]
    assert data["status"] == "open"
    assert data["subject"] == "Login issue"
    assert data["ticket_number"].startswith("SUP-")


@pytest.mark.asyncio
async def test_create_ticket_identity_taken_from_session(client: AsyncClient, db_session: AsyncSession):
    """AC-08: created_by_user_id is the authenticated caller, never client-supplied."""
    headers, user, account = await _make_tenant(db_session)
    resp = await client.post(
        SUPPORT_URL,
        json={"subject": "Billing question", "description": "Why was I charged twice?"},
        headers=headers,
    )
    assert resp.status_code == 201
    ticket_id = resp.json()["data"]["id"]

    result = await db_session.execute(select(SupportTicket).where(SupportTicket.id == ticket_id))
    ticket = result.scalar_one()
    assert ticket.created_by_user_id == user.id
    assert ticket.tenant_id == account.id


@pytest.mark.asyncio
async def test_create_ticket_stores_description_as_first_message(client: AsyncClient, db_session: AsyncSession):
    """The description is persisted as message #1 in the same transaction."""
    headers, user, _ = await _make_tenant(db_session)
    resp = await client.post(
        SUPPORT_URL,
        json={"subject": "Map not loading", "description": "The interactive map is blank on my end."},
        headers=headers,
    )
    ticket_id = resp.json()["data"]["id"]

    result = await db_session.execute(
        select(SupportTicketMessage).where(SupportTicketMessage.ticket_id == ticket_id)
    )
    messages = result.scalars().all()
    assert len(messages) == 1
    assert messages[0].body == "The interactive map is blank on my end."
    assert messages[0].author_user_id == user.id
    assert messages[0].author_role_snapshot == "view_only"


@pytest.mark.asyncio
async def test_create_ticket_appears_at_top_of_list(client: AsyncClient, db_session: AsyncSession):
    """AC-09: The new ticket appears at the top of Your Support Requests immediately."""
    headers, _, _ = await _make_tenant(db_session)
    await client.post(SUPPORT_URL, json={"subject": "First", "description": "First ticket body."}, headers=headers)
    resp2 = await client.post(SUPPORT_URL, json={"subject": "Second", "description": "Second ticket body."}, headers=headers)
    second_id = resp2.json()["data"]["id"]

    list_resp = await client.get(SUPPORT_URL, headers=headers)
    items = list_resp.json()["data"]
    assert items[0]["id"] == second_id
    assert items[0]["subject"] == "Second"


# ── AC-10: Validation — empty / blank / oversized fields ─────────────────────


@pytest.mark.asyncio
async def test_create_ticket_missing_subject_returns_422(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(SUPPORT_URL, json={"description": "No subject provided."}, headers=headers)
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_create_ticket_missing_description_returns_422(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(SUPPORT_URL, json={"subject": "No description"}, headers=headers)
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_create_ticket_empty_string_subject_returns_422(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(SUPPORT_URL, json={"subject": "", "description": "Valid body text."}, headers=headers)
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_create_ticket_whitespace_only_subject_returns_422(client: AsyncClient, db_session: AsyncSession):
    """AC-10: whitespace-only is not a valid non-blank subject."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(SUPPORT_URL, json={"subject": "    ", "description": "Valid body text."}, headers=headers)
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_create_ticket_whitespace_only_description_returns_422(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(SUPPORT_URL, json={"subject": "Valid subject", "description": "   \n\t  "}, headers=headers)
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_create_ticket_oversized_subject_returns_422(client: AsyncClient, db_session: AsyncSession):
    """Subject > 200 chars is rejected."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(
        SUPPORT_URL,
        json={"subject": "A" * 201, "description": "Valid body text."},
        headers=headers,
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_create_ticket_max_length_subject_accepted(client: AsyncClient, db_session: AsyncSession):
    """Subject at exactly 200 chars is accepted (boundary)."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(
        SUPPORT_URL,
        json={"subject": "A" * 200, "description": "Valid body text."},
        headers=headers,
    )
    assert resp.status_code == 201


@pytest.mark.asyncio
async def test_create_ticket_oversized_description_returns_422(client: AsyncClient, db_session: AsyncSession):
    """Description > 5000 chars is rejected."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(
        SUPPORT_URL,
        json={"subject": "Valid subject", "description": "B" * 5001},
        headers=headers,
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_create_ticket_max_length_description_accepted(client: AsyncClient, db_session: AsyncSession):
    """Description at exactly 5000 chars is accepted (boundary)."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(
        SUPPORT_URL,
        json={"subject": "Valid subject", "description": "B" * 5000},
        headers=headers,
    )
    assert resp.status_code == 201


# ── S-03: Mass assignment (also see test_support_security.py) ───────────────


@pytest.mark.asyncio
async def test_create_ticket_rejects_status_field(client: AsyncClient, db_session: AsyncSession):
    """S-03 / OWASP API3: 'status' in the create payload is rejected (422),
    not silently ignored and not persisted."""
    headers, _, account = await _make_tenant(db_session)
    resp = await client.post(
        SUPPORT_URL,
        json={"subject": "Attempted override", "description": "Trying to set status.", "status": "resolved"},
        headers=headers,
    )
    assert resp.status_code == 422

    result = await db_session.execute(select(SupportTicket).where(SupportTicket.tenant_id == account.id))
    assert result.scalars().all() == []


@pytest.mark.asyncio
async def test_create_ticket_rejects_tenant_id_field(client: AsyncClient, db_session: AsyncSession):
    """S-03: 'tenant_id' in the create payload is rejected (422)."""
    headers, _, _ = await _make_tenant(db_session)
    other_tenant_id = str(uuid.uuid4())
    resp = await client.post(
        SUPPORT_URL,
        json={"subject": "Override attempt", "description": "Trying to set tenant.", "tenant_id": other_tenant_id},
        headers=headers,
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_create_ticket_rejects_ticket_number_field(client: AsyncClient, db_session: AsyncSession):
    """S-03: 'ticket_number' in the create payload is rejected (422)."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(
        SUPPORT_URL,
        json={"subject": "Override attempt", "description": "Trying to set number.", "ticket_number": "SUP-999999"},
        headers=headers,
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_create_ticket_rejects_created_by_user_id_field(client: AsyncClient, db_session: AsyncSession):
    """S-03: 'created_by_user_id' in the create payload is rejected (422)."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(
        SUPPORT_URL,
        json={
            "subject": "Override attempt",
            "description": "Trying to set creator.",
            "created_by_user_id": str(uuid.uuid4()),
        },
        headers=headers,
    )
    assert resp.status_code == 422


# ── Ticket number: format, uniqueness, sequential increment ─────────────────


@pytest.mark.asyncio
async def test_ticket_number_format(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(SUPPORT_URL, json={"subject": "Format check", "description": "Body text."}, headers=headers)
    number = resp.json()["data"]["ticket_number"]
    assert len(number) == 10  # "SUP-" + 6 digits
    assert number[:4] == "SUP-"
    assert number[4:].isdigit()


@pytest.mark.asyncio
async def test_ticket_number_sequential_increment(client: AsyncClient, db_session: AsyncSession):
    """SVC-09: consecutive tickets get consecutive platform-wide sequence numbers."""
    headers, _, _ = await _make_tenant(db_session)
    resp1 = await client.post(SUPPORT_URL, json={"subject": "One", "description": "Body one."}, headers=headers)
    resp2 = await client.post(SUPPORT_URL, json={"subject": "Two", "description": "Body two."}, headers=headers)

    n1 = int(resp1.json()["data"]["ticket_number"].split("-")[1])
    n2 = int(resp2.json()["data"]["ticket_number"].split("-")[1])
    assert n2 == n1 + 1


@pytest.mark.asyncio
async def test_ticket_number_uniqueness_across_tenants(client: AsyncClient, db_session: AsyncSession):
    """Ticket numbers are unique platform-wide, even across different tenants."""
    headers_a, _, _ = await _make_tenant(db_session)
    headers_b, _, _ = await _make_tenant(db_session)

    resp_a = await client.post(SUPPORT_URL, json={"subject": "A", "description": "Body A."}, headers=headers_a)
    resp_b = await client.post(SUPPORT_URL, json={"subject": "B", "description": "Body B."}, headers=headers_b)

    assert resp_a.json()["data"]["ticket_number"] != resp_b.json()["data"]["ticket_number"]


# ── AC-06 / AC-07: List — pagination, ordering, tenant isolation ────────────


@pytest.mark.asyncio
async def test_list_tickets_empty_state(client: AsyncClient, db_session: AsyncSession):
    """AC-07: A tenant with zero tickets sees an empty list, total=0."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.get(SUPPORT_URL, headers=headers)
    assert resp.status_code == 200
    body = resp.json()
    assert body["data"] == []
    assert body["total"] == 0


@pytest.mark.asyncio
async def test_list_tickets_newest_first(client: AsyncClient, db_session: AsyncSession):
    """AC-06: Tickets are listed newest first."""
    headers, _, _ = await _make_tenant(db_session)
    for subject in ["Oldest", "Middle", "Newest"]:
        await client.post(SUPPORT_URL, json={"subject": subject, "description": "Body text."}, headers=headers)

    resp = await client.get(SUPPORT_URL, headers=headers)
    subjects = [item["subject"] for item in resp.json()["data"]]
    assert subjects == ["Newest", "Middle", "Oldest"]


@pytest.mark.asyncio
async def test_list_tickets_isolated_per_tenant(client: AsyncClient, db_session: AsyncSession):
    """A tenant's list never includes another tenant's tickets."""
    headers_a, _, _ = await _make_tenant(db_session)
    headers_b, _, _ = await _make_tenant(db_session)

    await client.post(SUPPORT_URL, json={"subject": "Tenant A ticket", "description": "Body."}, headers=headers_a)
    await client.post(SUPPORT_URL, json={"subject": "Tenant B ticket", "description": "Body."}, headers=headers_b)

    resp_a = await client.get(SUPPORT_URL, headers=headers_a)
    items_a = resp_a.json()["data"]
    assert len(items_a) == 1
    assert items_a[0]["subject"] == "Tenant A ticket"


@pytest.mark.asyncio
async def test_list_tickets_shared_across_tenant_users(client: AsyncClient, db_session: AsyncSession):
    """AC-06: All users in a tenant share one ticket list, not filtered per-submitter."""
    account = await _create_account(db_session)
    user_a = await _create_user(db_session, account, role="view_only")
    user_b = await _create_user(db_session, account, role="staff")
    headers_a = _headers(user_a, account)
    headers_b = _headers(user_b, account)

    await client.post(SUPPORT_URL, json={"subject": "Submitted by A", "description": "Body."}, headers=headers_a)

    resp_b = await client.get(SUPPORT_URL, headers=headers_b)
    items = resp_b.json()["data"]
    assert len(items) == 1
    assert items[0]["subject"] == "Submitted by A"


@pytest.mark.asyncio
async def test_list_tickets_pagination(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    for i in range(5):
        await client.post(SUPPORT_URL, json={"subject": f"Ticket {i}", "description": "Body."}, headers=headers)

    resp = await client.get(f"{SUPPORT_URL}?page=1&page_size=2", headers=headers)
    body = resp.json()
    assert body["total"] == 5
    assert len(body["data"]) == 2
    assert body["pages"] == 3

    resp2 = await client.get(f"{SUPPORT_URL}?page=3&page_size=2", headers=headers)
    assert len(resp2.json()["data"]) == 1


@pytest.mark.asyncio
async def test_list_tickets_page_size_upper_boundary_rejected(client: AsyncClient, db_session: AsyncSession):
    """page_size > 100 is rejected with 422."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.get(f"{SUPPORT_URL}?page_size=101", headers=headers)
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_list_tickets_page_size_upper_boundary_accepted(client: AsyncClient, db_session: AsyncSession):
    """page_size == 100 is accepted."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.get(f"{SUPPORT_URL}?page_size=100", headers=headers)
    assert resp.status_code == 200


@pytest.mark.asyncio
async def test_list_tickets_page_size_zero_rejected(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.get(f"{SUPPORT_URL}?page_size=0", headers=headers)
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_list_tickets_page_zero_rejected(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.get(f"{SUPPORT_URL}?page=0", headers=headers)
    assert resp.status_code == 422


# ── AC-11 / AC-12: Ticket detail + thread + reply ────────────────────────────


@pytest.mark.asyncio
async def test_get_ticket_detail_thread_oldest_first(client: AsyncClient, db_session: AsyncSession):
    """AC-11: The full thread (initial description + replies) is oldest first."""
    headers, _, _ = await _make_tenant(db_session)
    create_resp = await client.post(
        SUPPORT_URL, json={"subject": "Thread order", "description": "Initial message."}, headers=headers
    )
    ticket_id = create_resp.json()["data"]["id"]
    await client.post(f"{SUPPORT_URL}/{ticket_id}/messages", json={"body": "Follow-up reply."}, headers=headers)

    resp = await client.get(f"{SUPPORT_URL}/{ticket_id}", headers=headers)
    assert resp.status_code == 200
    data = resp.json()["data"]
    assert data["ticket_number"].startswith("SUP-")
    assert data["subject"] == "Thread order"
    assert data["status"] == "open"
    bodies = [m["body"] for m in data["messages"]]
    assert bodies == ["Initial message.", "Follow-up reply."]


@pytest.mark.asyncio
async def test_get_ticket_detail_message_shape(client: AsyncClient, db_session: AsyncSession):
    """Message entries include author_user_id, author_role_snapshot, author_name, body, created_at."""
    headers, user, _ = await _make_tenant(db_session, role="staff")
    create_resp = await client.post(
        SUPPORT_URL, json={"subject": "Shape check", "description": "Initial message."}, headers=headers
    )
    ticket_id = create_resp.json()["data"]["id"]

    resp = await client.get(f"{SUPPORT_URL}/{ticket_id}", headers=headers)
    message = resp.json()["data"]["messages"][0]
    assert message["author_user_id"] == str(user.id)
    assert message["author_role_snapshot"] == "staff"
    assert message["author_name"] == "Test Staff"
    assert "created_at" in message


@pytest.mark.asyncio
async def test_get_ticket_nonexistent_uuid_returns_404(client: AsyncClient, db_session: AsyncSession):
    """A well-formed but nonexistent ticket UUID returns 404, not 500."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.get(f"{SUPPORT_URL}/{uuid.uuid4()}", headers=headers)
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_get_ticket_malformed_uuid_returns_422(client: AsyncClient, db_session: AsyncSession):
    """A malformed UUID in the path returns 422, not 500."""
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.get(f"{SUPPORT_URL}/not-a-uuid", headers=headers)
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_get_ticket_cross_tenant_returns_404(client: AsyncClient, db_session: AsyncSession):
    """AC-13 / S-02: Tenant B cannot fetch Tenant A's ticket — 404, never 403."""
    headers_a, _, _ = await _make_tenant(db_session)
    headers_b, _, _ = await _make_tenant(db_session)

    create_resp = await client.post(
        SUPPORT_URL, json={"subject": "Tenant A only", "description": "Private."}, headers=headers_a
    )
    ticket_id = create_resp.json()["data"]["id"]

    resp = await client.get(f"{SUPPORT_URL}/{ticket_id}", headers=headers_b)
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_add_message_reply_appears_in_thread(client: AsyncClient, db_session: AsyncSession):
    """AC-12: A follow-up reply appears in the thread immediately."""
    headers, user, _ = await _make_tenant(db_session)
    create_resp = await client.post(
        SUPPORT_URL, json={"subject": "Reply test", "description": "Initial message."}, headers=headers
    )
    ticket_id = create_resp.json()["data"]["id"]

    reply_resp = await client.post(f"{SUPPORT_URL}/{ticket_id}/messages", json={"body": "A follow-up."}, headers=headers)
    assert reply_resp.status_code == 201
    reply_data = reply_resp.json()["data"]
    assert reply_data["body"] == "A follow-up."
    assert reply_data["author_user_id"] == str(user.id)

    detail_resp = await client.get(f"{SUPPORT_URL}/{ticket_id}", headers=headers)
    bodies = [m["body"] for m in detail_resp.json()["data"]["messages"]]
    assert bodies[-1] == "A follow-up."


@pytest.mark.asyncio
async def test_add_message_bumps_last_message_at(client: AsyncClient, db_session: AsyncSession):
    """AC-12: Replying bumps last_message_at on the ticket."""
    headers, _, _ = await _make_tenant(db_session)
    create_resp = await client.post(
        SUPPORT_URL, json={"subject": "Bump test", "description": "Initial message."}, headers=headers
    )
    ticket_id = create_resp.json()["data"]["id"]

    result = await db_session.execute(select(SupportTicket).where(SupportTicket.id == ticket_id))
    ticket_before = result.scalar_one()
    original_last_message_at = ticket_before.last_message_at

    await client.post(f"{SUPPORT_URL}/{ticket_id}/messages", json={"body": "Bumping this."}, headers=headers)

    await db_session.refresh(ticket_before)
    assert ticket_before.last_message_at >= original_last_message_at


@pytest.mark.asyncio
async def test_add_message_cross_tenant_returns_404(client: AsyncClient, db_session: AsyncSession):
    """AC-13 / S-02: Tenant B cannot post to Tenant A's ticket — 404."""
    headers_a, _, _ = await _make_tenant(db_session)
    headers_b, _, _ = await _make_tenant(db_session)

    create_resp = await client.post(
        SUPPORT_URL, json={"subject": "Tenant A only", "description": "Private."}, headers=headers_a
    )
    ticket_id = create_resp.json()["data"]["id"]

    resp = await client.post(f"{SUPPORT_URL}/{ticket_id}/messages", json={"body": "Sneaky reply"}, headers=headers_b)
    assert resp.status_code == 404

    # Confirm nothing was persisted against tenant A's ticket from tenant B.
    result = await db_session.execute(
        select(SupportTicketMessage).where(SupportTicketMessage.ticket_id == ticket_id)
    )
    bodies = [m.body for m in result.scalars().all()]
    assert "Sneaky reply" not in bodies


@pytest.mark.asyncio
async def test_add_message_nonexistent_ticket_returns_404(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(f"{SUPPORT_URL}/{uuid.uuid4()}/messages", json={"body": "Hello"}, headers=headers)
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_add_message_malformed_uuid_returns_422(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    resp = await client.post(f"{SUPPORT_URL}/not-a-uuid/messages", json={"body": "Hello"}, headers=headers)
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_add_message_blank_body_returns_422(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    create_resp = await client.post(SUPPORT_URL, json={"subject": "X", "description": "Y"}, headers=headers)
    ticket_id = create_resp.json()["data"]["id"]
    resp = await client.post(f"{SUPPORT_URL}/{ticket_id}/messages", json={"body": "   "}, headers=headers)
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_add_message_oversized_body_returns_422(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    create_resp = await client.post(SUPPORT_URL, json={"subject": "X", "description": "Y"}, headers=headers)
    ticket_id = create_resp.json()["data"]["id"]
    resp = await client.post(f"{SUPPORT_URL}/{ticket_id}/messages", json={"body": "Z" * 5001}, headers=headers)
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_add_message_max_length_body_accepted(client: AsyncClient, db_session: AsyncSession):
    headers, _, _ = await _make_tenant(db_session)
    create_resp = await client.post(SUPPORT_URL, json={"subject": "X", "description": "Y"}, headers=headers)
    ticket_id = create_resp.json()["data"]["id"]
    resp = await client.post(f"{SUPPORT_URL}/{ticket_id}/messages", json={"body": "Z" * 5000}, headers=headers)
    assert resp.status_code == 201


@pytest.mark.asyncio
async def test_add_message_rejects_extra_fields(client: AsyncClient, db_session: AsyncSession):
    """S-03: Reply schema exposes only 'body' — extra fields rejected."""
    headers, _, _ = await _make_tenant(db_session)
    create_resp = await client.post(SUPPORT_URL, json={"subject": "X", "description": "Y"}, headers=headers)
    ticket_id = create_resp.json()["data"]["id"]
    resp = await client.post(
        f"{SUPPORT_URL}/{ticket_id}/messages",
        json={"body": "Hello", "author_role_snapshot": "site_admin"},
        headers=headers,
    )
    assert resp.status_code == 422


# ── Role hierarchy: view_only is the floor for tenant endpoints ─────────────


@pytest.mark.asyncio
async def test_view_only_can_create_list_get_and_reply(client: AsyncClient, db_session: AsyncSession):
    """view_only is the floor — full CRUD access on the tenant's own tickets."""
    headers, _, _ = await _make_tenant(db_session, role="view_only")

    create_resp = await client.post(SUPPORT_URL, json={"subject": "VO test", "description": "Body."}, headers=headers)
    assert create_resp.status_code == 201
    ticket_id = create_resp.json()["data"]["id"]

    assert (await client.get(SUPPORT_URL, headers=headers)).status_code == 200
    assert (await client.get(f"{SUPPORT_URL}/{ticket_id}", headers=headers)).status_code == 200

    reply_resp = await client.post(f"{SUPPORT_URL}/{ticket_id}/messages", json={"body": "VO reply"}, headers=headers)
    assert reply_resp.status_code == 201


@pytest.mark.parametrize("role", ["staff", "manager", "administrator"])
@pytest.mark.asyncio
async def test_higher_tenant_roles_can_use_support_endpoints(client: AsyncClient, db_session: AsyncSession, role: str):
    """Every tenant role at or above the view_only floor can use support endpoints."""
    headers, _, _ = await _make_tenant(db_session, role=role)
    resp = await client.post(SUPPORT_URL, json={"subject": f"{role} test", "description": "Body."}, headers=headers)
    assert resp.status_code == 201


# ── S-05: Unauthenticated requests -> 401 ────────────────────────────────────


@pytest.mark.asyncio
async def test_unauthenticated_list_returns_401(client: AsyncClient):
    resp = await client.get(SUPPORT_URL)
    assert resp.status_code == 401


@pytest.mark.asyncio
async def test_unauthenticated_create_returns_401(client: AsyncClient):
    resp = await client.post(SUPPORT_URL, json={"subject": "X", "description": "Y"})
    assert resp.status_code == 401


@pytest.mark.asyncio
async def test_unauthenticated_get_detail_returns_401(client: AsyncClient):
    resp = await client.get(f"{SUPPORT_URL}/{uuid.uuid4()}")
    assert resp.status_code == 401


@pytest.mark.asyncio
async def test_unauthenticated_add_message_returns_401(client: AsyncClient):
    resp = await client.post(f"{SUPPORT_URL}/{uuid.uuid4()}/messages", json={"body": "Hi"})
    assert resp.status_code == 401
