"""
Unit tests for registry/auth/dependencies.py

Tests all authentication dependencies including:
- Session validation and extraction
- User context building
- Scope mapping
- Permission checking
- UI permissions
- Server access control
"""

import logging
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, Mock, patch

import pytest
import yaml
from fastapi import HTTPException, Request
from itsdangerous import SignatureExpired, URLSafeTimedSerializer

from registry.auth.dependencies import (
    _user_is_admin,
    api_auth,
    enhanced_auth,
    get_accessible_agents_for_user,
    get_accessible_services_for_user,
    get_current_user,
    get_servers_for_scope,
    get_ui_permissions_for_user,
    get_user_accessible_servers,
    get_user_session_data,
    map_cognito_groups_to_scopes,
    nginx_proxied_auth,
    user_can_access_server,
    user_can_modify_servers,
    user_has_ui_permission_for_service,
    user_has_wildcard_access,
    web_auth,
)
from tests.fixtures.mocks.mock_auth import MockSessionValidator

logger = logging.getLogger(__name__)


# =============================================================================
# FIXTURES
# =============================================================================


@pytest.fixture
def test_secret_key() -> str:
    """Secret key for session signing."""
    return "test-secret-key-for-unit-tests"


@pytest.fixture
def mock_signer(test_secret_key: str, monkeypatch):
    """Mock URLSafeTimedSerializer for session signing."""
    signer = URLSafeTimedSerializer(test_secret_key)
    # Patch the module-level signer
    monkeypatch.setattr("registry.auth.dependencies.signer", signer)
    return signer


@pytest.fixture
def mock_session_store(monkeypatch):
    """Mock the server-side session store lookup.

    The test sets `mock_session_store.next_value = {...}` (or None) and any
    `signer.dumps(<session_id>)` cookie passed through resolve_session_from_cookie
    will resolve to that value, regardless of what session_id was used.
    """

    class _Stub:
        next_value: dict | None = None

    stub = _Stub()

    async def _fake_resolve(session_id: str):
        return stub.next_value

    monkeypatch.setattr("registry.auth.dependencies._store_resolve_session", _fake_resolve)
    return stub


def _make_session_cookie(signer: URLSafeTimedSerializer, session_id: str = "sid-test") -> str:
    """Helper: produce a signed opaque-session_id cookie matching the new format."""
    return signer.dumps(session_id)


@pytest.fixture
def sample_scopes_config() -> dict[str, Any]:
    """Sample scopes configuration for testing."""
    return {
        "UI-Scopes": {
            "mcp-registry-admin": {
                "list_agents": ["all"],
                "get_agent": ["all"],
                "publish_agent": ["all"],
                "modify_agent": ["all"],
                "delete_agent": ["all"],
                "list_service": ["all"],
                "register_service": ["all"],
                "toggle_service": ["all"],
            },
            "registry-admins": {
                "list_agents": ["all"],
                "get_agent": ["all"],
                "publish_agent": ["all"],
                "modify_agent": ["all"],
                "delete_agent": ["all"],
                "list_service": ["all"],
                "register_service": ["all"],
                "toggle_service": ["all"],
            },
            "registry-users-lob1": {
                "list_agents": ["/code-reviewer", "/test-automation"],
                "get_agent": ["/code-reviewer", "/test-automation"],
                "list_service": ["currenttime", "mcpgw"],
            },
        },
        "group_mappings": {
            "mcp-registry-admin": [
                "mcp-registry-admin",
                "mcp-servers-unrestricted/read",
                "mcp-servers-unrestricted/execute",
            ],
            "registry-admins": [
                "registry-admins",
                "mcp-servers-unrestricted/read",
                "mcp-servers-unrestricted/execute",
            ],
            "registry-users-lob1": ["registry-users-lob1"],
        },
        "mcp-servers-unrestricted/read": [
            {
                "server": "*",
                "methods": ["initialize", "tools/list", "tools/call"],
                "tools": "*",
            }
        ],
        "mcp-servers-unrestricted/execute": [
            {
                "server": "*",
                "methods": ["initialize", "GET", "POST", "PUT", "DELETE"],
                "tools": "*",
            }
        ],
        "registry-admins": [
            {
                "server": "*",
                "methods": [
                    "initialize",
                    "GET",
                    "POST",
                    "PUT",
                    "DELETE",
                    "tools/list",
                    "tools/call",
                ],
                "tools": "*",
            }
        ],
        "registry-users-lob1": [
            {
                "server": "currenttime",
                "methods": ["initialize", "tools/list"],
                "tools": ["current_time_by_timezone"],
            }
        ],
    }


@pytest.fixture
def mock_scopes_config(sample_scopes_config: dict[str, Any], monkeypatch):
    """Mock SCOPES_CONFIG global variable and scope repository."""
    # Keep existing monkeypatch for backward compatibility
    monkeypatch.setattr("registry.auth.dependencies.SCOPES_CONFIG", sample_scopes_config)

    # Create mock repository
    mock_repo = AsyncMock()

    # Configure get_group_mappings based on sample config
    async def mock_get_group_mappings(group: str):
        group_mappings = sample_scopes_config.get("group_mappings", {})
        return group_mappings.get(group, [])

    # Configure get_ui_scopes based on sample config
    async def mock_get_ui_scopes(scope: str):
        ui_scopes = sample_scopes_config.get("UI-Scopes", {})
        return ui_scopes.get(scope, {})

    # Configure get_server_scopes based on sample config
    async def mock_get_server_scopes(scope: str):
        # Check in the main config for scope definitions
        # The scope config is stored directly as a key in sample_scopes_config
        # Return the raw config (list of dicts), not extracted server names
        scope_config = sample_scopes_config.get(scope, [])
        if scope_config and isinstance(scope_config, list):
            return scope_config
        return []

    mock_repo.get_group_mappings.side_effect = mock_get_group_mappings
    mock_repo.get_ui_scopes.side_effect = mock_get_ui_scopes
    mock_repo.get_server_scopes.side_effect = mock_get_server_scopes

    # Patch get_scope_repository to return our mock using patch context manager
    # Since it's imported locally in functions, we need to patch the import
    with patch("registry.repositories.factory.get_scope_repository", return_value=mock_repo):
        yield sample_scopes_config


@pytest.fixture
def mock_session_validator(test_secret_key: str):
    """Create a mock session validator."""
    return MockSessionValidator(secret_key=test_secret_key)


# =============================================================================
# TEST: get_current_user
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestGetCurrentUser:
    """Tests for get_current_user dependency."""

    @pytest.mark.asyncio
    async def test_get_current_user_with_valid_session(
        self, mock_signer: URLSafeTimedSerializer, mock_session_store
    ):
        """Test extracting user from valid session cookie."""
        mock_session_store.next_value = {"username": "testuser", "auth_method": "oauth2"}
        session_cookie = _make_session_cookie(mock_signer)

        username = await get_current_user(session=session_cookie)

        assert username == "testuser"

    @pytest.mark.asyncio
    async def test_get_current_user_no_session_cookie(self, mock_session_store):
        """Test that missing session cookie raises 401."""
        with pytest.raises(HTTPException) as exc_info:
            await get_current_user(session=None)

        assert exc_info.value.status_code == 401
        assert "Authentication required" in exc_info.value.detail

    @pytest.mark.asyncio
    async def test_get_current_user_expired_session(
        self, mock_signer: URLSafeTimedSerializer, mock_session_store
    ):
        """Test that expired session raises 401."""
        session_cookie = _make_session_cookie(mock_signer)

        # Force the cookie's signature check to report expiry — short-circuits
        # before the store lookup.
        with patch.object(mock_signer, "loads", side_effect=SignatureExpired("Expired")):
            with pytest.raises(HTTPException) as exc_info:
                await get_current_user(session=session_cookie)

        assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_get_current_user_invalid_signature(self, mock_session_store):
        """Test that invalid signature raises 401."""
        with pytest.raises(HTTPException) as exc_info:
            await get_current_user(session="invalid.session.cookie")

        assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_get_current_user_no_username_in_session(
        self, mock_signer: URLSafeTimedSerializer, mock_session_store
    ):
        """Test that session without username raises 401."""
        # Store returns a hydrated record with no username.
        mock_session_store.next_value = {"username": None, "auth_method": "oauth2"}
        session_cookie = _make_session_cookie(mock_signer)

        with pytest.raises(HTTPException) as exc_info:
            await get_current_user(session=session_cookie)

        assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_get_current_user_legacy_cookie_rejected(
        self, mock_signer: URLSafeTimedSerializer, mock_session_store
    ):
        """Legacy dict-payload cookies must be rejected — forces re-login."""
        legacy_cookie = mock_signer.dumps({"username": "testuser", "auth_method": "oauth2"})

        with pytest.raises(HTTPException) as exc_info:
            await get_current_user(session=legacy_cookie)

        assert exc_info.value.status_code == 401


# =============================================================================
# TEST: get_user_session_data
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestGetUserSessionData:
    """Tests for get_user_session_data dependency."""

    @pytest.mark.asyncio
    async def test_get_session_data_traditional_user_rejected(
        self, mock_signer: URLSafeTimedSerializer, mock_session_store
    ):
        """Test that non-OAuth2 sessions are rejected."""
        mock_session_store.next_value = {"username": "admin", "auth_method": "traditional"}
        session_cookie = _make_session_cookie(mock_signer)

        with pytest.raises(HTTPException) as exc_info:
            await get_user_session_data(session=session_cookie)
        assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_get_session_data_oauth2_user(
        self, mock_signer: URLSafeTimedSerializer, mock_session_store
    ):
        """Test extracting session data for OAuth2 user."""
        mock_session_store.next_value = {
            "username": "oauth_user",
            "auth_method": "oauth2",
            "groups": ["registry-users-lob1"],
            "provider": "cognito",
        }
        session_cookie = _make_session_cookie(mock_signer)

        result = await get_user_session_data(session=session_cookie)

        assert result["username"] == "oauth_user"
        assert result["auth_method"] == "oauth2"
        assert result["groups"] == ["registry-users-lob1"]
        assert "scopes" not in result or "mcp-registry-admin" not in result.get("scopes", [])

    @pytest.mark.asyncio
    async def test_get_session_data_no_session(self, mock_session_store):
        """Test that missing session raises 401."""
        with pytest.raises(HTTPException) as exc_info:
            await get_user_session_data(session=None)

        assert exc_info.value.status_code == 401
        assert "Authentication required" in exc_info.value.detail

    @pytest.mark.asyncio
    async def test_get_session_data_expired(
        self, mock_signer: URLSafeTimedSerializer, mock_session_store
    ):
        """Test that expired session raises 401."""
        session_cookie = _make_session_cookie(mock_signer)

        with patch.object(mock_signer, "loads", side_effect=SignatureExpired("Expired")):
            with pytest.raises(HTTPException) as exc_info:
                await get_user_session_data(session=session_cookie)

        assert exc_info.value.status_code == 401


# =============================================================================
# TEST: load_scopes_config
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
@pytest.mark.skip(reason="load_scopes_config function does not exist in dependencies.py")
class TestLoadScopesConfig:
    """Tests for load_scopes_config function."""

    def test_load_scopes_config_from_default_path(self, tmp_path: Path, monkeypatch):
        """Test loading scopes config from default path."""
        # Arrange
        scopes_file = tmp_path / "auth_server" / "scopes.yml"
        scopes_file.parent.mkdir(parents=True)

        test_config = {
            "group_mappings": {
                "test-group": ["test-scope"],
            }
        }

        with open(scopes_file, "w") as f:
            yaml.safe_dump(test_config, f)

        # Set env var to point to our test file
        monkeypatch.setenv("SCOPES_CONFIG_PATH", str(scopes_file))

        # Act
        config = load_scopes_config()

        # Assert
        assert "group_mappings" in config
        assert "test-group" in config["group_mappings"]

    def test_load_scopes_config_from_env_var(self, tmp_path: Path, monkeypatch):
        """Test loading scopes config from SCOPES_CONFIG_PATH env var."""
        # Arrange
        scopes_file = tmp_path / "custom_scopes.yml"
        test_config = {
            "group_mappings": {
                "custom-group": ["custom-scope"],
            }
        }

        with open(scopes_file, "w") as f:
            yaml.safe_dump(test_config, f)

        monkeypatch.setenv("SCOPES_CONFIG_PATH", str(scopes_file))

        # Act
        config = load_scopes_config()

        # Assert
        assert "group_mappings" in config
        assert "custom-group" in config["group_mappings"]

    def test_load_scopes_config_file_not_found(self, monkeypatch):
        """Test that missing scopes file returns empty dict."""
        # Arrange
        monkeypatch.delenv("SCOPES_CONFIG_PATH", raising=False)

        # Mock Path to always return non-existent file
        with patch("registry.auth.dependencies.Path") as mock_path:
            mock_path.return_value.exists.return_value = False
            mock_path.return_value.parent.exists.return_value = True
            mock_path.return_value.parent.iterdir.return_value = []

            # Act
            config = load_scopes_config()

        # Assert
        assert config == {}

    def test_load_scopes_config_yaml_error(self, tmp_path: Path, monkeypatch):
        """Test that YAML parsing error returns empty dict."""
        # Arrange
        scopes_file = tmp_path / "invalid_scopes.yml"
        scopes_file.write_text("invalid: yaml: content: [")

        monkeypatch.setenv("SCOPES_CONFIG_PATH", str(scopes_file))

        # Act
        config = load_scopes_config()

        # Assert
        assert config == {}


# =============================================================================
# TEST: map_cognito_groups_to_scopes
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestMapCognitoGroupsToScopes:
    """Tests for map_cognito_groups_to_scopes function."""

    @pytest.mark.asyncio
    async def test_map_admin_group(self, mock_scopes_config: dict[str, Any]):
        """Test mapping admin group to scopes."""
        # Arrange
        groups = ["mcp-registry-admin"]

        # Act
        scopes = await map_cognito_groups_to_scopes(groups)

        # Assert
        assert "mcp-registry-admin" in scopes
        assert "mcp-servers-unrestricted/read" in scopes
        assert "mcp-servers-unrestricted/execute" in scopes

    @pytest.mark.asyncio
    async def test_map_lob1_group(self, mock_scopes_config: dict[str, Any]):
        """Test mapping LOB1 group to scopes."""
        # Arrange
        groups = ["registry-users-lob1"]

        # Act
        scopes = await map_cognito_groups_to_scopes(groups)

        # Assert
        assert "registry-users-lob1" in scopes
        assert "mcp-registry-admin" not in scopes

    @pytest.mark.asyncio
    async def test_map_multiple_groups(self, mock_scopes_config: dict[str, Any]):
        """Test mapping multiple groups removes duplicates."""
        # Arrange
        groups = ["mcp-registry-admin", "registry-users-lob1"]

        # Act
        scopes = await map_cognito_groups_to_scopes(groups)

        # Assert
        assert "mcp-registry-admin" in scopes
        assert "registry-users-lob1" in scopes
        # Verify no duplicates
        assert len(scopes) == len(set(scopes))

    @pytest.mark.asyncio
    async def test_map_unknown_group(self, mock_scopes_config: dict[str, Any]):
        """Test mapping unknown group returns empty list."""
        # Arrange
        groups = ["unknown-group"]

        # Act
        scopes = await map_cognito_groups_to_scopes(groups)

        # Assert
        assert scopes == []

    @pytest.mark.asyncio
    async def test_map_empty_groups(self, mock_scopes_config: dict[str, Any]):
        """Test mapping empty groups list."""
        # Arrange
        groups = []

        # Act
        scopes = await map_cognito_groups_to_scopes(groups)

        # Assert
        assert scopes == []


# =============================================================================
# TEST: get_ui_permissions_for_user
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestGetUIPermissionsForUser:
    """Tests for get_ui_permissions_for_user function."""

    @pytest.mark.asyncio
    async def test_admin_ui_permissions(self, mock_scopes_config: dict[str, Any]):
        """Test admin user gets all UI permissions."""
        # Arrange
        user_scopes = ["mcp-registry-admin"]

        # Act
        permissions = await get_ui_permissions_for_user(user_scopes)

        # Assert
        assert "list_agents" in permissions
        assert "all" in permissions["list_agents"]
        assert "list_service" in permissions
        assert "all" in permissions["list_service"]

    @pytest.mark.asyncio
    async def test_lob1_ui_permissions(self, mock_scopes_config: dict[str, Any]):
        """Test LOB1 user gets restricted UI permissions."""
        # Arrange
        user_scopes = ["registry-users-lob1"]

        # Act
        permissions = await get_ui_permissions_for_user(user_scopes)

        # Assert
        assert "list_agents" in permissions
        assert "/code-reviewer" in permissions["list_agents"]
        assert "/test-automation" in permissions["list_agents"]
        assert "all" not in permissions["list_agents"]

    @pytest.mark.asyncio
    async def test_no_scopes_no_permissions(self, mock_scopes_config: dict[str, Any]):
        """Test user with no scopes gets no permissions."""
        # Arrange
        user_scopes = []

        # Act
        permissions = await get_ui_permissions_for_user(user_scopes)

        # Assert
        assert permissions == {}

    @pytest.mark.asyncio
    async def test_unknown_scope_no_permissions(self, mock_scopes_config: dict[str, Any]):
        """Test unknown scope grants no permissions."""
        # Arrange
        user_scopes = ["unknown-scope"]

        # Act
        permissions = await get_ui_permissions_for_user(user_scopes)

        # Assert
        assert permissions == {}


# =============================================================================
# TEST: user_has_ui_permission_for_service
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestUserHasUIPermissionForService:
    """Tests for user_has_ui_permission_for_service function."""

    def test_has_permission_for_all_services(self):
        """Test user with 'all' permission can access any service."""
        # Arrange
        permissions = {"list_service": ["all"]}

        # Act & Assert
        assert user_has_ui_permission_for_service("list_service", "any_service", permissions)

    def test_has_permission_for_specific_service(self):
        """Test user with specific service permission."""
        # Arrange
        permissions = {"list_service": ["currenttime", "mcpgw"]}

        # Act & Assert
        assert user_has_ui_permission_for_service("list_service", "currenttime", permissions)
        assert user_has_ui_permission_for_service("list_service", "mcpgw", permissions)

    def test_no_permission_for_service(self):
        """Test user without permission for service."""
        # Arrange
        permissions = {"list_service": ["currenttime"]}

        # Act & Assert
        assert not user_has_ui_permission_for_service("list_service", "other_service", permissions)

    def test_permission_not_in_user_permissions(self):
        """Test permission type not in user's permissions."""
        # Arrange
        permissions = {"list_service": ["currenttime"]}

        # Act & Assert
        assert not user_has_ui_permission_for_service(
            "register_service", "currenttime", permissions
        )


# =============================================================================
# TEST: get_accessible_services_for_user
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestGetAccessibleServicesForUser:
    """Tests for get_accessible_services_for_user function."""

    def test_all_services_accessible(self):
        """Test user with 'all' can access all services."""
        # Arrange
        permissions = {"list_service": ["all"]}

        # Act
        services = get_accessible_services_for_user(permissions)

        # Assert
        assert services == ["all"]

    def test_specific_services_accessible(self):
        """Test user with specific services."""
        # Arrange
        permissions = {"list_service": ["currenttime", "mcpgw"]}

        # Act
        services = get_accessible_services_for_user(permissions)

        # Assert
        assert "currenttime" in services
        assert "mcpgw" in services

    def test_no_list_permission(self):
        """Test user without list_service permission."""
        # Arrange
        permissions = {"other_permission": ["service1"]}

        # Act
        services = get_accessible_services_for_user(permissions)

        # Assert
        assert services == []


# =============================================================================
# TEST: get_accessible_agents_for_user
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestGetAccessibleAgentsForUser:
    """Tests for get_accessible_agents_for_user function."""

    def test_all_agents_accessible(self):
        """Test user with 'all' can access all agents."""
        # Arrange
        permissions = {"list_agents": ["all"]}

        # Act
        agents = get_accessible_agents_for_user(permissions)

        # Assert
        assert agents == ["all"]

    def test_specific_agents_accessible(self):
        """Test user with specific agents."""
        # Arrange
        permissions = {"list_agents": ["/code-reviewer", "/test-automation"]}

        # Act
        agents = get_accessible_agents_for_user(permissions)

        # Assert
        assert "/code-reviewer" in agents
        assert "/test-automation" in agents

    def test_no_list_agents_permission(self):
        """Test user without list_agents permission."""
        # Arrange
        permissions = {"other_permission": ["/agent1"]}

        # Act
        agents = get_accessible_agents_for_user(permissions)

        # Assert
        assert agents == []


# =============================================================================
# TEST: get_servers_for_scope
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestGetServersForScope:
    """Tests for get_servers_for_scope function."""

    @pytest.mark.asyncio
    async def test_wildcard_scope_returns_wildcard(self, mock_scopes_config: dict[str, Any]):
        """Test wildcard scope returns wildcard server."""
        # Act
        servers = await get_servers_for_scope("mcp-servers-unrestricted/read")

        # Assert
        assert "*" in servers

    @pytest.mark.asyncio
    async def test_specific_scope_returns_servers(self, mock_scopes_config: dict[str, Any]):
        """Test specific scope returns specific servers."""
        # Act
        servers = await get_servers_for_scope("registry-users-lob1")

        # Assert
        assert "currenttime" in servers

    @pytest.mark.asyncio
    async def test_unknown_scope_returns_empty(self, mock_scopes_config: dict[str, Any]):
        """Test unknown scope returns empty list."""
        # Act
        servers = await get_servers_for_scope("unknown-scope")

        # Assert
        assert servers == []


# =============================================================================
# TEST: user_has_wildcard_access
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestUserHasWildcardAccess:
    """Tests for user_has_wildcard_access function."""

    @pytest.mark.asyncio
    async def test_admin_has_wildcard_access(self, mock_scopes_config: dict[str, Any]):
        """Test admin user has wildcard access."""
        # Arrange
        scopes = ["mcp-servers-unrestricted/read"]

        # Act
        has_access = await user_has_wildcard_access(scopes)

        # Assert
        assert has_access is True

    @pytest.mark.asyncio
    async def test_restricted_user_no_wildcard_access(self, mock_scopes_config: dict[str, Any]):
        """Test restricted user has no wildcard access."""
        # Arrange
        scopes = ["registry-users-lob1"]

        # Act
        has_access = await user_has_wildcard_access(scopes)

        # Assert
        assert has_access is False

    @pytest.mark.asyncio
    async def test_no_scopes_no_wildcard_access(self, mock_scopes_config: dict[str, Any]):
        """Test user with no scopes has no wildcard access."""
        # Arrange
        scopes = []

        # Act
        has_access = await user_has_wildcard_access(scopes)

        # Assert
        assert has_access is False


# =============================================================================
# TEST: get_user_accessible_servers
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestGetUserAccessibleServers:
    """Tests for get_user_accessible_servers function."""

    @pytest.mark.asyncio
    async def test_admin_access_all_servers(self, mock_scopes_config: dict[str, Any]):
        """Test admin user can access all servers (wildcard)."""
        # Arrange
        scopes = ["mcp-servers-unrestricted/read"]

        # Act
        servers = await get_user_accessible_servers(scopes)

        # Assert
        assert "*" in servers

    @pytest.mark.asyncio
    async def test_lob1_access_specific_servers(self, mock_scopes_config: dict[str, Any]):
        """Test LOB1 user can access specific servers."""
        # Arrange
        scopes = ["registry-users-lob1"]

        # Act
        servers = await get_user_accessible_servers(scopes)

        # Assert
        assert "currenttime" in servers
        assert "*" not in servers

    @pytest.mark.asyncio
    async def test_multiple_scopes_combine_servers(self, mock_scopes_config: dict[str, Any]):
        """Test multiple scopes combine accessible servers."""
        # Arrange
        scopes = [
            "registry-users-lob1",
            "mcp-servers-unrestricted/read",
        ]

        # Act
        servers = await get_user_accessible_servers(scopes)

        # Assert
        assert "currenttime" in servers
        assert "*" in servers


# =============================================================================
# TEST: user_can_modify_servers
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestUserCanModifyServers:
    """Tests for user_can_modify_servers function."""

    def test_admin_can_modify(self):
        """Test admin group can modify servers."""
        # Arrange
        groups = ["mcp-registry-admin"]
        scopes = ["mcp-servers-unrestricted/execute"]

        # Act
        can_modify = user_can_modify_servers(groups, scopes)

        # Assert
        assert can_modify is True

    def test_execute_scope_can_modify(self):
        """Test user with execute scope can modify."""
        # Arrange
        groups = []
        scopes = ["mcp-servers-unrestricted/execute"]

        # Act
        can_modify = user_can_modify_servers(groups, scopes)

        # Assert
        assert can_modify is True

    def test_read_only_cannot_modify(self):
        """Test read-only user cannot modify."""
        # Arrange
        groups = ["registry-users-lob1"]
        scopes = ["registry-users-lob1"]

        # Act
        can_modify = user_can_modify_servers(groups, scopes)

        # Assert
        assert can_modify is False

    def test_any_execute_scope_can_modify(self):
        """Test any execute scope grants modify permission."""
        # Arrange
        groups = []
        scopes = ["some-scope/execute"]

        # Act
        can_modify = user_can_modify_servers(groups, scopes)

        # Assert
        assert can_modify is True


# =============================================================================
# TEST: user_can_access_server
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestUserCanAccessServer:
    """Tests for user_can_access_server function."""

    @pytest.mark.asyncio
    async def test_admin_can_access_any_server(self, mock_scopes_config: dict[str, Any]):
        """Test admin can access any server."""
        # Arrange
        scopes = ["mcp-servers-unrestricted/read"]

        # Act & Assert
        # Admin has wildcard in accessible servers
        # Note: The implementation checks if server name is in accessible_servers list
        # For wildcard access, "*" is in the list, but specific server names won't match
        # This test documents current behavior - wildcard doesn't match arbitrary names
        # User needs to check for "*" in accessible_servers separately
        accessible_servers = await get_user_accessible_servers(scopes)
        assert "*" in accessible_servers

        # The function doesn't expand wildcard, so specific server check returns False
        # This is expected behavior - caller should check for "*" separately
        assert not await user_can_access_server("any-server", scopes)

    @pytest.mark.asyncio
    async def test_user_can_access_allowed_server(self, mock_scopes_config: dict[str, Any]):
        """Test user can access allowed server."""
        # Arrange
        scopes = ["registry-users-lob1"]

        # Act & Assert
        assert await user_can_access_server("currenttime", scopes)

    @pytest.mark.asyncio
    async def test_user_cannot_access_disallowed_server(self, mock_scopes_config: dict[str, Any]):
        """Test user cannot access disallowed server."""
        # Arrange
        scopes = ["registry-users-lob1"]

        # Act & Assert
        assert not await user_can_access_server("other-server", scopes)


# =============================================================================
# TEST: api_auth and web_auth
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestAuthWrappers:
    """Tests for api_auth and web_auth wrapper functions."""

    @pytest.mark.asyncio
    async def test_api_auth_calls_get_current_user(
        self, mock_signer: URLSafeTimedSerializer, mock_session_store
    ):
        """Test api_auth delegates to get_current_user."""
        mock_session_store.next_value = {"username": "apiuser", "auth_method": "oauth2"}
        session_cookie = _make_session_cookie(mock_signer)

        username = await api_auth(session=session_cookie)

        assert username == "apiuser"

    @pytest.mark.asyncio
    async def test_web_auth_calls_get_current_user(
        self, mock_signer: URLSafeTimedSerializer, mock_session_store
    ):
        """Test web_auth delegates to get_current_user."""
        mock_session_store.next_value = {"username": "webuser", "auth_method": "oauth2"}
        session_cookie = _make_session_cookie(mock_signer)

        username = await web_auth(session=session_cookie)

        assert username == "webuser"


# =============================================================================
# TEST: enhanced_auth
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestEnhancedAuth:
    """Tests for enhanced_auth dependency."""

    @pytest.mark.asyncio
    async def test_enhanced_auth_traditional_user_rejected(
        self,
        mock_signer: URLSafeTimedSerializer,
        mock_session_store,
        mock_scopes_config: dict[str, Any],
    ):
        """Test enhanced_auth rejects traditional (non-OAuth2) sessions."""
        mock_session_store.next_value = {
            "username": "admin",
            "auth_method": "traditional",
            "provider": "local",
        }
        session_cookie = _make_session_cookie(mock_signer)
        mock_request = Mock(spec=Request)
        mock_request.state = Mock()

        with pytest.raises(HTTPException) as exc_info:
            await enhanced_auth(request=mock_request, session=session_cookie)
        assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_enhanced_auth_oauth2_user(
        self,
        mock_signer: URLSafeTimedSerializer,
        mock_session_store,
        mock_scopes_config: dict[str, Any],
    ):
        """Test enhanced_auth for OAuth2 user."""
        mock_session_store.next_value = {
            "session_id": "sid-1",
            "username": "oauth_user",
            "auth_method": "oauth2",
            "provider": "cognito",
            "groups": ["registry-users-lob1"],
        }
        session_cookie = _make_session_cookie(mock_signer)
        mock_request = Mock(spec=Request)
        mock_request.state = Mock()

        context = await enhanced_auth(request=mock_request, session=session_cookie)

        assert context["username"] == "oauth_user"
        assert context["auth_method"] == "oauth2"
        assert "registry-users-lob1" in context["groups"]
        assert context["can_modify_servers"] is False
        assert context["is_admin"] is False

    @pytest.mark.asyncio
    async def test_enhanced_auth_no_session(self):
        """Test enhanced_auth raises 401 without session."""
        # Arrange
        mock_request = Mock(spec=Request)
        mock_request.state = Mock()

        # Act & Assert
        with pytest.raises(HTTPException) as exc_info:
            await enhanced_auth(request=mock_request, session=None)

        assert exc_info.value.status_code == 401


# =============================================================================
# TEST: nginx_proxied_auth
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestNginxProxiedAuth:
    """Tests for nginx_proxied_auth dependency."""

    @pytest.mark.asyncio
    async def test_nginx_auth_without_x_groups_keeps_groups_empty(
        self, mock_scopes_config: dict[str, Any]
    ):
        """When X-Groups is absent, groups stay empty (#933).

        Previously the proxied path synthesized "mcp-registry-admin" from a
        scope heuristic when X-Groups was missing — granting admin to
        non-admins on the proxied path while the cookie path correctly said
        non-admin for the same user. The synthesis is now removed; admin
        status is derived purely from scope-based ui_permissions.
        """
        mock_request = Mock(spec=Request)
        mock_request.url.path = "/api/test"
        mock_request.method = "GET"
        mock_request.state = Mock()
        mock_request.headers = {}

        context = await nginx_proxied_auth(
            request=mock_request,
            session=None,
            x_user="nginx_user",
            x_username="nginx_user",
            x_scopes="mcp-servers-unrestricted/read mcp-servers-unrestricted/execute",
            x_auth_method="keycloak",
            x_groups=None,
        )

        assert context["username"] == "nginx_user"
        assert context["auth_method"] == "keycloak"
        assert "mcp-servers-unrestricted/read" in context["scopes"]
        # No synthesis: groups stay empty when X-Groups isn't forwarded.
        assert context["groups"] == []

    @pytest.mark.asyncio
    async def test_nginx_auth_uses_forwarded_x_groups(self, mock_scopes_config: dict[str, Any]):
        """X-Groups header is propagated verbatim into user_context."""
        mock_request = Mock(spec=Request)
        mock_request.url.path = "/api/test"
        mock_request.method = "GET"
        mock_request.state = Mock()
        mock_request.headers = {}

        context = await nginx_proxied_auth(
            request=mock_request,
            session=None,
            x_user="nginx_user",
            x_username="nginx_user",
            x_scopes="mcp-servers-unrestricted/read",
            x_auth_method="keycloak",
            x_groups="registry-admins developers",
        )

        assert context["username"] == "nginx_user"
        assert context["groups"] == ["registry-admins", "developers"]

    @pytest.mark.asyncio
    async def test_nginx_auth_fallback_to_session_oauth2(
        self,
        mock_signer: URLSafeTimedSerializer,
        mock_session_store,
        mock_scopes_config: dict[str, Any],
    ):
        """Test nginx auth falls back to OAuth2 session cookie."""
        mock_request = Mock(spec=Request)
        mock_request.url.path = "/api/test"
        mock_request.method = "GET"
        mock_request.state = Mock()
        mock_request.headers = {}

        mock_session_store.next_value = {
            "session_id": "sid-1",
            "username": "session_user",
            "auth_method": "oauth2",
            "provider": "cognito",
            "groups": ["registry-admins"],
        }
        session_cookie = _make_session_cookie(mock_signer)

        context = await nginx_proxied_auth(
            request=mock_request,
            session=session_cookie,
            x_user=None,
            x_username=None,
            x_scopes=None,
            x_auth_method=None,
            x_client_id=None,
        )

        assert context["username"] == "session_user"
        assert context["auth_method"] == "oauth2"

    @pytest.mark.asyncio
    async def test_nginx_auth_fallback_rejects_traditional_session(
        self,
        mock_signer: URLSafeTimedSerializer,
        mock_session_store,
        mock_scopes_config: dict[str, Any],
    ):
        """Test nginx auth rejects traditional (non-OAuth2) session cookies."""
        mock_request = Mock(spec=Request)
        mock_request.url.path = "/api/test"
        mock_request.method = "GET"
        mock_request.state = Mock()
        mock_request.headers = {}

        mock_session_store.next_value = {
            "username": "session_user",
            "auth_method": "traditional",
        }
        session_cookie = _make_session_cookie(mock_signer)

        # Act & Assert - traditional sessions should be rejected
        with pytest.raises(HTTPException) as exc_info:
            await nginx_proxied_auth(
                request=mock_request,
                session=session_cookie,
                x_user=None,
                x_username=None,
                x_scopes=None,
                x_auth_method=None,
                x_client_id=None,
            )
        assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_nginx_auth_oauth2_user_without_admin_scopes(
        self, mock_scopes_config: dict[str, Any]
    ):
        """Non-admin scopes → not admin, regardless of group synthesis.

        Previously this test asserted the synthesized group "mcp-registry-user"
        was injected. After the synthesis removal (#933), groups stay empty
        when X-Groups isn't forwarded; what matters is that is_admin reflects
        the scopes correctly via ui_permissions.
        """
        mock_request = Mock(spec=Request)
        mock_request.url.path = "/api/test"
        mock_request.method = "GET"
        mock_request.state = Mock()
        mock_request.headers = {}

        context = await nginx_proxied_auth(
            request=mock_request,
            session=None,
            x_user="oauth_user",
            x_username="oauth_user",
            x_scopes="registry-users-lob1",
            x_auth_method="cognito",
        )

        assert context["username"] == "oauth_user"
        assert context["groups"] == []
        assert context["is_admin"] is False


# =============================================================================
# TEST: Edge Cases and Error Handling
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestEdgeCases:
    """Tests for edge cases and error handling."""

    @pytest.mark.asyncio
    async def test_session_with_empty_username(
        self, mock_signer: URLSafeTimedSerializer, mock_session_store
    ):
        """Test session with empty string username."""
        mock_session_store.next_value = {"username": "", "auth_method": "oauth2"}
        session_cookie = _make_session_cookie(mock_signer)

        with pytest.raises(HTTPException) as exc_info:
            await get_current_user(session=session_cookie)

        assert exc_info.value.status_code == 401

    @pytest.mark.asyncio
    async def test_scopes_deduplication(self, mock_scopes_config: dict[str, Any]):
        """Test that duplicate scopes are removed."""
        # Arrange - create mock repository that returns duplicate scopes
        mock_repo = AsyncMock()

        async def mock_get_group_mappings_with_duplicates(group: str):
            if group == "test-group":
                return ["scope1", "scope2", "scope1"]
            return []

        mock_repo.get_group_mappings.side_effect = mock_get_group_mappings_with_duplicates

        with patch("registry.repositories.factory.get_scope_repository", return_value=mock_repo):
            # Act
            scopes = await map_cognito_groups_to_scopes(["test-group"])

            # Assert
            assert len(scopes) == len(set(scopes))  # No duplicates
            assert scopes.count("scope1") == 1

    @pytest.mark.asyncio
    async def test_enhanced_auth_oauth2_no_groups(
        self,
        mock_signer: URLSafeTimedSerializer,
        mock_session_store,
        mock_scopes_config: dict[str, Any],
    ):
        """Test OAuth2 user with no groups gets minimal permissions."""
        mock_session_store.next_value = {
            "session_id": "sid-1",
            "username": "no_groups_user",
            "auth_method": "oauth2",
            "groups": [],
        }
        session_cookie = _make_session_cookie(mock_signer)
        mock_request = Mock(spec=Request)
        mock_request.state = Mock()

        context = await enhanced_auth(request=mock_request, session=session_cookie)

        assert context["username"] == "no_groups_user"
        assert context["groups"] == []
        assert context["scopes"] == []
        assert context["can_modify_servers"] is False

    def test_ui_permissions_with_all_and_specific(self, mock_scopes_config: dict[str, Any]):
        """Test UI permissions handles 'all' with specific services."""
        # Arrange - Create permissions with both 'all' and specific
        permissions = {"list_service": ["all", "currenttime"]}

        # Act & Assert
        assert user_has_ui_permission_for_service("list_service", "any_service", permissions)


# =============================================================================
# NETWORK-TRUSTED AUTH METHOD TESTS
# =============================================================================


class TestNetworkTrustedAuthMethod:
    """Tests for network-trusted auth method in nginx_proxied_auth (issue #357)."""

    @pytest.mark.asyncio
    async def test_network_trusted_with_admin_scopes_gets_admin(
        self, mock_scopes_config: dict[str, Any]
    ):
        """Test network-trusted auth method with admin scopes resolves to admin.

        After issue #779, network-trusted goes through the standard resolution
        path (hard-coded admin branch removed). The auth server now returns the
        full scope set including UI scope names (e.g. mcp-registry-admin) so
        the registry can derive admin status via _user_is_admin. After #933,
        groups are not synthesized from scopes; admin status here is driven
        by ui_permissions resolved from the mcp-registry-admin scope.
        """
        mock_request = Mock(spec=Request)
        mock_request.url.path = "/api/servers"
        mock_request.method = "GET"
        mock_request.headers = {}

        context = await nginx_proxied_auth(
            request=mock_request,
            session=None,
            x_user="network-user",
            x_username="network-user",
            x_scopes="mcp-registry-admin mcp-servers-unrestricted/read mcp-servers-unrestricted/execute",
            x_auth_method="network-trusted",
        )

        assert context["username"] == "network-user"
        assert context["auth_method"] == "network-trusted"
        assert "mcp-servers-unrestricted/read" in context["scopes"]
        assert "mcp-servers-unrestricted/execute" in context["scopes"]
        # is_admin derives from scope→ui_permissions, not from synthesized groups.
        assert context["is_admin"] is True

    @pytest.mark.asyncio
    async def test_network_trusted_readonly_scopes_not_admin(
        self, mock_scopes_config: dict[str, Any]
    ):
        """Test network-trusted with read-only scopes does NOT get admin (issue #779)."""
        # Arrange
        mock_request = Mock(spec=Request)
        mock_request.url.path = "/api/servers"
        mock_request.method = "GET"
        mock_request.headers = {}

        # Act: read-only scopes only
        context = await nginx_proxied_auth(
            request=mock_request,
            session=None,
            x_user="monitoring-script",
            x_username="monitoring-script",
            x_scopes="registry-users-lob1",
            x_auth_method="network-trusted",
        )

        # Assert
        assert context["username"] == "monitoring-script"
        assert context["is_admin"] is False


# =============================================================================
# REGRESSION: cookie path and proxied path must agree on is_admin (#933)
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestAuthPathsAgreeOnIsAdmin:
    """Same user, same groups → same is_admin regardless of which dependency
    resolved them. This is the core invariant #933 broke."""

    @pytest.mark.asyncio
    async def test_admin_user_is_admin_on_both_paths(
        self,
        mock_signer: URLSafeTimedSerializer,
        mock_session_store,
        mock_scopes_config: dict[str, Any],
    ):
        admin_groups = ["mcp-registry-admin"]

        # Cookie path: enhanced_auth via session_store
        mock_session_store.next_value = {
            "session_id": "sid-1",
            "username": "alice",
            "auth_method": "oauth2",
            "provider": "cognito",
            "groups": admin_groups,
        }
        session_cookie = _make_session_cookie(mock_signer)
        cookie_request = Mock(spec=Request)
        cookie_request.state = Mock()
        cookie_context = await enhanced_auth(request=cookie_request, session=session_cookie)

        # Proxied path: nginx_proxied_auth with X-Groups + X-Scopes pre-computed
        # by the auth_server (mirrors what /validate would forward).
        proxied_request = Mock(spec=Request)
        proxied_request.url.path = "/api/test"
        proxied_request.method = "GET"
        proxied_request.state = Mock()
        proxied_request.headers = {}
        proxied_context = await nginx_proxied_auth(
            request=proxied_request,
            session=None,
            x_user="alice",
            x_username="alice",
            x_scopes="mcp-registry-admin",
            x_auth_method="cognito",
            x_groups=" ".join(admin_groups),
        )

        assert cookie_context["is_admin"] == proxied_context["is_admin"]
        assert cookie_context["is_admin"] is True
        assert cookie_context["ui_permissions"] == proxied_context["ui_permissions"]

    @pytest.mark.asyncio
    async def test_non_admin_is_not_admin_on_either_path(
        self,
        mock_signer: URLSafeTimedSerializer,
        mock_session_store,
        mock_scopes_config: dict[str, Any],
    ):
        user_groups = ["registry-users-lob1"]

        mock_session_store.next_value = {
            "session_id": "sid-2",
            "username": "bob",
            "auth_method": "oauth2",
            "provider": "cognito",
            "groups": user_groups,
        }
        session_cookie = _make_session_cookie(mock_signer)
        cookie_request = Mock(spec=Request)
        cookie_request.state = Mock()
        cookie_context = await enhanced_auth(request=cookie_request, session=session_cookie)

        proxied_request = Mock(spec=Request)
        proxied_request.url.path = "/api/test"
        proxied_request.method = "GET"
        proxied_request.state = Mock()
        proxied_request.headers = {}
        proxied_context = await nginx_proxied_auth(
            request=proxied_request,
            session=None,
            x_user="bob",
            x_username="bob",
            x_scopes="registry-users-lob1",
            x_auth_method="cognito",
            x_groups=" ".join(user_groups),
        )

        assert cookie_context["is_admin"] is False
        assert proxied_context["is_admin"] is False


# =============================================================================
# TEST: _user_is_admin (issue #663)
# =============================================================================


@pytest.mark.unit
@pytest.mark.auth
class TestUserIsAdmin:
    """Tests for _user_is_admin function.

    Verifies that admin status is derived from mutating UI-Scopes actions
    (register_, modify_, toggle_, delete_, publish_, create_) with 'all'
    resources, NOT from server: '*' wildcard access.

    See GitHub issue #663.
    """

    @pytest.mark.parametrize(
        "action",
        [
            "register_service",
            "modify_service",
            "toggle_service",
            "delete_service",
            "publish_agent",
            "modify_agent",
            "delete_agent",
            "create_virtual_server",
            "modify_virtual_server",
            "delete_virtual_server",
        ],
    )
    def test_admin_with_mutating_action_all(self, action: str):
        """User with any mutating action for [all] is admin."""
        # Arrange
        ui_permissions = {action: ["all"], "list_service": ["all"]}

        # Act
        result = _user_is_admin(ui_permissions)

        # Assert
        assert result is True

    def test_not_admin_with_only_read_actions(self):
        """Consumer with only read-only permissions is not admin (issue #663 core fix)."""
        # Arrange
        ui_permissions = {
            "list_service": ["all"],
            "health_check_service": ["all"],
            "list_agents": ["all"],
            "get_agent": ["all"],
            "list_virtual_server": ["all"],
        }

        # Act
        result = _user_is_admin(ui_permissions)

        # Assert
        assert result is False

    def test_not_admin_with_specific_server_modify(self):
        """User with modify_service for specific servers only is not admin."""
        # Arrange
        ui_permissions = {"modify_service": ["server1", "server2"]}

        # Act
        result = _user_is_admin(ui_permissions)

        # Assert
        assert result is False

    def test_not_admin_empty_permissions(self):
        """User with no UI permissions is not admin."""
        # Arrange / Act
        result = _user_is_admin({})

        # Assert
        assert result is False

    def test_full_admin_permissions_match_registry_admins_json(self):
        """Full admin role (matching scripts/registry-admins.json) is admin."""
        # Arrange
        ui_permissions = {
            "list_agents": ["all"],
            "get_agent": ["all"],
            "publish_agent": ["all"],
            "modify_agent": ["all"],
            "delete_agent": ["all"],
            "list_service": ["all"],
            "register_service": ["all"],
            "health_check_service": ["all"],
            "toggle_service": ["all"],
            "modify_service": ["all"],
            "delete_service": ["all"],
            "list_virtual_server": ["all"],
            "create_virtual_server": ["all"],
            "modify_virtual_server": ["all"],
            "delete_virtual_server": ["all"],
        }

        # Act
        result = _user_is_admin(ui_permissions)

        # Assert
        assert result is True

    def test_consumer_with_wildcard_server_not_admin(self):
        """Issue #663: server: '*' in scopes should NOT trigger is_admin.

        A consumer role with server: '*' but only read-only UI-Scopes
        must not be treated as admin.
        """
        # Arrange - consumer has only read-only UI-Scopes
        ui_permissions = {
            "list_service": ["all"],
            "health_check_service": ["all"],
            "list_agents": ["all"],
            "get_agent": ["all"],
        }

        # Act - even though the user's scopes contain server: '*',
        # _user_is_admin only checks ui_permissions, not server access
        result = _user_is_admin(ui_permissions)

        # Assert
        assert result is False

    @pytest.mark.parametrize(
        "action",
        [
            "list_service",
            "get_agent",
            "health_check_service",
            "list_agents",
            "list_virtual_server",
        ],
    )
    def test_read_only_actions_never_grant_admin(self, action: str):
        """Read-only actions with 'all' do not grant admin status."""
        # Arrange
        ui_permissions = {action: ["all"]}

        # Act
        result = _user_is_admin(ui_permissions)

        # Assert
        assert result is False
