package handler

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/service"
	"bytes"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gofiber/fiber/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

// Mock services
type mockEmailService struct {
	lastTo      string
	lastSubject string
	lastBody    string
}

func (m *mockEmailService) SendEmail(to, subject, body string) error {
	m.lastTo = to
	m.lastSubject = subject
	m.lastBody = body
	return nil
}

type mockSmsService struct {
	lastTo      string
	lastContent string
}

func (m *mockSmsService) SendSms(to, content string) error {
	m.lastTo = to
	m.lastContent = content
	return nil
}

func newHeadlessLinkTestApp(h *AuthHandler) *fiber.App {
	app := fiber.New()
	app.Post("/api/v1/auth/headless/link/init", h.HeadlessLinkInit)
	app.Post("/api/v1/auth/headless/link/poll", h.HeadlessLinkPoll)
	app.Post("/api/v1/auth/magic-link/verify", h.VerifyMagicLink)
	return app
}

func newKratosWhoamiTestServer(t *testing.T, identityID string) string {
	t.Helper()
	origDefaultClient := http.DefaultClient
	http.DefaultClient = &http.Client{
		Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
			if r.URL.Path != "/sessions/whoami" {
				return httpResponse(r, http.StatusNotFound, "not found"), nil
			}
			if r.Header.Get("Cookie") == "" && r.Header.Get("X-Session-Token") == "" {
				return httpResponse(r, http.StatusUnauthorized, "missing session"), nil
			}
			body, err := json.Marshal(map[string]any{
				"id":               "session-123",
				"authenticated_at": "2026-05-21T00:00:00Z",
				"identity": map[string]any{
					"id": identityID,
					"traits": map[string]any{
						"email": "user@example.com",
					},
				},
			})
			if err != nil {
				return nil, err
			}
			return httpResponse(r, http.StatusOK, string(body)), nil
		}),
	}
	t.Cleanup(func() {
		http.DefaultClient = origDefaultClient
	})
	return "http://kratos.test"
}

func TestEnchantedLinkFlow_Email_Success(t *testing.T) {
	redis := &mockRedisRepo{data: make(map[string]string)}
	// Force "Not Supported" for InitiateLinkLogin only to trigger custom Enchanted Link logic
	idp := &mockIdpProvider{
		userExists:      true,
		initiateLinkErr: domain.ErrNotSupported,
	}

	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  idp,
		EmailService: &mockEmailService{},
		SmsService:   &mockSmsService{},
	}
	app := fiber.New()
	app.Post("/api/v1/auth/enchanted-link/init", h.InitEnchantedLink)
	app.Post("/api/v1/auth/enchanted-link/poll", h.PollEnchantedLink)
	app.Post("/api/v1/auth/magic-link/verify", h.VerifyMagicLink)

	t.Setenv("USERFRONT_URL", "http://userfront.test")

	// 1. Init Enchanted Link (Email)
	body, _ := json.Marshal(map[string]string{
		"loginId": "user@example.com",
		"method":  "email",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/enchanted-link/init", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var initResp map[string]any
	json.NewDecoder(resp.Body).Decode(&initResp)
	pendingRef := initResp["pendingRef"].(string)
	assert.NotEmpty(t, pendingRef)

	// Find the token key "enchanted_token:..." in mock redis
	var token string
	for k := range redis.data {
		if len(k) > 16 && k[:16] == "enchanted_token:" {
			token = k[16:]
			break
		}
	}
	assert.NotEmpty(t, token)

	// 2. Verify Magic Link
	verifyBody, _ := json.Marshal(map[string]any{
		"token":      token,
		"verifyOnly": true,
	})
	req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/magic-link/verify", bytes.NewReader(verifyBody))
	req.Header.Set("Content-Type", "application/json")
	resp, _ = app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	// 3. Poll (Success)
	pollBody, _ := json.Marshal(map[string]string{"pendingRef": pendingRef})
	req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/enchanted-link/poll", bytes.NewReader(pollBody))
	req.Header.Set("Content-Type", "application/json")
	resp, _ = app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var pollResp map[string]any
	json.NewDecoder(resp.Body).Decode(&pollResp)
	assert.Equal(t, "ok", pollResp["status"])
	assert.Equal(t, "valid-jwt", pollResp["sessionJwt"])
}

func TestEnchantedLinkFlow_Sms_Success(t *testing.T) {
	redis := &mockRedisRepo{data: make(map[string]string)}
	idp := &mockIdpProvider{
		userExists:      true,
		initiateLinkErr: domain.ErrNotSupported,
	}

	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  idp,
		SmsService:   &mockSmsService{},
	}
	app := fiber.New()
	app.Post("/api/v1/auth/enchanted-link/init", h.InitEnchantedLink)

	// 1. Init Enchanted Link (SMS)
	body, _ := json.Marshal(map[string]string{
		"loginId": "010-1234-5678",
		"method":  "sms",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/enchanted-link/init", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var initResp map[string]any
	json.NewDecoder(resp.Body).Decode(&initResp)
	assert.NotEmpty(t, initResp["userCode"])
}

func TestVerifyMagicLink_VerifyOnlyWithoutSharedBrowserSessionApprovesOnly(t *testing.T) {
	redis := &mockRedisRepo{data: map[string]string{
		prefixToken + "token-123": `{"pendingRef":"pending-123","loginId":"user@example.com"}`,
	}}
	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  &mockIdpProvider{},
	}
	app := fiber.New()
	app.Post("/api/v1/auth/magic-link/verify", h.VerifyMagicLink)

	body, _ := json.Marshal(map[string]any{
		"token":      "token-123",
		"verifyOnly": true,
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/magic-link/verify", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Empty(t, resp.Cookies())

	var got map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&got)
	assert.Equal(t, "approved", got["status"])
	assert.Nil(t, got["sessionJwt"])
	assert.Nil(t, got["token"])
}

func TestVerifyMagicLink_VerifyOnlySharedBrowserSameSubjectApprovesOnly(t *testing.T) {
	redis := &mockRedisRepo{data: map[string]string{
		prefixToken + "token-123": `{"pendingRef":"pending-123","loginId":"user@example.com"}`,
	}}
	t.Setenv("KRATOS_PUBLIC_URL", newKratosWhoamiTestServer(t, "kratos-user-1"))

	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  &mockIdpProvider{},
	}
	app := fiber.New()
	app.Post("/api/v1/auth/magic-link/verify", h.VerifyMagicLink)

	body, _ := json.Marshal(map[string]any{
		"token":      "token-123",
		"verifyOnly": true,
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/magic-link/verify", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Cookie", "ory_kratos_session=shared-browser-session")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Empty(t, resp.Cookies())

	var got map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&got)
	assert.Equal(t, "approved", got["status"])
	assert.Nil(t, got["sessionJwt"])
	assert.Nil(t, got["token"])
}

func TestVerifyMagicLink_VerifyOnlySharedBrowserDifferentSubjectApprovesOnly(t *testing.T) {
	redis := &mockRedisRepo{data: map[string]string{
		prefixToken + "token-123": `{"pendingRef":"pending-123","loginId":"user@example.com"}`,
	}}
	t.Setenv("KRATOS_PUBLIC_URL", newKratosWhoamiTestServer(t, "kratos-other-user"))

	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  &mockIdpProvider{},
	}
	app := fiber.New()
	app.Post("/api/v1/auth/magic-link/verify", h.VerifyMagicLink)

	body, _ := json.Marshal(map[string]any{
		"token":      "token-123",
		"verifyOnly": true,
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/magic-link/verify", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Cookie", "ory_kratos_session=shared-browser-session")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Empty(t, resp.Cookies())

	var got map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&got)
	assert.Equal(t, "approved", got["status"])
	assert.Nil(t, got["sessionJwt"])
	assert.Nil(t, got["token"])
	assert.Contains(t, redis.data[prefixSession+"pending-123"], "approved")
}

func TestResolveUserfrontURL_DevLocalhostUsesConfiguredPort(t *testing.T) {
	t.Setenv("APP_ENV", "dev")
	t.Setenv("USERFRONT_URL", "http://localhost:5000")

	h := &AuthHandler{}
	app := fiber.New()
	app.Get("/probe", func(c *fiber.Ctx) error {
		return c.SendString(h.resolveUserfrontURL(c))
	})

	req := httptest.NewRequest(http.MethodGet, "http://localhost/probe", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	body, _ := io.ReadAll(resp.Body)
	assert.Equal(t, "http://localhost:5000", string(body))
}

func TestVerifyLoginCode_VerifyOnlySharedBrowserDifferentSubjectApprovesOnly(t *testing.T) {
	redis := &mockRedisRepo{data: map[string]string{
		prefixLoginCode + "user@example.com":        "flow-123",
		prefixLoginCodePending + "user@example.com": "pending-123",
		prefixLoginCodeValue + "pending-123":        "569765",
	}}
	t.Setenv("KRATOS_PUBLIC_URL", newKratosWhoamiTestServer(t, "kratos-other-user"))

	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  &mockIdpProvider{},
	}
	app := fiber.New()
	app.Post("/api/v1/auth/login/code/verify", h.VerifyLoginCode)

	body, _ := json.Marshal(map[string]any{
		"loginId":    "user@example.com",
		"code":       "569765",
		"pendingRef": "pending-123",
		"verifyOnly": true,
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Cookie", "ory_kratos_session=shared-browser-session")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Empty(t, resp.Cookies())

	var got map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&got)
	assert.Equal(t, "approved", got["status"])
	assert.Nil(t, got["sessionJwt"])
	assert.Nil(t, got["token"])
	assert.Contains(t, redis.data[prefixSession+"pending-123"], "approved")
}

func TestVerifyLoginCode_MapsSmsPhoneBeforeFlowLookup(t *testing.T) {
	redis := &mockRedisRepo{data: map[string]string{
		prefixLoginCode + "su-@samaneng.com":          "flow-123",
		prefixLoginCodePending + "su-@samaneng.com":   "pending-123",
		prefixLoginCodeSmsLookup + "+821041585840":    "su-@samaneng.com",
		prefixLoginCodeSmsTarget + "su-@samaneng.com": "+821041585840",
		prefixLoginCodeValue + "pending-123":          "569765",
	}}
	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  &mockIdpProvider{},
	}
	app := fiber.New()
	app.Post("/api/v1/auth/login/code/verify", h.VerifyLoginCode)

	body, _ := json.Marshal(map[string]any{
		"loginId":    "01041585840",
		"code":       "569765",
		"pendingRef": "pending-123",
		"verifyOnly": true,
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var got map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&got)
	assert.Equal(t, "approved", got["status"])
	assert.Equal(t, "pending-123", got["pendingRef"])
}

func TestPollEnchantedLink_ExpiredToken_ReturnsCode(t *testing.T) {
	redis := &mockRedisRepo{data: make(map[string]string)}
	h := &AuthHandler{
		RedisService: redis,
	}
	app := fiber.New()
	app.Post("/api/v1/auth/enchanted-link/poll", h.PollEnchantedLink)

	body, _ := json.Marshal(map[string]string{
		"pendingRef": "missing-ref",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/enchanted-link/poll", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var got map[string]any
	json.NewDecoder(resp.Body).Decode(&got)
	assert.Equal(t, "expired_token", got["error"])
	assert.Equal(t, "expired_token", got["code"])
}

func TestPollEnchantedLink_SharedBrowserSameSubjectIssuesSession(t *testing.T) {
	redis := &mockRedisRepo{data: map[string]string{
		prefixSession + "pending-123": `{"status":"approved","loginId":"user@example.com"}`,
	}}
	t.Setenv("KRATOS_PUBLIC_URL", newKratosWhoamiTestServer(t, "kratos-user-1"))

	h := &AuthHandler{
		RedisService: redis,
		IdpProvider: &mockIdpProvider{
			issueSession: &domain.AuthInfo{
				SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "new-session-id"},
				Subject:      "kratos-user-1",
			},
		},
	}
	app := fiber.New()
	app.Post("/api/v1/auth/enchanted-link/poll", h.PollEnchantedLink)

	body, _ := json.Marshal(map[string]string{"pendingRef": "pending-123"})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/enchanted-link/poll", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Cookie", "ory_kratos_session=shared-browser-session")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var got map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&got)
	assert.Equal(t, "ok", got["status"])
	assert.Equal(t, "valid-jwt", got["sessionJwt"])
}

func TestPollEnchantedLink_SharedBrowserDifferentSubjectConflicts(t *testing.T) {
	redis := &mockRedisRepo{data: map[string]string{
		prefixSession + "pending-123": `{"status":"approved","loginId":"user@example.com"}`,
	}}
	t.Setenv("KRATOS_PUBLIC_URL", newKratosWhoamiTestServer(t, "kratos-other-user"))

	h := &AuthHandler{
		RedisService: redis,
		IdpProvider: &mockIdpProvider{
			issueSession: &domain.AuthInfo{
				SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "new-session-id"},
				Subject:      "kratos-user-1",
			},
		},
	}
	app := fiber.New()
	app.Post("/api/v1/auth/enchanted-link/poll", h.PollEnchantedLink)

	body, _ := json.Marshal(map[string]string{"pendingRef": "pending-123"})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/enchanted-link/poll", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Cookie", "ory_kratos_session=shared-browser-session")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusConflict, resp.StatusCode)
	var got map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&got)
	assert.Equal(t, "session_subject_conflict", got["code"])
	assert.NotContains(t, redis.data[prefixSession+"pending-123"], "valid-jwt")
}

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

	redis := &mockRedisRepo{data: make(map[string]string)}
	privateKey, jwks := mustHeadlessRSAJWK(t)
	jwksBody, _ := json.Marshal(jwks)
	jwksClient := newJWKSHTTPClient(t, jwksBody)
	jwksURI := jwksURL()

	idp := &mockIdpProvider{
		userExists:      true,
		initiateLinkErr: domain.ErrNotSupported,
	}

	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_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   jwksURI,
					},
				},
			})
			return
		}
		http.NotFound(w, r)
	})

	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  idp,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient),
		SmsService:   &mockSmsService{},
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessLinkTestApp(h)
	t.Setenv("USERFRONT_URL", "http://userfront.test")

	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/link/init"),
		"loginId":          "010-1234-5678",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/link/init", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var got map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&got)
	assert.NotEmpty(t, got["pendingRef"])
	_, hasUserCode := got["userCode"]
	assert.False(t, hasUserCode)
}

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

	redis := &mockRedisRepo{data: make(map[string]string)}
	privateKey, jwks := mustHeadlessRSAJWK(t)
	jwksBody, _ := json.Marshal(jwks)

	idp := &mockIdpProvider{
		userExists:      true,
		initiateLinkErr: domain.ErrNotSupported,
	}

	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:              "local-demo-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
		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)
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "+821012345678").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{
		RedisService: redis,
		IdpProvider:  idp,
		SmsService:   &mockSmsService{},
		KratosAdmin:  mockKratos,
		AuditRepo:    auditRepo,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, headlessClient),
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessLinkTestApp(h)
	t.Setenv("USERFRONT_URL", "http://userfront.test")

	initBody, _ := 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/link/init"),
		"loginId":          "010-1234-5678",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/link/init", bytes.NewReader(initBody))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var initResp map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&initResp)
	pendingRef := initResp["pendingRef"].(string)
	assert.NotEmpty(t, pendingRef)

	var token string
	for k := range redis.data {
		if len(k) > 16 && k[:16] == "enchanted_token:" {
			token = k[16:]
			break
		}
	}
	assert.NotEmpty(t, token)

	verifyBody, _ := json.Marshal(map[string]any{
		"token":      token,
		"verifyOnly": true,
	})
	req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/magic-link/verify", bytes.NewReader(verifyBody))
	req.Header.Set("Content-Type", "application/json")
	resp, _ = app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	pollBody, _ := 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/link/poll"),
		"pendingRef":       pendingRef,
	})
	req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/link/poll", bytes.NewReader(pollBody))
	req.Header.Set("Content-Type", "application/json")
	resp, _ = app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var pollResp map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&pollResp)
	assert.Equal(t, "http://rp/cb", pollResp["redirectTo"])
	assert.Equal(t, "ok", pollResp["status"])
	assert.Nil(t, pollResp["sessionJwt"])
	assert.Nil(t, pollResp["token"])
	assert.Empty(t, resp.Cookies())
	if assert.Len(t, auditRepo.logs, 1) {
		assert.Contains(t, auditRepo.logs[0].EventType, "/api/v1/auth/")
		details, err := parseAuditDetails(auditRepo.logs[0].Details)
		if err != nil {
			t.Fatalf("failed to parse audit details: %v", err)
		}
		assert.Equal(t, "headless-login-client", details["client_id"])
		assert.Equal(t, "local-demo-rp", details["client_name"])
		assert.Equal(t, "challenge-123", details["login_challenge"])
	}
}

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

	redis := &mockRedisRepo{data: make(map[string]string)}
	privateKey, jwks := mustHeadlessRSAJWK(t)
	jwksBody, _ := json.Marshal(jwks)
	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",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					ClientName:              "local-demo-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
		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"})
			return
		}
		http.NotFound(w, r)
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "+821012345678").Return("kratos-target-b", 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{
		RedisService: redis,
		IdpProvider: &mockIdpProvider{
			userExists:      true,
			initiateLinkErr: domain.ErrNotSupported,
		},
		SmsService:   &mockSmsService{},
		KratosAdmin:  mockKratos,
		AuditRepo:    auditRepo,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, headlessClient),
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessLinkTestApp(h)
	t.Setenv("USERFRONT_URL", "http://userfront.test")

	initBody, _ := 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/link/init"),
		"loginId":          "010-1234-5678",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/link/init", bytes.NewReader(initBody))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var initResp map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&initResp)
	pendingRef := initResp["pendingRef"].(string)
	assert.NotEmpty(t, pendingRef)

	var token string
	for k := range redis.data {
		if len(k) > 16 && k[:16] == "enchanted_token:" {
			token = k[16:]
			break
		}
	}
	assert.NotEmpty(t, token)

	t.Setenv("KRATOS_PUBLIC_URL", newKratosWhoamiTestServer(t, "kratos-userfront-a"))

	verifyBody, _ := json.Marshal(map[string]any{
		"token":      token,
		"verifyOnly": true,
	})
	req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/magic-link/verify", bytes.NewReader(verifyBody))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Cookie", "ory_kratos_session=userfront-a-session")
	resp, _ = app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	pollBody, _ := 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/link/poll"),
		"pendingRef":       pendingRef,
	})
	req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/link/poll", bytes.NewReader(pollBody))
	req.Header.Set("Content-Type", "application/json")
	resp, _ = app.Test(req, -1)

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

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

	redis := &mockRedisRepo{data: make(map[string]string)}
	privateKey, jwks := mustHeadlessRSAJWK(t)
	jwksBody, _ := json.Marshal(jwks)
	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",
				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:
			acceptCalled = true
			_ = json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
			return
		}
		http.NotFound(w, r)
	})

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "+821012345678").Return("kratos-target-b", nil)
	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{
		RedisService: redis,
		IdpProvider: &mockIdpProvider{
			userExists:      true,
			initiateLinkErr: domain.ErrNotSupported,
		},
		SmsService:   &mockSmsService{},
		KratosAdmin:  mockKratos,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, headlessClient),
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
		},
	}

	app := newHeadlessLinkTestApp(h)
	t.Setenv("USERFRONT_URL", "http://userfront.test")

	initBody, _ := 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/link/init"),
		"loginId":          "010-1234-5678",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/link/init", bytes.NewReader(initBody))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var initResp map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&initResp)
	pendingRef := initResp["pendingRef"].(string)
	assert.NotEmpty(t, pendingRef)

	var token string
	for k := range redis.data {
		if len(k) > 16 && k[:16] == "enchanted_token:" {
			token = k[16:]
			break
		}
	}
	assert.NotEmpty(t, token)

	verifyBody, _ := json.Marshal(map[string]any{
		"token":      token,
		"verifyOnly": true,
	})
	req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/magic-link/verify", bytes.NewReader(verifyBody))
	req.Header.Set("Content-Type", "application/json")
	resp, _ = app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	t.Setenv("KRATOS_PUBLIC_URL", newKratosWhoamiTestServer(t, "kratos-userfront-a"))

	pollBody, _ := 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/link/poll"),
		"pendingRef":       pendingRef,
	})
	req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/link/poll", bytes.NewReader(pollBody))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Cookie", "ory_kratos_session=userfront-a-session")
	resp, _ = app.Test(req, -1)

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