"""
Tests for INDL-52 — Marketing Home FAQ Section CMS, site-admin routes.

Covers the PRD's "Technical Scope" backend bullet:
  - GET /site-admin/faq returns settings + items ordered by sort_order,
    gated to site_admin (401 unauthenticated, 403 for other roles)
  - PATCH /site-admin/faq/settings validation (heading required + max
    lengths on heading/subheading/intro) and a valid persisted update
  - POST /site-admin/faq/items validation (question 5-300, answer
    10-2000) and a valid create appended at the end (max(sort_order)+1)
  - PATCH /site-admin/faq/items/{id} same validation, 404 for unknown id,
    partial update (only the field(s) present in the payload change)
  - DELETE /site-admin/faq/items/{id}: 204 above the 5-item floor, 409 at
    the floor, 404 for unknown id
  - PATCH /site-admin/faq/items/reorder persists sort_order in submitted
    order; 404 for an unknown id in the list
  - Field whitelisting: schemas use `extra="forbid"`, so a client-supplied
    `sort_order`/`deleted_at` is rejected outright (422) rather than
    silently stripped or applied

The `indelis_test` DB is migration-managed and already seeded (migration
0059) with 1 `faq_section_settings` row and 7 `faq_items` rows (sort_order
0-6). Per `tests/conftest.py`, `db_session` only ever `flush()`s within a
test and is rolled back in teardown, so tests never mutate that baseline
seed for other tests -- but they DO observe it (every test starts from
that same 7-item baseline), so assertions here are written relative to
whatever the current active count is, not hardcoded to "7".
"""
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.faq.{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="Faq Test Cemetery",
        subdomain=f"faqtest-{uuid4().hex[:8]}",
        contact_email="admin@faqtest.ca",
        plan="starter",
        status="active",
    )
    db.add(account)
    await db.flush()

    user = User(
        tenant_id=account.id,
        email=f"{role}.faq.{uuid4().hex[:8]}@faqtest.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_faq(client: AsyncClient, token: str) -> dict:
    resp = await client.get(
        "/api/site-admin/faq", headers={"Authorization": f"Bearer {token}"}
    )
    assert resp.status_code == 200
    return resp.json()["data"]


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


# ── GET /site-admin/faq ──────────────────────────────────────────────────────


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


@pytest.mark.asyncio
async def test_get_faq_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/faq", headers=_auth(token))
    assert resp.status_code == 403


@pytest.mark.asyncio
async def test_get_faq_returns_settings_and_items_ordered(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    data = await _get_faq(client, token)

    assert data["settings"] is not None
    assert "heading" in data["settings"]
    assert data["settings"]["heading"]

    items = data["items"]
    assert len(items) >= 7  # migration 0059 seeds 7
    sort_orders = [i["sort_order"] for i in items]
    assert sort_orders == sorted(sort_orders)
    for i in items:
        assert set(i.keys()) >= {"id", "question", "answer", "sort_order"}


# ── PATCH /site-admin/faq/settings — validation ──────────────────────────────


@pytest.mark.asyncio
async def test_update_settings_blank_heading_returns_422(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/faq/settings",
        json={"heading": "   ", "subheading": "ok", "intro": None},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Heading is required"


@pytest.mark.asyncio
async def test_update_settings_missing_heading_field_returns_422(
    client: AsyncClient, db_session: AsyncSession
):
    """Omitting `heading` entirely defaults it to None -- still required."""
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/faq/settings",
        json={"subheading": "ok"},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Heading is required"


@pytest.mark.asyncio
async def test_update_settings_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/faq/settings",
        json={"heading": "H" * 201},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Max 200 characters"


@pytest.mark.asyncio
async def test_update_settings_subheading_max_300_chars_returns_422(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        "/api/site-admin/faq/settings",
        json={"heading": "Valid heading", "subheading": "S" * 301},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Max 300 characters"


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


@pytest.mark.asyncio
async def test_update_settings_valid_returns_200_and_persists(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    payload = {
        "heading": "New FAQ heading",
        "subheading": "New subheading",
        "intro": "New intro paragraph.",
    }
    resp = await client.patch(
        "/api/site-admin/faq/settings", json=payload, headers=_auth(token)
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["message"] == "FAQ section updated."
    assert body["data"]["heading"] == "New FAQ heading"
    assert body["data"]["subheading"] == "New subheading"
    assert body["data"]["intro"] == "New intro paragraph."

    data = await _get_faq(client, token)
    assert data["settings"]["heading"] == "New FAQ heading"
    assert data["settings"]["subheading"] == "New subheading"
    assert data["settings"]["intro"] == "New intro paragraph."


@pytest.mark.asyncio
async def test_update_settings_extra_field_rejected(
    client: AsyncClient, db_session: AsyncSession
):
    """extra="forbid" must reject an unknown field outright (422), not
    silently strip it and proceed."""
    token = await _site_admin_token(db_session)
    before = await _get_faq(client, token)

    resp = await client.patch(
        "/api/site-admin/faq/settings",
        json={"heading": "Should not apply", "id": str(uuid4())},
        headers=_auth(token),
    )
    assert resp.status_code == 422

    after = await _get_faq(client, token)
    assert after["settings"]["heading"] == before["settings"]["heading"]


# ── POST /site-admin/faq/items — validation ──────────────────────────────────


@pytest.mark.asyncio
async def test_create_item_blank_question_returns_422(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    resp = await client.post(
        "/api/site-admin/faq/items",
        json={"question": "   ", "answer": "A valid answer here."},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Question is required"


@pytest.mark.asyncio
async def test_create_item_question_too_short_returns_422(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    resp = await client.post(
        "/api/site-admin/faq/items",
        json={"question": "Hi?", "answer": "A valid answer here."},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Must be at least 5 characters"


@pytest.mark.asyncio
async def test_create_item_question_too_long_returns_422(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    resp = await client.post(
        "/api/site-admin/faq/items",
        json={"question": "Q" * 301, "answer": "A valid answer here."},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Max 300 characters"


@pytest.mark.asyncio
async def test_create_item_blank_answer_returns_422(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    resp = await client.post(
        "/api/site-admin/faq/items",
        json={"question": "A valid question?", "answer": "   "},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Answer is required"


@pytest.mark.asyncio
async def test_create_item_answer_too_short_returns_422(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    resp = await client.post(
        "/api/site-admin/faq/items",
        json={"question": "A valid question?", "answer": "short"},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Must be at least 10 characters"


@pytest.mark.asyncio
async def test_create_item_answer_too_long_returns_422(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    resp = await client.post(
        "/api/site-admin/faq/items",
        json={"question": "A valid question?", "answer": "A" * 2001},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Max 2,000 characters"


@pytest.mark.asyncio
async def test_create_item_valid_returns_201_and_appends_at_end(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    before = await _get_faq(client, token)
    max_sort_before = max(i["sort_order"] for i in before["items"])

    resp = await client.post(
        "/api/site-admin/faq/items",
        json={"question": "Is this a brand new FAQ question?", "answer": "Yes, appended at the end."},
        headers=_auth(token),
    )
    assert resp.status_code == 201
    body = resp.json()
    assert body["message"] == "FAQ added."
    new_item = body["data"]
    assert new_item["sort_order"] == max_sort_before + 1

    after = await _get_faq(client, token)
    assert after["items"][-1]["id"] == new_item["id"]
    assert len(after["items"]) == len(before["items"]) + 1


@pytest.mark.asyncio
async def test_create_item_extra_field_sort_order_rejected(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    before = await _get_faq(client, token)

    resp = await client.post(
        "/api/site-admin/faq/items",
        json={
            "question": "Can a client set sort_order directly?",
            "answer": "No, this must be rejected outright.",
            "sort_order": 9999,
        },
        headers=_auth(token),
    )
    assert resp.status_code == 422

    after = await _get_faq(client, token)
    assert len(after["items"]) == len(before["items"])
    assert all(i["sort_order"] != 9999 for i in after["items"])


# ── PATCH /site-admin/faq/items/{id} ─────────────────────────────────────────


@pytest.mark.asyncio
async def test_update_item_not_found_returns_404(client: AsyncClient, db_session: AsyncSession):
    token = await _site_admin_token(db_session)
    resp = await client.patch(
        f"/api/site-admin/faq/items/{uuid4()}",
        json={"question": "Updated question here?"},
        headers=_auth(token),
    )
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_update_item_validation_reused_on_patch(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    created = (
        await client.post(
            "/api/site-admin/faq/items",
            json={"question": "Original question here?", "answer": "Original answer text."},
            headers=_auth(token),
        )
    ).json()["data"]

    resp = await client.patch(
        f"/api/site-admin/faq/items/{created['id']}",
        json={"question": "no"},
        headers=_auth(token),
    )
    assert resp.status_code == 422
    assert resp.json()["message"] == "Must be at least 5 characters"


@pytest.mark.asyncio
async def test_update_item_partial_update_only_changes_supplied_field(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    created = (
        await client.post(
            "/api/site-admin/faq/items",
            json={"question": "Original question here?", "answer": "Original answer text."},
            headers=_auth(token),
        )
    ).json()["data"]

    resp = await client.patch(
        f"/api/site-admin/faq/items/{created['id']}",
        json={"question": "Updated question only, answer stays?"},
        headers=_auth(token),
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["message"] == "FAQ updated."
    assert body["data"]["question"] == "Updated question only, answer stays?"
    assert body["data"]["answer"] == "Original answer text."


@pytest.mark.asyncio
async def test_update_item_extra_field_deleted_at_rejected(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    created = (
        await client.post(
            "/api/site-admin/faq/items",
            json={"question": "Original question here?", "answer": "Original answer text."},
            headers=_auth(token),
        )
    ).json()["data"]

    resp = await client.patch(
        f"/api/site-admin/faq/items/{created['id']}",
        json={"question": "Still a valid question?", "deleted_at": "2020-01-01T00:00:00Z"},
        headers=_auth(token),
    )
    assert resp.status_code == 422

    data = await _get_faq(client, token)
    match = next(i for i in data["items"] if i["id"] == created["id"])
    # The extra-field request was rejected outright -- question must be unchanged.
    assert match["question"] == "Original question here?"


# ── DELETE /site-admin/faq/items/{id} — min-5 floor ──────────────────────────


@pytest.mark.asyncio
async def test_delete_item_not_found_returns_404(client: AsyncClient, db_session: AsyncSession):
    token = await _site_admin_token(db_session)
    resp = await client.delete(
        f"/api/site-admin/faq/items/{uuid4()}", headers=_auth(token)
    )
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_delete_item_succeeds_when_more_than_5_active_remain(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    before = await _get_faq(client, token)
    active_before = len(before["items"])
    assert active_before > 5  # seed baseline guarantees this

    target = before["items"][0]["id"]
    resp = await client.delete(
        f"/api/site-admin/faq/items/{target}", headers=_auth(token)
    )
    assert resp.status_code == 204

    after = await _get_faq(client, token)
    assert len(after["items"]) == active_before - 1
    assert all(i["id"] != target for i in after["items"])


@pytest.mark.asyncio
async def test_delete_item_blocked_at_5_active_floor(
    client: AsyncClient, db_session: AsyncSession
):
    """Deleting must succeed all the way down to exactly 5 active items,
    then a further delete (which would drop to 4) must 409."""
    token = await _site_admin_token(db_session)
    data = await _get_faq(client, token)
    active_ids = [i["id"] for i in data["items"]]

    # Delete items one at a time until exactly 5 active items remain.
    while len(active_ids) > 5:
        item_id = active_ids.pop()
        resp = await client.delete(
            f"/api/site-admin/faq/items/{item_id}", headers=_auth(token)
        )
        assert resp.status_code == 204

    data = await _get_faq(client, token)
    assert len(data["items"]) == 5

    # One more delete would drop below the floor -- must be blocked.
    last_id = data["items"][0]["id"]
    resp = await client.delete(
        f"/api/site-admin/faq/items/{last_id}", headers=_auth(token)
    )
    assert resp.status_code == 409
    assert "5" in resp.json()["message"]

    # Confirm the item was NOT deleted.
    after = await _get_faq(client, token)
    assert len(after["items"]) == 5
    assert any(i["id"] == last_id for i in after["items"])


# ── PATCH /site-admin/faq/items/reorder ──────────────────────────────────────


@pytest.mark.asyncio
async def test_reorder_persists_submitted_order(
    client: AsyncClient, db_session: AsyncSession
):
    token = await _site_admin_token(db_session)
    data = await _get_faq(client, token)
    ids = [i["id"] for i in data["items"]]
    reversed_ids = list(reversed(ids))

    resp = await client.patch(
        "/api/site-admin/faq/items/reorder",
        json={"order": reversed_ids},
        headers=_auth(token),
    )
    assert resp.status_code == 200
    assert resp.json()["message"] == "FAQ order updated."

    after = await _get_faq(client, token)
    after_ids = [i["id"] for i in after["items"]]
    assert after_ids == reversed_ids
    # sort_order values match the submitted list's index order
    for index, item in enumerate(after["items"]):
        assert item["sort_order"] == index


@pytest.mark.asyncio
async def test_reorder_unknown_id_returns_404(client: AsyncClient, db_session: AsyncSession):
    token = await _site_admin_token(db_session)
    data = await _get_faq(client, token)
    ids = [i["id"] for i in data["items"]]
    ids_with_unknown = ids + [str(uuid4())]

    resp = await client.patch(
        "/api/site-admin/faq/items/reorder",
        json={"order": ids_with_unknown},
        headers=_auth(token),
    )
    assert resp.status_code == 404
