"""Tests for SCIM Directory Sync webhook handler."""

from __future__ import annotations

import hashlib
import hmac
import json
from unittest.mock import AsyncMock, MagicMock, patch

from fastapi.testclient import TestClient

_WEBHOOK_SECRET = "whsec_test_123"
_SCIM_MOD = "backend.api.routes.scim"


def _make_app() -> TestClient:
    """Create test FastAPI app with SCIM router."""
    from backend.api.routes.scim import scim_router
    from fastapi import FastAPI

    app = FastAPI()
    app.include_router(scim_router)
    return TestClient(app, raise_server_exceptions=False)


def _sign(payload: bytes) -> str:
    """Generate HMAC-SHA256 signature."""
    return hmac.new(_WEBHOOK_SECRET.encode(), payload, hashlib.sha256).hexdigest()


def _sso_config_data() -> dict[str, object]:
    return {
        "customer_id": "cust-123",
        "broker_connection_id": "conn_abc",
        "group_mapping": {
            "AdminGroup": "admin",
            "ViewerGroup": "viewer",
        },
    }


def _mock_sso_stream_factory():
    """Return an async generator yielding SSO config doc."""

    async def _stream():
        doc = MagicMock()
        doc.to_dict.return_value = _sso_config_data()
        yield doc

    return _stream


def _deep_mock(mock_db: MagicMock) -> MagicMock:
    """Get the deep subcollection.document mock."""
    col = mock_db.collection.return_value
    doc = col.document.return_value
    sub = doc.collection.return_value
    return sub.document.return_value


def _post_webhook(
    payload: dict[str, object],
) -> tuple[bytes, str]:
    """Serialize payload and sign it."""
    payload_bytes = json.dumps(payload).encode()
    return payload_bytes, _sign(payload_bytes)


def _patch_ctx(*extra_patches):
    """Return common patches for SCIM webhook tests."""
    return [
        patch(
            f"{_SCIM_MOD}._get_webhook_secret",
            return_value=_WEBHOOK_SECRET,
        ),
        *extra_patches,
    ]


class TestSCIMUserCreated:
    async def test_scim_user_created(self) -> None:
        """dsync.user.created -> creates member doc."""
        payload_bytes, sig = _post_webhook(
            {
                "event": "dsync.user.created",
                "connection_id": "conn_abc",
                "data": {
                    "emails": [{"value": "new@acme.com"}],
                    "groups": [{"name": "ViewerGroup"}],
                },
            }
        )

        mock_cust_doc = MagicMock()
        mock_cust_doc.exists = True
        mock_cust_doc.to_dict.return_value = {
            "tier": "enterprise",
        }

        async def mock_member_stream():
            yield MagicMock()

        mock_db = MagicMock()
        doc_rv = mock_db.collection.return_value.document
        doc_rv.return_value.get = AsyncMock(return_value=mock_cust_doc)
        sub = doc_rv.return_value.collection.return_value
        sub.document.return_value.set = AsyncMock()
        sub.where.return_value.stream = mock_member_stream

        sso_q = mock_db.collection.return_value.where.return_value.limit.return_value
        sso_q.stream = _mock_sso_stream_factory()

        with (
            patch(
                f"{_SCIM_MOD}._get_webhook_secret",
                return_value=_WEBHOOK_SECRET,
            ),
            patch(
                "backend.shared.firestore.get_db",
                return_value=mock_db,
            ),
            patch(
                f"{_SCIM_MOD}._write_scim_audit",
                new_callable=AsyncMock,
            ),
        ):
            client = _make_app()
            resp = client.post(
                "/api/v1/scim/webhook",
                content=payload_bytes,
                headers={
                    "workos-signature": sig,
                    "content-type": "application/json",
                },
            )

        assert resp.status_code == 201


class TestSCIMUserDeleted:
    async def test_scim_user_deleted(self) -> None:
        """dsync.user.deleted -> deprovisioned."""
        payload_bytes, sig = _post_webhook(
            {
                "event": "dsync.user.deleted",
                "connection_id": "conn_abc",
                "data": {
                    "emails": [{"value": "old@acme.com"}],
                },
            }
        )

        mock_member = MagicMock()
        mock_member.exists = True
        mock_member.to_dict.return_value = {
            "role": "viewer",
            "status": "active",
        }

        mock_db = MagicMock()
        doc_rv = mock_db.collection.return_value.document
        doc_rv.return_value.get = AsyncMock(return_value=mock_member)
        deep = _deep_mock(mock_db)
        deep.get = AsyncMock(return_value=mock_member)
        deep.update = AsyncMock()

        sso_q = mock_db.collection.return_value.where.return_value.limit.return_value
        sso_q.stream = _mock_sso_stream_factory()

        async def empty_stream():
            return
            yield  # type: ignore[misc]

        sessions_q = mock_db.collection.return_value.where.return_value
        sessions_q.stream = empty_stream

        with (
            patch(
                f"{_SCIM_MOD}._get_webhook_secret",
                return_value=_WEBHOOK_SECRET,
            ),
            patch(
                "backend.shared.firestore.get_db",
                return_value=mock_db,
            ),
            patch(
                f"{_SCIM_MOD}._write_scim_audit",
                new_callable=AsyncMock,
            ),
            patch(
                f"{_SCIM_MOD}._revoke_sessions_for_email",
                new_callable=AsyncMock,
            ),
        ):
            client = _make_app()
            resp = client.post(
                "/api/v1/scim/webhook",
                content=payload_bytes,
                headers={
                    "workos-signature": sig,
                    "content-type": "application/json",
                },
            )

        assert resp.status_code == 200
        assert resp.json()["status"] == "deprovisioned"


class TestSCIMOwnerProtection:
    async def test_scim_owner_cannot_be_deprovisioned(
        self,
    ) -> None:
        """Owner role -> silently skipped."""
        payload_bytes, sig = _post_webhook(
            {
                "event": "dsync.user.deleted",
                "connection_id": "conn_abc",
                "data": {
                    "emails": [{"value": "owner@acme.com"}],
                },
            }
        )

        mock_member = MagicMock()
        mock_member.exists = True
        mock_member.to_dict.return_value = {
            "role": "owner",
            "status": "active",
        }

        mock_db = MagicMock()
        doc_rv = mock_db.collection.return_value.document
        doc_rv.return_value.get = AsyncMock(return_value=mock_member)
        deep = _deep_mock(mock_db)
        deep.get = AsyncMock(return_value=mock_member)

        sso_q = mock_db.collection.return_value.where.return_value.limit.return_value
        sso_q.stream = _mock_sso_stream_factory()

        with (
            patch(
                f"{_SCIM_MOD}._get_webhook_secret",
                return_value=_WEBHOOK_SECRET,
            ),
            patch(
                "backend.shared.firestore.get_db",
                return_value=mock_db,
            ),
        ):
            client = _make_app()
            resp = client.post(
                "/api/v1/scim/webhook",
                content=payload_bytes,
                headers={
                    "workos-signature": sig,
                    "content-type": "application/json",
                },
            )

        assert resp.status_code == 200
        assert resp.json()["status"] == "owner_skipped"


class TestSCIMMemberLimit:
    async def test_scim_member_limit_reached(self) -> None:
        """At member limit -> 409."""
        payload_bytes, sig = _post_webhook(
            {
                "event": "dsync.user.created",
                "connection_id": "conn_abc",
                "data": {
                    "emails": [{"value": "new@acme.com"}],
                    "groups": [],
                },
            }
        )

        mock_cust = MagicMock()
        mock_cust.exists = True
        mock_cust.to_dict.return_value = {"tier": "team"}

        async def many_members():
            for _ in range(25):
                yield MagicMock()

        mock_db = MagicMock()
        doc_rv = mock_db.collection.return_value.document
        doc_rv.return_value.get = AsyncMock(return_value=mock_cust)
        sub = doc_rv.return_value.collection.return_value
        sub.where.return_value.stream = many_members

        sso_q = mock_db.collection.return_value.where.return_value.limit.return_value
        sso_q.stream = _mock_sso_stream_factory()

        with (
            patch(
                f"{_SCIM_MOD}._get_webhook_secret",
                return_value=_WEBHOOK_SECRET,
            ),
            patch(
                "backend.shared.firestore.get_db",
                return_value=mock_db,
            ),
        ):
            client = _make_app()
            resp = client.post(
                "/api/v1/scim/webhook",
                content=payload_bytes,
                headers={
                    "workos-signature": sig,
                    "content-type": "application/json",
                },
            )

        assert resp.status_code == 409


class TestSCIMSignatureValidation:
    async def test_scim_webhook_signature_invalid(
        self,
    ) -> None:
        """Invalid signature -> error."""
        payload = json.dumps({"event": "dsync.user.created"}).encode()

        with patch(
            f"{_SCIM_MOD}._get_webhook_secret",
            return_value=_WEBHOOK_SECRET,
        ):
            client = _make_app()
            resp = client.post(
                "/api/v1/scim/webhook",
                content=payload,
                headers={
                    "workos-signature": "invalid",
                    "content-type": "application/json",
                },
            )

        assert resp.status_code in (401, 500)


class TestSCIMGroupMapping:
    async def test_scim_group_mapping(self) -> None:
        """IdP group -> role lookup via group_mapping."""
        payload_bytes, sig = _post_webhook(
            {
                "event": "dsync.group.user_added",
                "connection_id": "conn_abc",
                "data": {
                    "user": {
                        "emails": [{"value": "user@acme.com"}],
                    },
                    "group": {"name": "AdminGroup"},
                },
            }
        )

        mock_member = MagicMock()
        mock_member.exists = True
        mock_member.to_dict.return_value = {
            "role": "viewer",
            "status": "active",
        }

        mock_db = MagicMock()
        doc_rv = mock_db.collection.return_value.document
        doc_rv.return_value.get = AsyncMock(return_value=mock_member)
        deep = _deep_mock(mock_db)
        deep.get = AsyncMock(return_value=mock_member)
        deep.update = AsyncMock()

        sso_q = mock_db.collection.return_value.where.return_value.limit.return_value
        sso_q.stream = _mock_sso_stream_factory()

        with (
            patch(
                f"{_SCIM_MOD}._get_webhook_secret",
                return_value=_WEBHOOK_SECRET,
            ),
            patch(
                "backend.shared.firestore.get_db",
                return_value=mock_db,
            ),
            patch(
                f"{_SCIM_MOD}._write_scim_audit",
                new_callable=AsyncMock,
            ),
        ):
            client = _make_app()
            resp = client.post(
                "/api/v1/scim/webhook",
                content=payload_bytes,
                headers={
                    "workos-signature": sig,
                    "content-type": "application/json",
                },
            )

        assert resp.status_code == 200
        assert resp.json()["status"] == "role_updated"
