"""
Explicit Security Acceptance Criteria tests for INDL-57 — Support Tickets.

Per the PRD's threat model: "Support Tickets is ... where a site_admin
endpoint deliberately performs a cross-tenant query by design ... any bug
that lets a tenant user reach the cross-tenant code path is a full
multi-tenant data breach." These tests are the single highest-priority
coverage in this module.

  S-01 — Any tenant JWT (any role, including administrator) calling any
         /api/site-admin/support/* endpoint returns 403.
  S-02 — GET /api/v1/support/tickets/{id} for another tenant's ticket
         returns 404 (IDOR check, not just "some error").
  S-03 — Tenant create/reply payloads containing status/tenant_id/
         ticket_number do not change persisted values (422, nothing written).
  S-04 — Only PATCH /api/site-admin/support/tickets/{id}/status can change
         status; no tenant-facing endpoint accepts a status field; invalid
         enum values are rejected (422).
  S-05 — Unauthenticated requests to any support endpoint under either
         prefix return 401.

AC-23 (only site_admin reaches /admin/support or its API) is exercised by
the same S-01 matrix below.
"""
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.tenants.models.account import Account
from src.core.security import build_token_payload, create_access_token, hash_password

TENANT_URL = "/api/v1/support/tickets"
ADMIN_URL = "/api/site-admin/support/tickets"


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


async def _create_account(db: AsyncSession, *, org: str = "Security Test Cemetery", plan: str = "starter") -> Account:
    subdomain = f"sec-{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 | None, *, role: str, email: str | None = None) -> User:
    email = email or f"{role}-{uuid.uuid4().hex[:8]}@test.ca"
    user = User(
        tenant_id=account.id if account else None,
        email=email,
        password_hash=hash_password("Password123!"),
        first_name="Sec",
        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 _tenant_headers(db: AsyncSession, role: str):
    account = await _create_account(db)
    user = await _create_user(db, account, role=role)
    return _headers(user, account)


async def _site_admin_headers(db: AsyncSession) -> dict:
    user = await _create_user(db, None, role="site_admin", email=f"sa-sec-{uuid.uuid4().hex[:8]}@indelis.com")
    return _headers(user)


async def _create_ticket(client: AsyncClient, tenant_headers: dict) -> dict:
    resp = await client.post(
        TENANT_URL, json={"subject": "Security fixture ticket", "description": "Body."}, headers=tenant_headers
    )
    assert resp.status_code == 201
    return resp.json()["data"]


TENANT_ROLES = ["administrator", "manager", "staff", "view_only"]


# ── S-01 / AC-23: Every tenant role is forbidden on every site-admin endpoint ─


@pytest.mark.parametrize("role", TENANT_ROLES)
@pytest.mark.asyncio
async def test_s01_tenant_role_forbidden_on_list(client: AsyncClient, db_session: AsyncSession, role: str):
    headers = await _tenant_headers(db_session, role)
    resp = await client.get(ADMIN_URL, headers=headers)
    assert resp.status_code == 403


@pytest.mark.parametrize("role", TENANT_ROLES)
@pytest.mark.asyncio
async def test_s01_tenant_role_forbidden_on_detail(client: AsyncClient, db_session: AsyncSession, role: str):
    headers = await _tenant_headers(db_session, role)
    resp = await client.get(f"{ADMIN_URL}/{uuid.uuid4()}", headers=headers)
    assert resp.status_code == 403


@pytest.mark.parametrize("role", TENANT_ROLES)
@pytest.mark.asyncio
async def test_s01_tenant_role_forbidden_on_reply(client: AsyncClient, db_session: AsyncSession, role: str):
    headers = await _tenant_headers(db_session, role)
    resp = await client.post(f"{ADMIN_URL}/{uuid.uuid4()}/messages", json={"body": "Hi"}, headers=headers)
    assert resp.status_code == 403


@pytest.mark.parametrize("role", TENANT_ROLES)
@pytest.mark.asyncio
async def test_s01_tenant_role_forbidden_on_status_update(client: AsyncClient, db_session: AsyncSession, role: str):
    headers = await _tenant_headers(db_session, role)
    resp = await client.patch(f"{ADMIN_URL}/{uuid.uuid4()}/status", json={"status": "open"}, headers=headers)
    assert resp.status_code == 403


@pytest.mark.asyncio
async def test_s01_administrator_forbidden_even_with_real_ticket_id(client: AsyncClient, db_session: AsyncSession):
    """A tenant administrator must be forbidden even against a real, existing
    ticket ID (not just a random UUID) — proves the role check runs before
    any cross-tenant lookup can leak existence information."""
    owner_headers = await _tenant_headers(db_session, "view_only")
    ticket = await _create_ticket(client, owner_headers)

    admin_headers = await _tenant_headers(db_session, "administrator")
    resp = await client.get(f"{ADMIN_URL}/{ticket['id']}", headers=admin_headers)
    assert resp.status_code == 403


@pytest.mark.asyncio
async def test_site_admin_role_is_permitted_on_every_endpoint(client: AsyncClient, db_session: AsyncSession):
    """Positive control: site_admin (and only site_admin) reaches all four endpoints."""
    sa_headers = await _site_admin_headers(db_session)
    tenant_headers = await _tenant_headers(db_session, "view_only")
    ticket = await _create_ticket(client, tenant_headers)

    assert (await client.get(ADMIN_URL, headers=sa_headers)).status_code == 200
    assert (await client.get(f"{ADMIN_URL}/{ticket['id']}", headers=sa_headers)).status_code == 200
    assert (
        await client.post(f"{ADMIN_URL}/{ticket['id']}/messages", json={"body": "Ack"}, headers=sa_headers)
    ).status_code == 201
    assert (
        await client.patch(f"{ADMIN_URL}/{ticket['id']}/status", json={"status": "in_progress"}, headers=sa_headers)
    ).status_code == 200


# ── S-02: Cross-tenant IDOR -> 404, never 403/200 ────────────────────────────


@pytest.mark.asyncio
async def test_s02_get_other_tenant_ticket_returns_404(client: AsyncClient, db_session: AsyncSession):
    headers_a = await _tenant_headers(db_session, "view_only")
    headers_b = await _tenant_headers(db_session, "view_only")
    ticket = await _create_ticket(client, headers_a)

    resp = await client.get(f"{TENANT_URL}/{ticket['id']}", headers=headers_b)
    assert resp.status_code == 404
    assert resp.status_code != 403  # a 403 would confirm the resource exists


@pytest.mark.asyncio
async def test_s02_post_message_to_other_tenant_ticket_returns_404(client: AsyncClient, db_session: AsyncSession):
    headers_a = await _tenant_headers(db_session, "view_only")
    headers_b = await _tenant_headers(db_session, "view_only")
    ticket = await _create_ticket(client, headers_a)

    resp = await client.post(f"{TENANT_URL}/{ticket['id']}/messages", json={"body": "Reading your ticket"}, headers=headers_b)
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_s02_isolation_holds_across_many_tenants(client: AsyncClient, db_session: AsyncSession):
    """Guessing/enumerating IDs from several other tenants never succeeds."""
    victim_headers = await _tenant_headers(db_session, "view_only")
    victim_ticket = await _create_ticket(client, victim_headers)

    for _ in range(3):
        attacker_headers = await _tenant_headers(db_session, "administrator")
        resp = await client.get(f"{TENANT_URL}/{victim_ticket['id']}", headers=attacker_headers)
        assert resp.status_code == 404


# ── S-03: Mass assignment does not change persisted values ──────────────────


@pytest.mark.asyncio
async def test_s03_create_with_forbidden_fields_persists_nothing(client: AsyncClient, db_session: AsyncSession):
    """Sending status/tenant_id/ticket_number on create is rejected outright
    (422) — nothing is persisted with attacker-controlled values."""
    headers = await _tenant_headers(db_session, "view_only")
    other_tenant_id = str(uuid.uuid4())

    resp = await client.post(
        TENANT_URL,
        json={
            "subject": "Mass assignment attempt",
            "description": "Trying to override server-assigned fields.",
            "status": "resolved",
            "tenant_id": other_tenant_id,
            "ticket_number": "SUP-000001",
        },
        headers=headers,
    )
    assert resp.status_code == 422

    result = await db_session.execute(
        select(SupportTicket).where(SupportTicket.subject == "Mass assignment attempt")
    )
    assert result.scalar_one_or_none() is None


@pytest.mark.asyncio
async def test_s03_reply_with_forbidden_fields_rejected(client: AsyncClient, db_session: AsyncSession):
    headers = await _tenant_headers(db_session, "view_only")
    create_resp = await client.post(
        TENANT_URL, json={"subject": "S-03 reply test", "description": "Body."}, headers=headers
    )
    ticket_id = create_resp.json()["data"]["id"]

    resp = await client.post(
        f"{TENANT_URL}/{ticket_id}/messages",
        json={"body": "Trying to override", "author_role_snapshot": "site_admin", "tenant_id": str(uuid.uuid4())},
        headers=headers,
    )
    assert resp.status_code == 422


# ── S-04: Only the site-admin status endpoint can change status ─────────────


@pytest.mark.asyncio
async def test_s04_no_tenant_facing_status_endpoint_exists(client: AsyncClient, db_session: AsyncSession):
    """There is no PATCH .../status route under /api/v1/support — confirm
    405/404, not a silently-accepted status change."""
    headers = await _tenant_headers(db_session, "view_only")
    create_resp = await client.post(
        TENANT_URL, json={"subject": "No self-resolve", "description": "Body."}, headers=headers
    )
    ticket_id = create_resp.json()["data"]["id"]

    resp = await client.patch(f"{TENANT_URL}/{ticket_id}/status", json={"status": "resolved"}, headers=headers)
    assert resp.status_code in (404, 405)


@pytest.mark.asyncio
async def test_s04_tenant_create_schema_has_no_status_field(client: AsyncClient, db_session: AsyncSession):
    """A tenant cannot self-resolve/self-close via the create endpoint either."""
    headers = await _tenant_headers(db_session, "view_only")
    resp = await client.post(
        TENANT_URL,
        json={"subject": "Self resolve attempt", "description": "Body.", "status": "closed"},
        headers=headers,
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_s04_invalid_status_enum_value_rejected(client: AsyncClient, db_session: AsyncSession):
    sa_headers = await _site_admin_headers(db_session)
    headers = await _tenant_headers(db_session, "view_only")
    ticket = await _create_ticket(client, headers)

    resp = await client.patch(
        f"{ADMIN_URL}/{ticket['id']}/status", json={"status": "not_a_real_status"}, headers=sa_headers
    )
    assert resp.status_code == 422

    # Confirm the ticket's status is unchanged in the DB.
    result = await db_session.execute(select(SupportTicket).where(SupportTicket.id == ticket["id"]))
    assert result.scalar_one().status == "open"


@pytest.mark.asyncio
async def test_s04_only_site_admin_status_endpoint_changes_status(client: AsyncClient, db_session: AsyncSession):
    """Confirms the one legitimate status-change path works end to end,
    contrasted with the blocked tenant-side attempts above."""
    sa_headers = await _site_admin_headers(db_session)
    headers = await _tenant_headers(db_session, "view_only")
    ticket = await _create_ticket(client, headers)

    resp = await client.patch(f"{ADMIN_URL}/{ticket['id']}/status", json={"status": "closed"}, headers=sa_headers)
    assert resp.status_code == 200
    assert resp.json()["data"]["status"] == "closed"


# ── S-05: Unauthenticated requests -> 401 on both prefixes ───────────────────


@pytest.mark.asyncio
async def test_s05_unauthenticated_tenant_list_401(client: AsyncClient):
    assert (await client.get(TENANT_URL)).status_code == 401


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


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


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


@pytest.mark.asyncio
async def test_s05_unauthenticated_admin_list_401(client: AsyncClient):
    assert (await client.get(ADMIN_URL)).status_code == 401


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


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


@pytest.mark.asyncio
async def test_s05_unauthenticated_admin_status_update_401(client: AsyncClient):
    resp = await client.patch(f"{ADMIN_URL}/{uuid.uuid4()}/status", json={"status": "open"})
    assert resp.status_code == 401


@pytest.mark.asyncio
async def test_s05_invalid_bearer_token_returns_401(client: AsyncClient):
    """A tampered/garbage token is rejected the same as no token."""
    resp = await client.get(TENANT_URL, headers={"Authorization": "Bearer not-a-real-jwt"})
    assert resp.status_code == 401


# ── Closed ticket freeze — terminal-state business rule ──────────────────────
# A ticket closed by site_admin is frozen: no further replies from either
# side, and no further status transitions (including re-closing it). Not a
# tenant-isolation concern per se, but it lives here because it needs both
# a tenant client and a site_admin client, same as the S-01/S-04 tests above.


@pytest.mark.asyncio
async def test_tenant_reply_rejected_once_ticket_is_closed(client: AsyncClient, db_session: AsyncSession):
    headers = await _tenant_headers(db_session, "view_only")
    sa_headers = await _site_admin_headers(db_session)
    ticket = await _create_ticket(client, headers)

    close_resp = await client.patch(
        f"{ADMIN_URL}/{ticket['id']}/status", json={"status": "closed"}, headers=sa_headers
    )
    assert close_resp.status_code == 200

    reply_resp = await client.post(
        f"{TENANT_URL}/{ticket['id']}/messages", json={"body": "Still need help"}, headers=headers
    )
    assert reply_resp.status_code == 409

    detail = await client.get(f"{TENANT_URL}/{ticket['id']}", headers=headers)
    assert detail.json()["data"]["status"] == "closed"
    assert len(detail.json()["data"]["messages"]) == 1
