"""Tests for backend.dashboard.session."""

from __future__ import annotations

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

import pytest

# ---------------------------------------------------------------------------
# Encryption tests
# ---------------------------------------------------------------------------


class TestEncryptDecrypt:
    """Test AES-256-GCM roundtrip encryption."""

    def test_encrypt_decrypt_session_id(self) -> None:
        """Verify roundtrip AES-256-GCM encryption."""
        from cryptography.hazmat.primitives.ciphers.aead import AESGCM

        test_key = AESGCM.generate_key(bit_length=256)
        session_id = "a1b2c3d4e5f6" * 6  # 72 chars

        with patch(
            "backend.dashboard.session._get_encryption_key", return_value=test_key
        ):
            from backend.dashboard.session import (
                _decrypt_session_id,
                _encrypt_session_id,
            )

            encrypted = _encrypt_session_id(session_id)
            decrypted = _decrypt_session_id(encrypted)

        assert decrypted == session_id

    def test_decrypt_invalid_data(self) -> None:
        """Invalid ciphertext returns None."""
        from cryptography.hazmat.primitives.ciphers.aead import AESGCM

        test_key = AESGCM.generate_key(bit_length=256)

        with patch(
            "backend.dashboard.session._get_encryption_key", return_value=test_key
        ):
            from backend.dashboard.session import _decrypt_session_id

            result = _decrypt_session_id("not-valid-base64!!!")
            assert result is None

            result = _decrypt_session_id(base64.b64encode(b"short").decode())
            assert result is None

    def test_decrypt_tampered_ciphertext(self) -> None:
        """Tampered ciphertext returns None (auth tag fails)."""
        from cryptography.hazmat.primitives.ciphers.aead import AESGCM

        test_key = AESGCM.generate_key(bit_length=256)

        with patch(
            "backend.dashboard.session._get_encryption_key", return_value=test_key
        ):
            from backend.dashboard.session import (
                _decrypt_session_id,
                _encrypt_session_id,
            )

            encrypted = _encrypt_session_id("test-session")
            raw = base64.b64decode(encrypted)
            # Flip a byte in ciphertext
            tampered = raw[:15] + bytes([raw[15] ^ 0xFF]) + raw[16:]
            tampered_b64 = base64.b64encode(tampered).decode()
            assert _decrypt_session_id(tampered_b64) is None


# ---------------------------------------------------------------------------
# Session creation
# ---------------------------------------------------------------------------


class TestCreateSession:
    """Test session creation."""

    def test_create_session(self) -> None:
        """Verify session doc has all required fields."""
        from cryptography.hazmat.primitives.ciphers.aead import AESGCM

        test_key = AESGCM.generate_key(bit_length=256)

        mock_request = MagicMock()
        mock_request.client.host = "192.168.1.1"
        mock_request.headers.get.return_value = "Mozilla/5.0"

        with patch(
            "backend.dashboard.session._get_encryption_key", return_value=test_key
        ):
            from backend.dashboard.session import create_session

            session_id, encrypted_value, session_doc, csrf_token = create_session(
                email="user@example.com",
                customer_id="cus_test",
                tier="team",
                request=mock_request,
            )

        assert len(session_id) == 64  # token_hex(32)
        assert encrypted_value  # non-empty
        assert session_doc["session_id"] == session_id
        assert session_doc["customer_id"] == "cus_test"
        assert session_doc["email"] == "user@example.com"
        assert session_doc["tier"] == "team"
        assert session_doc["csrf_token"] == csrf_token
        assert session_doc["ip_hash"]  # non-empty hash
        assert session_doc["user_agent_hash"]  # non-empty hash
        assert session_doc["created_at"]
        assert session_doc["last_accessed_at"]
        assert session_doc["expires_at"]
        assert session_doc["theme_preference"] is None


# ---------------------------------------------------------------------------
# Cookie attributes
# ---------------------------------------------------------------------------


class TestCookieAttributes:
    """Verify cookie configuration constants."""

    def test_cookie_name(self) -> None:
        """Cookie name must be __Host-session."""
        from backend.dashboard.session import get_cookie_name

        assert get_cookie_name() == "__Host-session"

    def test_cookie_max_age(self) -> None:
        """Cookie Max-Age must be 604800 (7 days)."""
        from backend.dashboard.session import get_cookie_max_age

        assert get_cookie_max_age() == 604800


# ---------------------------------------------------------------------------
# Session validation
# ---------------------------------------------------------------------------


class TestGetSession:
    """Test session lookup and expiry."""

    @pytest.mark.asyncio
    async def test_session_expiry_check(self) -> None:
        """Expired session returns None."""
        from cryptography.hazmat.primitives.ciphers.aead import AESGCM

        test_key = AESGCM.generate_key(bit_length=256)

        expired_time = datetime.now(UTC) - timedelta(days=8)
        session_data = {
            "session_id": "test_sid",
            "expires_at": expired_time.isoformat(),
        }

        fake_doc = MagicMock()
        fake_doc.exists = True
        fake_doc.to_dict.return_value = session_data

        with (
            patch(
                "backend.dashboard.session._get_encryption_key", return_value=test_key
            ),
            patch("backend.shared.firestore.get_db") as mock_db,
        ):
            from backend.dashboard.session import _encrypt_session_id, get_session

            encrypted = _encrypt_session_id("test_sid")

            db_instance = MagicMock()
            mock_db.return_value = db_instance
            db_instance.collection.return_value.document.return_value.get = AsyncMock(
                return_value=fake_doc
            )

            result = await get_session(encrypted)

        assert result is None


# ---------------------------------------------------------------------------
# Sliding expiry
# ---------------------------------------------------------------------------


class TestTouchSession:
    """Test sliding expiry update."""

    @pytest.mark.asyncio
    async def test_sliding_expiry(self) -> None:
        """Touch session extends expires_at by 7 days."""
        with patch("backend.shared.firestore.get_db") as mock_db:
            from backend.dashboard.session import touch_session

            db_instance = MagicMock()
            mock_db.return_value = db_instance
            db_instance.collection.return_value.document.return_value.update = (
                AsyncMock()
            )

            await touch_session("test_sid")

            update_call = (
                db_instance.collection.return_value.document.return_value.update
            )
            update_call.assert_called_once()
            update_data = update_call.call_args[0][0]
            assert "last_accessed_at" in update_data
            assert "expires_at" in update_data


# ---------------------------------------------------------------------------
# CSRF validation
# ---------------------------------------------------------------------------


class TestCSRF:
    """Test CSRF token validation."""

    def test_csrf_validation_success(self) -> None:
        """Matching token passes."""
        from backend.dashboard.session import validate_csrf

        assert validate_csrf("token123", "token123") is True

    def test_csrf_validation_failure(self) -> None:
        """Wrong token fails."""
        from backend.dashboard.session import validate_csrf

        assert validate_csrf("wrong", "token123") is False

    def test_csrf_timing_safe(self) -> None:
        """Verify uses hmac.compare_digest (not ==)."""
        import hmac

        with patch(
            "backend.dashboard.session.hmac.compare_digest", wraps=hmac.compare_digest
        ) as mock_cmp:
            from backend.dashboard.session import validate_csrf

            validate_csrf("a", "b")
            mock_cmp.assert_called_once_with("a", "b")
