/*
이 테스트 파일은 AuthHandler의 PasswordLogin 메서드 내 OIDC/Hydra 관련 로직을 검증합니다.
특히 Hydra Login Request 조회, 검증(Inactive 체크), 승인(Accept) 흐름을 중점적으로 테스트합니다.
*/

package handler

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/middleware"
	"baron-sso-backend/internal/service"
	"bytes"
	"context"
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/rsa"
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/go-jose/go-jose/v4"
	josejwt "github.com/go-jose/go-jose/v4/jwt"
	"github.com/gofiber/fiber/v2"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"gorm.io/gorm"
)

// --- Mocks ---

type MockIdentityProvider struct {
	mock.Mock
}

func (m *MockIdentityProvider) Name() string {
	return "mock-idp"
}

func (m *MockIdentityProvider) GetMetadata() (*domain.IDPMetadata, error) {
	return nil, nil
}

func (m *MockIdentityProvider) CreateUser(user *domain.BrokerUser, password string) (string, error) {
	return "", nil
}

func (m *MockIdentityProvider) SignIn(loginID, password string) (*domain.AuthInfo, error) {
	args := m.Called(loginID, password)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.AuthInfo), args.Error(1)
}

func (m *MockIdentityProvider) UserExists(loginID string) (bool, error) {
	return true, nil
}

func (m *MockIdentityProvider) IssueSession(loginID string) (*domain.AuthInfo, error) {
	return nil, nil
}

func (m *MockIdentityProvider) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkLoginInit, error) {
	return nil, nil
}

func (m *MockIdentityProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) {
	return nil, nil
}

func (m *MockIdentityProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) {
	return nil, nil
}

func (m *MockIdentityProvider) InitiatePasswordReset(loginID, redirectUrl string) error {
	return nil
}

func (m *MockIdentityProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) {
	return nil, nil
}

func (m *MockIdentityProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error {
	return nil
}

type MockKratosAdminService struct {
	mock.Mock
}

func (m *MockKratosAdminService) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
	args := m.Called(ctx, identifier)
	return args.String(0), args.Error(1)
}

func (m *MockKratosAdminService) GetIdentity(ctx context.Context, id string) (*service.KratosIdentity, error) {
	args := m.Called(ctx, id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*service.KratosIdentity), args.Error(1)
}

func (m *MockKratosAdminService) ListIdentities(ctx context.Context) ([]service.KratosIdentity, error) {
	return nil, nil
}

func (m *MockKratosAdminService) UpdateIdentity(ctx context.Context, identityID string, traits map[string]any, state string) (*service.KratosIdentity, error) {
	return nil, nil
}

func (m *MockKratosAdminService) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error {
	return nil
}

func (m *MockKratosAdminService) DeleteIdentity(ctx context.Context, identityID string) error {
	return nil
}

func (m *MockKratosAdminService) ListIdentitySessions(ctx context.Context, identityID string) ([]service.KratosSession, error) {
	args := m.Called(ctx, identityID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]service.KratosSession), args.Error(1)
}

func (m *MockKratosAdminService) GetSession(ctx context.Context, sessionID string) (*service.KratosSession, error) {
	args := m.Called(ctx, sessionID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*service.KratosSession), args.Error(1)
}

func (m *MockKratosAdminService) DeleteSession(ctx context.Context, sessionID string) error {
	args := m.Called(ctx, sessionID)
	return args.Error(0)
}

// --- Helper ---

func newAuthLoginTestApp(h *AuthHandler) *fiber.App {
	app := fiber.New()
	app.Post("/api/v1/auth/login", h.PasswordLogin)
	return app
}

func newHeadlessPasswordLoginTestApp(h *AuthHandler) *fiber.App {
	app := fiber.New()
	app.Post("/api/v1/auth/headless/password/login", h.HeadlessPasswordLogin)
	return app
}

type passwordLoginUserRepo struct {
	usersByID map[string]domain.User
}

func (r *passwordLoginUserRepo) Create(ctx context.Context, user *domain.User) error { return nil }
func (r *passwordLoginUserRepo) Update(ctx context.Context, user *domain.User) error { return nil }

func (r *passwordLoginUserRepo) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
	return nil, errors.New("not found")
}

func (r *passwordLoginUserRepo) FindByID(ctx context.Context, id string) (*domain.User, error) {
	if r != nil {
		if user, ok := r.usersByID[id]; ok {
			return &user, nil
		}
	}
	return nil, errors.New("not found")
}

func (r *passwordLoginUserRepo) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) {
	return nil, nil
}

func (r *passwordLoginUserRepo) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) {
	return nil, nil
}

func (r *passwordLoginUserRepo) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
	return nil, 0, "", nil
}

func (r *passwordLoginUserRepo) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
	return 0, nil
}

func (r *passwordLoginUserRepo) CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error) {
	return nil, nil
}

func (r *passwordLoginUserRepo) CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error) {
	return nil, nil
}

func (r *passwordLoginUserRepo) FindByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.User, error) {
	return nil, nil
}

func (r *passwordLoginUserRepo) FindByCompanyCodes(ctx context.Context, codes []string) ([]domain.User, error) {
	return nil, nil
}

func (r *passwordLoginUserRepo) Delete(ctx context.Context, id string) error { return nil }

func (r *passwordLoginUserRepo) DB() *gorm.DB { return nil }

func (r *passwordLoginUserRepo) UpdateUserLoginIDs(ctx context.Context, userID string, loginIDs []domain.UserLoginID) error {
	return nil
}

func (r *passwordLoginUserRepo) GetUserLoginIDs(ctx context.Context, userID string) ([]domain.UserLoginID, error) {
	return nil, nil
}

func (r *passwordLoginUserRepo) IsLoginIDTaken(ctx context.Context, loginID string) (bool, error) {
	return false, nil
}

func (r *passwordLoginUserRepo) FindTenantIDByLoginID(ctx context.Context, loginID string) (string, error) {
	return "", nil
}

func mustHeadlessRSAJWK(t *testing.T) (*rsa.PrivateKey, map[string]any) {
	t.Helper()

	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("failed to generate rsa key: %v", err)
	}

	keySet := jose.JSONWebKeySet{
		Keys: []jose.JSONWebKey{
			{
				Key:       &privateKey.PublicKey,
				KeyID:     "test-kid",
				Use:       "sig",
				Algorithm: string(jose.RS256),
			},
		},
	}

	raw, err := json.Marshal(keySet)
	if err != nil {
		t.Fatalf("failed to marshal jwks: %v", err)
	}

	var jwks map[string]any
	if err := json.Unmarshal(raw, &jwks); err != nil {
		t.Fatalf("failed to decode jwks map: %v", err)
	}

	return privateKey, jwks
}

func mustHeadlessClientAssertion(t *testing.T, privateKey *rsa.PrivateKey, clientID, audience string) string {
	t.Helper()

	signer, err := jose.NewSigner(jose.SigningKey{
		Algorithm: jose.RS256,
		Key: jose.JSONWebKey{
			Key:       privateKey,
			KeyID:     "test-kid",
			Use:       "sig",
			Algorithm: string(jose.RS256),
		},
	}, nil)
	if err != nil {
		t.Fatalf("failed to create signer: %v", err)
	}

	now := time.Now()
	raw, err := josejwt.Signed(signer).Claims(josejwt.Claims{
		Issuer:   clientID,
		Subject:  clientID,
		Audience: josejwt.Audience{audience},
		Expiry:   josejwt.NewNumericDate(now.Add(5 * time.Minute)),
		IssuedAt: josejwt.NewNumericDate(now),
		NotBefore: josejwt.NewNumericDate(
			now.Add(-1 * time.Minute),
		),
		ID: "assertion-1",
	}).Serialize()
	if err != nil {
		t.Fatalf("failed to sign client assertion: %v", err)
	}

	return raw
}

func mustHeadlessClientAssertionWithCustomClaims(
	t *testing.T,
	privateKey *rsa.PrivateKey,
	clientID string,
	claims josejwt.Claims,
) string {
	t.Helper()

	signer, err := jose.NewSigner(jose.SigningKey{
		Algorithm: jose.RS256,
		Key: jose.JSONWebKey{
			Key:       privateKey,
			KeyID:     "test-kid",
			Use:       "sig",
			Algorithm: string(jose.RS256),
		},
	}, nil)
	if err != nil {
		t.Fatalf("failed to create signer: %v", err)
	}

	raw, err := josejwt.Signed(signer).Claims(claims).Serialize()
	if err != nil {
		t.Fatalf("failed to sign client assertion: %v", err)
	}

	return raw
}

func mustHeadlessJWKForAlgorithm(t *testing.T, alg jose.SignatureAlgorithm) (any, map[string]any) {
	t.Helper()

	var privateKey any
	var publicKey any

	switch alg {
	case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512:
		key, err := rsa.GenerateKey(rand.Reader, 2048)
		if err != nil {
			t.Fatalf("failed to generate rsa key: %v", err)
		}
		privateKey = key
		publicKey = &key.PublicKey
	case jose.ES256:
		key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
		if err != nil {
			t.Fatalf("failed to generate ecdsa key: %v", err)
		}
		privateKey = key
		publicKey = &key.PublicKey
	case jose.ES384:
		key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
		if err != nil {
			t.Fatalf("failed to generate ecdsa key: %v", err)
		}
		privateKey = key
		publicKey = &key.PublicKey
	case jose.ES512:
		key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
		if err != nil {
			t.Fatalf("failed to generate ecdsa key: %v", err)
		}
		privateKey = key
		publicKey = &key.PublicKey
	case jose.EdDSA:
		_, key, err := ed25519.GenerateKey(rand.Reader)
		if err != nil {
			t.Fatalf("failed to generate ed25519 key: %v", err)
		}
		privateKey = key
		publicKey = key.Public()
	default:
		t.Fatalf("unsupported test algorithm: %s", alg)
	}

	keySet := jose.JSONWebKeySet{
		Keys: []jose.JSONWebKey{
			{
				Key:       publicKey,
				KeyID:     "test-kid",
				Use:       "sig",
				Algorithm: string(alg),
			},
		},
	}

	raw, err := json.Marshal(keySet)
	if err != nil {
		t.Fatalf("failed to marshal jwks: %v", err)
	}

	var jwks map[string]any
	if err := json.Unmarshal(raw, &jwks); err != nil {
		t.Fatalf("failed to decode jwks map: %v", err)
	}

	return privateKey, jwks
}

func mustHeadlessClientAssertionWithAlgorithm(t *testing.T, privateKey any, alg jose.SignatureAlgorithm, clientID, audience string) string {
	t.Helper()

	signer, err := jose.NewSigner(jose.SigningKey{
		Algorithm: alg,
		Key: jose.JSONWebKey{
			Key:       privateKey,
			KeyID:     "test-kid",
			Use:       "sig",
			Algorithm: string(alg),
		},
	}, nil)
	if err != nil {
		t.Fatalf("failed to create signer: %v", err)
	}

	now := time.Now()
	raw, err := josejwt.Signed(signer).Claims(josejwt.Claims{
		Issuer:    clientID,
		Subject:   clientID,
		Audience:  josejwt.Audience{audience},
		Expiry:    josejwt.NewNumericDate(now.Add(5 * time.Minute)),
		IssuedAt:  josejwt.NewNumericDate(now),
		NotBefore: josejwt.NewNumericDate(now.Add(-1 * time.Minute)),
		ID:        "assertion-1",
	}).Serialize()
	if err != nil {
		t.Fatalf("failed to sign client assertion: %v", err)
	}

	return raw
}

func runHeadlessPasswordLoginWithAssertion(t *testing.T, jwks map[string]any, clientAssertion string) *http.Response {
	t.Helper()
	t.Setenv("BACKEND_PUBLIC_URL", "")
	return runHeadlessPasswordLoginWithAssertionRequest(t, jwks, clientAssertion, "http://example.com/api/v1/auth/headless/password/login", nil)
}

func runHeadlessPasswordLoginWithAssertionRequest(
	t *testing.T,
	jwks map[string]any,
	clientAssertion string,
	requestURL string,
	headers map[string]string,
) *http.Response {
	t.Helper()
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-identity-id",
	}, nil)

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil)

	jwksBody, err := json.Marshal(jwks)
	if err != nil {
		t.Fatalf("failed to marshal jwks body: %v", err)
	}
	jwksClient := newJWKSHTTPClient(t, jwksBody)
	jwksURI := jwksURL()

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   jwksURI,
					},
				},
			})
			return
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
			return
		}
		http.NotFound(w, r)
	})

	h := &AuthHandler{
		IdpProvider:  mockIdp,
		KratosAdmin:  mockKratos,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient),
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessPasswordLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"client_id":        "headless-login-client",
		"client_assertion": clientAssertion,
		"loginId":          "employee001",
		"password":         "password",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, requestURL, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	for key, value := range headers {
		req.Header.Set(key, value)
	}

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	return resp
}

func runHeadlessPasswordLoginWithAssertionAndLogger(
	t *testing.T,
	jwks map[string]any,
	clientAssertion string,
	logger *slog.Logger,
) *http.Response {
	t.Helper()
	t.Setenv("BACKEND_PUBLIC_URL", "")
	return runHeadlessPasswordLoginWithAssertionAndLoggerRequest(
		t,
		jwks,
		clientAssertion,
		"http://example.com/api/v1/auth/headless/password/login",
		nil,
		logger,
	)
}

func runHeadlessPasswordLoginWithAssertionAndLoggerRequest(
	t *testing.T,
	jwks map[string]any,
	clientAssertion string,
	requestURL string,
	headers map[string]string,
	logger *slog.Logger,
) *http.Response {
	t.Helper()
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-identity-id",
	}, nil)

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil)

	jwksBody, err := json.Marshal(jwks)
	if err != nil {
		t.Fatalf("failed to marshal jwks body: %v", err)
	}
	jwksClient := newJWKSHTTPClient(t, jwksBody)
	jwksURI := jwksURL()

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   jwksURI,
					},
				},
			})
			return
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
			return
		}
		http.NotFound(w, r)
	})

	h := &AuthHandler{
		IdpProvider:  mockIdp,
		KratosAdmin:  mockKratos,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient),
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessPasswordLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"client_id":        "headless-login-client",
		"client_assertion": clientAssertion,
		"loginId":          "employee001",
		"password":         "password",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, requestURL, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Request-Id", "req-headless-test-123")
	for key, value := range headers {
		req.Header.Set(key, value)
	}

	if logger != nil {
		previous := slog.Default()
		slog.SetDefault(logger.With())
		t.Cleanup(func() {
			slog.SetDefault(previous)
		})
	}

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	return resp
}

// mockHydraTransport simulates Hydra API responses
func mockHydraTransport(handler http.Handler) http.RoundTripper {
	return roundTripFunc(func(req *http.Request) (*http.Response, error) {
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, req)
		return w.Result(), nil
	})
}

// --- Tests ---

func TestPasswordLogin_OIDC_Success(t *testing.T) {
	mockIdp := new(MockIdentityProvider)

	// Mock IDP SignIn Success
	mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-identity-id",
	}, nil)

	// Mock Hydra Responses
	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			// GetLoginRequest
			challenge := r.URL.Query().Get("login_challenge")
			if challenge != "challenge-123" {
				http.Error(w, "invalid challenge", http.StatusBadRequest)
				return
			}
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: challenge,
				Client:    domain.HydraClient{ClientID: "client-1", Metadata: map[string]any{"status": "active"}},
			})
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			// AcceptLoginRequest
			json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
		default:
			http.NotFound(w, r)
		}
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil)

	h := &AuthHandler{
		IdpProvider: mockIdp,
		KratosAdmin: mockKratos,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newAuthLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"loginId":         "user@example.com",
		"password":        "password",
		"login_challenge": "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	json.NewDecoder(resp.Body).Decode(&got)
	if got["redirectTo"] != "http://rp/cb" {
		t.Errorf("expected redirectTo http://rp/cb, got %v", got["redirectTo"])
	}
	if got["sessionJwt"] != "valid-jwt" {
		t.Errorf("expected sessionJwt to be valid-jwt, got %v", got["sessionJwt"])
	}
	if got["token"] != "valid-jwt" {
		t.Errorf("expected token to be valid-jwt, got %v", got["token"])
	}
}

func TestPasswordLogin_OIDC_AuditIncludesClientMetadata(t *testing.T) {
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "session-123"},
		Subject:      "kratos-identity-id",
	}, nil)

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:   "devfront",
					ClientName: "DevFront",
					Metadata:   map[string]any{"status": "active"},
				},
			})
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
		default:
			http.NotFound(w, r)
		}
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil)

	auditRepo := &mockAuditRepo{}
	h := &AuthHandler{
		IdpProvider: mockIdp,
		KratosAdmin: mockKratos,
		AuditRepo:   auditRepo,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := fiber.New()
	app.Use(middleware.AuditMiddleware(middleware.AuditConfig{
		Repo:     auditRepo,
		BodyDump: true,
	}))
	app.Post("/api/v1/auth/password/login", h.PasswordLogin)

	body, _ := json.Marshal(map[string]string{
		"loginId":         "user@example.com",
		"password":        "password",
		"login_challenge": "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	if len(auditRepo.logs) != 1 {
		t.Fatalf("expected 1 audit log, got %d", len(auditRepo.logs))
	}

	log := auditRepo.logs[0]
	if log.EventType != "POST /api/v1/auth/password/login" {
		t.Fatalf("expected password login audit event, got %q", log.EventType)
	}
	if log.UserID != "kratos-identity-id" {
		t.Fatalf("expected audit user_id kratos-identity-id, got %q", log.UserID)
	}

	details, err := parseAuditDetails(log.Details)
	if err != nil {
		t.Fatalf("failed to parse audit details: %v", err)
	}
	if got, _ := details["client_id"].(string); got != "devfront" {
		t.Fatalf("expected client_id devfront, got %v", details["client_id"])
	}
	if got, _ := details["client_name"].(string); got != "DevFront" {
		t.Fatalf("expected client_name DevFront, got %v", details["client_name"])
	}
}

func TestPasswordLogin_UserFront_AuditIncludesDefaultClientMetadata(t *testing.T) {
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "session-123"},
		Subject:      "kratos-identity-id",
	}, nil)

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil)

	auditRepo := &mockAuditRepo{}
	h := &AuthHandler{
		IdpProvider: mockIdp,
		KratosAdmin: mockKratos,
		AuditRepo:   auditRepo,
	}

	app := fiber.New()
	app.Use(middleware.AuditMiddleware(middleware.AuditConfig{
		Repo:     auditRepo,
		BodyDump: true,
	}))
	app.Post("/api/v1/auth/password/login", h.PasswordLogin)

	body, _ := json.Marshal(map[string]string{
		"loginId":  "user@example.com",
		"password": "password",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	if len(auditRepo.logs) != 1 {
		t.Fatalf("expected 1 audit log, got %d", len(auditRepo.logs))
	}
	if auditRepo.logs[0].UserID != "kratos-identity-id" {
		t.Fatalf("expected audit user_id kratos-identity-id, got %q", auditRepo.logs[0].UserID)
	}

	details, err := parseAuditDetails(auditRepo.logs[0].Details)
	if err != nil {
		t.Fatalf("failed to parse audit details: %v", err)
	}
	if got, _ := details["client_id"].(string); got != "userfront" {
		t.Fatalf("expected client_id userfront, got %v", details["client_id"])
	}
	if got, _ := details["client_name"].(string); got != "UserFront" {
		t.Fatalf("expected client_name UserFront, got %v", details["client_name"])
	}
}

func TestHeadlessPasswordLogin_HeadlessLoginClientSuccess(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "")

	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-identity-id",
	}, nil)

	privateKey, jwks := mustHeadlessRSAJWK(t)
	jwksBody, _ := json.Marshal(jwks)
	jwksClient := newJWKSHTTPClient(t, jwksBody)
	jwksURI := jwksURL()

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   jwksURI,
					},
				},
			})
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
		default:
			http.NotFound(w, r)
		}
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil)

	h := &AuthHandler{
		IdpProvider:  mockIdp,
		KratosAdmin:  mockKratos,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient),
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessPasswordLoginTestApp(h)

	clientAssertion := mustHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		"http://example.com/api/v1/auth/headless/password/login",
	)
	body, _ := json.Marshal(map[string]string{
		"client_id":        "headless-login-client",
		"client_assertion": clientAssertion,
		"loginId":          "employee001",
		"password":         "password",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["redirectTo"] != "http://rp/cb" {
		t.Fatalf("expected redirectTo http://rp/cb, got %v", got["redirectTo"])
	}
	if _, ok := got["sessionJwt"]; ok {
		t.Fatalf("expected headless response to omit sessionJwt, got %v", got["sessionJwt"])
	}
	if len(resp.Cookies()) != 0 {
		t.Fatalf("expected headless response to omit cookies, got %v", resp.Cookies())
	}
}

func TestHeadlessPasswordLogin_OIDCSubjectConflictBlocksMixedRP(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "")

	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "employee002", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-target-b",
	}, nil)

	privateKey, jwks := mustHeadlessRSAJWK(t)
	jwksBody, _ := json.Marshal(jwks)
	jwksClient := newJWKSHTTPClient(t, jwksBody)
	jwksURI := jwksURL()

	acceptCalled := false
	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			_ = json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Skip:      true,
				Subject:   "kratos-userfront-a",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   jwksURI,
					},
				},
			})
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			acceptCalled = true
			_ = json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
		default:
			http.NotFound(w, r)
		}
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee002").Return("kratos-target-b", nil)

	h := &AuthHandler{
		IdpProvider:  mockIdp,
		KratosAdmin:  mockKratos,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient),
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}
	app := newHeadlessPasswordLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"client_id":        "headless-login-client",
		"client_assertion": mustHeadlessClientAssertion(t, privateKey, "headless-login-client", "http://example.com/api/v1/auth/headless/password/login"),
		"loginId":          "employee002",
		"password":         "password",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	require.Equal(t, http.StatusConflict, resp.StatusCode)
	require.False(t, acceptCalled)
	require.Empty(t, resp.Cookies())
	var got map[string]any
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
	require.Equal(t, "oidc_subject_conflict", got["code"])
	require.Equal(t, "redirect_to_userfront_login", got["recommendedAction"])
	require.Equal(t, "kratos-userfront-a", got["currentSubject"])
	require.Equal(t, "kratos-target-b", got["targetSubject"])
}

func TestHeadlessPasswordLogin_OIDCSubjectSameAllowsMixedRP(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "")

	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-userfront-a",
	}, nil)

	privateKey, jwks := mustHeadlessRSAJWK(t)
	jwksBody, _ := json.Marshal(jwks)
	jwksClient := newJWKSHTTPClient(t, jwksBody)
	jwksURI := jwksURL()

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			_ = json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Skip:      true,
				Subject:   "kratos-userfront-a",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   jwksURI,
					},
				},
			})
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			_ = json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
		default:
			http.NotFound(w, r)
		}
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-userfront-a", nil)

	h := &AuthHandler{
		IdpProvider:  mockIdp,
		KratosAdmin:  mockKratos,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient),
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}
	app := newHeadlessPasswordLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"client_id":        "headless-login-client",
		"client_assertion": mustHeadlessClientAssertion(t, privateKey, "headless-login-client", "http://example.com/api/v1/auth/headless/password/login"),
		"loginId":          "employee001",
		"password":         "password",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.Empty(t, resp.Cookies())
	var got map[string]any
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
	require.Equal(t, "http://rp/cb", got["redirectTo"])
	require.Nil(t, got["sessionJwt"])
}

func TestHeadlessPasswordLogin_AuditIncludesClientMetadata(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "")

	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "session-123"},
		Subject:      "kratos-identity-id",
	}, nil)

	privateKey, jwks := mustHeadlessRSAJWK(t)
	jwksBody, _ := json.Marshal(jwks)

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					ClientName:              "Headless Login Portal",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   "https://rp.example.com/.well-known/jwks.json",
					},
				},
			})
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
		default:
			http.NotFound(w, r)
		}
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil)

	auditRepo := &mockAuditRepo{}
	headlessClient := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Host == "rp.example.com" && r.URL.Path == "/.well-known/jwks.json" {
			return httpResponse(r, http.StatusOK, string(jwksBody)), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})}
	h := &AuthHandler{
		IdpProvider: mockIdp,
		KratosAdmin: mockKratos,
		AuditRepo:   auditRepo,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(
			nil,
			headlessClient,
		),
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := fiber.New()
	app.Use(middleware.AuditMiddleware(middleware.AuditConfig{
		Repo:     auditRepo,
		BodyDump: true,
	}))
	app.Post("/api/v1/auth/headless/password/login", h.HeadlessPasswordLogin)

	clientAssertion := mustHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		"http://example.com/api/v1/auth/headless/password/login",
	)
	body, _ := json.Marshal(map[string]string{
		"client_id":        "headless-login-client",
		"client_assertion": clientAssertion,
		"loginId":          "employee001",
		"password":         "password",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/146.0.0.0 Safari/537.36")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	if len(auditRepo.logs) != 1 {
		t.Fatalf("expected 1 audit log, got %d", len(auditRepo.logs))
	}

	log := auditRepo.logs[0]
	if log.EventType != "POST /api/v1/auth/headless/password/login" {
		t.Fatalf("expected headless password login audit event, got %q", log.EventType)
	}
	if log.UserID != "kratos-identity-id" {
		t.Fatalf("expected audit user_id kratos-identity-id, got %q", log.UserID)
	}
	if log.SessionID != "session-123" {
		t.Fatalf("expected audit session_id session-123, got %q", log.SessionID)
	}

	details, err := parseAuditDetails(log.Details)
	if err != nil {
		t.Fatalf("failed to parse audit details: %v", err)
	}
	if got, _ := details["client_id"].(string); got != "headless-login-client" {
		t.Fatalf("expected client_id headless-login-client, got %v", details["client_id"])
	}
	if got, _ := details["client_name"].(string); got != "Headless Login Portal" {
		t.Fatalf("expected client_name Headless Login Portal, got %v", details["client_name"])
	}
	if got, _ := details["login_challenge"].(string); got != "challenge-123" {
		t.Fatalf("expected login_challenge challenge-123, got %v", details["login_challenge"])
	}
}

func TestHeadlessPasswordLogin_IgnoresInlineHeadlessJWKSWhenJWKSURIIsConfigured(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "")

	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-identity-id",
	}, nil)

	privateKey, jwks := mustHeadlessRSAJWK(t)
	jwksBody, _ := json.Marshal(jwks)
	jwksClient := newJWKSHTTPClient(t, jwksBody)
	jwksURI := jwksURL()

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   jwksURI,
						"headless_jwks": map[string]any{
							"keys": []map[string]any{},
						},
					},
				},
			})
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
		default:
			http.NotFound(w, r)
		}
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil)

	h := &AuthHandler{
		IdpProvider:  mockIdp,
		KratosAdmin:  mockKratos,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient),
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessPasswordLoginTestApp(h)

	clientAssertion := mustHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		"http://example.com/api/v1/auth/headless/password/login",
	)
	body, _ := json.Marshal(map[string]string{
		"client_id":        "headless-login-client",
		"client_assertion": clientAssertion,
		"loginId":          "employee001",
		"password":         "password",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}
}

func TestHeadlessPasswordLogin_RefreshesJWKSWhenSignatureFailsForCachedKid(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "")

	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-identity-id",
	}, nil)

	stalePrivateKey, staleJWKS := mustHeadlessRSAJWK(t)
	staleRaw, err := json.Marshal(staleJWKS)
	if err != nil {
		t.Fatalf("failed to marshal stale jwks: %v", err)
	}

	freshPrivateKey, freshJWKS := mustHeadlessRSAJWK(t)
	freshRaw, err := json.Marshal(freshJWKS)
	if err != nil {
		t.Fatalf("failed to marshal fresh jwks: %v", err)
	}

	fetchCount := 0
	jwksClient := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
		fetchCount++
		return httpResponse(r, http.StatusOK, string(freshRaw)), nil
	})}
	jwksURI := jwksURL()

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   jwksURI,
					},
				},
			})
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
		default:
			http.NotFound(w, r)
		}
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil)

	redisRepo := &testRedisRepo{values: map[string]string{}}
	cacheService := service.NewHeadlessJWKSCacheService(redisRepo, jwksClient)
	now := time.Now()
	expiresAt := now.Add(30 * time.Minute)
	if err := cacheService.SaveState("headless-login-client", domain.HeadlessJWKSCacheState{
		ClientID:          "headless-login-client",
		JWKSURI:           jwksURI,
		RawJWKS:           string(staleRaw),
		CachedKids:        []string{"test-kid"},
		CachedAt:          &now,
		LastCheckedAt:     &now,
		ExpiresAt:         &expiresAt,
		LastRefreshStatus: "success",
	}); err != nil {
		t.Fatalf("failed to save cached jwks state: %v", err)
	}

	h := &AuthHandler{
		IdpProvider:  mockIdp,
		KratosAdmin:  mockKratos,
		RedisService: redisRepo,
		HeadlessJWKS: cacheService,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessPasswordLoginTestApp(h)

	clientAssertion := mustHeadlessClientAssertion(
		t,
		freshPrivateKey,
		"headless-login-client",
		"http://example.com/api/v1/auth/headless/password/login",
	)
	body, _ := json.Marshal(map[string]string{
		"client_id":        "headless-login-client",
		"client_assertion": clientAssertion,
		"loginId":          "employee001",
		"password":         "password",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}
	if fetchCount != 1 {
		t.Fatalf("expected exactly one jwks refresh fetch, got %d", fetchCount)
	}
	if stalePrivateKey == nil {
		t.Fatalf("expected stale key to be generated")
	}
}

func TestHeadlessPasswordLogin_MissingClientAssertionRejected(t *testing.T) {
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-identity-id",
	}, nil)
	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil)

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   "https://rp.example.com/.well-known/jwks.json",
					},
				},
			})
			return
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
			return
		}
		http.NotFound(w, r)
	})

	h := &AuthHandler{
		IdpProvider: mockIdp,
		KratosAdmin: mockKratos,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessPasswordLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"client_id":       "headless-login-client",
		"loginId":         "employee001",
		"password":        "password",
		"login_challenge": "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusBadRequest {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 400, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}
}

func TestHeadlessPasswordLogin_InvalidClientAssertionRejected(t *testing.T) {
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-identity-id",
	}, nil)
	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil)

	validKey, jwks := mustHeadlessRSAJWK(t)
	invalidKey, _ := mustHeadlessRSAJWK(t)
	_ = validKey
	jwksBody, _ := json.Marshal(jwks)
	jwksClient := newJWKSHTTPClient(t, jwksBody)
	jwksURI := jwksURL()

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   jwksURI,
					},
				},
			})
			return
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
			return
		}
		http.NotFound(w, r)
	})

	h := &AuthHandler{
		IdpProvider:  mockIdp,
		KratosAdmin:  mockKratos,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient),
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessPasswordLoginTestApp(h)

	clientAssertion := mustHeadlessClientAssertion(
		t,
		invalidKey,
		"headless-login-client",
		"http://example.com/api/v1/auth/headless/password/login",
	)
	body, _ := json.Marshal(map[string]string{
		"client_id":        "headless-login-client",
		"client_assertion": clientAssertion,
		"loginId":          "employee001",
		"password":         "password",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusUnauthorized {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 401, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["code"] != "invalid_client_assertion_signature" {
		t.Fatalf("expected code=invalid_client_assertion_signature, got=%v", got["code"])
	}
	if got["error"] != "Client assertion signature verification failed" {
		t.Fatalf("expected detailed signature error, got=%v", got["error"])
	}
}

func TestHeadlessPasswordLogin_AudienceMismatchReturnsDetailedCode(t *testing.T) {
	privateKey, jwks := mustHeadlessRSAJWK(t)
	clientAssertion := mustHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		"https://rp.example.com/oidc/token",
	)

	resp := runHeadlessPasswordLoginWithAssertion(t, jwks, clientAssertion)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusUnauthorized {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 401, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["code"] != "invalid_client_assertion_audience" {
		t.Fatalf("expected code=invalid_client_assertion_audience, got=%v", got["code"])
	}
	if got["error"] != "Client assertion audience mismatch" {
		t.Fatalf("expected audience mismatch error, got=%v", got["error"])
	}
}

func TestHeadlessPasswordLogin_AcceptsForwardedHTTPSAudience(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "")

	privateKey, jwks := mustHeadlessRSAJWK(t)
	clientAssertion := mustHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		"https://sso.hmac.kr/api/v1/auth/headless/password/login",
	)

	resp := runHeadlessPasswordLoginWithAssertionRequest(
		t,
		jwks,
		clientAssertion,
		"http://sso.hmac.kr/api/v1/auth/headless/password/login",
		map[string]string{
			"X-Forwarded-Proto": "https",
			"X-Forwarded-Host":  "sso.hmac.kr",
		},
	)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200 for forwarded https audience, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["redirectTo"] != "http://rp/cb" {
		t.Fatalf("expected redirectTo, got=%v", got["redirectTo"])
	}
}

func TestHeadlessPasswordLogin_AcceptsConfiguredPublicHTTPSAudience(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "https://sso.hmac.kr")

	privateKey, jwks := mustHeadlessRSAJWK(t)
	clientAssertion := mustHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		"https://sso.hmac.kr/api/v1/auth/headless/password/login",
	)

	resp := runHeadlessPasswordLoginWithAssertionRequest(
		t,
		jwks,
		clientAssertion,
		"http://sso.hmac.kr/api/v1/auth/headless/password/login",
		nil,
	)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200 for configured public https audience, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["redirectTo"] != "http://rp/cb" {
		t.Fatalf("expected redirectTo, got=%v", got["redirectTo"])
	}
}

func TestHeadlessPasswordLogin_RejectsHTTPAudienceWhenConfiguredPublicURLIsHTTPS(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "https://sso.hmac.kr")

	privateKey, jwks := mustHeadlessRSAJWK(t)
	clientAssertion := mustHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		"http://sso.hmac.kr/api/v1/auth/headless/password/login",
	)

	resp := runHeadlessPasswordLoginWithAssertionRequest(
		t,
		jwks,
		clientAssertion,
		"http://sso.hmac.kr/api/v1/auth/headless/password/login",
		nil,
	)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusUnauthorized {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 401 for mismatched http audience, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["code"] != "invalid_client_assertion_audience" {
		t.Fatalf("expected invalid_client_assertion_audience, got=%v", got["code"])
	}
}

func TestHeadlessPasswordLogin_IssSubMismatchReturnsDetailedCode(t *testing.T) {
	privateKey, jwks := mustHeadlessRSAJWK(t)
	now := time.Now()
	clientAssertion := mustHeadlessClientAssertionWithCustomClaims(
		t,
		privateKey,
		"headless-login-client",
		josejwt.Claims{
			Issuer:   "other-client",
			Subject:  "headless-login-client",
			Audience: josejwt.Audience{"http://example.com/api/v1/auth/headless/password/login"},
			Expiry:   josejwt.NewNumericDate(now.Add(5 * time.Minute)),
			IssuedAt: josejwt.NewNumericDate(now),
			NotBefore: josejwt.NewNumericDate(
				now.Add(-1 * time.Minute),
			),
			ID: "assertion-iss-mismatch",
		},
	)

	resp := runHeadlessPasswordLoginWithAssertion(t, jwks, clientAssertion)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusUnauthorized {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 401, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["code"] != "invalid_client_assertion_iss_sub" {
		t.Fatalf("expected code=invalid_client_assertion_iss_sub, got=%v", got["code"])
	}
	if got["error"] != "Client assertion issuer or subject mismatch" {
		t.Fatalf("expected iss/sub mismatch error, got=%v", got["error"])
	}
}

func TestHeadlessPasswordLogin_ExpiredAssertionReturnsDetailedCode(t *testing.T) {
	privateKey, jwks := mustHeadlessRSAJWK(t)
	now := time.Now()
	clientAssertion := mustHeadlessClientAssertionWithCustomClaims(
		t,
		privateKey,
		"headless-login-client",
		josejwt.Claims{
			Issuer:   "headless-login-client",
			Subject:  "headless-login-client",
			Audience: josejwt.Audience{"http://example.com/api/v1/auth/headless/password/login"},
			Expiry:   josejwt.NewNumericDate(now.Add(-1 * time.Minute)),
			IssuedAt: josejwt.NewNumericDate(now.Add(-10 * time.Minute)),
			NotBefore: josejwt.NewNumericDate(
				now.Add(-11 * time.Minute),
			),
			ID: "assertion-expired",
		},
	)

	resp := runHeadlessPasswordLoginWithAssertion(t, jwks, clientAssertion)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusUnauthorized {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 401, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["code"] != "invalid_client_assertion_expired" {
		t.Fatalf("expected code=invalid_client_assertion_expired, got=%v", got["code"])
	}
	if got["error"] != "Client assertion has expired" {
		t.Fatalf("expected expired assertion error, got=%v", got["error"])
	}
}

func TestHeadlessPasswordLogin_DebugLogIncludesDiagnostics(t *testing.T) {
	privateKey, jwks := mustHeadlessRSAJWK(t)
	clientAssertion := mustHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		"https://rp.example.com/oidc/token",
	)

	buf := &bytes.Buffer{}
	logger := slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}))

	resp := runHeadlessPasswordLoginWithAssertionAndLogger(t, jwks, clientAssertion, logger)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusUnauthorized {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 401, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	logOutput := buf.String()
	if !strings.Contains(logOutput, "expected_audiences") {
		t.Fatalf("expected debug log to include expected_audiences, got=%s", logOutput)
	}
	if !strings.Contains(logOutput, "received_audiences") {
		t.Fatalf("expected debug log to include received_audiences, got=%s", logOutput)
	}
	if !strings.Contains(logOutput, "invalid_client_assertion_audience") {
		t.Fatalf("expected debug log to include reason code, got=%s", logOutput)
	}
}

func TestHeadlessPasswordLogin_InfoLogOmitsDebugDiagnostics(t *testing.T) {
	privateKey, jwks := mustHeadlessRSAJWK(t)
	clientAssertion := mustHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		"https://rp.example.com/oidc/token",
	)

	buf := &bytes.Buffer{}
	logger := slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelInfo}))

	resp := runHeadlessPasswordLoginWithAssertionAndLogger(t, jwks, clientAssertion, logger)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusUnauthorized {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 401, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	logOutput := buf.String()
	if strings.Contains(logOutput, "expected_audiences") {
		t.Fatalf("expected info log to omit expected_audiences, got=%s", logOutput)
	}
	if strings.Contains(logOutput, "received_audiences") {
		t.Fatalf("expected info log to omit received_audiences, got=%s", logOutput)
	}
}

func TestHeadlessPasswordLogin_SuccessLogIncludesCorrelationFields(t *testing.T) {
	privateKey, jwks := mustHeadlessRSAJWK(t)
	clientAssertion := mustHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		"http://example.com/api/v1/auth/headless/password/login",
	)

	buf := &bytes.Buffer{}
	logger := slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelInfo}))

	resp := runHeadlessPasswordLoginWithAssertionAndLogger(t, jwks, clientAssertion, logger)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	logOutput := buf.String()
	for _, needle := range []string{
		"headless password login succeeded",
		`"req_id":"req-headless-test-123"`,
		`"path":"/api/v1/auth/headless/password/login"`,
		`"client_id":"headless-login-client"`,
		`"login_challenge_prefix":"challenge-12"`,
		`"response_status":200`,
	} {
		if !strings.Contains(logOutput, needle) {
			t.Fatalf("expected success log to include %s, got=%s", needle, logOutput)
		}
	}
}

func TestHeadlessPasswordLogin_AcceptsConfiguredClientAssertionAlgorithms(t *testing.T) {
	algorithms := []jose.SignatureAlgorithm{
		jose.RS256,
		jose.RS384,
		jose.RS512,
		jose.PS256,
		jose.PS384,
		jose.PS512,
		jose.ES256,
		jose.ES384,
		jose.ES512,
		jose.EdDSA,
	}

	for _, algorithm := range algorithms {
		t.Run(string(algorithm), func(t *testing.T) {
			privateKey, jwks := mustHeadlessJWKForAlgorithm(t, algorithm)
			clientAssertion := mustHeadlessClientAssertionWithAlgorithm(
				t,
				privateKey,
				algorithm,
				"headless-login-client",
				"http://example.com/api/v1/auth/headless/password/login",
			)

			resp := runHeadlessPasswordLoginWithAssertion(t, jwks, clientAssertion)
			defer resp.Body.Close()

			if resp.StatusCode != http.StatusOK {
				bodyBytes, _ := io.ReadAll(resp.Body)
				t.Fatalf("expected 200 for %s, got %d, body: %s", algorithm, resp.StatusCode, string(bodyBytes))
			}
		})
	}
}

func TestHeadlessPasswordLogin_HeadlessDisabledRejected(t *testing.T) {
	mockIdp := new(MockIdentityProvider)

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet {
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_jwks_uri":                   "https://rp.example.com/.well-known/jwks.json",
						"headless_token_endpoint_auth_method": "private_key_jwt",
					},
				},
			})
			return
		}
		http.NotFound(w, r)
	})

	h := &AuthHandler{
		IdpProvider: mockIdp,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessPasswordLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"client_id":       "headless-login-client",
		"loginId":         "employee001",
		"password":        "password",
		"login_challenge": "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusForbidden {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 403, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}
}

func TestHeadlessPasswordLogin_ClientIDMismatchRejected(t *testing.T) {
	mockIdp := new(MockIdentityProvider)

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet {
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:                "other-rp",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]any{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   "https://rp.example.com/.well-known/jwks.json",
					},
				},
			})
			return
		}
		http.NotFound(w, r)
	})

	h := &AuthHandler{
		IdpProvider: mockIdp,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessPasswordLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"client_id":       "headless-login-client",
		"loginId":         "employee001",
		"password":        "password",
		"login_challenge": "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusForbidden {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 403, got %d, body: %s", resp.StatusCode, string(bodyBytes))
	}
}

func TestPasswordLogin_OIDC_InactiveClient(t *testing.T) {
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-identity-id",
	}, nil)

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet {
			json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Client: domain.HydraClient{ClientID: "client-inactive", Metadata: map[string]any{"status": "inactive"}},
			})
			return
		}
		http.NotFound(w, r)
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil)

	h := &AuthHandler{
		IdpProvider: mockIdp,
		KratosAdmin: mockKratos,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newAuthLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"loginId":         "user@example.com",
		"password":        "password",
		"login_challenge": "challenge-inactive",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	// Should be Forbidden (403)
	if resp.StatusCode != http.StatusForbidden {
		t.Errorf("expected 403 Forbidden, got %d", resp.StatusCode)
	}
}

func TestPasswordLogin_NoOIDC_Success(t *testing.T) {
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-identity-id",
	}, nil)

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil)

	h := &AuthHandler{
		IdpProvider: mockIdp,
		KratosAdmin: mockKratos,
		Hydra:       service.NewHydraAdminService(),
	}

	app := newAuthLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"loginId":  "user@example.com",
		"password": "password",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Errorf("expected 200, got %d", resp.StatusCode)
	}
	var got map[string]string
	json.NewDecoder(resp.Body).Decode(&got)
	if got["sessionJwt"] != "valid-jwt" {
		t.Errorf("expected jwt valid-jwt, got %s", got["sessionJwt"])
	}
	// No redirectTo
	if _, ok := got["redirectTo"]; ok {
		t.Errorf("expected no redirectTo, got %s", got["redirectTo"])
	}
}

func TestPasswordLogin_SharedBrowserSameSubjectAllowed(t *testing.T) {
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "new-session-id"},
		Subject:      "kratos-user-1",
	}, nil)

	t.Setenv("KRATOS_PUBLIC_URL", newKratosWhoamiTestServer(t, "kratos-user-1"))

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-user-1", nil)

	h := &AuthHandler{
		IdpProvider: mockIdp,
		KratosAdmin: mockKratos,
	}
	app := newAuthLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"loginId":  "user@example.com",
		"password": "password",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Cookie", "ory_kratos_session=shared-browser-session")

	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	require.Equal(t, http.StatusOK, resp.StatusCode)
	var got map[string]any
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
	require.Equal(t, "valid-jwt", got["sessionJwt"])
	mockIdp.AssertExpectations(t)
	mockKratos.AssertExpectations(t)
}

func TestPasswordLogin_SharedBrowserDifferentSubjectConflicts(t *testing.T) {
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "new-session-id"},
		Subject:      "kratos-user-1",
	}, nil)

	t.Setenv("KRATOS_PUBLIC_URL", newKratosWhoamiTestServer(t, "kratos-other-user"))

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-user-1", nil)

	h := &AuthHandler{
		IdpProvider: mockIdp,
		KratosAdmin: mockKratos,
	}
	app := newAuthLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"loginId":  "user@example.com",
		"password": "password",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Cookie", "ory_kratos_session=shared-browser-session")

	resp, err := app.Test(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	require.Equal(t, http.StatusConflict, resp.StatusCode)
	var got map[string]any
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
	require.Equal(t, "session_subject_conflict", got["code"])
	require.Empty(t, resp.Cookies())
	mockIdp.AssertExpectations(t)
	mockKratos.AssertExpectations(t)
}

func TestPasswordLogin_ArchivedUserRejected(t *testing.T) {
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "archived@example.com", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "archived-jwt"},
		Subject:      "archived-user-id",
	}, nil)

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "archived@example.com").Return("archived-user-id", nil)

	h := &AuthHandler{
		IdpProvider: mockIdp,
		KratosAdmin: mockKratos,
		Hydra:       service.NewHydraAdminService(),
		UserRepo: &passwordLoginUserRepo{usersByID: map[string]domain.User{
			"archived-user-id": {
				ID:     "archived-user-id",
				Email:  "archived@example.com",
				Name:   "Archived User",
				Status: domain.UserStatusArchived,
			},
		}},
	}

	app := newAuthLoginTestApp(h)
	body, _ := json.Marshal(map[string]string{
		"loginId":  "archived@example.com",
		"password": "password",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusForbidden {
		t.Fatalf("expected 403, got %d", resp.StatusCode)
	}
}

func TestEnsureUserActivityAllowedByStatus(t *testing.T) {
	tests := []struct {
		name    string
		status  string
		wantErr bool
	}{
		{name: "active allowed", status: domain.UserStatusActive},
		{name: "temporary leave allowed", status: domain.UserStatusTemporaryLeave},
		{name: "baron guest allowed", status: domain.UserStatusBaronGuest},
		{name: "suspended rejected", status: domain.UserStatusSuspended, wantErr: true},
		{name: "preboarding rejected", status: domain.UserStatusPreboarding, wantErr: true},
		{name: "extended leave rejected", status: domain.UserStatusExtendedLeave, wantErr: true},
		{name: "archived rejected", status: domain.UserStatusArchived, wantErr: true},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			h := &AuthHandler{
				UserRepo: &passwordLoginUserRepo{usersByID: map[string]domain.User{
					"user-id": {
						ID:     "user-id",
						Email:  "user@example.com",
						Name:   "User",
						Status: tc.status,
					},
				}},
			}

			err := h.ensureUserActivityAllowed(context.Background(), "user-id")

			if tc.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
		})
	}
}

func TestPasswordLogin_InvalidCredentials_ReturnsCode(t *testing.T) {
	mockIdp := new(MockIdentityProvider)
	mockIdp.On("SignIn", "user@example.com", "wrong-password").Return(nil, errors.New("비밀번호가 일치하지 않습니다"))

	h := &AuthHandler{
		IdpProvider: mockIdp,
		KratosAdmin: service.NewKratosAdminService(),
		Hydra:       service.NewHydraAdminService(),
	}

	app := newAuthLoginTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"loginId":  "user@example.com",
		"password": "wrong-password",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusUnauthorized {
		t.Fatalf("expected 401, got %d", resp.StatusCode)
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["code"] != "password_or_email_mismatch" {
		t.Fatalf("expected code=password_or_email_mismatch, got=%v", got["code"])
	}
	if got["error"] != "Invalid credentials" {
		t.Fatalf("expected error=Invalid credentials, got=%v", got["error"])
	}
}

func (m *MockKratosAdminService) CreateUser(ctx context.Context, user *domain.BrokerUser, password string) (string, error) {
	return "", nil
}
