"""Tests for OIDC domain entities."""

from __future__ import annotations

from uuid import UUID, uuid4

import pytest

from noteflow.domain.auth.oidc import (
    ClaimMapping,
    OidcDiscoveryConfig,
    OidcProviderConfig,
    OidcProviderCreateParams,
    OidcProviderPreset,
)


class TestClaimMapping:
    """Tests for ClaimMapping entity."""

    def test_claim_mapping_default_values(self) -> None:
        """Verify default claim names match OIDC standard."""
        mapping = ClaimMapping()

        assert mapping.subject_claim == "sub", "subject_claim should default to 'sub'"
        assert mapping.email_claim == "email", "email_claim should default to 'email'"
        assert mapping.email_verified_claim == "email_verified", "email_verified_claim should default"
        assert mapping.name_claim == "name", "name_claim should default to 'name'"
        assert mapping.preferred_username_claim == "preferred_username", "preferred_username default"
        assert mapping.groups_claim == "groups", "groups_claim should default to 'groups'"
        assert mapping.picture_claim == "picture", "picture_claim should default to 'picture'"

    def test_custom_values(self) -> None:
        """Verify custom claim names are preserved."""
        mapping = ClaimMapping(
            subject_claim="user_id",
            email_claim="mail",
            groups_claim="roles",
        )

        assert mapping.subject_claim == "user_id", f"expected 'user_id', got {mapping.subject_claim!r}"
        assert mapping.email_claim == "mail", f"expected 'mail', got {mapping.email_claim!r}"
        assert mapping.groups_claim == "roles", f"expected 'roles', got {mapping.groups_claim!r}"

    def test_claim_mapping_to_dict_roundtrip(self) -> None:
        """Verify serialization and deserialization."""
        original = ClaimMapping(
            subject_claim="user_id",
            groups_claim="roles",
            first_name_claim="given_name",
        )

        data = original.to_dict()
        restored = ClaimMapping.from_dict(data)

        assert restored.subject_claim == original.subject_claim, (
            f"subject_claim should survive roundtrip: expected {original.subject_claim!r}, got {restored.subject_claim!r}"
        )
        assert restored.groups_claim == original.groups_claim, (
            f"groups_claim should survive roundtrip: expected {original.groups_claim!r}, got {restored.groups_claim!r}"
        )
        assert restored.first_name_claim == original.first_name_claim, (
            f"first_name_claim should survive roundtrip: expected {original.first_name_claim!r}, got {restored.first_name_claim!r}"
        )

    def test_from_dict_with_missing_keys_uses_defaults(self) -> None:
        """Verify missing keys in from_dict use default values."""
        data: dict[str, str | None] = {"groups_claim": "roles"}
        mapping = ClaimMapping.from_dict(data)

        assert mapping.groups_claim == "roles", f"groups_claim should be 'roles', got {mapping.groups_claim!r}"
        assert mapping.subject_claim == "sub", f"subject_claim should default to 'sub', got {mapping.subject_claim!r}"
        assert mapping.email_claim == "email", f"email_claim should default to 'email', got {mapping.email_claim!r}"


class TestOidcDiscoveryConfig:
    """Tests for OidcDiscoveryConfig entity."""

    @pytest.fixture
    def discovery_data(self) -> dict[str, object]:
        """Sample OIDC discovery document data."""
        return {
            "issuer": "https://auth.example.com",
            "authorization_endpoint": "https://auth.example.com/authorize",
            "token_endpoint": "https://auth.example.com/token",
            "userinfo_endpoint": "https://auth.example.com/userinfo",
            "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
            "scopes_supported": ["openid", "profile", "email", "groups"],
            "response_types_supported": ["code", "token"],
            "code_challenge_methods_supported": ["S256", "plain"],
            "claims_supported": ["sub", "email", "name", "groups"],
        }

    def test_from_dict(self, discovery_data: dict[str, object]) -> None:
        """Verify parsing from discovery document."""
        config = OidcDiscoveryConfig.from_dict(discovery_data)

        assert config.issuer == "https://auth.example.com", "issuer should be parsed"
        assert config.authorization_endpoint == "https://auth.example.com/authorize", "auth endpoint"
        assert config.token_endpoint == "https://auth.example.com/token", "token endpoint"
        assert config.userinfo_endpoint == "https://auth.example.com/userinfo", "userinfo endpoint"
        assert "openid" in config.scopes_supported, "scopes should include openid"
        assert "S256" in config.code_challenge_methods_supported, "PKCE S256 should be supported"

    def test_supports_pkce(self, discovery_data: dict[str, object]) -> None:
        """Verify PKCE support detection."""
        config = OidcDiscoveryConfig.from_dict(discovery_data)
        assert config.supports_pkce() is True, "should support PKCE when S256 is in code_challenge_methods_supported"

        # Without S256
        discovery_data["code_challenge_methods_supported"] = ["plain"]
        config_no_pkce = OidcDiscoveryConfig.from_dict(discovery_data)
        assert config_no_pkce.supports_pkce() is False, "should not support PKCE when S256 is not available"

    def test_discovery_config_to_dict_roundtrip(self, discovery_data: dict[str, object]) -> None:
        """Verify serialization roundtrip."""
        original = OidcDiscoveryConfig.from_dict(discovery_data)
        data = original.to_dict()
        restored = OidcDiscoveryConfig.from_dict(data)

        assert restored.issuer == original.issuer, (
            f"issuer should survive roundtrip: expected {original.issuer!r}, got {restored.issuer!r}"
        )
        assert restored.authorization_endpoint == original.authorization_endpoint, (
            f"authorization_endpoint should survive roundtrip: expected {original.authorization_endpoint!r}, got {restored.authorization_endpoint!r}"
        )
        assert restored.scopes_supported == original.scopes_supported, (
            f"scopes_supported should survive roundtrip: expected {original.scopes_supported!r}, got {restored.scopes_supported!r}"
        )


class TestOidcProviderConfig:
    """Tests for OidcProviderConfig entity."""

    @pytest.fixture
    def workspace_id(self) -> UUID:
        """Sample workspace ID."""
        return uuid4()

    def test_create_factory(self, workspace_id: UUID) -> None:
        """Verify factory method creates valid provider."""
        provider = OidcProviderConfig.create(
            workspace_id=workspace_id,
            name="Test Provider",
            issuer_url="https://auth.example.com",
            client_id="test-client-id",
        )

        assert provider.workspace_id == workspace_id, "workspace_id should match"
        assert provider.name == "Test Provider", "name should match"
        assert provider.issuer_url == "https://auth.example.com", "issuer_url should match"
        assert provider.client_id == "test-client-id", "client_id should match"
        assert provider.preset == OidcProviderPreset.CUSTOM, "preset should default to CUSTOM"
        assert provider.enabled is True, "provider should be enabled by default"
        assert provider.discovery is None, "discovery should be None initially"

    def test_create_with_preset(self, workspace_id: UUID) -> None:
        """Verify preset is applied correctly."""
        params = OidcProviderCreateParams(preset=OidcProviderPreset.AUTHENTIK)
        provider = OidcProviderConfig.create(
            workspace_id=workspace_id,
            name="Authentik",
            issuer_url="https://auth.example.com",
            client_id="test-client-id",
            params=params,
        )

        assert provider.preset == OidcProviderPreset.AUTHENTIK, (
            f"preset should be AUTHENTIK, got {provider.preset!r}"
        )

    def test_issuer_url_trailing_slash_stripped(self, workspace_id: UUID) -> None:
        """Verify trailing slash is removed from issuer URL."""
        provider = OidcProviderConfig.create(
            workspace_id=workspace_id,
            name="Test",
            issuer_url="https://auth.example.com/",
            client_id="test",
        )

        assert provider.issuer_url == "https://auth.example.com", (
            f"trailing slash should be stripped: got {provider.issuer_url!r}"
        )

    def test_discovery_url_property(self, workspace_id: UUID) -> None:
        """Verify discovery URL is correctly formed."""
        provider = OidcProviderConfig.create(
            workspace_id=workspace_id,
            name="Test",
            issuer_url="https://auth.example.com",
            client_id="test",
        )

        assert provider.discovery_url == "https://auth.example.com/.well-known/openid-configuration", (
            f"discovery_url should be well-known path: got {provider.discovery_url!r}"
        )

    def test_enable_disable(self, workspace_id: UUID) -> None:
        """Verify enable/disable methods."""
        provider = OidcProviderConfig.create(
            workspace_id=workspace_id,
            name="Test",
            issuer_url="https://auth.example.com",
            client_id="test",
        )
        original_updated = provider.updated_at

        provider.disable()
        assert provider.enabled is False, "provider should be disabled after disable()"
        assert provider.updated_at >= original_updated, (
            f"updated_at should be >= original: {provider.updated_at} vs {original_updated}"
        )

        provider.enable()
        assert provider.enabled, "provider should be enabled after enable()"

    def test_update_discovery(self, workspace_id: UUID) -> None:
        """Verify discovery update."""
        provider = OidcProviderConfig.create(
            workspace_id=workspace_id,
            name="Test",
            issuer_url="https://auth.example.com",
            client_id="test",
        )
        assert provider.discovery is None, "discovery should be None before update"
        assert provider.discovery_refreshed_at is None, "refresh timestamp should be None"

        discovery = OidcDiscoveryConfig(
            issuer="https://auth.example.com",
            authorization_endpoint="https://auth.example.com/authorize",
            token_endpoint="https://auth.example.com/token",
        )

        provider.update_discovery(discovery)

        assert provider.discovery is not None, "discovery should be set after update"
        assert provider.discovery.issuer == "https://auth.example.com", "discovery issuer"
        assert provider.discovery_refreshed_at is not None, "refresh timestamp should be set"

    def test_to_dict_from_dict_roundtrip(self, workspace_id: UUID) -> None:
        """Verify full serialization roundtrip."""
        params = OidcProviderCreateParams(
            preset=OidcProviderPreset.AUTHENTIK,
            scopes=("openid", "profile", "email", "groups"),
            allowed_groups=("admins", "users"),
        )
        original = OidcProviderConfig.create(
            workspace_id=workspace_id,
            name="Test Provider",
            issuer_url="https://auth.example.com",
            client_id="test-client-id",
            params=params,
        )

        data = original.to_dict()
        restored = OidcProviderConfig.from_dict(data)

        assert restored.id == original.id, "id should survive roundtrip"
        assert restored.workspace_id == original.workspace_id, "workspace_id should survive"
        assert restored.name == original.name, "name should survive roundtrip"
        assert restored.issuer_url == original.issuer_url, "issuer_url should survive"
        assert restored.preset == original.preset, "preset should survive roundtrip"
        assert restored.scopes == original.scopes, "scopes should survive roundtrip"
        assert restored.allowed_groups == original.allowed_groups, "allowed_groups should survive"

    def test_default_scopes(self, workspace_id: UUID) -> None:
        """Verify default scopes are applied."""
        provider = OidcProviderConfig.create(
            workspace_id=workspace_id,
            name="Test",
            issuer_url="https://auth.example.com",
            client_id="test",
        )

        assert "openid" in provider.scopes, f"'openid' should be in default scopes: {provider.scopes!r}"
        assert "profile" in provider.scopes, f"'profile' should be in default scopes: {provider.scopes!r}"
        assert "email" in provider.scopes, f"'email' should be in default scopes: {provider.scopes!r}"


class TestOidcProviderPreset:
    """Tests for OidcProviderPreset enum."""

    def test_all_presets_have_values(self) -> None:
        """Verify all presets have string values."""
        presets = list(OidcProviderPreset)

        assert len(presets) == 7, "should have 7 presets"
        assert OidcProviderPreset.AUTHENTIK.value == "authentik", "authentik preset value"
        assert OidcProviderPreset.AUTHELIA.value == "authelia", "authelia preset value"
        assert OidcProviderPreset.KEYCLOAK.value == "keycloak", "keycloak preset value"
        assert OidcProviderPreset.AUTH0.value == "auth0", "auth0 preset value"
        assert OidcProviderPreset.OKTA.value == "okta", "okta preset value"
        assert OidcProviderPreset.AZURE_AD.value == "azure_ad", "azure_ad preset value"
        assert OidcProviderPreset.CUSTOM.value == "custom", "custom preset value"
