"""Tests for OIDC provider registry."""

from __future__ import annotations

from uuid import uuid4

import pytest
from pytest_httpx import HTTPXMock

from noteflow.domain.auth.oidc import OidcProviderCreateParams, OidcProviderPreset
from noteflow.infrastructure.auth.oidc_discovery import OidcDiscoveryError
from noteflow.infrastructure.auth.oidc_registry import (
    PROVIDER_PRESETS,
    OidcAuthService,
    OidcProviderRegistry,
)


@pytest.fixture
def registry() -> OidcProviderRegistry:
    """Create a registry for testing."""
    return OidcProviderRegistry()


# valid_discovery_document fixture is in conftest.py


class TestProviderPresets:
    """Tests for provider preset configurations."""

    def test_all_presets_defined(self) -> None:
        """Verify all preset enum values have configurations."""
        defined_presets = set(PROVIDER_PRESETS.keys())
        enum_presets = set(OidcProviderPreset)

        assert defined_presets == enum_presets, "all presets should have configs"

    def test_authentik_preset(self) -> None:
        """Verify Authentik preset configuration."""
        preset = PROVIDER_PRESETS[OidcProviderPreset.AUTHENTIK]

        assert preset.display_name == "Authentik", "display_name should be Authentik"
        assert "openid" in preset.default_scopes, "should include openid scope"
        assert "groups" in preset.default_scopes, "should include groups scope"
        assert preset.documentation_url is not None, "should have documentation URL"

    def test_authelia_preset(self) -> None:
        """Verify Authelia preset configuration."""
        preset = PROVIDER_PRESETS[OidcProviderPreset.AUTHELIA]

        assert preset.display_name == "Authelia", "display_name should be Authelia"
        assert "openid" in preset.default_scopes, "should include openid scope"
        assert preset.documentation_url is not None, "should have documentation URL"

    def test_keycloak_preset(self) -> None:
        """Verify Keycloak preset configuration."""
        preset = PROVIDER_PRESETS[OidcProviderPreset.KEYCLOAK]

        assert preset.display_name == "Keycloak", "display_name should be Keycloak"
        assert "openid" in preset.default_scopes, "should include openid scope"
        assert preset.notes is not None, "should have mapper configuration note"

    def test_custom_preset(self) -> None:
        """Verify custom preset has sensible defaults."""
        preset = PROVIDER_PRESETS[OidcProviderPreset.CUSTOM]

        assert preset.display_name == "Custom OIDC Provider", "display_name for custom"
        assert "openid" in preset.default_scopes, "should include openid scope"
        assert "profile" in preset.default_scopes, "should include profile scope"
        assert "email" in preset.default_scopes, "should include email scope"


class TestOidcProviderRegistry:
    """Tests for OidcProviderRegistry."""

    @pytest.mark.asyncio
    async def test_create_provider_with_discovery(
        self,
        registry: OidcProviderRegistry,
        httpx_mock: HTTPXMock,
        valid_discovery_document: dict[str, object],
    ) -> None:
        """Verify provider creation with auto-discovery."""
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            json=valid_discovery_document,
        )
        workspace_id = uuid4()

        provider = await registry.create_provider(
            workspace_id=workspace_id,
            name="Test Provider",
            issuer_url="https://auth.example.com",
            client_id="test-client-id",
        )

        assert provider.name == "Test Provider", "provider name should match"
        assert provider.workspace_id == workspace_id, "workspace_id should match"
        assert provider.discovery is not None, "discovery should be populated"
        assert provider.discovery.issuer == "https://auth.example.com", "discovery issuer"

    @pytest.mark.asyncio
    async def test_create_provider_without_discovery(
        self,
        registry: OidcProviderRegistry,
    ) -> None:
        """Verify provider creation without auto-discovery."""
        workspace_id = uuid4()

        provider = await registry.create_provider(
            workspace_id=workspace_id,
            name="Test Provider",
            issuer_url="https://auth.example.com",
            client_id="test-client-id",
            auto_discover=False,
        )

        assert provider.name == "Test Provider", "provider name should match"
        assert provider.discovery is None, "discovery should be None when disabled"

    @pytest.mark.asyncio
    async def test_create_provider_with_preset(
        self,
        registry: OidcProviderRegistry,
    ) -> None:
        """Verify provider creation applies preset defaults."""
        workspace_id = uuid4()

        params = OidcProviderCreateParams(preset=OidcProviderPreset.AUTHENTIK)
        provider = await registry.create_provider(
            workspace_id=workspace_id,
            name="Authentik",
            issuer_url="https://auth.example.com",
            client_id="test-client-id",
            params=params,
            auto_discover=False,
        )

        authentik_preset = PROVIDER_PRESETS[OidcProviderPreset.AUTHENTIK]
        assert provider.scopes == authentik_preset.default_scopes, "scopes from preset"
        assert provider.claim_mapping == authentik_preset.claim_mapping, "claim mapping"

    @pytest.mark.asyncio
    async def test_create_provider_discovery_failure(
        self,
        registry: OidcProviderRegistry,
        httpx_mock: HTTPXMock,
    ) -> None:
        """Verify error handling when discovery fails."""
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            status_code=404,
        )

        with pytest.raises(OidcDiscoveryError, match="HTTP 404"):
            await registry.create_provider(
                workspace_id=uuid4(),
                name="Test Provider",
                issuer_url="https://auth.example.com",
                client_id="test-client-id",
            )

    def test_get_provider(self, registry: OidcProviderRegistry) -> None:
        """Verify get_provider returns correct provider."""
        # Provider not in registry yet
        result = registry.get_provider(uuid4())
        assert result is None, "should return None for unknown provider"

    @pytest.mark.asyncio
    async def test_list_providers(
        self,
        registry: OidcProviderRegistry,
    ) -> None:
        """Verify list_providers with filters."""
        workspace1 = uuid4()
        workspace2 = uuid4()

        # Create providers without discovery
        await registry.create_provider(
            workspace_id=workspace1,
            name="Provider 1",
            issuer_url="https://auth1.example.com",
            client_id="client1",
            auto_discover=False,
        )
        provider2 = await registry.create_provider(
            workspace_id=workspace1,
            name="Provider 2",
            issuer_url="https://auth2.example.com",
            client_id="client2",
            auto_discover=False,
        )
        await registry.create_provider(
            workspace_id=workspace2,
            name="Provider 3",
            issuer_url="https://auth3.example.com",
            client_id="client3",
            auto_discover=False,
        )

        # List all
        all_providers = registry.list_providers()
        assert len(all_providers) == 3, "should list all 3 providers"

        # Filter by workspace
        workspace1_providers = registry.list_providers(workspace_id=workspace1)
        assert len(workspace1_providers) == 2, "should filter to workspace1 providers"

        # Filter by enabled
        provider2.disable()
        enabled = registry.list_providers(workspace_id=workspace1, enabled_only=True)
        assert len(enabled) == 1, "should filter to enabled providers only"

    @pytest.mark.asyncio
    async def test_remove_provider(
        self,
        registry: OidcProviderRegistry,
    ) -> None:
        """Verify provider removal."""
        workspace_id = uuid4()

        provider = await registry.create_provider(
            workspace_id=workspace_id,
            name="Test Provider",
            issuer_url="https://auth.example.com",
            client_id="test-client-id",
            auto_discover=False,
        )

        assert registry.get_provider(provider.id) is not None, "provider should exist"

        result = registry.remove_provider(provider.id)
        assert result is True, "remove should return True"
        assert registry.get_provider(provider.id) is None, "provider should be removed"

        # Remove non-existent
        result = registry.remove_provider(uuid4())
        assert result is False, "remove non-existent should return False"

    def test_get_preset_config(self, registry: OidcProviderRegistry) -> None:
        """Verify preset config retrieval."""
        authentik = registry.get_preset_config(OidcProviderPreset.AUTHENTIK)
        assert authentik.display_name == "Authentik", "should return Authentik config"

        # Unknown falls back to custom
        custom = registry.get_preset_config(OidcProviderPreset.CUSTOM)
        assert custom.preset == OidcProviderPreset.CUSTOM, "custom preset should be CUSTOM"


class TestOidcAuthService:
    """Tests for OidcAuthService."""

    @pytest.fixture
    def auth_service(self) -> OidcAuthService:
        """Create an auth service for testing."""
        return OidcAuthService()

    @pytest.mark.asyncio
    async def test_register_oidc_provider(
        self,
        auth_service: OidcAuthService,
        httpx_mock: HTTPXMock,
        valid_discovery_document: dict[str, object],
    ) -> None:
        """Verify provider registration with validation."""
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            json=valid_discovery_document,
        )
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            json=valid_discovery_document,
        )
        workspace_id = uuid4()

        provider, warnings = await auth_service.register_provider(
            workspace_id=workspace_id,
            name="Test Provider",
            issuer_url="https://auth.example.com",
            client_id="test-client-id",
        )

        assert provider.name == "Test Provider", "provider name should match"
        assert provider.discovery is not None, "discovery should be populated"
        assert isinstance(warnings, list), "warnings should be a list"

    def test_get_preset_options(self, auth_service: OidcAuthService) -> None:
        """Verify preset options retrieval."""
        options = auth_service.get_preset_options()

        assert len(options) == len(OidcProviderPreset), "should have all preset options"
        preset_values = {opt["preset"] for opt in options}
        assert "authentik" in preset_values, "should include authentik"
        assert "authelia" in preset_values, "should include authelia"
        assert "keycloak" in preset_values, "should include keycloak"
        assert "custom" in preset_values, "should include custom"

    @pytest.mark.asyncio
    async def test_refresh_all_discovery(
        self,
        auth_service: OidcAuthService,
        httpx_mock: HTTPXMock,
        valid_discovery_document: dict[str, object],
    ) -> None:
        """Verify bulk discovery refresh."""
        # Create two providers
        httpx_mock.add_response(json=valid_discovery_document)
        httpx_mock.add_response(json=valid_discovery_document)
        httpx_mock.add_response(json=valid_discovery_document)
        httpx_mock.add_response(json=valid_discovery_document)

        workspace_id = uuid4()

        await auth_service.register_provider(
            workspace_id=workspace_id,
            name="Provider 1",
            issuer_url="https://auth1.example.com",
            client_id="client1",
        )
        await auth_service.register_provider(
            workspace_id=workspace_id,
            name="Provider 2",
            issuer_url="https://auth2.example.com",
            client_id="client2",
        )

        # Refresh all
        httpx_mock.add_response(json=valid_discovery_document)
        httpx_mock.add_response(json=valid_discovery_document)

        results = await auth_service.refresh_all_discovery(workspace_id=workspace_id)

        assert len(results) == 2, "should return results for both providers"
        # All should succeed (None means no error)
        assert all(v is None for v in results.values()), "all refreshes should succeed"
