"""Tests for OIDC discovery client."""


from __future__ import annotations

from uuid import uuid4

import httpx
import pytest
from pytest_httpx import HTTPXMock

from noteflow.domain.auth.oidc import OidcProviderConfig, OidcProviderCreateParams
from noteflow.infrastructure.auth.oidc_discovery import (
    OidcDiscoveryClient,
    OidcDiscoveryError,
)


@pytest.fixture
def discovery_client() -> OidcDiscoveryClient:
    """Create a discovery client for testing."""
    return OidcDiscoveryClient(timeout=5.0, verify_ssl=True)


# valid_discovery_document fixture is in conftest.py


class TestOidcDiscoveryClient:
    """Tests for OidcDiscoveryClient."""

    @pytest.mark.asyncio
    async def test_discover_success(
        self,
        discovery_client: OidcDiscoveryClient,
        httpx_mock: HTTPXMock,
        valid_discovery_document: dict[str, object],
    ) -> None:
        """Verify successful discovery document fetch."""
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            json=valid_discovery_document,
        )

        config = await discovery_client.discover("https://auth.example.com")

        assert config.issuer == "https://auth.example.com", "issuer should match URL"
        assert config.authorization_endpoint == "https://auth.example.com/authorize", "auth endpoint"
        assert config.token_endpoint == "https://auth.example.com/token", "token endpoint"
        assert config.supports_pkce() is True, "PKCE should be supported"

    @pytest.mark.asyncio
    async def test_discover_with_trailing_slash(
        self,
        discovery_client: OidcDiscoveryClient,
        httpx_mock: HTTPXMock,
        valid_discovery_document: dict[str, object],
    ) -> None:
        """Verify trailing slash in issuer URL is handled."""
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            json=valid_discovery_document,
        )

        config = await discovery_client.discover("https://auth.example.com/")

        assert config.issuer == "https://auth.example.com", "trailing slash should be stripped"

    @pytest.mark.asyncio
    async def test_discover_timeout(
        self,
        discovery_client: OidcDiscoveryClient,
        httpx_mock: HTTPXMock,
    ) -> None:
        """Verify timeout handling."""
        httpx_mock.add_exception(
            httpx.TimeoutException("Connection timed out"),
            url="https://auth.example.com/.well-known/openid-configuration",
        )

        with pytest.raises(OidcDiscoveryError, match="Timeout"):
            await discovery_client.discover("https://auth.example.com")

    @pytest.mark.asyncio
    async def test_discover_http_error(
        self,
        discovery_client: OidcDiscoveryClient,
        httpx_mock: HTTPXMock,
    ) -> None:
        """Verify HTTP error handling."""
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            status_code=404,
        )

        with pytest.raises(OidcDiscoveryError, match="HTTP 404"):
            await discovery_client.discover("https://auth.example.com")

    @pytest.mark.asyncio
    async def test_discover_invalid_json(
        self,
        discovery_client: OidcDiscoveryClient,
        httpx_mock: HTTPXMock,
    ) -> None:
        """Verify invalid JSON handling."""
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            content=b"not valid json",
        )

        with pytest.raises(OidcDiscoveryError, match="Invalid JSON"):
            await discovery_client.discover("https://auth.example.com")

    @pytest.mark.asyncio
    async def test_discover_missing_issuer(
        self,
        discovery_client: OidcDiscoveryClient,
        httpx_mock: HTTPXMock,
    ) -> None:
        """Verify missing issuer field handling."""
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            json={
                "authorization_endpoint": "https://auth.example.com/authorize",
                "token_endpoint": "https://auth.example.com/token",
            },
        )

        with pytest.raises(OidcDiscoveryError, match="Missing required 'issuer'"):
            await discovery_client.discover("https://auth.example.com")

    @pytest.mark.asyncio
    async def test_discover_missing_authorization_endpoint(
        self,
        discovery_client: OidcDiscoveryClient,
        httpx_mock: HTTPXMock,
    ) -> None:
        """Verify missing authorization_endpoint field handling."""
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            json={
                "issuer": "https://auth.example.com",
                "token_endpoint": "https://auth.example.com/token",
            },
        )

        with pytest.raises(OidcDiscoveryError, match="Missing required 'authorization_endpoint'"):
            await discovery_client.discover("https://auth.example.com")

    @pytest.mark.asyncio
    async def test_discover_missing_token_endpoint(
        self,
        discovery_client: OidcDiscoveryClient,
        httpx_mock: HTTPXMock,
    ) -> None:
        """Verify missing token_endpoint field handling."""
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            json={
                "issuer": "https://auth.example.com",
                "authorization_endpoint": "https://auth.example.com/authorize",
            },
        )

        with pytest.raises(OidcDiscoveryError, match="Missing required 'token_endpoint'"):
            await discovery_client.discover("https://auth.example.com")

    @pytest.mark.asyncio
    async def test_discover_and_update(
        self,
        discovery_client: OidcDiscoveryClient,
        httpx_mock: HTTPXMock,
        valid_discovery_document: dict[str, object],
    ) -> None:
        """Verify discover_and_update updates provider config."""
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            json=valid_discovery_document,
        )

        provider = OidcProviderConfig.create(
            workspace_id=uuid4(),
            name="Test Provider",
            issuer_url="https://auth.example.com",
            client_id="test-client-id",
        )
        assert provider.discovery is None, "discovery should be None initially"

        updated = await discovery_client.discover_and_update(provider)

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

    @pytest.mark.asyncio
    async def test_validate_provider_no_pkce(
        self,
        discovery_client: OidcDiscoveryClient,
        httpx_mock: HTTPXMock,
        valid_discovery_document: dict[str, object],
    ) -> None:
        """Verify warning when provider doesn't support PKCE."""
        valid_discovery_document["code_challenge_methods_supported"] = ["plain"]
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            json=valid_discovery_document,
        )

        provider = OidcProviderConfig.create(
            workspace_id=uuid4(),
            name="Test Provider",
            issuer_url="https://auth.example.com",
            client_id="test-client-id",
        )

        warnings = await discovery_client.validate_provider(provider)

        assert len(warnings) >= 1, "should have at least one warning"
        assert any("PKCE" in w for w in warnings), "should warn about missing PKCE"

    @pytest.mark.asyncio
    async def test_validate_provider_unsupported_scopes(
        self,
        discovery_client: OidcDiscoveryClient,
        httpx_mock: HTTPXMock,
        valid_discovery_document: dict[str, object],
    ) -> None:
        """Verify warning for unsupported scopes."""
        valid_discovery_document["scopes_supported"] = ["openid", "profile"]
        httpx_mock.add_response(
            url="https://auth.example.com/.well-known/openid-configuration",
            json=valid_discovery_document,
        )

        params = OidcProviderCreateParams(
            scopes=("openid", "profile", "email", "custom_scope"),
        )
        provider = OidcProviderConfig.create(
            workspace_id=uuid4(),
            name="Test Provider",
            issuer_url="https://auth.example.com",
            client_id="test-client-id",
            params=params,
        )

        warnings = await discovery_client.validate_provider(provider)

        assert len(warnings) >= 1, "should have at least one warning"
        assert any("scope" in w.lower() for w in warnings), "should warn about unsupported scopes"
