"""
Tests for INDL-53 — Marketing Who We Serve Category CMS, site-admin routes.

Covers the PRD's "Technical Scope" backend bullet:
  - GET /site-admin/who-we-serve returns all 4 fixed categories ordered by
    sort_order, gated to site_admin (401 unauthenticated, 403 for other
    roles)
  - PATCH /site-admin/who-we-serve/{category_key} validation (heading
    1-200, intro 10-1000, points 1-200 each, 4-8 point count) and a valid
    persisted update
  - Unknown category_key returns 404
  - Field whitelisting: schema uses `extra="forbid"`, so a client-supplied
    `category_key`/`sort_order` is rejected outright (422) rather than
    silently stripped or applied

The `indelis_test` DB is migration-managed and already seeded (migration
0060) with exactly 4 `who_we_serve_categories` rows (municipal, private,
funeral_groups, religious), sort_order 0-3. Per `tests/conftest.py`,
`db_session` only ever `flush()`s within a test and is rolled back in
teardown, so tests never permanently mutate that baseline seed for other
tests.
"""
import pytest
from uuid import uuid4

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

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


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


async def _site_admin_token(db: AsyncSession) -> str:
    from src.core.security import create_access_token, build_token_payload

    user = User(
        tenant_id=None,
        email=f"sa.ww.{id(db)}.{uuid4().hex[:8]}@indelis.com",
        password_hash=hash_password("Test1234!"),
        first_name="Site",
        last_name="Admin",
        role="site_admin",
        status="active",
    )
    db.add(user)
    await db.flush()
    return create_access_token(build_token_payload(user))


async def _tenant_role_token(db: AsyncSession, *, role: str = "administrator") -> str:
    from src.core.security import create_access_token, build_token_payload

    account = Account(
        organization_name="Who We Serve Test Cemetery",
        subdomain=f"wwtest-{uuid4().hex[:8]}",
        contact_email="admin@wwtest.ca",
        plan="starter",
        status="active",
    )
    db.add(account)
    await db.flush()

    user = User(
        tenant_id=account.id,
        email=f"{role}.ww.{uuid4().hex[:8]}@wwtest.ca",
        password_hash=hash_password("Test1234!"),
        first_name="Tenant",
        last_name="User",
        role=role,
        status="active",
    )
    db.add(user)
    await db.flush()
    return create_access_token(build_token_payload(user, account))


async def _get_who_we_serve(client: AsyncClient, token: str) -> list:
    resp = await client.get(
        "/api/site-admin/who-we-serve", headers={"Authorization": f"Bearer {token}"}
    )
    assert resp.status_code == 200
    return resp.json()["data"]


def _auth(token: str) -> dict:
    return {"Authorization": f"Bearer {token}"}


def _valid_points(n: int = 4) -> list:
    return [f"Valid point number {i}" for i in range(1, n + 1)]


# ── GET /site-admin/who-we-serve ─────────────────────────────────────────────


@pytest.mark.asyncio
async def test_get_who_we_serve_unauthenticated_returns_401(
    client: AsyncClient, db_session: AsyncSession
):
    resp = await client.get("/api/site-admin/who-we-serve")
    assert resp.status_code == 401


@pytest.mark.asyncio
async def test_get_who_we_serve_non_site_admin_returns_403(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _tenant_role_token(db_session, role="administrator")
    resp = await client.get("/api/site-admin/who-we-serve", headers=_auth(token))
    assert resp.status_code == 403


@pytest.mark.asyncio
async def test_get_who_we_serve_returns_4_categories_ordered(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    categories = await _get_who_we_serve(client, token)

    assert len(categories) == 4
    sort_orders = [c["sort_order"] for c in categories]
    assert sort_orders == [0, 1, 2, 3]

    keys = {c["category_key"] for c in categories}
    assert keys == {"municipal", "private", "funeral_groups", "religious"}

    for c in categories:
        assert set(c.keys()) >= {
            "category_key",
            "heading",
            "intro",
            "points",
            "sort_order",
        }


# ── PATCH /site-admin/who-we-serve/{category_key} — validation ──────────────


@pytest.mark.asyncio
async def test_update_blank_heading_returns_422(client: AsyncClient, db_session: AsyncSession):
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal",
        json={"heading": "   ", "intro": "A valid intro paragraph.", "points": _valid_points()},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Heading is required"


@pytest.mark.asyncio
async def test_update_heading_max_200_chars_returns_422(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal",
        json={"heading": "H" * 201, "intro": "A valid intro paragraph.", "points": _valid_points()},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Max 200 characters"


@pytest.mark.asyncio
async def test_update_blank_intro_returns_422(client: AsyncClient, db_session: AsyncSession):
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal",
        json={"heading": "Valid heading", "intro": "   ", "points": _valid_points()},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Intro is required"


@pytest.mark.asyncio
async def test_update_intro_too_short_returns_422(client: AsyncClient, db_session: AsyncSession):
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal",
        json={"heading": "Valid heading", "intro": "short", "points": _valid_points()},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Must be at least 10 characters"


@pytest.mark.asyncio
async def test_update_intro_too_long_returns_422(client: AsyncClient, db_session: AsyncSession):
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal",
        json={"heading": "Valid heading", "intro": "I" * 1001, "points": _valid_points()},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Max 1,000 characters"


@pytest.mark.asyncio
async def test_update_blank_point_returns_422(client: AsyncClient, db_session: AsyncSession):
    token = await _site_admin_token(db_session)
    points = _valid_points()
    points[1] = "   "
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal",
        json={"heading": "Valid heading", "intro": "A valid intro paragraph.", "points": points},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Point text is required"


@pytest.mark.asyncio
async def test_update_point_too_long_returns_422(client: AsyncClient, db_session: AsyncSession):
    token = await _site_admin_token(db_session)
    points = _valid_points()
    points[0] = "P" * 201
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal",
        json={"heading": "Valid heading", "intro": "A valid intro paragraph.", "points": points},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Max 200 characters"


@pytest.mark.asyncio
async def test_update_points_below_4_returns_422(client: AsyncClient, db_session: AsyncSession):
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal",
        json={
            "heading": "Valid heading",
            "intro": "A valid intro paragraph.",
            "points": _valid_points(3),
        },
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "At least 4 points are required"


@pytest.mark.asyncio
async def test_update_points_above_8_returns_422(client: AsyncClient, db_session: AsyncSession):
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal",
        json={
            "heading": "Valid heading",
            "intro": "A valid intro paragraph.",
            "points": _valid_points(9),
        },
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Maximum 8 points"


@pytest.mark.asyncio
async def test_update_exactly_4_points_succeeds(client: AsyncClient, db_session: AsyncSession):
    """The lower bound of BETWEEN 4 AND 8 is inclusive -- exactly 4 points
    must succeed, not just >4. Guards against a regression that flips
    `< 4` to `<= 4` in _validate_ww_points."""
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal",
        json={
            "heading": "Valid heading",
            "intro": "A valid intro paragraph.",
            "points": _valid_points(4),
        },
        headers=_auth(token),
    )
    assert resp.status_code == 200
    assert len(resp.json()["data"]["points"]) == 4


@pytest.mark.asyncio
async def test_update_exactly_8_points_succeeds(client: AsyncClient, db_session: AsyncSession):
    """The upper bound of BETWEEN 4 AND 8 is inclusive -- exactly 8 points
    must succeed, not just <8. Guards against a regression that flips
    `> 8` to `>= 8` in _validate_ww_points."""
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal",
        json={
            "heading": "Valid heading",
            "intro": "A valid intro paragraph.",
            "points": _valid_points(8),
        },
        headers=_auth(token),
    )
    assert resp.status_code == 200
    assert len(resp.json()["data"]["points"]) == 8


@pytest.mark.asyncio
async def test_update_valid_returns_200_and_persists(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    payload = {
        "heading": "Updated municipal heading",
        "intro": "Updated municipal intro paragraph text.",
        "points": _valid_points(5),
    }
    resp = await client.patch(
        "/api/site-admin/who-we-serve/municipal", json=payload, headers=_auth(token)
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["message"] == "Municipal category updated."
    assert body["data"]["heading"] == "Updated municipal heading"
    assert body["data"]["intro"] == "Updated municipal intro paragraph text."
    assert body["data"]["points"] == _valid_points(5)

    categories = await _get_who_we_serve(client, token)
    municipal = next(c for c in categories if c["category_key"] == "municipal")
    assert municipal["heading"] == "Updated municipal heading"
    assert municipal["intro"] == "Updated municipal intro paragraph text."
    assert municipal["points"] == _valid_points(5)


@pytest.mark.asyncio
async def test_update_unknown_category_key_returns_404(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/who-we-serve/not-a-real-key",
        json={
            "heading": "Valid heading",
            "intro": "A valid intro paragraph.",
            "points": _valid_points(),
        },
        headers=_auth(token),
    )
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_update_extra_field_category_key_rejected(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    before = await _get_who_we_serve(client, token)
    before_private = next(c for c in before if c["category_key"] == "private")

    resp = await client.patch(
        "/api/site-admin/who-we-serve/private",
        json={
            "heading": "Should not apply",
            "intro": "A valid intro paragraph.",
            "points": _valid_points(),
            "category_key": "private",
        },
        headers=_auth(token),
    )
    assert resp.status_code == 422

    after = await _get_who_we_serve(client, token)
    after_private = next(c for c in after if c["category_key"] == "private")
    assert after_private["heading"] == before_private["heading"]


@pytest.mark.asyncio
async def test_update_extra_field_sort_order_rejected(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    before = await _get_who_we_serve(client, token)
    before_private = next(c for c in before if c["category_key"] == "private")

    resp = await client.patch(
        "/api/site-admin/who-we-serve/private",
        json={
            "heading": "Should not apply",
            "intro": "A valid intro paragraph.",
            "points": _valid_points(),
            "sort_order": 99,
        },
        headers=_auth(token),
    )
    assert resp.status_code == 422

    after = await _get_who_we_serve(client, token)
    after_private = next(c for c in after if c["category_key"] == "private")
    assert after_private["heading"] == before_private["heading"]
    assert after_private["sort_order"] != 99
