"""Tests for backend.dashboard.middleware."""

from __future__ import annotations

import hashlib
import logging
from datetime import UTC, datetime, timedelta
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from backend.dashboard.middleware import (
    CustomerContext,
    DashboardAuthError,
    require_dashboard_auth,
)

# ---------------------------------------------------------------------------
# Mock helpers
# ---------------------------------------------------------------------------


class _FakeDoc:
    def __init__(
        self, data: dict[str, Any] | None = None, doc_id: str = "cus_test"
    ) -> None:
        self._data = data
        self.exists = data is not None
        self.id = doc_id

    def to_dict(self) -> dict[str, Any] | None:
        return self._data


def _mock_request(
    *,
    cookie: str | None = "encrypted_value",
    htmx: bool = False,
    ip: str = "192.168.1.1",
) -> MagicMock:
    request = MagicMock()
    request.cookies = {}
    if cookie is not None:
        request.cookies["__Host-session"] = cookie
    headers_dict: dict[str, str] = {}
    if htmx:
        headers_dict["HX-Request"] = "true"
    request.headers = MagicMock()
    request.headers.get = lambda k, d="": headers_dict.get(k, d)
    request.client = MagicMock()
    request.client.host = ip
    return request


def _active_customer(tier: str = "team") -> dict[str, Any]:
    return {
        "email": "user@example.com",
        "tier": tier,
        "status": "active",
        "consent_accepted": True,
        "tos_version": "2024-01",
        "privacy_version": "2024-01",
        "subscription_expires_at": (
            datetime.now(UTC) + timedelta(days=365)
        ).isoformat(),
    }


def _session_data() -> dict[str, Any]:
    ip_hash = hashlib.sha256(b"192.168.1.1").hexdigest()
    return {
        "session_id": "sid_test",
        "email": "user@example.com",
        "customer_id": "cus_test",
        "tier": "team",
        "csrf_token": "csrf_tok",
        "ip_hash": ip_hash,
        "user_agent_hash": "ua_hash",
        "expires_at": (datetime.now(UTC) + timedelta(days=7)).isoformat(),
    }


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestRequireDashboardAuth:
    """Tests for require_dashboard_auth dependency."""

    @pytest.mark.asyncio
    async def test_require_auth_valid_session(self) -> None:
        """Valid session + matching customer → CustomerContext returned."""
        request = _mock_request()
        session = _session_data()
        customer = _active_customer()
        customer_doc = _FakeDoc(customer, doc_id="cus_test")

        with (
            patch(
                "backend.dashboard.session.get_session",
                new_callable=AsyncMock,
                return_value=session,
            ),
            patch("backend.dashboard.session.touch_session", new_callable=AsyncMock),
            patch("backend.shared.firestore.get_db") as mock_db,
        ):
            db_instance = MagicMock()
            mock_db.return_value = db_instance

            async def _stream() -> Any:
                yield customer_doc

            where_ref = db_instance.collection.return_value.where
            query_ref = where_ref.return_value.limit.return_value
            query_ref.stream = _stream

            ctx = await require_dashboard_auth(request)

        assert isinstance(ctx, CustomerContext)
        assert ctx.customer_id == "cus_test"
        assert ctx.email == "user@example.com"
        assert ctx.tier == "team"
        assert ctx.role == "owner"
        assert "hosted_dashboard" in ctx.features

    @pytest.mark.asyncio
    async def test_require_auth_no_cookie(self) -> None:
        """No session cookie → DashboardAuthError (redirect)."""
        request = _mock_request(cookie=None)

        with pytest.raises(DashboardAuthError) as exc_info:
            await require_dashboard_auth(request)

        assert exc_info.value.redirect is True

    @pytest.mark.asyncio
    async def test_require_auth_expired_session(self) -> None:
        """Expired session → redirect to /auth/login."""
        request = _mock_request()

        with (
            patch(
                "backend.dashboard.session.get_session",
                new_callable=AsyncMock,
                return_value=None,
            ),
            pytest.raises(DashboardAuthError) as exc_info,
        ):
            await require_dashboard_auth(request)

        assert exc_info.value.redirect is True

    @pytest.mark.asyncio
    async def test_require_auth_community_tier(self) -> None:
        """Community tier → 403 with upgrade message."""
        request = _mock_request()
        session = _session_data()
        session["tier"] = "community"
        customer = _active_customer(tier="community")
        customer_doc = _FakeDoc(customer, doc_id="cus_free")

        with (
            patch(
                "backend.dashboard.session.get_session",
                new_callable=AsyncMock,
                return_value=session,
            ),
            patch("backend.shared.firestore.get_db") as mock_db,
        ):
            db_instance = MagicMock()
            mock_db.return_value = db_instance

            async def _stream() -> Any:
                yield customer_doc

            where_ref = db_instance.collection.return_value.where
            query_ref = where_ref.return_value.limit.return_value
            query_ref.stream = _stream

            with pytest.raises(DashboardAuthError) as exc_info:
                await require_dashboard_auth(request)

        assert exc_info.value.redirect is False
        assert "community" in exc_info.value.message

    @pytest.mark.asyncio
    async def test_require_auth_cancelled_grace_period(self) -> None:
        """Cancelled within 30 days → read-only (role=viewer)."""
        request = _mock_request()
        session = _session_data()
        customer = _active_customer()
        customer["status"] = "cancelled"
        # Expired 10 days ago (within 30-day grace)
        customer["subscription_expires_at"] = (
            datetime.now(UTC) - timedelta(days=10)
        ).isoformat()
        customer_doc = _FakeDoc(customer, doc_id="cus_test")

        with (
            patch(
                "backend.dashboard.session.get_session",
                new_callable=AsyncMock,
                return_value=session,
            ),
            patch("backend.dashboard.session.touch_session", new_callable=AsyncMock),
            patch("backend.shared.firestore.get_db") as mock_db,
        ):
            db_instance = MagicMock()
            mock_db.return_value = db_instance

            async def _stream() -> Any:
                yield customer_doc

            where_ref = db_instance.collection.return_value.where
            query_ref = where_ref.return_value.limit.return_value
            query_ref.stream = _stream

            ctx = await require_dashboard_auth(request)

        assert ctx.role == "viewer"
        assert ctx.cancellation_pending is True

    @pytest.mark.asyncio
    async def test_require_auth_cancelled_expired(self) -> None:
        """Cancelled >30 days → 403."""
        request = _mock_request()
        session = _session_data()
        customer = _active_customer()
        customer["status"] = "cancelled"
        customer["subscription_expires_at"] = (
            datetime.now(UTC) - timedelta(days=45)
        ).isoformat()
        customer_doc = _FakeDoc(customer, doc_id="cus_test")

        with (
            patch(
                "backend.dashboard.session.get_session",
                new_callable=AsyncMock,
                return_value=session,
            ),
            patch("backend.shared.firestore.get_db") as mock_db,
        ):
            db_instance = MagicMock()
            mock_db.return_value = db_instance

            async def _stream() -> Any:
                yield customer_doc

            where_ref = db_instance.collection.return_value.where
            query_ref = where_ref.return_value.limit.return_value
            query_ref.stream = _stream

            with pytest.raises(DashboardAuthError) as exc_info:
                await require_dashboard_auth(request)

        assert "expired" in exc_info.value.message.lower()

    @pytest.mark.asyncio
    async def test_require_auth_htmx_redirect(self) -> None:
        """Expired session + HX-Request → DashboardAuthError with htmx=True."""
        request = _mock_request(htmx=True)

        with (
            patch(
                "backend.dashboard.session.get_session",
                new_callable=AsyncMock,
                return_value=None,
            ),
            pytest.raises(DashboardAuthError) as exc_info,
        ):
            await require_dashboard_auth(request)

        assert exc_info.value.htmx is True

    @pytest.mark.asyncio
    async def test_require_auth_ip_anomaly_logging(
        self, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Different IP from session → warning log, session NOT invalidated."""
        request = _mock_request(ip="10.0.0.1")
        session = _session_data()
        # ip_hash in session is for 192.168.1.1, request IP is 10.0.0.1
        customer = _active_customer()
        customer_doc = _FakeDoc(customer, doc_id="cus_test")

        with (
            patch(
                "backend.dashboard.session.get_session",
                new_callable=AsyncMock,
                return_value=session,
            ),
            patch("backend.dashboard.session.touch_session", new_callable=AsyncMock),
            patch("backend.shared.firestore.get_db") as mock_db,
            caplog.at_level(logging.WARNING, logger="backend.dashboard.middleware"),
        ):
            db_instance = MagicMock()
            mock_db.return_value = db_instance

            async def _stream() -> Any:
                yield customer_doc

            where_ref = db_instance.collection.return_value.where
            query_ref = where_ref.return_value.limit.return_value
            query_ref.stream = _stream

            ctx = await require_dashboard_auth(request)

        # Session should still be valid (not invalidated)
        assert ctx.customer_id == "cus_test"
        # Warning should be logged
        assert any("IP anomaly" in record.message for record in caplog.records)
