package server

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	oauth "github.com/giantswarm/mcp-oauth"
	oauthhandler "github.com/giantswarm/mcp-oauth/handler"
	"github.com/giantswarm/mcp-oauth/providers"
	oauthserver "github.com/giantswarm/mcp-oauth/server"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/giantswarm/muster/internal/api"
	"github.com/giantswarm/muster/internal/config"
)

// mockMCPServerManager implements api.MCPServerManagerHandler for testing.
type mockMCPServerManager struct {
	listMCPServersFn func() []api.MCPServerInfo
	getMCPServerFn   func(name string) (*api.MCPServerInfo, error)
	getToolsFn       func() []api.ToolMetadata
	executeToolFn    func(ctx context.Context, toolName string, args map[string]interface{}) (*api.CallToolResult, error)
}

func (m *mockMCPServerManager) ListMCPServers() []api.MCPServerInfo {
	if m.listMCPServersFn != nil {
		return m.listMCPServersFn()
	}
	return nil
}

func (m *mockMCPServerManager) GetMCPServer(name string) (*api.MCPServerInfo, error) {
	if m.getMCPServerFn != nil {
		return m.getMCPServerFn(name)
	}
	return nil, nil
}

func (m *mockMCPServerManager) GetTools() []api.ToolMetadata {
	if m.getToolsFn != nil {
		return m.getToolsFn()
	}
	return nil
}

func (m *mockMCPServerManager) ExecuteTool(ctx context.Context, toolName string, args map[string]interface{}) (*api.CallToolResult, error) {
	if m.executeToolFn != nil {
		return m.executeToolFn(ctx, toolName, args)
	}
	return nil, nil
}

func TestBuildDexScopes(t *testing.T) {
	tests := []struct {
		name              string
		requiredAudiences []string
		expectedScopes    []string
	}{
		{
			name:              "no required audiences returns base scopes only",
			requiredAudiences: nil,
			expectedScopes:    []string{"openid", "profile", "email", "groups", "offline_access"},
		},
		{
			name:              "empty required audiences returns base scopes only",
			requiredAudiences: []string{},
			expectedScopes:    []string{"openid", "profile", "email", "groups", "offline_access"},
		},
		{
			name:              "single required audience adds cross-client scope",
			requiredAudiences: []string{"dex-k8s-authenticator"},
			expectedScopes: []string{
				"openid", "profile", "email", "groups", "offline_access",
				"audience:server:client_id:dex-k8s-authenticator",
			},
		},
		{
			name:              "multiple required audiences adds multiple cross-client scopes",
			requiredAudiences: []string{"audience-a", "audience-b"},
			expectedScopes: []string{
				"openid", "profile", "email", "groups", "offline_access",
				"audience:server:client_id:audience-a",
				"audience:server:client_id:audience-b",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := buildDexScopes(tt.requiredAudiences)
			assert.Equal(t, tt.expectedScopes, result)
		})
	}
}

func TestBuildDexScopesDoesNotModifyBaseScopes(t *testing.T) {
	// Verify that calling buildDexScopes doesn't modify the global dexOAuthScopes
	originalLen := len(dexOAuthScopes)

	// Call with audiences
	_ = buildDexScopes([]string{"test-audience"})

	// Verify base scopes unchanged
	assert.Equal(t, originalLen, len(dexOAuthScopes), "dexOAuthScopes should not be modified")
	assert.Equal(t, []string{"openid", "profile", "email", "groups", "offline_access"}, dexOAuthScopes)
}

// TestCollectRequiredAudiencesIntegrationWithBuildDexScopes tests the full integration
// between MCPServer requiredAudiences configuration and OAuth scope building.
func TestCollectRequiredAudiencesIntegrationWithBuildDexScopes(t *testing.T) {
	// This test verifies that when MCPServers with requiredAudiences are registered,
	// the CollectRequiredAudiences function returns them, and buildDexScopes
	// correctly formats them as cross-client scopes for Dex.

	t.Run("MCPServers with requiredAudiences are included in Dex scopes", func(t *testing.T) {
		// Register a mock MCPServer manager with servers that have requiredAudiences
		api.RegisterMCPServerManager(&mockMCPServerManager{
			listMCPServersFn: func() []api.MCPServerInfo {
				return []api.MCPServerInfo{
					{
						Name: "kubernetes-mcp",
						Auth: &api.MCPServerAuth{
							ForwardToken:      true,
							RequiredAudiences: []string{"dex-k8s-authenticator"},
						},
					},
					{
						Name: "another-mcp",
						Auth: &api.MCPServerAuth{
							ForwardToken:      true,
							RequiredAudiences: []string{"another-client"},
						},
					},
				}
			},
		})
		defer api.RegisterMCPServerManager(nil)

		// Collect audiences (simulates what createOAuthServer does)
		audiences := api.CollectRequiredAudiences()

		// Build Dex scopes (simulates what createOAuthServer does)
		scopes := buildDexScopes(audiences)

		// Verify the cross-client scopes are present
		assert.Contains(t, scopes, "audience:server:client_id:another-client")
		assert.Contains(t, scopes, "audience:server:client_id:dex-k8s-authenticator")

		// Verify base scopes are still present
		assert.Contains(t, scopes, "openid")
		assert.Contains(t, scopes, "profile")
		assert.Contains(t, scopes, "email")
		assert.Contains(t, scopes, "groups")
		assert.Contains(t, scopes, "offline_access")
	})

	t.Run("MCPServers without forwardToken do not add scopes", func(t *testing.T) {
		api.RegisterMCPServerManager(&mockMCPServerManager{
			listMCPServersFn: func() []api.MCPServerInfo {
				return []api.MCPServerInfo{
					{
						Name: "regular-mcp",
						Auth: &api.MCPServerAuth{
							ForwardToken:      false, // Not forwarding tokens
							RequiredAudiences: []string{"should-be-ignored"},
						},
					},
				}
			},
		})
		defer api.RegisterMCPServerManager(nil)

		audiences := api.CollectRequiredAudiences()
		scopes := buildDexScopes(audiences)

		// Should only have base scopes, no cross-client scopes
		assert.Equal(t, []string{"openid", "profile", "email", "groups", "offline_access"}, scopes)
	})

	t.Run("Invalid audiences are filtered by CollectRequiredAudiences before reaching buildDexScopes", func(t *testing.T) {
		// Invalid audiences (containing whitespace) are filtered at the API layer
		// by CollectRequiredAudiences's isValidAudience check. This provides
		// defense-in-depth: API validation + mcp-oauth library validation.
		api.RegisterMCPServerManager(&mockMCPServerManager{
			listMCPServersFn: func() []api.MCPServerInfo {
				return []api.MCPServerInfo{
					{
						Name: "mcp-with-invalid-audiences",
						Auth: &api.MCPServerAuth{
							ForwardToken: true,
							RequiredAudiences: []string{
								"valid-audience",
								"invalid audience", // space makes it invalid - filtered by API layer
								"",                 // empty string - filtered by API layer
								"another-valid",
							},
						},
					},
				}
			},
		})
		defer api.RegisterMCPServerManager(nil)

		audiences := api.CollectRequiredAudiences()
		scopes := buildDexScopes(audiences)

		// Only valid audiences should be included (invalid ones filtered by API layer)
		assert.Contains(t, scopes, "audience:server:client_id:another-valid")
		assert.Contains(t, scopes, "audience:server:client_id:valid-audience")

		// Invalid audiences should NOT be included
		assert.NotContains(t, scopes, "audience:server:client_id:invalid audience")
		assert.NotContains(t, scopes, "audience:server:client_id:")

		// Base scopes still present
		assert.Contains(t, scopes, "openid")
	})
}

func TestNewOAuthHTTPServer_DisabledReturnsError(t *testing.T) {
	cfg := config.OAuthServerConfig{
		Enabled: false,
	}

	_, err := NewOAuthHTTPServer(cfg, http.DefaultServeMux, false)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not enabled")
}

func TestNewOAuthHTTPServer_InvalidProviderReturnsError(t *testing.T) {
	cfg := config.OAuthServerConfig{
		Enabled:  true,
		BaseURL:  "http://localhost:8080",
		Provider: "unsupported",
	}

	_, err := NewOAuthHTTPServer(cfg, http.DefaultServeMux, false)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "unsupported OAuth provider")
}

func TestNewOAuthHTTPServer_MissingDexIssuerReturnsError(t *testing.T) {
	cfg := config.OAuthServerConfig{
		Enabled:  true,
		BaseURL:  "http://localhost:8080",
		Provider: OAuthProviderDex,
		Dex: config.DexConfig{
			// IssuerURL is required but empty
			ClientID:     "test-client",
			ClientSecret: "test-secret",
		},
	}

	_, err := NewOAuthHTTPServer(cfg, http.DefaultServeMux, false)
	assert.Error(t, err)
	// The Dex provider will fail to initialize without issuer URL
}

func TestNewOAuthHTTPServer_JWTModeWithoutKeyFileReturnsError(t *testing.T) {
	cfg := config.OAuthServerConfig{
		Enabled:       true,
		BaseURL:       "http://localhost:8080",
		Provider:      OAuthProviderGoogle,
		EnableJWTMode: true,
	}

	_, err := NewOAuthHTTPServer(cfg, http.DefaultServeMux, false)
	require.Error(t, err)
	require.ErrorContains(t, err, "jwtSigningKeyFile")
}

func TestValidateHTTPSRequirement(t *testing.T) {
	tests := []struct {
		name    string
		baseURL string
		wantErr bool
	}{
		{
			name:    "HTTPS URL is valid",
			baseURL: "https://muster.example.com",
			wantErr: false,
		},
		{
			name:    "HTTP localhost is valid",
			baseURL: "http://localhost:8080",
			wantErr: false,
		},
		{
			name:    "HTTP 127.0.0.1 is valid",
			baseURL: "http://127.0.0.1:8080",
			wantErr: false,
		},
		{
			name:    "HTTP ::1 is valid",
			baseURL: "http://[::1]:8080",
			wantErr: false,
		},
		{
			name:    "HTTP on non-loopback is invalid",
			baseURL: "http://muster.example.com",
			wantErr: true,
		},
		{
			name:    "Empty URL is invalid",
			baseURL: "",
			wantErr: true,
		},
		{
			name:    "Invalid scheme is invalid",
			baseURL: "ftp://example.com",
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateHTTPSRequirement(tt.baseURL)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestTruncateEmail(t *testing.T) {
	tests := []struct {
		name     string
		email    string
		expected string
	}{
		{
			name:     "Long email returns first 8 chars",
			email:    "test@example.com",
			expected: "test@exa",
		},
		{
			name:     "Short email returns as-is",
			email:    "short",
			expected: "short",
		},
		{
			name:     "Exactly 8 chars returns as-is",
			email:    "12345678",
			expected: "12345678",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := truncateEmail(tt.email)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestTokenProviderContextFunctions(t *testing.T) {
	t.Run("ContextWithIDToken and GetIDTokenFromContext", func(t *testing.T) {
		ctx := httptest.NewRequest("GET", "/", nil).Context()
		token := "test-token-123"

		// Initially no token
		_, ok := GetIDTokenFromContext(ctx)
		assert.False(t, ok)

		// Add token to context
		ctx = ContextWithIDToken(ctx, token)

		// Retrieve token
		retrieved, ok := GetIDTokenFromContext(ctx)
		assert.True(t, ok)
		assert.Equal(t, token, retrieved)
	})

	t.Run("GetIDTokenFromContext with empty token returns false", func(t *testing.T) {
		ctx := httptest.NewRequest("GET", "/", nil).Context()
		ctx = ContextWithIDToken(ctx, "")

		_, ok := GetIDTokenFromContext(ctx)
		assert.False(t, ok)
	})

}

func TestGetIDToken(t *testing.T) {
	t.Run("Nil token returns empty", func(t *testing.T) {
		result := GetIDToken(nil)
		assert.Empty(t, result)
	})

	// Note: Testing with actual oauth2.Token requires mocking Extra() which
	// is not straightforward since it reads from internal fields
}

func TestOAuthHTTPServerCreateMux(t *testing.T) {
	// Skip this test if we can't create a valid OAuth server
	// This test verifies that CreateMux returns a valid handler
	t.Skip("Requires valid Dex/Google provider configuration to test")
}

// TestOAuthHTTPServerEndpoints verifies that OAuth endpoints are registered correctly.
// This is an integration test that requires a valid OAuth server configuration.
func TestOAuthHTTPServerEndpoints(t *testing.T) {
	t.Skip("Requires valid Dex/Google provider configuration to test")
}

// TestMCPEndpointProtection verifies that MCP endpoints require authentication.
func TestMCPEndpointProtection(t *testing.T) {
	// This test would require mocking the mcp-oauth library which is complex
	// The actual protection is tested via scenario tests
	t.Skip("MCP endpoint protection is tested via scenario tests")
}

// TestCreateAccessTokenInjectorMiddleware tests the middleware that injects tokens
// into the request context for downstream authentication.
func TestCreateAccessTokenInjectorMiddleware(t *testing.T) {
	t.Run("Middleware passes request without user info", func(t *testing.T) {
		// Create a simple handler that just returns 200
		called := false
		next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			called = true
			w.WriteHeader(http.StatusOK)
		})

		// We can't easily test the middleware without a valid OAuth server
		// but we can verify the pattern works
		require.NotNil(t, next)
		assert.False(t, called) // Not called yet
	})

	// A Kubernetes ServiceAccount identity carries a `sub` but no email. The
	// forwarded-token (TrustedAudiences) path authenticates on `sub`, so an
	// emailless UserInfo must still attempt injectExternalIDToken rather than
	// being dropped outright.
	t.Run("emailless identity attempts forwarded ID token injection", func(t *testing.T) {
		token := fakeJWT(t, map[string]interface{}{"sub": "system:serviceaccount:ns:sa"})
		defer stubAcceptForwardedIDToken(func(_ context.Context, bearer string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return acceptanceFor(bearer, "system:serviceaccount:ns:sa", ""), nil
		})()

		s := &OAuthHTTPServer{config: config.OAuthServerConfig{BaseURL: "https://muster.test"}}

		var capturedCtx context.Context
		next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
			capturedCtx = r.Context()
		})

		r := requestWithBearer(token)
		r = r.WithContext(oauthhandler.ContextWithUserInfo(r.Context(), &providers.UserInfo{ID: "system:serviceaccount:ns:sa"}))

		s.createAccessTokenInjectorMiddleware(next).ServeHTTP(httptest.NewRecorder(), r)

		require.NotNil(t, capturedCtx, "next must be invoked with the enriched context")
		idToken, ok := GetIDTokenFromContext(capturedCtx)
		require.True(t, ok, "forwarded ID token should be injected for the emailless identity")
		assert.Equal(t, token, idToken)
	})

	// When the bearer is not an acceptable forwarded ID token, the emailless
	// path must fall through unchanged: next is still called, but no ID token
	// is injected.
	t.Run("emailless identity falls through when forwarded token rejected", func(t *testing.T) {
		defer stubAcceptForwardedIDToken(func(context.Context, string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return nil, oauth.ErrTrustedAudienceMismatch
		})()

		s := &OAuthHTTPServer{config: config.OAuthServerConfig{BaseURL: "https://muster.test"}}

		var capturedCtx context.Context
		next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
			capturedCtx = r.Context()
		})

		r := requestWithBearer("opaque-or-wrong-aud")
		r = r.WithContext(oauthhandler.ContextWithUserInfo(r.Context(), &providers.UserInfo{ID: "system:serviceaccount:ns:sa"}))

		s.createAccessTokenInjectorMiddleware(next).ServeHTTP(httptest.NewRecorder(), r)

		require.NotNil(t, capturedCtx, "next must still be called when the bearer is not a forwarded ID token")
		_, ok := GetIDTokenFromContext(capturedCtx)
		assert.False(t, ok, "no ID token should be injected when the forwarded token is rejected")
	})
}

func TestFireOnAuthenticated(t *testing.T) {
	t.Run("fires callback when session ID is present", func(t *testing.T) {
		s := &OAuthHTTPServer{}
		var capturedSessionID string
		s.SetOnAuthenticated(func(ctx context.Context, sessionID string) {
			capturedSessionID = sessionID
		})

		ctx := api.WithSessionID(context.Background(), "test-session-123")
		s.fireOnAuthenticated(ctx)

		assert.Equal(t, "test-session-123", capturedSessionID)
	})

	t.Run("does not fire callback when session ID is missing", func(t *testing.T) {
		s := &OAuthHTTPServer{}
		called := false
		s.SetOnAuthenticated(func(ctx context.Context, sessionID string) {
			called = true
		})

		s.fireOnAuthenticated(context.Background())

		assert.False(t, called, "callback should not fire without session ID")
	})

	t.Run("does not panic when no callback is registered", func(t *testing.T) {
		s := &OAuthHTTPServer{}
		ctx := api.WithSessionID(context.Background(), "test-session-123")

		assert.NotPanics(t, func() {
			s.fireOnAuthenticated(ctx)
		})
	})
}

// fakeJWT returns a syntactically valid JWT whose payload is the given claims
// JSON-encoded. The header and signature parts are placeholders — the function
// under test does not verify signatures.
func fakeJWT(t *testing.T, claims map[string]interface{}) string {
	t.Helper()
	payload, err := json.Marshal(claims)
	require.NoError(t, err)
	header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
	body := base64.RawURLEncoding.EncodeToString(payload)
	sig := base64.RawURLEncoding.EncodeToString([]byte("sig"))
	return header + "." + body + "." + sig
}

// capturingOAuthHandler is a minimal api.OAuthHandler stub that records
// StoreToken calls and pretends OAuth is enabled.
type capturingOAuthHandler struct {
	api.OAuthHandler // embed to inherit zero-value methods for unused ones
	enabled          bool
	stored           []struct {
		SessionID string
		UserID    string
		Issuer    string
		IDToken   string
		ExpiresAt time.Time
	}
}

func (c *capturingOAuthHandler) IsEnabled() bool { return c.enabled }
func (c *capturingOAuthHandler) StoreToken(sessionID, userID, issuer string, token *api.OAuthToken) {
	var (
		idToken   string
		expiresAt time.Time
	)
	if token != nil {
		idToken = token.IDToken
		expiresAt = token.ExpiresAt
	}
	c.stored = append(c.stored, struct {
		SessionID string
		UserID    string
		Issuer    string
		IDToken   string
		ExpiresAt time.Time
	}{sessionID, userID, issuer, idToken, expiresAt})
}

// requestWithBearer builds a request with the given Authorization header.
func requestWithBearer(bearer string) *http.Request {
	r := httptest.NewRequest("POST", "/mcp", nil)
	if bearer != "" {
		r.Header.Set("Authorization", "Bearer "+bearer)
	}
	return r
}

// stubAcceptForwardedIDToken overrides the package-level test seam around
// (*oauth.Server).AcceptForwardedIDToken for the duration of a subtest, so
// tests don't need a real JWKS/provider wired up. It returns a restore
// function that callers must defer.
func stubAcceptForwardedIDToken(
	fn func(ctx context.Context, bearerToken string) (*oauthserver.ForwardedIDTokenAcceptance, error),
) func() {
	prev := acceptForwardedIDToken
	acceptForwardedIDToken = func(_ *oauth.Server, ctx context.Context, bearerToken string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
		return fn(ctx, bearerToken)
	}
	return func() { acceptForwardedIDToken = prev }
}

// acceptanceFor builds a ForwardedIDTokenAcceptance that mirrors the library's
// shape closely enough for the muster-side tests: a deterministic session ID
// derived from the full bearer token (so "same token same session, different
// tokens different sessions" still holds in test stubs) plus the requested
// subject/email. Uses sha256 to avoid the "two JWTs share a common header
// prefix" collision trap.
func acceptanceFor(bearer, subject, email string) *oauthserver.ForwardedIDTokenAcceptance {
	sum := sha256.Sum256([]byte(bearer))
	sessionID := "ext-" + hex.EncodeToString(sum[:8])
	return &oauthserver.ForwardedIDTokenAcceptance{
		SessionID: sessionID,
		Subject:   subject,
		UserInfo:  &providers.UserInfo{ID: subject, Email: email},
		Issuer:    "https://idp.example.com",
		Audience:  "muster",
	}
}

func TestInjectExternalIDToken(t *testing.T) {
	baseURL := "https://muster.test"

	t.Run("rejects request without bearer token", func(t *testing.T) {
		s := &OAuthHTTPServer{config: config.OAuthServerConfig{BaseURL: baseURL}}
		nextCalled := false
		next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { nextCalled = true })

		r := httptest.NewRequest("POST", "/mcp", nil)
		w := httptest.NewRecorder()

		// No stub needed: the function short-circuits before calling the library
		// when there's no bearer token.
		handled := s.injectExternalIDToken(w, r, r.Context(), next)
		assert.False(t, handled, "should not handle request without bearer")
		assert.False(t, nextCalled, "next should not be called when unhandled")
	})

	t.Run("returns false when library rejects audience mismatch", func(t *testing.T) {
		defer stubAcceptForwardedIDToken(func(context.Context, string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return nil, oauth.ErrTrustedAudienceMismatch
		})()

		s := &OAuthHTTPServer{config: config.OAuthServerConfig{BaseURL: baseURL}}
		nextCalled := false
		next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { nextCalled = true })

		r := requestWithBearer("opaque-or-wrong-aud")
		handled := s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next)
		assert.False(t, handled)
		assert.False(t, nextCalled)
	})

	t.Run("returns false when library returns generic error (sig/parse/issuer)", func(t *testing.T) {
		defer stubAcceptForwardedIDToken(func(context.Context, string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return nil, errors.New("signature invalid")
		})()

		s := &OAuthHTTPServer{config: config.OAuthServerConfig{BaseURL: baseURL}}
		nextCalled := false
		next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { nextCalled = true })

		r := requestWithBearer("not-a-jwt")
		handled := s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next)
		assert.False(t, handled)
		assert.False(t, nextCalled)
	})

	t.Run("injects session, subject and ID token into context on success", func(t *testing.T) {
		token := fakeJWT(t, map[string]interface{}{"sub": "sub-from-jwt"})
		defer stubAcceptForwardedIDToken(func(_ context.Context, bearer string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			assert.Equal(t, token, bearer, "bearer token must be forwarded to the library unchanged")
			return acceptanceFor(bearer, "sub-from-library", "a@b.com"), nil
		})()

		s := &OAuthHTTPServer{config: config.OAuthServerConfig{BaseURL: baseURL}}
		var capturedCtx context.Context
		next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
			capturedCtx = r.Context()
		})

		r := requestWithBearer(token)

		handled := s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next)
		require.True(t, handled)
		require.NotNil(t, capturedCtx)

		assert.Equal(t, "sub-from-library", api.GetSubjectFromContext(capturedCtx),
			"subject must come from acceptance.Subject (library-verified)")

		sessionID := api.GetSessionIDFromContext(capturedCtx)
		assert.Regexp(t, `^ext-[0-9a-f]{16}$`, sessionID,
			"session ID must be the library-derived ext-<16 hex> format")

		idToken, ok := GetIDTokenFromContext(capturedCtx)
		require.True(t, ok)
		assert.Equal(t, token, idToken, "bearer token should be treated as ID token")
	})

	t.Run("session ID is deterministic per bearer token", func(t *testing.T) {
		defer stubAcceptForwardedIDToken(func(_ context.Context, bearer string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return acceptanceFor(bearer, "u", ""), nil
		})()

		s := &OAuthHTTPServer{config: config.OAuthServerConfig{BaseURL: baseURL}}
		var sessions []string
		next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
			sessions = append(sessions, api.GetSessionIDFromContext(r.Context()))
		})

		tokenA := fakeJWT(t, map[string]interface{}{"sub": "u1"})
		tokenB := fakeJWT(t, map[string]interface{}{"sub": "u2"})

		// Same token → same session ID across two requests.
		for i := 0; i < 2; i++ {
			r := requestWithBearer(tokenA)
			s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next)
		}
		// Different token → different session ID.
		r := requestWithBearer(tokenB)
		s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next)

		require.Len(t, sessions, 3)
		assert.Equal(t, sessions[0], sessions[1], "same token must produce same session ID")
		assert.NotEqual(t, sessions[0], sessions[2], "different tokens must produce different session IDs")
	})

	t.Run("fires onAuthenticated with the synthetic session ID", func(t *testing.T) {
		token := fakeJWT(t, map[string]interface{}{"sub": "u1"})
		defer stubAcceptForwardedIDToken(func(_ context.Context, bearer string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return acceptanceFor(bearer, "u1", ""), nil
		})()

		s := &OAuthHTTPServer{config: config.OAuthServerConfig{BaseURL: baseURL}}
		var capturedSession string
		s.SetOnAuthenticated(func(_ context.Context, sessionID string) {
			capturedSession = sessionID
		})

		var ctxSession string
		next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
			ctxSession = api.GetSessionIDFromContext(r.Context())
		})

		r := requestWithBearer(token)
		require.True(t, s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next))
		assert.Equal(t, ctxSession, capturedSession,
			"onAuthenticated should fire with the same session ID exposed to the next handler")
	})

	t.Run("mirrors ID token into OAuth handler keyed by Dex issuer when provider=dex", func(t *testing.T) {
		token := fakeJWT(t, map[string]interface{}{"sub": "u1", "exp": int64(9999999999)})
		defer stubAcceptForwardedIDToken(func(_ context.Context, bearer string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return acceptanceFor(bearer, "u1", "a@b.com"), nil
		})()

		handler := &capturingOAuthHandler{enabled: true}
		api.RegisterOAuthHandler(handler)
		defer api.RegisterOAuthHandler(nil)

		dexIssuer := "https://dex.example.com"
		s := &OAuthHTTPServer{config: config.OAuthServerConfig{
			BaseURL:  baseURL,
			Provider: OAuthProviderDex,
			Dex:      config.DexConfig{IssuerURL: dexIssuer},
		}}
		next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})

		r := requestWithBearer(token)
		require.True(t, s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next))

		require.Len(t, handler.stored, 1)
		got := handler.stored[0]
		assert.Equal(t, dexIssuer, got.Issuer,
			"for Dex provider the token must be keyed by the Dex issuer URL, not muster's BaseURL")
		assert.Equal(t, "u1", got.UserID)
		assert.Equal(t, token, got.IDToken)
		assert.Regexp(t, `^ext-[0-9a-f]{16}$`, got.SessionID)
	})

	t.Run("falls back to BaseURL for non-dex providers", func(t *testing.T) {
		token := fakeJWT(t, map[string]interface{}{"sub": "u1", "exp": int64(9999999999)})
		defer stubAcceptForwardedIDToken(func(_ context.Context, bearer string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return acceptanceFor(bearer, "u1", ""), nil
		})()

		handler := &capturingOAuthHandler{enabled: true}
		api.RegisterOAuthHandler(handler)
		defer api.RegisterOAuthHandler(nil)

		s := &OAuthHTTPServer{config: config.OAuthServerConfig{
			BaseURL:  baseURL,
			Provider: OAuthProviderGoogle,
		}}
		next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})

		r := requestWithBearer(token)
		require.True(t, s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next))

		require.Len(t, handler.stored, 1)
		assert.Equal(t, baseURL, handler.stored[0].Issuer)
	})

	t.Run("populates ExpiresAt from JWT exp claim on the mirrored entry", func(t *testing.T) {
		token := fakeJWT(t, map[string]interface{}{"sub": "u1", "exp": int64(9999999999)})
		defer stubAcceptForwardedIDToken(func(_ context.Context, bearer string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return acceptanceFor(bearer, "u1", ""), nil
		})()

		handler := &capturingOAuthHandler{enabled: true}
		api.RegisterOAuthHandler(handler)
		defer api.RegisterOAuthHandler(nil)

		s := &OAuthHTTPServer{config: config.OAuthServerConfig{BaseURL: baseURL}}
		next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})

		r := requestWithBearer(token)
		require.True(t, s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next))

		require.Len(t, handler.stored, 1)
		assert.Equal(t, time.Unix(9999999999, 0), handler.stored[0].ExpiresAt,
			"ExpiresAt must come from the JWT exp claim — without it IsExpiredWithMargin treats the entry as never-expiring")
	})

	t.Run("refuses to mirror forwarded ID token without parseable JWT exp", func(t *testing.T) {
		token := fakeJWT(t, map[string]interface{}{"sub": "u1"}) // no exp claim
		defer stubAcceptForwardedIDToken(func(_ context.Context, bearer string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return acceptanceFor(bearer, "u1", ""), nil
		})()

		handler := &capturingOAuthHandler{enabled: true}
		api.RegisterOAuthHandler(handler)
		defer api.RegisterOAuthHandler(nil)

		s := &OAuthHTTPServer{config: config.OAuthServerConfig{BaseURL: baseURL}}
		next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})

		r := requestWithBearer(token)
		require.True(t, s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next))
		assert.Empty(t, handler.stored,
			"forwarded ID token without parseable exp must not land as a never-expiring entry")
	})

	t.Run("does not call OAuth handler when disabled", func(t *testing.T) {
		token := fakeJWT(t, map[string]interface{}{"sub": "u1"})
		defer stubAcceptForwardedIDToken(func(_ context.Context, bearer string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return acceptanceFor(bearer, "u1", ""), nil
		})()

		handler := &capturingOAuthHandler{enabled: false}
		api.RegisterOAuthHandler(handler)
		defer api.RegisterOAuthHandler(nil)

		s := &OAuthHTTPServer{config: config.OAuthServerConfig{BaseURL: baseURL}}
		next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})

		r := requestWithBearer(token)
		require.True(t, s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next))
		assert.Empty(t, handler.stored,
			"StoreToken should not be invoked when the handler reports disabled")
	})

	t.Run("does not call OAuth handler when no issuer can be resolved", func(t *testing.T) {
		token := fakeJWT(t, map[string]interface{}{"sub": "u1"})
		defer stubAcceptForwardedIDToken(func(_ context.Context, bearer string) (*oauthserver.ForwardedIDTokenAcceptance, error) {
			return acceptanceFor(bearer, "u1", ""), nil
		})()

		handler := &capturingOAuthHandler{enabled: true}
		api.RegisterOAuthHandler(handler)
		defer api.RegisterOAuthHandler(nil)

		// No BaseURL and no Dex issuer -> musterIssuer returns "".
		s := &OAuthHTTPServer{config: config.OAuthServerConfig{}}
		next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})

		r := requestWithBearer(token)
		require.True(t, s.injectExternalIDToken(httptest.NewRecorder(), r, r.Context(), next))
		assert.Empty(t, handler.stored,
			"StoreToken should not be invoked without an issuer to key by")
	})
}
