"""Tests for Phase 17 — Team member management."""

from __future__ import annotations

import hashlib
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from backend.dashboard.middleware import CustomerContext, require_dashboard_auth
from backend.dashboard.routes import router
from fastapi import FastAPI
from fastapi.testclient import TestClient

_PATCH_FIRESTORE_DB = "backend.shared.firestore.get_db"
_PATCH_SEND_EMAIL = "backend.billing.email_sender.send_template_email"
_PATCH_TIER_CONFIG = "backend.shared.tier_config.TIER_CONFIG"


def _ctx(
    tier: str = "team",
    role: str = "owner",
    features: list[str] | None = None,
) -> CustomerContext:
    return CustomerContext(
        customer_id="cus_test",
        email="owner@example.com",
        tier=tier,
        role=role,
        member_email="owner@example.com",
        features=features
        or [
            "hosted_dashboard",
            "team_members",
            "audit_log",
            "webhooks",
            "shared_scan_links",
        ],
        project_limit=1,
        batch_limit=5,
        cancellation_pending=False,
        subscription_expires_at="2027-01-01T00:00:00+00:00",
        consent_accepted=True,
        tos_version="1.0",
        privacy_version="1.0",
    )


def _build_app(ctx: CustomerContext | None = None) -> FastAPI:
    app = FastAPI()
    app.include_router(router)
    if ctx is not None:
        app.dependency_overrides[require_dashboard_auth] = lambda: ctx
    return app


def _mock_db(
    members: list[dict[str, Any]] | None = None,
    sessions: list[dict[str, Any]] | None = None,
) -> MagicMock:
    """Build chainable mock Firestore DB with member docs."""
    mock_db = MagicMock()

    # Track per-collection behavior
    member_docs: list[MagicMock] = []
    if members:
        for m in members:
            doc = MagicMock()
            doc.id = m.get("_id", hashlib.sha256(m["email"].encode()).hexdigest())
            doc.exists = True
            doc.to_dict = MagicMock(return_value=m)
            member_docs.append(doc)

    session_docs: list[MagicMock] = []
    if sessions:
        for s in sessions:
            doc = MagicMock()
            doc.id = s.get("session_id", "sess_1")
            doc.exists = True
            doc.to_dict = MagicMock(return_value=s)
            session_docs.append(doc)

    # members subcollection
    members_coll = MagicMock()

    async def _members_stream() -> Any:
        for d in member_docs:
            yield d

    members_coll.stream = _members_stream

    # Individual member doc lookup
    def _member_doc(doc_id: str) -> MagicMock:
        doc_mock = MagicMock()
        found = [d for d in member_docs if d.id == doc_id]
        if found:
            doc_mock.get = AsyncMock(return_value=found[0])
        else:
            not_found = MagicMock()
            not_found.exists = False
            doc_mock.get = AsyncMock(return_value=not_found)
        doc_mock.set = AsyncMock()
        doc_mock.update = AsyncMock()
        doc_mock.delete = AsyncMock()
        return doc_mock

    members_coll.document = MagicMock(side_effect=_member_doc)

    # sessions collection
    sessions_coll = MagicMock()

    async def _sessions_stream() -> Any:
        for d in session_docs:
            yield d

    sessions_query = MagicMock()
    sessions_query.stream = _sessions_stream
    sessions_coll.where = MagicMock(return_value=sessions_query)

    def _sessions_doc(doc_id: str) -> MagicMock:
        d = MagicMock()
        d.delete = AsyncMock()
        return d

    sessions_coll.document = MagicMock(side_effect=_sessions_doc)

    # audit_log subcollection (for _audit helper)
    audit_coll = MagicMock()
    audit_doc_ref = MagicMock()
    audit_doc_ref.id = "audit_1"
    audit_coll.add = AsyncMock(return_value=(None, audit_doc_ref))

    # Wire up collection routing
    def _get_collection(name: str) -> MagicMock:
        if name == "sessions":
            return sessions_coll
        # "customers" returns a mock that chains to subcollections
        cust_coll = MagicMock()
        cust_doc = MagicMock()

        def _subcoll(sub_name: str) -> MagicMock:
            if sub_name == "members":
                return members_coll
            if sub_name == "audit_log":
                return audit_coll
            return MagicMock()

        cust_doc.collection = MagicMock(side_effect=_subcoll)
        cust_coll.document = MagicMock(return_value=cust_doc)
        return cust_coll

    mock_db.collection = MagicMock(side_effect=_get_collection)
    return mock_db


class TestListMembers:
    def test_list_members(self) -> None:
        ctx = _ctx()
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        members = [
            {
                "email": "admin@ex.com",
                "role": "admin",
                "status": "active",
                "invited_by": "owner@example.com",
                "invited_at": "2025-01-01T00:00:00+00:00",
                "accepted_at": "2025-01-02T00:00:00+00:00",
                "last_accessed_at": None,
            },
            {
                "email": "pending@ex.com",
                "role": "viewer",
                "status": "pending",
                "invited_by": "owner@example.com",
                "invited_at": "2025-01-01T00:00:00+00:00",
                "accepted_at": None,
                "last_accessed_at": None,
            },
            {
                "email": "removed@ex.com",
                "role": "viewer",
                "status": "removed",
                "invited_by": "owner@example.com",
                "invited_at": "2025-01-01T00:00:00+00:00",
                "accepted_at": None,
                "last_accessed_at": None,
            },
        ]

        with patch(_PATCH_FIRESTORE_DB, return_value=_mock_db(members=members)):
            resp = client.get("/dashboard/settings/members")

        assert resp.status_code == 200
        assert "admin@ex.com" in resp.text
        assert "pending@ex.com" in resp.text
        # removed members excluded from list
        assert "removed@ex.com" not in resp.text


class TestInviteMember:
    def test_invite_member_success(self) -> None:
        ctx = _ctx()
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        db = _mock_db(members=[])
        with (
            patch(_PATCH_FIRESTORE_DB, return_value=db),
            patch(_PATCH_SEND_EMAIL, new_callable=AsyncMock, return_value=True),
            patch("backend.dashboard.routes._validate_csrf", new_callable=AsyncMock),
        ):
            resp = client.post(
                "/dashboard/settings/members/invite",
                data={"email": "new@example.com", "role": "admin"},
                follow_redirects=False,
            )

        assert resp.status_code == 303

    def test_invite_member_duplicate(self) -> None:
        ctx = _ctx()
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        existing = [
            {
                "email": "dup@example.com",
                "role": "admin",
                "status": "active",
                "invited_by": "x",
                "invited_at": "2025-01-01T00:00:00+00:00",
                "accepted_at": "2025-01-01T00:00:00+00:00",
                "last_accessed_at": None,
            },
        ]
        db = _mock_db(members=existing)
        with (
            patch(_PATCH_FIRESTORE_DB, return_value=db),
            patch("backend.dashboard.routes._validate_csrf", new_callable=AsyncMock),
        ):
            resp = client.post(
                "/dashboard/settings/members/invite",
                data={"email": "dup@example.com", "role": "admin"},
                follow_redirects=False,
            )

        assert resp.status_code == 409

    def test_invite_member_over_limit(self) -> None:
        ctx = _ctx(tier="team")
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        # Create 10 active members (team limit)
        existing = [
            {
                "email": f"m{i}@ex.com",
                "role": "admin",
                "status": "active",
                "invited_by": "x",
                "invited_at": "2025-01-01T00:00:00+00:00",
                "accepted_at": "2025-01-01T00:00:00+00:00",
                "last_accessed_at": None,
            }
            for i in range(10)
        ]
        db = _mock_db(members=existing)
        with (
            patch(_PATCH_FIRESTORE_DB, return_value=db),
            patch("backend.dashboard.routes._validate_csrf", new_callable=AsyncMock),
        ):
            resp = client.post(
                "/dashboard/settings/members/invite",
                data={"email": "new@example.com", "role": "admin"},
                follow_redirects=False,
            )

        assert resp.status_code == 409

    def test_invite_member_viewer_forbidden(self) -> None:
        ctx = _ctx(role="viewer")
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        with patch("backend.dashboard.routes._validate_csrf", new_callable=AsyncMock):
            resp = client.post(
                "/dashboard/settings/members/invite",
                data={"email": "x@ex.com", "role": "admin"},
                follow_redirects=False,
            )

        assert resp.status_code == 403

    def test_invite_member_no_feature(self) -> None:
        ctx = _ctx(features=["hosted_dashboard"])
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        with patch("backend.dashboard.routes._validate_csrf", new_callable=AsyncMock):
            resp = client.post(
                "/dashboard/settings/members/invite",
                data={"email": "x@ex.com", "role": "admin"},
                follow_redirects=False,
            )

        assert resp.status_code == 403


class TestRemoveMember:
    def test_remove_member_success(self) -> None:
        ctx = _ctx()
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        email = "admin@ex.com"
        email_h = hashlib.sha256(email.encode()).hexdigest()
        members = [
            {
                "email": email,
                "role": "admin",
                "status": "active",
                "invited_by": "x",
                "invited_at": "2025-01-01T00:00:00+00:00",
                "accepted_at": "2025-01-01T00:00:00+00:00",
                "last_accessed_at": None,
                "_id": email_h,
            },
        ]
        db = _mock_db(members=members, sessions=[{"email": email, "session_id": "s1"}])
        with (
            patch(_PATCH_FIRESTORE_DB, return_value=db),
            patch("backend.dashboard.routes._validate_csrf", new_callable=AsyncMock),
        ):
            resp = client.delete(
                f"/dashboard/settings/members/{email_h}",
                follow_redirects=False,
            )

        assert resp.status_code == 303

    def test_remove_member_owner_forbidden(self) -> None:
        ctx = _ctx()
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        email = "owner@example.com"
        email_h = hashlib.sha256(email.encode()).hexdigest()
        members = [
            {
                "email": email,
                "role": "owner",
                "status": "active",
                "invited_by": "",
                "invited_at": "2025-01-01T00:00:00+00:00",
                "accepted_at": "2025-01-01T00:00:00+00:00",
                "last_accessed_at": None,
                "_id": email_h,
            },
        ]
        db = _mock_db(members=members)
        with (
            patch(_PATCH_FIRESTORE_DB, return_value=db),
            patch("backend.dashboard.routes._validate_csrf", new_callable=AsyncMock),
        ):
            resp = client.delete(
                f"/dashboard/settings/members/{email_h}",
                follow_redirects=False,
            )

        assert resp.status_code == 409


class TestUpdateRole:
    def test_update_role_success(self) -> None:
        ctx = _ctx()
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        email = "admin@ex.com"
        email_h = hashlib.sha256(email.encode()).hexdigest()
        members = [
            {
                "email": email,
                "role": "admin",
                "status": "active",
                "invited_by": "x",
                "invited_at": "2025-01-01T00:00:00+00:00",
                "accepted_at": "2025-01-01T00:00:00+00:00",
                "last_accessed_at": None,
                "_id": email_h,
            },
        ]
        db = _mock_db(members=members)
        with (
            patch(_PATCH_FIRESTORE_DB, return_value=db),
            patch("backend.dashboard.routes._validate_csrf", new_callable=AsyncMock),
        ):
            resp = client.put(
                f"/dashboard/settings/members/{email_h}/role",
                data={"role": "viewer"},
                follow_redirects=False,
            )

        assert resp.status_code == 303

    def test_update_role_owner_forbidden(self) -> None:
        ctx = _ctx()
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        email = "owner@example.com"
        email_h = hashlib.sha256(email.encode()).hexdigest()
        members = [
            {
                "email": email,
                "role": "owner",
                "status": "active",
                "invited_by": "",
                "invited_at": "2025-01-01T00:00:00+00:00",
                "accepted_at": "2025-01-01T00:00:00+00:00",
                "last_accessed_at": None,
                "_id": email_h,
            },
        ]
        db = _mock_db(members=members)
        with (
            patch(_PATCH_FIRESTORE_DB, return_value=db),
            patch("backend.dashboard.routes._validate_csrf", new_callable=AsyncMock),
        ):
            resp = client.put(
                f"/dashboard/settings/members/{email_h}/role",
                data={"role": "viewer"},
                follow_redirects=False,
            )

        assert resp.status_code == 409


class TestAcceptInvitation:
    @pytest.mark.anyio
    async def test_accept_invitation(self) -> None:
        email = "new@example.com"
        email_h = hashlib.sha256(email.lower().encode()).hexdigest()
        members = [
            {
                "email": email,
                "role": "viewer",
                "status": "pending",
                "invited_by": "x",
                "invited_at": "2025-01-01T00:00:00+00:00",
                "accepted_at": None,
                "last_accessed_at": None,
                "_id": email_h,
            },
        ]
        db = _mock_db(members=members)
        with patch(_PATCH_FIRESTORE_DB, return_value=db):
            from backend.dashboard.members import accept_invitation

            await accept_invitation("cus_test", email)


class TestResendInvitation:
    def test_resend_invitation(self) -> None:
        ctx = _ctx()
        app = _build_app(ctx)
        client = TestClient(app, raise_server_exceptions=False)
        client.cookies.set("__Host-session", "encrypted")

        email = "pending@ex.com"
        email_h = hashlib.sha256(email.encode()).hexdigest()
        members = [
            {
                "email": email,
                "role": "viewer",
                "status": "pending",
                "invited_by": "x",
                "invited_at": "2025-01-01T00:00:00+00:00",
                "accepted_at": None,
                "last_accessed_at": None,
                "_id": email_h,
            },
        ]
        db = _mock_db(members=members)
        with (
            patch(_PATCH_FIRESTORE_DB, return_value=db),
            patch(_PATCH_SEND_EMAIL, new_callable=AsyncMock, return_value=True),
            patch("backend.dashboard.routes._validate_csrf", new_callable=AsyncMock),
        ):
            resp = client.post(
                f"/dashboard/settings/members/{email_h}/resend",
                follow_redirects=False,
            )

        assert resp.status_code == 303


class TestMemberLimitPerTier:
    @pytest.mark.parametrize(
        ("tier", "limit"),
        [
            ("team", 10),
            ("agency", 25),
            ("enterprise", 50),
            ("enterprise_unlimited", None),
        ],
    )
    def test_member_limit_per_tier(self, tier: str, limit: int | None) -> None:
        from backend.shared.tier_config import TIER_CONFIG

        cfg = TIER_CONFIG.get(tier, {})
        assert cfg.get("member_limit") == limit
