"""Tests for backend.dashboard.auth_routes."""

from __future__ import annotations

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

import pytest
from backend.dashboard.auth_routes import auth_router
from fastapi import FastAPI
from fastapi.testclient import TestClient

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


def _make_app() -> FastAPI:
    test_app = FastAPI()
    test_app.include_router(auth_router)
    return test_app


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

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


class TestLogin:
    """Tests for GET /auth/login."""

    @pytest.fixture
    def client(self) -> TestClient:
        return TestClient(_make_app(), follow_redirects=False)

    def test_login_redirects_to_google(self, client: TestClient) -> None:
        """Verify 302 redirect to accounts.google.com with state in URL."""
        with (
            patch(
                "backend.dashboard.auth_routes._get_oauth_client_id",
                return_value="test-client-id",
            ),
            patch("backend.shared.firestore.get_db") as mock_db,
        ):
            db_instance = MagicMock()
            mock_db.return_value = db_instance
            db_instance.collection.return_value.document.return_value.set = AsyncMock()

            resp = client.get("/auth/login")

        assert resp.status_code == 302
        location = resp.headers["location"]
        assert "accounts.google.com" in location
        assert "state=" in location
        assert "client_id=test-client-id" in location
        assert "select_account" in location

        # Verify state stored in Firestore
        db_instance.collection.return_value.document.return_value.set.assert_called_once()
        call_data = (
            db_instance.collection.return_value.document.return_value.set.call_args[0][
                0
            ]
        )
        assert call_data["type"] == "oauth_state"


class TestCallback:
    """Tests for GET /auth/callback."""

    @pytest.fixture
    def client(self) -> TestClient:
        return TestClient(_make_app(), follow_redirects=False)

    def test_callback_valid_code(self, client: TestClient) -> None:
        """Mock token exchange + JWKS: session created, cookie set, redirect."""
        state_data = {
            "type": "oauth_state",
            "created_at": datetime.now(UTC).isoformat(),
            "expires_at": (datetime.now(UTC) + timedelta(minutes=10)).isoformat(),
        }
        state_doc = _FakeDoc(state_data)

        fake_claims = {
            "email": "user@example.com",
            "iss": "accounts.google.com",
            "exp": 9999999999,
        }
        fake_token_resp = MagicMock()
        fake_token_resp.status_code = 200
        fake_token_resp.json.return_value = {"id_token": "fake.jwt.token"}
        fake_token_resp.raise_for_status = MagicMock()

        customer_doc = MagicMock()
        customer_doc.id = "cus_test"
        customer_doc.to_dict.return_value = {"tier": "team"}

        with (
            patch("backend.shared.firestore.get_db") as mock_db,
            patch(
                "backend.dashboard.auth_routes._get_oauth_client_id",
                return_value="test-client",
            ),
            patch(
                "backend.dashboard.auth_routes._get_oauth_client_secret",
                return_value="test-secret",
            ),
            patch(
                "backend.dashboard.auth_routes._verify_id_token",
                return_value=fake_claims,
            ),
            patch(
                "backend.dashboard.auth_routes._get_google_jwks",
                new_callable=AsyncMock,
                return_value={},
            ),
            patch("backend.dashboard.session.create_session") as mock_create,
            patch("backend.dashboard.session.write_session", new_callable=AsyncMock),
            patch("httpx.AsyncClient") as mock_httpx,
        ):
            db_instance = MagicMock()
            mock_db.return_value = db_instance

            # State doc lookup
            db_instance.collection.return_value.document.return_value.get = AsyncMock(
                return_value=state_doc
            )
            db_instance.collection.return_value.document.return_value.delete = (
                AsyncMock()
            )

            # Customer query
            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

            # Mock httpx POST for token exchange
            mock_client = AsyncMock()
            mock_client.post.return_value = fake_token_resp
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=False)
            mock_httpx.return_value = mock_client

            mock_create.return_value = (
                "sid_123",
                "encrypted_value",
                {"doc": "data"},
                "csrf_tok",
            )

            resp = client.get("/auth/callback?code=authcode&state=test-state")

        assert resp.status_code == 302
        assert resp.headers["location"] == "/dashboard/"
        # Cookie set
        cookies = resp.headers.get_list("set-cookie")
        cookie_str = "; ".join(cookies)
        assert "__Host-session" in cookie_str

    def test_callback_invalid_state(self, client: TestClient) -> None:
        """Wrong state param → redirect to /auth/login."""
        with patch("backend.shared.firestore.get_db") as mock_db:
            db_instance = MagicMock()
            mock_db.return_value = db_instance
            db_instance.collection.return_value.document.return_value.get = AsyncMock(
                return_value=_FakeDoc(None)
            )

            resp = client.get("/auth/callback?code=authcode&state=wrong-state")

        assert resp.status_code == 302
        assert "/auth/login" in resp.headers["location"]

    def test_callback_expired_state(self, client: TestClient) -> None:
        """State older than 10 min → redirect to /auth/login."""
        expired_state = {
            "type": "oauth_state",
            "created_at": (datetime.now(UTC) - timedelta(minutes=15)).isoformat(),
            "expires_at": (datetime.now(UTC) - timedelta(minutes=5)).isoformat(),
        }

        with patch("backend.shared.firestore.get_db") as mock_db:
            db_instance = MagicMock()
            mock_db.return_value = db_instance
            db_instance.collection.return_value.document.return_value.get = AsyncMock(
                return_value=_FakeDoc(expired_state)
            )
            db_instance.collection.return_value.document.return_value.delete = (
                AsyncMock()
            )

            resp = client.get("/auth/callback?code=authcode&state=expired-state")

        assert resp.status_code == 302
        assert "/auth/login" in resp.headers["location"]


class TestLogout:
    """Tests for POST /auth/logout."""

    @pytest.fixture
    def client(self) -> TestClient:
        return TestClient(_make_app(), follow_redirects=False)

    def test_logout_clears_session(self, client: TestClient) -> None:
        """Firestore session deleted, cookie cleared, redirect to complyform.dev."""
        session_data = {
            "session_id": "sid_123",
            "csrf_token": "valid_csrf",
            "email": "user@example.com",
        }

        with (
            patch(
                "backend.dashboard.session.get_session",
                new_callable=AsyncMock,
                return_value=session_data,
            ),
            patch(
                "backend.dashboard.session.delete_session", new_callable=AsyncMock
            ) as mock_delete,
            patch("backend.dashboard.session.validate_csrf", return_value=True),
        ):
            client.cookies.set("__Host-session", "encrypted_value")
            resp = client.post(
                "/auth/logout",
                headers={"X-CSRF-Token": "valid_csrf"},
            )

        assert resp.status_code == 302
        assert "complyform.dev" in resp.headers["location"]
        mock_delete.assert_called_once_with("sid_123")

    def test_logout_requires_csrf(self, client: TestClient) -> None:
        """POST without CSRF token → 403."""
        session_data = {
            "session_id": "sid_123",
            "csrf_token": "valid_csrf",
            "email": "user@example.com",
        }

        with (
            patch(
                "backend.dashboard.session.get_session",
                new_callable=AsyncMock,
                return_value=session_data,
            ),
            patch("backend.dashboard.session.validate_csrf", return_value=False),
        ):
            client.cookies.set("__Host-session", "encrypted_value")
            resp = client.post(
                "/auth/logout",
            )

        assert resp.status_code == 403
        assert "CSRF" in resp.json()["error"]
