"""INDL-33 Crew Members — schema validation + API integration tests.

Covers the security acceptance criteria: auth required (S-01), role guards
(S-02/S-03), cross-tenant isolation (S-04), soft-delete 404 (S-08),
mass-assignment rejection (S-05), page_size/search caps (S-06), duplicate
email (409) and email reuse after soft-delete.
"""
import pytest
import pytest_asyncio
from httpx import AsyncClient
from pydantic import ValidationError

from src.apps.crew_members.schemas.requests import (
    CreateCrewMemberRequest,
    UpdateCrewMemberRequest,
)

BASE = "/api/v1/crew-members"


def _payload(**overrides) -> dict:
    data = {
        "name": "John Smith",
        "email": "j.smith@example.com",
        "phone": "613-555-0101",
        "address": "123 Main St, ON",
    }
    data.update(overrides)
    return data


# ── Schema-level unit tests (no DB) ──────────────────────────────────────────

def test_create_requires_valid_email():
    with pytest.raises(ValidationError):
        CreateCrewMemberRequest(**_payload(email="not-an-email"))


def test_create_rejects_extra_fields():
    # Mass-assignment guard (S-05 / API3)
    with pytest.raises(ValidationError):
        CreateCrewMemberRequest(**_payload(tenant_id="00000000-0000-0000-0000-000000000000"))
    with pytest.raises(ValidationError):
        CreateCrewMemberRequest(**_payload(is_available=False))


def test_create_rejects_blank_name():
    with pytest.raises(ValidationError):
        CreateCrewMemberRequest(**_payload(name="   "))


def test_create_rejects_bad_phone():
    with pytest.raises(ValidationError):
        CreateCrewMemberRequest(**_payload(phone="abc"))


def test_update_rejects_extra_fields():
    with pytest.raises(ValidationError):
        UpdateCrewMemberRequest(**_payload(id="00000000-0000-0000-0000-000000000000"))


# ── Fixtures for role/tenant matrix ──────────────────────────────────────────

def _token(user, account) -> str:
    from src.core.security import build_token_payload, create_access_token
    return create_access_token(build_token_payload(user, account))


async def _make_user(db_session, account, role, email):
    from src.apps.auth.models.user import User
    from src.core.security import hash_password
    user = User(
        tenant_id=account.id,
        email=email,
        password_hash=hash_password("TestPassword123"),
        first_name=role,
        last_name="User",
        role=role,
        status="active",
    )
    db_session.add(user)
    await db_session.flush()
    return user


@pytest_asyncio.fixture
async def manager_headers(db_session, test_account):
    user = await _make_user(db_session, test_account, "manager", "mgr@testcemetery.com")
    return {"Authorization": f"Bearer {_token(user, test_account)}"}


@pytest_asyncio.fixture
async def staff_headers(db_session, test_account):
    user = await _make_user(db_session, test_account, "staff", "staff@testcemetery.com")
    return {"Authorization": f"Bearer {_token(user, test_account)}"}


@pytest_asyncio.fixture
async def viewonly_headers(db_session, test_account):
    user = await _make_user(db_session, test_account, "view_only", "vo@testcemetery.com")
    return {"Authorization": f"Bearer {_token(user, test_account)}"}


@pytest_asyncio.fixture
async def other_tenant(db_session):
    from src.apps.tenants.models.account import Account
    account = Account(
        organization_name="Other Cemetery",
        subdomain="othercemetery",
        contact_email="admin@othercemetery.com",
        plan="starter",
        status="active",
    )
    db_session.add(account)
    await db_session.flush()
    return account


# ── Happy-path CRUD ──────────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_create_and_list(client: AsyncClient, auth_headers):
    res = await client.post(BASE, headers=auth_headers, json=_payload())
    assert res.status_code == 201
    body = res.json()
    assert body["success"] is True
    assert body["data"]["is_available"] is True
    assert body["data"]["name"] == "John Smith"
    # tenant_id must not leak into the response payload
    assert "tenant_id" not in body["data"]

    res = await client.get(BASE, headers=auth_headers)
    assert res.status_code == 200
    lst = res.json()
    assert lst["total"] >= 1
    assert lst["available_count"] >= 1
    assert "page" in lst and "page_size" in lst and "pages" in lst


@pytest.mark.asyncio
async def test_update_and_get(client: AsyncClient, auth_headers):
    created = (await client.post(BASE, headers=auth_headers, json=_payload())).json()["data"]
    cid = created["id"]

    res = await client.put(
        f"{BASE}/{cid}", headers=auth_headers, json=_payload(name="Jane Doe")
    )
    assert res.status_code == 200
    assert res.json()["data"]["name"] == "Jane Doe"

    res = await client.get(f"{BASE}/{cid}", headers=auth_headers)
    assert res.status_code == 200
    assert res.json()["data"]["name"] == "Jane Doe"


@pytest.mark.asyncio
async def test_toggle_availability(client: AsyncClient, auth_headers):
    cid = (await client.post(BASE, headers=auth_headers, json=_payload())).json()["data"]["id"]
    res = await client.patch(f"{BASE}/{cid}/availability", headers=auth_headers)
    assert res.status_code == 200
    assert res.json()["data"]["is_available"] is False
    res = await client.patch(f"{BASE}/{cid}/availability", headers=auth_headers)
    assert res.json()["data"]["is_available"] is True


@pytest.mark.asyncio
async def test_delete_then_404(client: AsyncClient, auth_headers):
    cid = (await client.post(BASE, headers=auth_headers, json=_payload())).json()["data"]["id"]
    res = await client.delete(f"{BASE}/{cid}", headers=auth_headers)
    assert res.status_code == 204
    res = await client.get(f"{BASE}/{cid}", headers=auth_headers)
    assert res.status_code == 404  # S-08 soft-deleted not accessible


# ── Duplicate email / reuse after delete ─────────────────────────────────────

@pytest.mark.asyncio
async def test_duplicate_email_conflict(client: AsyncClient, auth_headers):
    await client.post(BASE, headers=auth_headers, json=_payload(email="dup@example.com"))
    res = await client.post(BASE, headers=auth_headers, json=_payload(email="dup@example.com"))
    assert res.status_code == 409


@pytest.mark.asyncio
async def test_email_reusable_after_soft_delete(client: AsyncClient, auth_headers):
    cid = (
        await client.post(BASE, headers=auth_headers, json=_payload(email="reuse@example.com"))
    ).json()["data"]["id"]
    await client.delete(f"{BASE}/{cid}", headers=auth_headers)
    res = await client.post(BASE, headers=auth_headers, json=_payload(email="reuse@example.com"))
    assert res.status_code == 201  # A04 — reuse allowed after soft-delete


# ── AuthN / AuthZ ────────────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_requires_authentication(client: AsyncClient):
    assert (await client.get(BASE)).status_code == 401  # S-01
    assert (await client.post(BASE, json=_payload())).status_code == 401


@pytest.mark.asyncio
async def test_staff_and_viewonly_cannot_write(
    client: AsyncClient, staff_headers, viewonly_headers
):
    for headers in (staff_headers, viewonly_headers):
        assert (await client.post(BASE, headers=headers, json=_payload())).status_code == 403


@pytest.mark.asyncio
async def test_viewonly_can_read(client: AsyncClient, viewonly_headers):
    assert (await client.get(BASE, headers=viewonly_headers)).status_code == 200


@pytest.mark.asyncio
async def test_manager_can_create_but_not_delete(
    client: AsyncClient, manager_headers
):
    cid = (
        await client.post(BASE, headers=manager_headers, json=_payload(email="m@example.com"))
    ).json()["data"]["id"]
    res = await client.delete(f"{BASE}/{cid}", headers=manager_headers)
    assert res.status_code == 403  # S-03 manager cannot delete


# ── Cross-tenant isolation ───────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_cross_tenant_returns_404(
    client: AsyncClient, auth_headers, db_session, other_tenant
):
    from src.apps.crew_members.models.crew_member import CrewMember
    other = CrewMember(
        tenant_id=other_tenant.id,
        name="Foreign Staff",
        email="foreign@example.com",
        phone="613-555-9999",
        address="Elsewhere",
    )
    db_session.add(other)
    await db_session.flush()

    # Tenant A (auth_headers) must not see Tenant B's record — 404, not 200/403.
    assert (await client.get(f"{BASE}/{other.id}", headers=auth_headers)).status_code == 404
    assert (
        await client.put(f"{BASE}/{other.id}", headers=auth_headers, json=_payload())
    ).status_code == 404


# ── Input caps ───────────────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_page_size_cap(client: AsyncClient, auth_headers):
    assert (await client.get(f"{BASE}?page_size=1000", headers=auth_headers)).status_code == 422


@pytest.mark.asyncio
async def test_search_length_cap(client: AsyncClient, auth_headers):
    res = await client.get(f"{BASE}?search={'a' * 201}", headers=auth_headers)
    assert res.status_code == 422
