"""Tests for ieum.server.auth -- token authentication middleware.

Covers:
- Token generation produces valid URL-safe tokens
- Token validation with timing-safe comparison
- Token extraction from header, query param, and cookie
- Auth disabled mode via environment variable
- Public paths bypass authentication
"""

import secrets
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from ieum.server.auth import (
    PUBLIC_PATHS,
    _extract_token_from_request,
    _extract_token_from_websocket,
    _validate_token,
    generate_auth_token,
    get_auth_token,
    init_auth,
    is_auth_disabled,
)


# ---------------------------------------------------------------------------
# Token generation
# ---------------------------------------------------------------------------


class TestTokenGeneration:
    """Test auth token generation."""

    def test_generate_returns_string(self):
        token = generate_auth_token()
        assert isinstance(token, str)
        assert len(token) > 0

    def test_generate_token_is_url_safe(self):
        token = generate_auth_token()
        # URL-safe base64 uses only these characters
        import re

        assert re.match(r"^[A-Za-z0-9_-]+$", token)

    def test_generate_token_has_sufficient_entropy(self):
        token = generate_auth_token()
        # secrets.token_urlsafe(32) produces ~43 characters
        assert len(token) >= 40

    def test_get_auth_token_returns_generated(self):
        token = generate_auth_token()
        assert get_auth_token() == token

    def test_successive_tokens_are_different(self):
        t1 = generate_auth_token()
        t2 = generate_auth_token()
        assert t1 != t2


# ---------------------------------------------------------------------------
# Token validation
# ---------------------------------------------------------------------------


class TestTokenValidation:
    """Test _validate_token with timing-safe comparison."""

    def test_correct_token_validates(self):
        import ieum.server.auth as auth_mod

        token = generate_auth_token()
        auth_mod._AUTH_DISABLED = False
        assert _validate_token(token) is True

    def test_wrong_token_rejected(self):
        import ieum.server.auth as auth_mod

        generate_auth_token()
        auth_mod._AUTH_DISABLED = False
        assert _validate_token("wrong-token-here") is False

    def test_none_token_rejected(self):
        import ieum.server.auth as auth_mod

        generate_auth_token()
        auth_mod._AUTH_DISABLED = False
        assert _validate_token(None) is False

    def test_empty_string_rejected(self):
        import ieum.server.auth as auth_mod

        generate_auth_token()
        auth_mod._AUTH_DISABLED = False
        assert _validate_token("") is False

    def test_auth_disabled_always_validates(self):
        import ieum.server.auth as auth_mod

        auth_mod._AUTH_DISABLED = True
        assert _validate_token(None) is True
        assert _validate_token("anything") is True
        auth_mod._AUTH_DISABLED = False  # restore

    def test_uses_timing_safe_comparison(self):
        """Verify that secrets.compare_digest is used to prevent timing attacks."""
        import ieum.server.auth as auth_mod

        token = generate_auth_token()
        auth_mod._AUTH_DISABLED = False
        with patch.object(secrets, "compare_digest", return_value=True) as mock_cmp:
            _validate_token(token)
            mock_cmp.assert_called_once_with(token, auth_mod._AUTH_TOKEN)


# ---------------------------------------------------------------------------
# Token extraction from requests
# ---------------------------------------------------------------------------


class TestTokenExtraction:
    """Test extracting tokens from various HTTP request locations."""

    def _make_request(self, *, headers=None, query_params=None, cookies=None):
        """Create a mock request object."""
        request = MagicMock()
        request.headers = headers or {}
        request.query_params = query_params or {}
        request.cookies = cookies or {}
        return request

    def test_extract_from_bearer_header(self):
        request = self._make_request(headers={"authorization": "Bearer my-token-123"})
        assert _extract_token_from_request(request) == "my-token-123"

    def test_extract_from_bearer_header_case_insensitive(self):
        request = self._make_request(headers={"authorization": "bearer my-token-123"})
        assert _extract_token_from_request(request) == "my-token-123"

    def test_extract_from_query_param(self):
        request = self._make_request(query_params={"token": "query-token"})
        assert _extract_token_from_request(request) == "query-token"

    def test_extract_from_cookie(self):
        request = self._make_request(cookies={"ieum_token": "cookie-token"})
        assert _extract_token_from_request(request) == "cookie-token"

    def test_header_takes_priority(self):
        request = self._make_request(
            headers={"authorization": "Bearer header-token"},
            query_params={"token": "query-token"},
            cookies={"ieum_token": "cookie-token"},
        )
        assert _extract_token_from_request(request) == "header-token"

    def test_no_token_returns_none(self):
        request = self._make_request()
        assert _extract_token_from_request(request) is None


class TestWebSocketTokenExtraction:
    """Test extracting tokens from WebSocket handshake."""

    def _make_ws(self, *, headers=None, query_params=None, cookies=None):
        ws = MagicMock()
        ws.headers = headers or {}
        ws.query_params = query_params or {}
        ws.cookies = cookies or {}
        return ws

    def test_extract_from_query_param(self):
        ws = self._make_ws(query_params={"token": "ws-token"})
        assert _extract_token_from_websocket(ws) == "ws-token"

    def test_extract_from_header(self):
        ws = self._make_ws(headers={"authorization": "Bearer ws-header-token"})
        assert _extract_token_from_websocket(ws) == "ws-header-token"

    def test_extract_from_cookie(self):
        ws = self._make_ws(cookies={"ieum_token": "ws-cookie"})
        assert _extract_token_from_websocket(ws) == "ws-cookie"

    def test_no_token_returns_none(self):
        ws = self._make_ws()
        assert _extract_token_from_websocket(ws) is None


# ---------------------------------------------------------------------------
# init_auth
# ---------------------------------------------------------------------------


class TestInitAuth:
    """Test authentication initialization."""

    def test_init_auth_generates_token(self, monkeypatch):
        import ieum.server.auth as auth_mod

        monkeypatch.delenv("IEUM_AUTH_DISABLED", raising=False)
        auth_mod._AUTH_DISABLED = False
        token = init_auth()
        assert token is not None
        assert len(token) > 0

    @pytest.mark.parametrize("value", ["true", "1", "yes", "True", "TRUE"])
    def test_init_auth_disabled_by_env(self, monkeypatch, value):
        import ieum.server.auth as auth_mod

        monkeypatch.setenv("IEUM_AUTH_DISABLED", value)
        result = init_auth()
        assert result is None
        assert is_auth_disabled() is True
        # Restore
        auth_mod._AUTH_DISABLED = False

    def test_public_paths_defined(self):
        assert "/auth" in PUBLIC_PATHS
        assert "/api/health" in PUBLIC_PATHS
        assert "/health" in PUBLIC_PATHS
