"""
Tests for OIDC backends such as Keycloak and CILogon.

This test suite covers additional functionality such as:
- PKCE support
- Custom well-known endpoint discovery
- IDP hint parameters
- Token verification
"""

from unittest.mock import (
    Mock,
    patch,
)

import pytest
from social_core.utils import setting_name

from galaxy.authnz.cilogon import CILogonOpenIdConnect
from galaxy.authnz.keycloak import KeycloakOpenIdConnect
from galaxy.authnz.psa_authnz import (
    associate_by_email_if_logged_in,
    check_user_creation_confirmation,
)


class MockStrategy:
    """Mock PSA strategy for testing."""

    def __init__(self, config=None):
        self.config = config or {}
        self.session = {}
        self.request = Mock()
        self.request_data = Mock(return_value={})
        self.storage = Mock()

    def get_setting(self, name, default=None):
        return self.config.get(name, default)

    def session_get(self, name, default=None):
        return self.session.get(name, default)

    def session_set(self, name, value):
        self.session[name] = value

    def session_pop(self, name):
        return self.session.pop(name, None)

    def setting(self, name, default=None, backend=None):
        """
        Compatibility method for backend.setting() calls.

        PSA backends call setting() with short names like "PKCE_SUPPORT",
        and the strategy should look up "SOCIAL_AUTH_{BACKEND_NAME}_{NAME}".
        """
        # If backend is provided, construct the full setting name
        if backend:
            full_name = f"SOCIAL_AUTH_{backend.name.upper()}_{name}"
        else:
            # Just try the name as-is
            full_name = name

        return self.get_setting(full_name, default)

    def absolute_uri(self, path=None):
        """Return absolute URI for given path."""
        base = "http://localhost:8080"
        if path:
            return f"{base}{path}"
        return base

    def build_absolute_uri(self, path=None):
        """Alias for absolute_uri."""
        return self.absolute_uri(path)

    def random_string(self, length=12):
        """Generate random string for state/nonce."""
        import secrets
        import string

        return "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(length))


class TestKeycloakPKCE:
    """Test PKCE support in Keycloak backend."""

    def test_pkce_disabled_by_default(self):
        """PKCE should be disabled by default."""
        strategy = MockStrategy()
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        assert backend.PKCE_ENABLED is False

    def test_pkce_enabled_via_config(self):
        """PKCE should be enabled when configured."""
        strategy = MockStrategy({"SOCIAL_AUTH_KEYCLOAK_PKCE_SUPPORT": True})
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        assert backend.PKCE_ENABLED is True

    @patch("galaxy.authnz.oidc.generate_pkce_pair")
    @patch.object(KeycloakOpenIdConnect, "oidc_config")
    def test_pkce_params_added_to_auth_request(self, mock_oidc_config, mock_generate_pkce):
        """PKCE parameters should be added to authorization request when enabled."""
        mock_oidc_config.return_value = {
            "authorization_endpoint": "https://example.com/auth",
            "token_endpoint": "https://example.com/token",
        }
        mock_generate_pkce.return_value = ("verifier123", "challenge456")

        strategy = MockStrategy({"SOCIAL_AUTH_KEYCLOAK_PKCE_SUPPORT": True})
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        # Get auth params - now handled by GalaxyOpenIdConnect base class
        params = backend.auth_params(state="test_state")

        assert "code_challenge" in params
        assert params["code_challenge"] == "challenge456"
        assert "code_challenge_method" in params
        assert params["code_challenge_method"] == "S256"
        assert strategy.session.get("pkce_code_verifier") == "verifier123"

    @patch.object(KeycloakOpenIdConnect, "oidc_config")
    def test_pkce_verifier_added_to_token_request(self, mock_oidc_config):
        """PKCE verifier should be added to token request."""
        mock_oidc_config.return_value = {
            "authorization_endpoint": "https://example.com/auth",
            "token_endpoint": "https://example.com/token",
        }

        strategy = MockStrategy({"SOCIAL_AUTH_KEYCLOAK_PKCE_SUPPORT": True})
        strategy.session["pkce_code_verifier"] = "verifier123"

        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        # Get auth complete params - handled by GalaxyOpenIdConnect base class
        params = backend.auth_complete_params(state="test_state")

        assert "code_verifier" in params
        assert params["code_verifier"] == "verifier123"


class TestKeycloakIDPHint:
    """Test IDP hint parameter support in Keycloak backend."""

    @patch.object(KeycloakOpenIdConnect, "oidc_config")
    def test_idphint_default_value(self, mock_oidc_config):
        """Default IDP hint should be 'oidc'."""
        mock_oidc_config.return_value = {
            "authorization_endpoint": "https://example.com/auth",
            "token_endpoint": "https://example.com/token",
        }

        strategy = MockStrategy()
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        # Get auth params - IDP hint is added by GalaxyOpenIdConnect base class
        params = backend.auth_params()

        assert "kc_idp_hint" in params
        assert params["kc_idp_hint"] == "oidc"

    @patch.object(KeycloakOpenIdConnect, "oidc_config")
    def test_idphint_custom_value(self, mock_oidc_config):
        """Custom IDP hint should be used when configured."""
        mock_oidc_config.return_value = {
            "authorization_endpoint": "https://example.com/auth",
            "token_endpoint": "https://example.com/token",
        }

        strategy = MockStrategy({"SOCIAL_AUTH_KEYCLOAK_IDPHINT": "custom_idp"})
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        # Get auth params - IDP hint is added by GalaxyOpenIdConnect base class
        params = backend.auth_params()

        assert params["kc_idp_hint"] == "custom_idp"


class TestKeycloakWellKnownDiscovery:
    """Test custom well-known endpoint discovery for Keycloak."""

    def test_custom_url_discovery(self):
        """Should construct well-known endpoint from base URL."""
        strategy = MockStrategy({"SOCIAL_AUTH_KEYCLOAK_URL": "https://keycloak.example.com/realms/test"})
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        endpoint = backend.oidc_endpoint()

        # Should return base URL
        assert endpoint == "https://keycloak.example.com/realms/test"

    def test_trailing_slash_removed(self):
        """Should remove trailing slash from URL."""
        strategy = MockStrategy({"SOCIAL_AUTH_KEYCLOAK_URL": "https://keycloak.example.com/realms/test/"})
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        endpoint = backend.oidc_endpoint()

        # Should remove trailing slash
        assert endpoint == "https://keycloak.example.com/realms/test"

    @patch.object(KeycloakOpenIdConnect.__bases__[0], "oidc_config")
    def test_fallback_to_parent_when_no_url(self, mock_parent_oidc_config):
        """Should fall back to parent's oidc_config when no custom URL."""
        mock_parent_oidc_config.return_value = {"issuer": "test"}

        strategy = MockStrategy()
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        backend.oidc_config()

        # Should call parent's oidc_config
        mock_parent_oidc_config.assert_called_once()


class TestCILogonSpecificFeatures:
    """Test CILogon-specific features."""

    def test_cilogon_default_scopes(self):
        """CILogon should include org.cilogon.userinfo scope by default."""
        strategy = MockStrategy()
        backend = CILogonOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        assert "org.cilogon.userinfo" in backend.DEFAULT_SCOPE
        assert "openid" in backend.DEFAULT_SCOPE
        assert "email" in backend.DEFAULT_SCOPE
        assert "profile" in backend.DEFAULT_SCOPE

    def test_cilogon_url_authorize_removal(self):
        """Should remove /authorize from CILogon URL for discovery."""
        strategy = MockStrategy({"SOCIAL_AUTH_CILOGON_URL": "https://cilogon.org/authorize"})
        backend = CILogonOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        endpoint = backend.oidc_endpoint()

        # Should remove /authorize
        assert endpoint == "https://cilogon.org"

    def test_cilogon_url_without_authorize(self):
        """Should work correctly with URL not ending in /authorize."""
        strategy = MockStrategy({"SOCIAL_AUTH_CILOGON_URL": "https://cilogon.org"})
        backend = CILogonOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        endpoint = backend.oidc_endpoint()

        assert endpoint == "https://cilogon.org"

    @patch.object(CILogonOpenIdConnect, "oidc_config")
    def test_cilogon_idphint_default(self, mock_oidc_config):
        """CILogon should use 'cilogon' as default IDP hint."""
        mock_oidc_config.return_value = {
            "authorization_endpoint": "https://cilogon.org/auth",
            "token_endpoint": "https://cilogon.org/token",
        }

        strategy = MockStrategy()
        backend = CILogonOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        # Get auth params - IDP hint is added by GalaxyOpenIdConnect base class
        params = backend.auth_params()

        assert params["idphint"] == "cilogon"


class TestJWKSSupport:
    """
    Test JWKS key retrieval support.
    """

    @patch.object(KeycloakOpenIdConnect, "request")
    @patch.object(KeycloakOpenIdConnect, "oidc_config")
    def test_get_jwks_keys_inherited_from_psa(self, mock_oidc_config, mock_request):
        """Verify that JWKS key retrieval works via PSA's inherited implementation."""
        mock_oidc_config.return_value = {"jwks_uri": "https://keycloak.example.com/jwks"}

        # Mock the response from PSA's request() method
        mock_response = Mock()
        mock_response.text = '{"keys": [{"kid": "key1", "kty": "RSA"}]}'
        mock_request.return_value = mock_response

        strategy = MockStrategy()
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        result = backend.get_jwks_keys()

        # Should return the keys array from PSA's implementation
        assert result == [{"kid": "key1", "kty": "RSA"}]


class TestBackendInstantiation:
    """Test backend instantiation and configuration."""

    def test_keycloak_backend_instantiation(self):
        """Keycloak backend should instantiate correctly."""
        strategy = MockStrategy()
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        assert backend.name == "keycloak"
        assert backend.REFRESH_TOKEN_METHOD == "POST"

    def test_cilogon_backend_instantiation(self):
        """CILogon backend should instantiate correctly."""
        strategy = MockStrategy()
        backend = CILogonOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        assert backend.name == "cilogon"
        assert backend.REFRESH_TOKEN_METHOD == "POST"


# Integration-style test to verify both backends work together
class TestBackendCompatibility:
    """Test that both backends maintain API compatibility."""

    def test_both_backends_support_pkce(self):
        """Both backends should support PKCE configuration."""
        keycloak_strategy = MockStrategy({"SOCIAL_AUTH_KEYCLOAK_PKCE_SUPPORT": True})
        cilogon_strategy = MockStrategy({"SOCIAL_AUTH_CILOGON_PKCE_SUPPORT": True})

        keycloak = KeycloakOpenIdConnect(keycloak_strategy, redirect_uri="http://localhost/callback")
        cilogon = CILogonOpenIdConnect(cilogon_strategy, redirect_uri="http://localhost/callback")

        assert keycloak.PKCE_ENABLED is True
        assert cilogon.PKCE_ENABLED is True

    def test_both_backends_have_required_methods(self):
        """Both backends should implement required methods."""
        strategy = MockStrategy()

        keycloak = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")
        cilogon = CILogonOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        required_methods = ["oidc_config", "auth_params", "auth_complete_params", "get_jwks_keys", "user_data"]

        for method in required_methods:
            assert hasattr(keycloak, method), f"Keycloak missing {method}"
            assert hasattr(cilogon, method), f"CILogon missing {method}"
            assert callable(getattr(keycloak, method))
            assert callable(getattr(cilogon, method))


class TestPSADisconnect:
    """Test disconnect functionality for PSA backends."""

    def test_disconnect_workflow(self):
        """Test basic disconnect workflow."""
        # This would require full PSA integration
        # For now, verify the backend supports disconnect
        strategy = MockStrategy()
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        # Verify backend has required attributes for disconnect
        assert hasattr(backend, "name")
        assert backend.name == "keycloak"

    def test_both_backends_support_disconnect(self):
        """Verify both backends can be disconnected."""
        strategy_keycloak = MockStrategy()
        strategy_cilogon = MockStrategy()

        keycloak = KeycloakOpenIdConnect(strategy_keycloak, redirect_uri="http://localhost/callback")
        cilogon = CILogonOpenIdConnect(strategy_cilogon, redirect_uri="http://localhost/callback")

        # Both should be properly named for disconnect routing
        assert keycloak.name == "keycloak"
        assert cilogon.name == "cilogon"


class TestPSALogout:
    """Test IDP logout functionality for PSA backends."""

    @patch.object(KeycloakOpenIdConnect, "oidc_config")
    def test_logout_endpoint_available(self, mock_oidc_config):
        """Test that logout endpoint is available from OIDC config."""
        mock_oidc_config.return_value = {"end_session_endpoint": "https://keycloak.example.com/logout"}

        strategy = MockStrategy()
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        config = backend.oidc_config()
        assert "end_session_endpoint" in config
        assert config["end_session_endpoint"] == "https://keycloak.example.com/logout"

    @patch.object(CILogonOpenIdConnect, "oidc_config")
    def test_cilogon_logout_endpoint(self, mock_oidc_config):
        """Test CILogon logout endpoint discovery."""
        mock_oidc_config.return_value = {"end_session_endpoint": "https://cilogon.org/logout"}

        strategy = MockStrategy()
        backend = CILogonOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        config = backend.oidc_config()
        assert "end_session_endpoint" in config


class TestPSAUserCreation:
    """Test user creation edge cases in PSA backends."""

    def test_backend_provides_user_details(self):
        """Test that backends provide necessary user details."""
        strategy = MockStrategy()
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        # Verify backend has user_data method
        assert hasattr(backend, "user_data")
        assert callable(backend.user_data)

    def test_both_backends_support_user_data(self):
        """Verify both backends support user data retrieval."""
        strategy = MockStrategy()

        keycloak = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")
        cilogon = CILogonOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        assert hasattr(keycloak, "user_data")
        assert hasattr(cilogon, "user_data")


class TestPSARegressionCoverage:
    """
    Tests to ensure PSA implementation covers all custos functionality.

    These tests verify that the PSA backends provide equivalent or better
    functionality compared to the deprecated custos implementation.
    """

    def test_state_parameter_support(self):
        """Test that state parameter is supported (CSRF protection)."""
        strategy = MockStrategy()
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        with patch.object(KeycloakOpenIdConnect.__bases__[0], "auth_params", return_value={"state": "test"}):
            params = backend.auth_params(state="test_state")

            # State should be present (inherited from OpenIdConnectAuth)
            assert "state" in params or "scope" in params  # Basic check that params are returned

    def test_token_storage_structure(self):
        """Test that backends work with PSA token storage."""
        # This test verifies that backends are compatible with PSA's
        # UserAuthnzToken storage model (JSON extra_data)
        strategy = MockStrategy()
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        # Verify backend can be instantiated (basic compatibility check)
        assert backend.name == "keycloak"
        assert backend.REFRESH_TOKEN_METHOD == "POST"

    def test_extra_scopes_support(self):
        """Test that extra scopes can be added to backends."""
        strategy = MockStrategy({"SOCIAL_AUTH_KEYCLOAK_SCOPE": ["openid", "email", "profile", "custom_scope"]})
        backend = KeycloakOpenIdConnect(strategy, redirect_uri="http://localhost/callback")

        # Backend should accept custom scopes via strategy
        assert backend.setting("SCOPE") is not None or True  # Basic check


class TestRequireCreateConfirmation:
    """
    Test the require_create_confirmation feature.

    This feature allows administrators to require new users to confirm
    account creation before the user is created. The user is redirected
    to a confirmation page with the token stored for later use.
    """

    def test_callback_user_not_created_when_does_not_exist(self):
        """
        Test that user is not created when require_create_confirmation is enabled.

        This replicates the custos test: test_callback_user_not_created_when_does_not_exists
        from test_custos_authnz.py (lines 395-427).

        When a new user tries to login and require_create_confirmation is enabled,
        they should be redirected to a confirmation page instead of having their
        account created immediately.
        """
        # Setup strategy with require_create_confirmation enabled
        strategy = MockStrategy(
            {
                "REQUIRE_CREATE_CONFIRMATION": True,
                setting_name("LOGIN_REDIRECT_URL"): "http://localhost:8080/",
                "provider": "keycloak",
            }
        )

        backend = Mock()
        details = {"email": "newuser@example.com", "username": "newuser"}
        response = {
            "access_token": "test_access_token",
            "id_token": "test_id_token",
            "refresh_token": "test_refresh_token",
            "expires_in": 3600,
        }

        # Mock the database query to return no existing user (new user scenario)
        with patch("galaxy.authnz.psa_authnz.UserAuthnzToken") as mock_token:
            mock_session = Mock()
            mock_query = Mock()
            mock_where = Mock()

            # Chain the mock calls: query().where().first()
            mock_session.query.return_value = mock_query
            mock_query.where.return_value = mock_where
            mock_where.first.return_value = None  # No existing user found

            mock_token.sa_session = mock_session

            # Call the pipeline step
            result = check_user_creation_confirmation(
                strategy=strategy,
                backend=backend,
                details=details,
                response=response,
                is_new=True,  # This is a new user
                user=None,  # No user exists yet
            )

            # Assertions matching the original custos test
            # 1. Should return a redirect URL (not None)
            assert result is not None, "Should return redirect URL when confirmation required"

            # 2. User should be None (not created)
            # (This is implicit - the pipeline is interrupted)

            # 3. Redirect URL should contain confirmation parameters
            assert (
                "http://localhost:8080/login/start?confirm=true&provider_token=" in result
            ), "Should redirect to confirmation page"
            assert "&provider=keycloak" in result, "Should include provider in redirect URL"

            # 4. Token should be stored in session for later use
            assert strategy.session.get("pending_oidc_token_keycloak") is not None, "Token should be stored in session"

    def test_user_created_normally_when_confirmation_not_required(self):
        """Test that user creation proceeds normally when confirmation is not required."""
        from galaxy.authnz.psa_authnz import check_user_creation_confirmation

        # Setup strategy without require_create_confirmation
        strategy = MockStrategy(
            {
                "REQUIRE_CREATE_CONFIRMATION": False,
                "provider": "keycloak",
            }
        )

        backend = Mock()
        details = {"email": "newuser@example.com"}
        response = {"access_token": "test_token"}

        # Call the pipeline step
        result = check_user_creation_confirmation(
            strategy=strategy,
            backend=backend,
            details=details,
            response=response,
            is_new=True,
            user=None,
        )

        # Should return None to continue the pipeline (user creation proceeds)
        assert result is None, "Should continue pipeline when confirmation not required"

    def test_existing_user_continues_when_confirmation_required(self):
        """Test that existing users continue normally even when confirmation is required."""
        from galaxy.authnz.psa_authnz import check_user_creation_confirmation

        # Setup strategy with require_create_confirmation enabled
        strategy = MockStrategy(
            {
                "REQUIRE_CREATE_CONFIRMATION": True,
                "provider": "keycloak",
            }
        )

        backend = Mock()
        details = {"email": "existing@example.com"}
        response = {"access_token": "test_token"}
        existing_user = Mock()  # User already exists

        # Call the pipeline step with existing user
        result = check_user_creation_confirmation(
            strategy=strategy,
            backend=backend,
            details=details,
            response=response,
            is_new=False,  # Not a new association
            user=existing_user,  # User already exists
        )

        # Should return None to continue the pipeline
        assert result is None, "Should continue pipeline for existing users"

    def test_existing_user_with_same_email_continues(self):
        """
        Test that when an existing user with the same email exists,
        the pipeline continues (doesn't trigger confirmation).
        """
        from galaxy.authnz.psa_authnz import check_user_creation_confirmation

        # Setup
        strategy = MockStrategy(
            {
                "REQUIRE_CREATE_CONFIRMATION": True,
                "provider": "keycloak",
            }
        )

        backend = Mock()
        details = {"email": "existing@example.com"}
        response = {"access_token": "test_token"}

        # Mock database to return existing user with this email
        with patch("galaxy.authnz.psa_authnz.UserAuthnzToken") as mock_token:
            mock_session = Mock()
            mock_query = Mock()
            mock_where = Mock()
            existing_user = Mock()

            mock_session.query.return_value = mock_query
            mock_query.where.return_value = mock_where
            mock_where.first.return_value = existing_user  # User exists

            mock_token.sa_session = mock_session

            # Call the pipeline step
            result = check_user_creation_confirmation(
                strategy=strategy,
                backend=backend,
                details=details,
                response=response,
                is_new=True,
                user=None,
            )

            # Should return None because existing user was found
            # (This means the pipeline continues, and associate_by_email_if_logged_in
            # will handle the account linking prompt)
            assert result is None, "Should continue pipeline when user exists"


class TestFixedDelegatedAuth:
    """Test the fixed_delegated_auth feature for auto-associating accounts by email."""

    def test_fixed_delegated_auth_auto_associates_existing_user(self):
        """Test that fixed_delegated_auth automatically associates with existing user."""
        strategy = MockStrategy(
            {
                "FIXED_DELEGATED_AUTH": True,
                setting_name("LOGIN_REDIRECT_URL"): "http://localhost:8080/",
                "provider": "keycloak",
            }
        )

        backend = Mock()
        details = {"email": "existinguser@example.com", "username": "existinguser"}

        with patch("galaxy.authnz.psa_authnz.UserAuthnzToken") as mock_token:
            mock_session = Mock()
            mock_query = Mock()
            mock_where = Mock()
            existing_user = Mock()
            existing_user.email = "existinguser@example.com"

            mock_session.query.return_value = mock_query
            mock_query.where.return_value = mock_where
            mock_where.first.return_value = existing_user

            mock_token.sa_session = mock_session

            result = associate_by_email_if_logged_in(
                strategy=strategy,
                backend=backend,
                details=details,
                user=None,  # Not logged in
                is_new=True,
            )

            # Should return the existing user for auto-association
            assert result is not None
            assert "user" in result
            assert result["user"] == existing_user

    def test_fixed_delegated_auth_continues_when_no_user(self):
        """Test that fixed_delegated_auth continues to user creation when no user exists."""
        strategy = MockStrategy(
            {
                "FIXED_DELEGATED_AUTH": True,
                setting_name("LOGIN_REDIRECT_URL"): "http://localhost:8080/",
                "provider": "keycloak",
            }
        )

        backend = Mock()
        details = {"email": "newuser@example.com", "username": "newuser"}

        with patch("galaxy.authnz.psa_authnz.UserAuthnzToken") as mock_token:
            mock_session = Mock()
            mock_query = Mock()
            mock_where = Mock()

            mock_session.query.return_value = mock_query
            mock_query.where.return_value = mock_where
            mock_where.first.return_value = None  # No existing user

            mock_token.sa_session = mock_session

            result = associate_by_email_if_logged_in(
                strategy=strategy,
                backend=backend,
                details=details,
                user=None,  # Not logged in
                is_new=True,
            )

            # Should return None to continue with user creation
            assert result is None

    def test_without_fixed_delegated_auth_prompts_for_login(self):
        """Test that without fixed_delegated_auth, users are prompted to log in."""
        strategy = MockStrategy(
            {
                "FIXED_DELEGATED_AUTH": False,
                setting_name("LOGIN_REDIRECT_URL"): "http://localhost:8080/",
                "provider": "keycloak",
                "LABEL": "Keycloak",
            }
        )

        backend = Mock()
        details = {"email": "existinguser@example.com", "username": "existinguser"}

        with patch("galaxy.authnz.psa_authnz.UserAuthnzToken") as mock_token:
            mock_session = Mock()
            mock_query = Mock()
            mock_where = Mock()
            existing_user = Mock()

            mock_session.query.return_value = mock_query
            mock_query.where.return_value = mock_where
            mock_where.first.return_value = existing_user

            mock_token.sa_session = mock_session

            result = associate_by_email_if_logged_in(
                strategy=strategy,
                backend=backend,
                details=details,
                user=None,  # Not logged in
                is_new=True,
            )

            # Should return redirect URL prompting for login
            assert result is not None
            assert isinstance(result, str)
            assert "login/start" in result
            assert "connect_external_provider=keycloak" in result
            assert "connect_external_email=" in result


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
