# -*- coding: utf-8 -*-
"""Location: ./tests/unit/mcpgateway/routers/test_auth.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

Tests for the auth router module.
"""

# Standard
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

# Third-Party
import pytest
from fastapi import HTTPException

# First-Party
from mcpgateway.routers.auth import LoginRequest, get_csrf_token, get_db, login


class TestLoginRequest:
    """Tests for LoginRequest model."""

    def test_get_email_from_email_field(self):
        """Test getting email from email field."""
        req = LoginRequest(email="test@example.com", password="pass")
        assert req.get_email() == "test@example.com"

    def test_get_email_from_username_with_at(self):
        """Test getting email from username field with @ symbol."""
        req = LoginRequest(username="user@domain.com", password="pass")
        assert req.get_email() == "user@domain.com"

    def test_get_email_from_username_without_at_raises(self):
        """Test that plain username raises ValueError."""
        req = LoginRequest(username="plainuser", password="pass")
        with pytest.raises(ValueError, match="Username format not supported"):
            req.get_email()

    def test_get_email_missing_both_raises(self):
        """Test that missing email and username raises ValueError."""
        req = LoginRequest(password="pass")
        with pytest.raises(ValueError, match="Either email or username must be provided"):
            req.get_email()

    def test_email_takes_precedence_over_username(self):
        """Test that email field takes precedence over username."""
        req = LoginRequest(email="email@example.com", username="user@domain.com", password="pass")
        assert req.get_email() == "email@example.com"


class TestGetDb:
    """Tests for get_db dependency."""

    def test_get_db_yields_session(self):
        """Test that get_db yields a session."""
        with patch("mcpgateway.routers.auth.SessionLocal") as mock_session_local:
            mock_db = MagicMock()
            mock_session_local.return_value = mock_db

            gen = get_db()
            db = next(gen)

            assert db == mock_db

            # Complete the generator
            try:
                next(gen)
            except StopIteration:
                pass

            mock_db.commit.assert_called_once()
            mock_db.close.assert_called_once()

    def test_get_db_rollback_on_exception(self):
        """Test that get_db rolls back on exception."""
        with patch("mcpgateway.routers.auth.SessionLocal") as mock_session_local:
            mock_db = MagicMock()
            mock_session_local.return_value = mock_db

            gen = get_db()
            next(gen)

            # Simulate exception during usage
            with pytest.raises(RuntimeError):
                gen.throw(RuntimeError("Test error"))

            mock_db.rollback.assert_called_once()
            mock_db.close.assert_called_once()

    def test_get_db_invalidate_on_rollback_failure(self):
        """Test that get_db invalidates on rollback failure."""
        with patch("mcpgateway.routers.auth.SessionLocal") as mock_session_local:
            mock_db = MagicMock()
            mock_db.rollback.side_effect = Exception("Rollback failed")
            mock_session_local.return_value = mock_db

            gen = get_db()
            next(gen)

            # Simulate exception during usage
            with pytest.raises(RuntimeError):
                gen.throw(RuntimeError("Test error"))

            mock_db.rollback.assert_called_once()
            mock_db.invalidate.assert_called_once()
            mock_db.close.assert_called_once()

    def test_get_db_passes_on_invalidate_failure(self):
        """Test that get_db passes on invalidate failure."""
        with patch("mcpgateway.routers.auth.SessionLocal") as mock_session_local:
            mock_db = MagicMock()
            mock_db.rollback.side_effect = Exception("Rollback failed")
            mock_db.invalidate.side_effect = Exception("Invalidate failed")
            mock_session_local.return_value = mock_db

            gen = get_db()
            next(gen)

            # Simulate exception during usage - should not raise additional errors
            with pytest.raises(RuntimeError):
                gen.throw(RuntimeError("Test error"))

            mock_db.close.assert_called_once()


class TestLogin:
    """Tests for login endpoint."""

    @pytest.fixture
    def mock_request(self):
        """Create a mock FastAPI request."""
        request = MagicMock()
        request.client = MagicMock()
        request.client.host = "127.0.0.1"
        request.headers = {"user-agent": "test-agent"}
        return request

    @pytest.fixture
    def mock_db(self):
        """Create a mock database session."""
        return MagicMock()

    @pytest.fixture
    def mock_user(self):
        """Create a mock email user."""
        user = MagicMock()
        user.id = "test-user-id"
        user.email = "test@example.com"
        user.full_name = "Test User"
        user.is_active = True
        user.is_admin = False
        user.auth_provider = "local"
        user.teams = []
        return user

    @pytest.mark.asyncio
    async def test_login_success(self, mock_request, mock_db, mock_user):
        """Test successful login."""
        with (
            patch("mcpgateway.routers.auth.EmailAuthService") as mock_auth_service,
            patch("mcpgateway.routers.auth.create_access_token", new_callable=AsyncMock) as mock_create_token,
        ):
            mock_service = MagicMock()
            mock_service.authenticate_user = AsyncMock(return_value=mock_user)
            mock_auth_service.return_value = mock_service

            mock_create_token.return_value = ("test_token", 3600)

            login_request = LoginRequest(email="test@example.com", password="password123")

            response = await login(login_request, mock_request, mock_db)

            assert response.access_token == "test_token"
            assert response.token_type == "bearer"
            assert response.expires_in == 3600
            mock_service.authenticate_user.assert_called_once()

    @pytest.mark.asyncio
    async def test_login_invalid_credentials(self, mock_request, mock_db):
        """Test login with invalid credentials.

        Note: Due to exception handling structure, HTTPException from failed auth
        is caught by the generic Exception handler and re-raised as 500.
        """
        with patch("mcpgateway.routers.auth.EmailAuthService") as mock_auth_service:
            mock_service = MagicMock()
            mock_service.authenticate_user = AsyncMock(return_value=None)
            mock_auth_service.return_value = mock_service

            login_request = LoginRequest(email="test@example.com", password="wrongpass")

            with pytest.raises(HTTPException) as exc_info:
                await login(login_request, mock_request, mock_db)

            assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_login_value_error(self, mock_request, mock_db):
        """Test login with missing email/username."""
        login_request = LoginRequest(password="password123")

        with pytest.raises(HTTPException) as exc_info:
            await login(login_request, mock_request, mock_db)

        assert exc_info.value.status_code == 400

    @pytest.mark.asyncio
    async def test_login_service_error(self, mock_request, mock_db):
        """Test login when auth service fails."""
        with patch("mcpgateway.routers.auth.EmailAuthService") as mock_auth_service:
            mock_service = MagicMock()
            mock_service.authenticate_user = AsyncMock(side_effect=Exception("Service error"))
            mock_auth_service.return_value = mock_service

            login_request = LoginRequest(email="test@example.com", password="password123")

            with pytest.raises(HTTPException) as exc_info:
                await login(login_request, mock_request, mock_db)

            assert exc_info.value.status_code == 500
            assert "Authentication service error" in exc_info.value.detail

    @pytest.mark.asyncio
    async def test_login_with_username_field(self, mock_request, mock_db, mock_user):
        """Test login using username field instead of email."""
        with (
            patch("mcpgateway.routers.auth.EmailAuthService") as mock_auth_service,
            patch("mcpgateway.routers.auth.create_access_token", new_callable=AsyncMock) as mock_create_token,
        ):
            mock_service = MagicMock()
            mock_service.authenticate_user = AsyncMock(return_value=mock_user)
            mock_auth_service.return_value = mock_service

            mock_create_token.return_value = ("test_token", 3600)

            login_request = LoginRequest(username="user@domain.com", password="password123")

            response = await login(login_request, mock_request, mock_db)

            assert response.access_token == "test_token"
            mock_service.authenticate_user.assert_called_once()

    @pytest.mark.asyncio
    async def test_login_csrf_rotation_sets_cookie(self, mock_request, mock_db, mock_user):
        """Test login with csrf_rotate_on_login=True sets CSRF cookie."""
        with (
            patch("mcpgateway.routers.auth.EmailAuthService") as mock_auth_service,
            patch("mcpgateway.routers.auth.create_access_token", new_callable=AsyncMock) as mock_create_token,
            patch("mcpgateway.config.settings") as mock_settings,
            patch("jwt.decode", return_value={"jti": "session-123"}),
            patch("mcpgateway.services.csrf_service.generate_csrf_token", return_value="csrf-token-123") as mock_generate,
            patch("mcpgateway.services.csrf_service.set_csrf_cookie") as mock_set_cookie,
        ):
            mock_service = MagicMock()
            mock_service.authenticate_user = AsyncMock(return_value=mock_user)
            mock_auth_service.return_value = mock_service

            mock_create_token.return_value = ("test_token", 3600)
            mock_settings.csrf_rotate_on_login = True
            mock_settings.csrf_secret_key = "secret"
            mock_settings.csrf_token_expiry = 60

            login_request = LoginRequest(email="test@example.com", password="password123")

            response = await login(login_request, mock_request, mock_db)

            assert response.status_code == 200
            mock_generate.assert_called_once()
            mock_set_cookie.assert_called_once()

    @pytest.mark.asyncio
    async def test_get_csrf_token_success(self):
        """Test CSRF token endpoint returns token and cookie."""
        request = MagicMock()
        request.state = SimpleNamespace(jti="session-123")
        current_user = SimpleNamespace(email="test@example.com")

        with (
            patch("mcpgateway.config.settings") as mock_settings,
            patch("mcpgateway.services.csrf_service.generate_csrf_token", return_value="csrf-token-123") as mock_generate,
            patch("mcpgateway.services.csrf_service.set_csrf_cookie") as mock_set_cookie,
        ):
            mock_settings.csrf_secret_key = "secret"
            mock_settings.csrf_token_expiry = 60

            response = await get_csrf_token(request, current_user)

            assert response.status_code == 200
            assert response.body is not None
            mock_generate.assert_called_once_with(user_id="test@example.com", session_id="session-123", secret="secret", expiry=60)
            mock_set_cookie.assert_called_once()

    @pytest.mark.asyncio
    async def test_get_csrf_token_missing_jti_raises_401(self):
        """Test CSRF token endpoint rejects missing session id."""
        request = MagicMock()
        request.state = SimpleNamespace()
        current_user = SimpleNamespace(email="test@example.com")

        with pytest.raises(HTTPException) as exc_info:
            await get_csrf_token(request, current_user)

        assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_get_csrf_token_exception_raises_500(self):
        """Test CSRF token endpoint handles unexpected errors."""
        request = MagicMock()
        request.state = SimpleNamespace(jti="session-123")
        current_user = SimpleNamespace(email="test@example.com")

        with (
            patch("mcpgateway.routers.auth.settings") as mock_settings,
            patch("mcpgateway.services.csrf_service.generate_csrf_token", side_effect=Exception("boom")),
        ):
            mock_settings.csrf_secret_key = "secret"
            mock_settings.csrf_token_expiry = 60

            with pytest.raises(HTTPException) as exc_info:
                await get_csrf_token(request, current_user)

        assert exc_info.value.status_code == 500

    @pytest.mark.asyncio
    async def test_login_with_plain_username_fails(self, mock_request, mock_db):
        """Test login with plain username (no @) fails."""
        login_request = LoginRequest(username="plainuser", password="password123")

        with pytest.raises(HTTPException) as exc_info:
            await login(login_request, mock_request, mock_db)

        assert exc_info.value.status_code == 400
        assert "Username format not supported" in exc_info.value.detail

    @pytest.mark.asyncio
    async def test_login_non_admin_blocked_when_sso_preserve_admin_enabled(self, mock_request, mock_db, mock_user):
        """Non-admin password login should be blocked when SSO preserve-admin mode is enabled."""
        mock_user.is_admin = False

        with (
            patch("mcpgateway.routers.auth.EmailAuthService") as mock_auth_service,
            patch("mcpgateway.routers.auth.create_access_token", new_callable=AsyncMock) as mock_create_token,
            patch("mcpgateway.routers.auth.settings") as mock_settings,
        ):
            mock_service = MagicMock()
            mock_service.authenticate_user = AsyncMock(return_value=mock_user)
            mock_auth_service.return_value = mock_service
            mock_settings.sso_enabled = True
            mock_settings.sso_preserve_admin_auth = True

            login_request = LoginRequest(email="test@example.com", password="password123")

            with pytest.raises(HTTPException) as exc_info:
                await login(login_request, mock_request, mock_db)

            assert exc_info.value.status_code == 400
            assert "restricted to admin accounts" in exc_info.value.detail
            mock_create_token.assert_not_called()


class TestLogout:
    """Tests for logout endpoint."""

    @pytest.fixture
    def mock_request(self):
        """Create a mock FastAPI request with auth header."""
        request = MagicMock()
        request.headers = {"Authorization": "Bearer test_token_with_jti"}
        return request

    @pytest.fixture
    def mock_db(self):
        """Create a mock database session."""
        return MagicMock()

    @pytest.fixture
    def mock_current_user(self):
        """Create a mock current user."""
        user = SimpleNamespace()
        user.email = "test@example.com"
        user.id = "test-user-id"
        return user

    @pytest.mark.asyncio
    async def test_logout_with_secret_str_jwt_key(self, mock_request, mock_db, mock_current_user):
        """Test logout when jwt_secret_key is a SecretStr type (covers line 239)."""
        from mcpgateway.routers.auth import logout

        # Create a mock SecretStr
        mock_secret_str = MagicMock()
        mock_secret_str.get_secret_value.return_value = "test-secret-key"

        with (
            patch("mcpgateway.services.token_blocklist_service.get_token_blocklist_service") as mock_blocklist_service,
            patch("mcpgateway.config.settings") as mock_settings,
        ):
            # Setup settings with SecretStr
            mock_settings.jwt_secret_key = mock_secret_str
            mock_settings.jwt_algorithm = "HS256"

            # Setup blocklist service
            mock_service = MagicMock()
            mock_service.revoke_token.return_value = True
            mock_blocklist_service.return_value = mock_service

            # Mock jwt.decode inside the function
            with patch("jwt.decode") as mock_jwt_decode:
                mock_jwt_decode.return_value = {
                    "jti": "test-jti-123",
                    "exp": 1234567890,
                    "iat": 1234567800,
                }

                response = await logout(mock_request, mock_current_user, mock_db)

                assert response["message"] == "Logged out successfully"
                assert response["revoked_token"] == "test-jti-123"
                mock_secret_str.get_secret_value.assert_called_once()
                mock_service.revoke_token.assert_called_once()
