package handler

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/service"
	"bytes"
	"context"
	"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"
)

func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) {
	traits := map[string]any{
		"email":     "user@baron.com",
		"name":      "홍길동",
		"tenant_id": "primary-tenant-999", // Added primary tenant
		"tenant-1": map[string]any{
			"department": "개발팀",
			"grade":      "선임",
		},
		"tenant-2": map[string]any{
			"department": "재무팀",
			"grade":      "팀장",
		},
	}
	scopes := []string{"openid", "profile"}

	t.Run("No tenantID", func(t *testing.T) {
		claims := buildOidcClaimsFromTraits(traits, scopes, "")
		assert.Equal(t, "user@baron.com", claims["email"])
		assert.Equal(t, "홍길동", claims["name"])
		assert.Equal(t, "primary-tenant-999", claims["tenant_id"])
		assert.Nil(t, claims["department"])
		assert.Nil(t, claims["grade"])

		assert.Nil(t, claims["tenants"])
		assert.Contains(t, claims["joined_tenants"], "tenant-1")
		assert.Contains(t, claims["joined_tenants"], "tenant-2")
		assert.Contains(t, claims["joined_tenants"], "primary-tenant-999") // Should contain primary
	})

	t.Run("With tenant-1", func(t *testing.T) {
		claims := buildOidcClaimsFromTraits(traits, scopes, "tenant-1")
		assert.Equal(t, "user@baron.com", claims["email"])
		assert.Equal(t, "홍길동", claims["name"])
		assert.Equal(t, "tenant-1", claims["tenant_id"])
		assert.Nil(t, claims["department"])
		assert.Nil(t, claims["grade"])

		assert.Nil(t, claims["tenants"])
		assert.Contains(t, claims["joined_tenants"], "tenant-1")
		assert.Contains(t, claims["joined_tenants"], "tenant-2")
		assert.Contains(t, claims["joined_tenants"], "primary-tenant-999")
	})

	t.Run("With tenant-2", func(t *testing.T) {
		claims := buildOidcClaimsFromTraits(traits, scopes, "tenant-2")
		assert.Equal(t, "user@baron.com", claims["email"])
		assert.Equal(t, "홍길동", claims["name"])
		assert.Equal(t, "tenant-2", claims["tenant_id"])
		assert.Nil(t, claims["department"])
		assert.Nil(t, claims["grade"])

		assert.Nil(t, claims["tenants"])
		assert.Contains(t, claims["joined_tenants"], "primary-tenant-999")
	})

	t.Run("With non-existent tenant", func(t *testing.T) {
		claims := buildOidcClaimsFromTraits(traits, scopes, "tenant-3")
		assert.Equal(t, "user@baron.com", claims["email"])
		assert.Equal(t, "홍길동", claims["name"])
		assert.Equal(t, "tenant-3", claims["tenant_id"])
		assert.Nil(t, claims["department"])
		assert.Nil(t, claims["grade"])

		assert.Nil(t, claims["tenants"])
		assert.Contains(t, claims["joined_tenants"], "tenant-1")
		assert.Contains(t, claims["joined_tenants"], "primary-tenant-999")
	})

	t.Run("Tenant scope includes detailed tenant metadata", func(t *testing.T) {
		claims := buildOidcClaimsFromTraits(traits, []string{"openid", "profile", "tenant"}, "tenant-1")
		assert.Equal(t, "tenant-1", claims["tenant_id"])
		assert.Equal(t, "개발팀", claims["department"])
		assert.Equal(t, "선임", claims["grade"])
		assert.NotNil(t, claims["tenants"])
		assert.Contains(t, claims["joined_tenants"], "tenant-1")
		assert.Contains(t, claims["joined_tenants"], "tenant-2")
		assert.Contains(t, claims["joined_tenants"], "primary-tenant-999")
	})
}

func TestRepresentativeTenantIDFromTraits(t *testing.T) {
	t.Run("explicit tenant_id wins", func(t *testing.T) {
		traits := map[string]any{
			"tenant_id": "01970f0a-5c28-74d8-a73a-f6e9e9a7b210",
			"additionalAppointments": []any{
				map[string]any{"tenantId": "01970f0b-3448-7bb8-bdc7-16b6a1d2e661", "isPrimary": true},
			},
		}
		assert.Equal(t, "01970f0a-5c28-74d8-a73a-f6e9e9a7b210", representativeTenantIDFromTraits(traits))
	})

	t.Run("primary appointment wins when tenant_id is absent", func(t *testing.T) {
		traits := map[string]any{
			"additionalAppointments": []any{
				map[string]any{"tenantId": "01970f0b-3448-7bb8-bdc7-16b6a1d2e661"},
				map[string]any{"tenantId": "01970f0c-8c44-7069-9f20-7d28c0b8e630", "representative": true},
			},
		}
		assert.Equal(t, "01970f0c-8c44-7069-9f20-7d28c0b8e630", representativeTenantIDFromTraits(traits))
	})

	t.Run("first appointment is fallback", func(t *testing.T) {
		traits := map[string]any{
			"additionalAppointments": []any{
				map[string]any{"tenantId": "01970f0b-3448-7bb8-bdc7-16b6a1d2e661"},
				map[string]any{"tenantId": "01970f0c-8c44-7069-9f20-7d28c0b8e630"},
			},
		}
		assert.Equal(t, "01970f0b-3448-7bb8-bdc7-16b6a1d2e661", representativeTenantIDFromTraits(traits))
	})
}

func TestAcceptConsentRequest_DynamicClaims(t *testing.T) {
	var capturedClaims map[string]any

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		// Hydra: Get Consent Request
		if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-dynamic" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"challenge":       "challenge-dynamic",
				"requested_scope": []string{"openid", "profile", "tenant"},
				"subject":         "user-123",
				"client": map[string]any{
					"client_id": "client-app",
					"metadata": map[string]any{
						"tenant_id": "tenant-abc",
					},
				},
			}), nil
		}
		// Kratos: Get Identity
		if r.URL.Path == "/admin/identities/user-123" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"id": "user-123",
				"traits": map[string]any{
					"email": "user@test.com",
					"name":  "Test User",
					"tenant-abc": map[string]any{
						"department": "Innovation",
						"position":   "Architect",
					},
				},
			}), nil
		}
		// Hydra: Accept Consent Request
		if r.URL.Path == "/oauth2/auth/requests/consent/accept" && r.URL.Query().Get("consent_challenge") == "challenge-dynamic" {
			// Capture the claims sent to Hydra
			body, _ := io.ReadAll(r.Body)
			var acceptReq map[string]any
			json.Unmarshal(body, &acceptReq)
			if session, ok := acceptReq["session"].(map[string]any); ok {
				capturedClaims = session["id_token"].(map[string]any)
			}

			return httpJSONAny(r, http.StatusOK, map[string]any{
				"redirect_to": "http://rp/cb",
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})

	client := &http.Client{Transport: transport}
	origDefault := http.DefaultClient
	http.DefaultClient = client
	defer func() { http.DefaultClient = origDefault }()

	h := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
		KratosAdmin: new(MockKratosAdminService),
	}
	h.KratosAdmin.(*MockKratosAdminService).On("GetIdentity", mock.Anything, "user-123").Return(&service.KratosIdentity{
		ID: "user-123",
		Traits: map[string]any{
			"email": "user@test.com",
			"name":  "Test User",
			"tenant-abc": map[string]any{
				"department": "Innovation",
				"position":   "Architect",
			},
		},
	}, nil)

	app := fiber.New()
	app.Post("/api/v1/auth/consent/accept", h.AcceptConsentRequest)

	reqBody, _ := json.Marshal(map[string]any{
		"consent_challenge": "challenge-dynamic",
		"grant_scope":       []string{"openid", "profile", "tenant"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(reqBody))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	// Verify captured claims
	assert.NotNil(t, capturedClaims)
	assert.Equal(t, "user@test.com", capturedClaims["email"])
	assert.Equal(t, "tenant-abc", capturedClaims["tenant_id"])
	assert.Equal(t, "Innovation", capturedClaims["department"])
	assert.Equal(t, "Architect", capturedClaims["position"])
}

func TestAcceptConsentRequest_UsesRepresentativeTenantIDInsteadOfClientTenantContext(t *testing.T) {
	var capturedClaims map[string]any

	representativeTenantID := "01970f0a-5c28-74d8-a73a-f6e9e9a7b210"
	rpContextTenantID := "01970f0b-3448-7bb8-bdc7-16b6a1d2e661"

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-representative-tenant" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"challenge":       "challenge-representative-tenant",
				"requested_scope": []string{"openid", "profile", "tenant"},
				"subject":         "user-representative",
				"client": map[string]any{
					"client_id": "client-app",
					"metadata": map[string]any{
						"tenant_id": rpContextTenantID,
					},
				},
			}), nil
		}
		if r.URL.Path == "/oauth2/auth/requests/consent/accept" && r.URL.Query().Get("consent_challenge") == "challenge-representative-tenant" {
			body, _ := io.ReadAll(r.Body)
			var acceptReq map[string]any
			json.Unmarshal(body, &acceptReq)
			if session, ok := acceptReq["session"].(map[string]any); ok {
				capturedClaims = session["id_token"].(map[string]any)
			}

			return httpJSONAny(r, http.StatusOK, map[string]any{
				"redirect_to": "http://rp/cb",
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})

	client := &http.Client{Transport: transport}
	h := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
		KratosAdmin: new(MockKratosAdminService),
	}
	h.KratosAdmin.(*MockKratosAdminService).On("GetIdentity", mock.Anything, "user-representative").Return(&service.KratosIdentity{
		ID: "user-representative",
		Traits: map[string]any{
			"email": "user@test.com",
			"name":  "Test User",
			"additionalAppointments": []any{
				map[string]any{"tenantId": representativeTenantID, "isPrimary": true},
				map[string]any{"tenantId": rpContextTenantID},
			},
		},
	}, nil)

	app := fiber.New()
	app.Post("/api/v1/auth/consent/accept", h.AcceptConsentRequest)

	reqBody, _ := json.Marshal(map[string]any{
		"consent_challenge": "challenge-representative-tenant",
		"grant_scope":       []string{"openid", "profile"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(reqBody))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	assert.NotNil(t, capturedClaims)
	assert.Equal(t, representativeTenantID, capturedClaims["tenant_id"])
	assert.Contains(t, capturedClaims["joined_tenants"], representativeTenantID)
	assert.Contains(t, capturedClaims["joined_tenants"], rpContextTenantID)
	assert.Nil(t, capturedClaims["tenants"])
}

func TestAcceptConsentRequest_IncludesHanmacFamilyTenantClaimDetails(t *testing.T) {
	var capturedClaims map[string]any
	deptID := "01970f0a-5c28-74d8-a73a-f6e9e9a7b210"
	secondDeptID := "01970f0b-3448-7bb8-bdc7-16b6a1d2e661"
	companyID := "01970f08-91da-7286-bd19-882fb98d1f2c"
	rootID := "01970f07-4f01-7d9a-a71e-b53ad508f345"

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-hanmac-tenant-claim" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"challenge":       "challenge-hanmac-tenant-claim",
				"requested_scope": []string{"openid", "profile", "tenant"},
				"subject":         "user-hanmac",
				"client": map[string]any{
					"client_id": "hanmac-rp",
					"metadata": map[string]any{
						"tenant_id": deptID,
					},
				},
			}), nil
		}
		if r.URL.Path == "/oauth2/auth/requests/consent/accept" && r.URL.Query().Get("consent_challenge") == "challenge-hanmac-tenant-claim" {
			body, _ := io.ReadAll(r.Body)
			var acceptReq map[string]any
			json.Unmarshal(body, &acceptReq)
			if session, ok := acceptReq["session"].(map[string]any); ok {
				capturedClaims = session["id_token"].(map[string]any)
			}

			return httpJSONAny(r, http.StatusOK, map[string]any{
				"redirect_to": "http://rp/cb",
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})

	client := &http.Client{Transport: transport}
	h := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
		KratosAdmin: new(MockKratosAdminService),
	}
	h.KratosAdmin.(*MockKratosAdminService).On("GetIdentity", mock.Anything, "user-hanmac").Return(&service.KratosIdentity{
		ID: "user-hanmac",
		Traits: map[string]any{
			"email": "hanmac-user@example.com",
			"name":  "한맥 사용자",
			"additionalAppointments": []any{
				map[string]any{
					"tenantId":  deptID,
					"isPrimary": true,
					"isOwner":   true,
					"grade":     "책임",
					"jobTitle":  "기술기획",
					"position":  "팀장",
				},
				map[string]any{
					"tenantId":  secondDeptID,
					"isPrimary": false,
					"isOwner":   false,
					"grade":     "선임",
					"jobTitle":  "품질관리",
					"position":  "파트원",
				},
			},
		},
	}, nil)

	mockTenantSvc := new(MockTenantService)
	mockTenantSvc.On("ListJoinedTenants", mock.Anything, "user-hanmac").Return([]domain.Tenant{}, nil)
	mockTenantSvc.On("GetTenant", mock.Anything, deptID).Return(&domain.Tenant{
		ID:       deptID,
		Slug:     "tech-planning",
		Name:     "기술기획팀",
		Type:     domain.TenantTypeUserGroup,
		ParentID: &companyID,
	}, nil)
	mockTenantSvc.On("GetTenant", mock.Anything, secondDeptID).Return(&domain.Tenant{
		ID:       secondDeptID,
		Slug:     "quality",
		Name:     "품질관리팀",
		Type:     domain.TenantTypeUserGroup,
		ParentID: &companyID,
	}, nil)
	mockTenantSvc.On("GetTenant", mock.Anything, companyID).Return(&domain.Tenant{
		ID:       companyID,
		Slug:     "hanmac",
		Name:     "한맥기술",
		Type:     domain.TenantTypeCompany,
		ParentID: &rootID,
	}, nil)
	mockTenantSvc.On("GetTenant", mock.Anything, rootID).Return(&domain.Tenant{
		ID:   rootID,
		Slug: "hanmac-family",
		Name: "한맥가족",
		Type: domain.TenantTypeCompanyGroup,
	}, nil)
	h.TenantService = mockTenantSvc

	app := fiber.New()
	app.Post("/api/v1/auth/consent/accept", h.AcceptConsentRequest)

	reqBody, _ := json.Marshal(map[string]any{
		"consent_challenge": "challenge-hanmac-tenant-claim",
		"grant_scope":       []string{"openid", "profile", "tenant"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(reqBody))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	assert.NotNil(t, capturedClaims)
	assert.Equal(t, []any{deptID}, capturedClaims["lead_tenants"])
	assert.ElementsMatch(t, []any{deptID, secondDeptID}, capturedClaims["joined_tenants"])
	tenants := capturedClaims["tenants"].(map[string]any)
	dept := tenants[deptID].(map[string]any)
	assert.Equal(t, true, dept["lead"])
	assert.Equal(t, true, dept["representative"])
	assert.Equal(t, "책임", dept["grade"])
	assert.Equal(t, "기술기획", dept["jobTitle"])
	assert.Equal(t, "팀장", dept["position"])
	assert.Equal(t, companyID, dept["parentTenantId"])
	assert.NotContains(t, dept, "parentTenant")

	ancestors := dept["ancestors"].([]any)
	assert.Len(t, ancestors, 2)
	companyAncestor := ancestors[0].(map[string]any)
	assert.Equal(t, companyID, companyAncestor["id"])
	assert.Equal(t, "hanmac", companyAncestor["slug"])
	assert.Equal(t, rootID, companyAncestor["parentTenantId"])
	assert.NotContains(t, companyAncestor, "parentTenant")
	rootAncestor := ancestors[1].(map[string]any)
	assert.Equal(t, rootID, rootAncestor["id"])
	assert.Equal(t, "hanmac-family", rootAncestor["slug"])
	assert.Contains(t, rootAncestor, "parentTenantId")
	assert.Nil(t, rootAncestor["parentTenantId"])
	assert.NotContains(t, rootAncestor, "parentTenant")

	secondDept := tenants[secondDeptID].(map[string]any)
	assert.Equal(t, false, secondDept["lead"])
	assert.Equal(t, false, secondDept["representative"])
	assert.Equal(t, "선임", secondDept["grade"])
	assert.Equal(t, "품질관리", secondDept["jobTitle"])
	assert.Equal(t, "파트원", secondDept["position"])
	assert.Equal(t, companyID, secondDept["parentTenantId"])
}

func TestWithHanmacFamilyTenantClaims_DefaultClaimsOnlyWithoutTenantScope(t *testing.T) {
	deptID := "01970f0a-5c28-74d8-a73a-f6e9e9a7b210"
	secondDeptID := "01970f0b-3448-7bb8-bdc7-16b6a1d2e661"
	companyID := "01970f08-91da-7286-bd19-882fb98d1f2c"
	rootID := "01970f07-4f01-7d9a-a71e-b53ad508f345"

	mockTenantSvc := new(MockTenantService)
	mockTenantSvc.On("GetTenant", mock.Anything, deptID).Return(&domain.Tenant{
		ID:       deptID,
		Slug:     "tech-planning",
		Name:     "기술기획팀",
		Type:     domain.TenantTypeUserGroup,
		ParentID: &companyID,
	}, nil)
	mockTenantSvc.On("GetTenant", mock.Anything, secondDeptID).Return(&domain.Tenant{
		ID:       secondDeptID,
		Slug:     "quality",
		Name:     "품질관리팀",
		Type:     domain.TenantTypeUserGroup,
		ParentID: &companyID,
	}, nil)
	mockTenantSvc.On("GetTenant", mock.Anything, companyID).Return(&domain.Tenant{
		ID:       companyID,
		Slug:     "hanmac",
		Name:     "한맥기술",
		Type:     domain.TenantTypeCompany,
		ParentID: &rootID,
	}, nil)
	mockTenantSvc.On("GetTenant", mock.Anything, rootID).Return(&domain.Tenant{
		ID:   rootID,
		Slug: "hanmac-family",
		Name: "한맥가족",
		Type: domain.TenantTypeCompanyGroup,
	}, nil)

	h := &AuthHandler{TenantService: mockTenantSvc}
	claims := map[string]any{"tenant_id": deptID}
	traits := map[string]any{
		"additionalAppointments": []any{
			map[string]any{
				"tenantId":  deptID,
				"isPrimary": true,
				"isOwner":   true,
				"grade":     "책임",
			},
			map[string]any{
				"tenantId": secondDeptID,
				"grade":    "선임",
			},
		},
	}

	claims = h.withHanmacFamilyTenantClaims(context.Background(), claims, traits, []string{"openid", "profile"})

	assert.Equal(t, deptID, claims["tenant_id"])
	assert.ElementsMatch(t, []string{deptID, secondDeptID}, claims["joined_tenants"])
	assert.NotContains(t, claims, "tenants")
	assert.NotContains(t, claims, "lead_tenants")
}

func TestAcceptConsentRequest_DoesNotEmitLegacyProfileArray(t *testing.T) {
	var capturedClaims map[string]any

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-rp-profile" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"challenge":       "challenge-rp-profile",
				"requested_scope": []string{"openid", "profile", "tenant"},
				"subject":         "user-123",
				"client": map[string]any{
					"client_id": "client-app",
					"metadata": map[string]any{
						"customUserSchema": []map[string]any{
							{
								"key":          "approvalLevel",
								"label":        "승인 등급",
								"type":         "text",
								"claimEnabled": true,
							},
							{
								"key":          "internalMemo",
								"label":        "내부 메모",
								"type":         "text",
								"claimEnabled": false,
							},
						},
					},
				},
			}), nil
		}
		if r.URL.Path == "/oauth2/auth/requests/consent/accept" && r.URL.Query().Get("consent_challenge") == "challenge-rp-profile" {
			body, _ := io.ReadAll(r.Body)
			var acceptReq map[string]any
			json.Unmarshal(body, &acceptReq)
			if session, ok := acceptReq["session"].(map[string]any); ok {
				capturedClaims = session["id_token"].(map[string]any)
			}

			return httpJSONAny(r, http.StatusOK, map[string]any{
				"redirect_to": "http://rp/cb",
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})

	client := &http.Client{Transport: transport}
	h := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
		KratosAdmin: new(MockKratosAdminService),
	}
	h.KratosAdmin.(*MockKratosAdminService).On("GetIdentity", mock.Anything, "user-123").Return(&service.KratosIdentity{
		ID: "user-123",
		Traits: map[string]any{
			"email": "user@test.com",
			"name":  "Test User",
		},
	}, nil)
	repo := new(devMockRPUserMetadataRepo)
	repo.On("Get", mock.Anything, "client-app", "user-123").Return(&domain.RPUserMetadata{
		ClientID: "client-app",
		UserID:   "user-123",
		Metadata: domain.JSONMap{
			"approvalLevel": "A",
			"internalMemo":  "관리자 전용",
		},
	}, nil).Maybe()
	h.RPUserMetadataRepo = repo

	app := fiber.New()
	app.Post("/api/v1/auth/consent/accept", h.AcceptConsentRequest)

	reqBody, _ := json.Marshal(map[string]any{
		"consent_challenge": "challenge-rp-profile",
		"grant_scope":       []string{"openid", "profile"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(reqBody))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	assert.NotNil(t, capturedClaims)
	assert.NotContains(t, capturedClaims, legacyProfileArrayClaimKeyForTest())
	repo.AssertExpectations(t)
}

func TestGetConsentRequest_Skip_DynamicClaims(t *testing.T) {
	var capturedClaims map[string]any

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		// Hydra: Get Consent Request
		if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-skip-dynamic" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"challenge":       "challenge-skip-dynamic",
				"requested_scope": []string{"openid", "profile", "tenant"},
				"skip":            true,
				"subject":         "user-456",
				"client": map[string]any{
					"client_id": "skip-app",
					"metadata": map[string]any{
						"tenant_id": "tenant-xyz",
					},
				},
			}), nil
		}
		// Kratos: Get Identity
		if r.URL.Path == "/admin/identities/user-456" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"id": "user-456",
				"traits": map[string]any{
					"email": "skip@test.com",
					"tenant-xyz": map[string]any{
						"department": "Security",
						"position":   "Officer",
					},
				},
			}), nil
		}
		// Hydra: Accept Consent Request
		if r.URL.Path == "/oauth2/auth/requests/consent/accept" && r.URL.Query().Get("consent_challenge") == "challenge-skip-dynamic" {
			// Capture the claims sent to Hydra
			body, _ := io.ReadAll(r.Body)
			var acceptReq map[string]any
			json.Unmarshal(body, &acceptReq)
			if session, ok := acceptReq["session"].(map[string]any); ok {
				capturedClaims = session["id_token"].(map[string]any)
			}

			return httpJSONAny(r, http.StatusOK, map[string]any{
				"redirect_to": "http://rp/cb",
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})

	client := &http.Client{Transport: transport}
	origDefault := http.DefaultClient
	http.DefaultClient = client
	defer func() { http.DefaultClient = origDefault }()

	h := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
		KratosAdmin: new(MockKratosAdminService),
	}
	h.KratosAdmin.(*MockKratosAdminService).On("GetIdentity", mock.Anything, "user-456").Return(&service.KratosIdentity{
		ID: "user-456",
		Traits: map[string]any{
			"email": "skip@test.com",
			"tenant-xyz": map[string]any{
				"department": "Security",
				"position":   "Officer",
			},
		},
	}, nil)

	app := fiber.New()
	app.Get("/api/v1/auth/consent", h.GetConsentRequest)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/consent?consent_challenge=challenge-skip-dynamic", nil)
	resp, err := app.Test(req)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	// Verify captured claims
	assert.NotNil(t, capturedClaims)
	assert.Equal(t, "skip@test.com", capturedClaims["email"])
	assert.Equal(t, "tenant-xyz", capturedClaims["tenant_id"])
	assert.Equal(t, "Security", capturedClaims["department"])
	assert.Equal(t, "Officer", capturedClaims["position"])
}

func TestBuildOidcClaimsFromTraits_IncludesGlobalCustomClaims(t *testing.T) {
	claims := buildOidcClaimsFromTraits(map[string]any{
		"email": "user@test.com",
		"name":  "Test User",
		"global_custom_claims": map[string]any{
			"contract_date": "2026-06-09",
			"approved_at":   "2026-06-09T09:30:00+09:00",
			"email":         "override@test.com",
			"rp_claims":     "reserved",
		},
		"global_custom_claim_permissions": map[string]any{
			"contract_date": map[string]any{
				"readPermission":  "user_and_admin",
				"writePermission": "admin_only",
			},
		},
	}, []string{"openid", "profile", "email"}, "")

	assert.Equal(t, "2026-06-09", claims["contract_date"])
	assert.Equal(t, "2026-06-09T09:30:00+09:00", claims["approved_at"])
	assert.Equal(t, "user@test.com", claims["email"])
	assert.NotEqual(t, "reserved", claims["rp_claims"])
	assert.NotContains(t, claims, "global_custom_claim_permissions")
}

func TestAcceptConsentRequest_AppliesConfiguredIDTokenClaims(t *testing.T) {
	var capturedClaims map[string]any

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-configured-claims" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"challenge":       "challenge-configured-claims",
				"requested_scope": []string{"openid", "profile", "tenant"},
				"subject":         "user-789",
				"client": map[string]any{
					"client_id": "client-configured-claims",
					"metadata": map[string]any{
						"tenant_id": "tenant-claims",
						"id_token_claims": []map[string]any{
							{
								"namespace": "top_level",
								"key":       "locale",
								"value":     "ko-KR",
								"valueType": "text",
							},
							{
								"namespace": "top_level",
								"key":       "email",
								"value":     "should-not-override@example.com",
								"valueType": "text",
							},
							{
								"namespace": "rp_claims",
								"key":       "tier",
								"value":     "2",
								"valueType": "number",
							},
							{
								"namespace": "rp_claims",
								"key":       "features",
								"value":     "[\"sso\",\"claims\"]",
								"valueType": "array",
							},
						},
					},
				},
			}), nil
		}
		if r.URL.Path == "/oauth2/auth/requests/consent/accept" && r.URL.Query().Get("consent_challenge") == "challenge-configured-claims" {
			body, _ := io.ReadAll(r.Body)
			var acceptReq map[string]any
			json.Unmarshal(body, &acceptReq)
			if session, ok := acceptReq["session"].(map[string]any); ok {
				capturedClaims = session["id_token"].(map[string]any)
			}

			return httpJSONAny(r, http.StatusOK, map[string]any{
				"redirect_to": "http://rp/cb",
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})

	client := &http.Client{Transport: transport}
	origDefault := http.DefaultClient
	http.DefaultClient = client
	defer func() { http.DefaultClient = origDefault }()

	h := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
		KratosAdmin: new(MockKratosAdminService),
	}
	h.KratosAdmin.(*MockKratosAdminService).On("GetIdentity", mock.Anything, "user-789").Return(&service.KratosIdentity{
		ID: "user-789",
		Traits: map[string]any{
			"email": "real-user@example.com",
			"name":  "Configured User",
			"tenant-claims": map[string]any{
				"department": "Platform",
			},
		},
	}, nil)

	app := fiber.New()
	app.Post("/api/v1/auth/consent/accept", h.AcceptConsentRequest)

	reqBody, _ := json.Marshal(map[string]any{
		"consent_challenge": "challenge-configured-claims",
		"grant_scope":       []string{"openid", "profile"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(reqBody))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	assert.NotNil(t, capturedClaims)
	assert.Equal(t, "real-user@example.com", capturedClaims["email"])
	assert.Equal(t, "ko-KR", capturedClaims["locale"])
	assert.Equal(t, "tenant-claims", capturedClaims["tenant_id"])

	rpClaims, ok := capturedClaims["rp_claims"].(map[string]any)
	if assert.True(t, ok) {
		tier := rpClaims["tier"].(map[string]any)
		assert.Equal(t, float64(2), tier["value"])
		assert.Equal(t, "admin_only", tier["readPermission"])
		assert.Equal(t, "admin_only", tier["writePermission"])

		features := rpClaims["features"].(map[string]any)
		assert.Equal(t, []any{"sso", "claims"}, features["value"])
	}
}

func TestAcceptConsentRequest_UsesUpdatedRPUserMetadataForRPClaims(t *testing.T) {
	var capturedClaims map[string]any

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-rp-user-claims" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"challenge":       "challenge-rp-user-claims",
				"requested_scope": []string{"openid", "profile", "tenant"},
				"subject":         "user-rp-claims",
				"client": map[string]any{
					"client_id": "client-rp-claims",
					"metadata": map[string]any{
						"id_token_claims": []map[string]any{
							{
								"namespace": "rp_claims",
								"key":       "approvalLevel",
								"value":     "A",
								"valueType": "text",
							},
							{
								"namespace": "rp_claims",
								"key":       "activeMember",
								"value":     "true",
								"valueType": "boolean",
							},
							{
								"namespace": "rp_claims",
								"key":       "score",
								"value":     "1",
								"valueType": "number",
							},
							{
								"namespace": "rp_claims",
								"key":       "featureList",
								"value":     `["default"]`,
								"valueType": "array",
							},
							{
								"namespace": "rp_claims",
								"key":       "preferences",
								"value":     `{"theme":"light","density":"comfortable"}`,
								"valueType": "object",
							},
							{
								"namespace": "rp_claims",
								"key":       "contractDate",
								"value":     "2026-06-09",
								"valueType": "date",
							},
							{
								"namespace": "rp_claims",
								"key":       "approvedAt",
								"value":     "2026-06-09T09:30",
								"valueType": "datetime",
							},
							{
								"namespace":       "rp_claims",
								"key":             "tenants",
								"value":           "must-not-shadow-tenants",
								"valueType":       "text",
								"readPermission":  "user_and_admin",
								"writePermission": "user_and_admin",
							},
						},
					},
				},
			}), nil
		}
		if r.URL.Path == "/oauth2/auth/requests/consent/accept" && r.URL.Query().Get("consent_challenge") == "challenge-rp-user-claims" {
			body, _ := io.ReadAll(r.Body)
			var acceptReq map[string]any
			json.Unmarshal(body, &acceptReq)
			if session, ok := acceptReq["session"].(map[string]any); ok {
				capturedClaims = session["id_token"].(map[string]any)
			}

			return httpJSONAny(r, http.StatusOK, map[string]any{
				"redirect_to": "http://rp/cb",
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})

	client := &http.Client{Transport: transport}
	h := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
		KratosAdmin: new(MockKratosAdminService),
	}
	h.KratosAdmin.(*MockKratosAdminService).On("GetIdentity", mock.Anything, "user-rp-claims").Return(&service.KratosIdentity{
		ID: "user-rp-claims",
		Traits: map[string]any{
			"email":     "rp-user@example.com",
			"name":      "RP User",
			"tenant_id": "tenant-leaf",
			"tenant-leaf": map[string]any{
				"department":       "Platform",
				"rp_claims":        map[string]any{"mustNotLeak": true},
				"rp_custom_claims": map[string]any{"client-rp-claims": map[string]any{"mustNotLeak": true}},
			},
			"rp_custom_claims": map[string]any{
				"client-rp-claims": map[string]any{"approvalLevel": "B"},
			},
		},
	}, nil)
	rootTenantID := "tenant-root"
	mockTenantSvc := new(MockTenantService)
	mockTenantSvc.On("GetTenant", mock.Anything, "tenant-leaf").Return(&domain.Tenant{
		ID:       "tenant-leaf",
		Slug:     "platform",
		Name:     "플랫폼팀",
		Type:     domain.TenantTypeUserGroup,
		ParentID: &rootTenantID,
	}, nil)
	mockTenantSvc.On("GetTenant", mock.Anything, rootTenantID).Return(&domain.Tenant{
		ID:   rootTenantID,
		Slug: "root",
		Name: "루트",
		Type: domain.TenantTypeCompany,
	}, nil)
	mockTenantSvc.On("ListJoinedTenants", mock.Anything, "user-rp-claims").Return([]domain.Tenant{}, nil)
	h.TenantService = mockTenantSvc
	repo := new(devMockRPUserMetadataRepo)
	repo.On("Get", mock.Anything, "client-rp-claims", "user-rp-claims").Return(&domain.RPUserMetadata{
		ClientID: "client-rp-claims",
		UserID:   "user-rp-claims",
		Metadata: domain.JSONMap{
			"approvalLevel": "B",
			"activeMember":  false,
			"score":         float64(42),
			"featureList":   []any{"sso", "claims"},
			"preferences": map[string]any{
				"theme":   "dark",
				"density": "compact",
			},
			"contractDate": float64(1781017200),
			"approvedAt":   float64(1780968600),
			"internalMemo": "must-not-leak",
			"approvalLevel_permissions": map[string]any{
				"readPermission":  "admin_only",
				"writePermission": "user_and_admin",
			},
		},
	}, nil).Once()
	h.RPUserMetadataRepo = repo

	app := fiber.New()
	app.Post("/api/v1/auth/consent/accept", h.AcceptConsentRequest)

	reqBody, _ := json.Marshal(map[string]any{
		"consent_challenge": "challenge-rp-user-claims",
		"grant_scope":       []string{"openid", "profile", "tenant"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(reqBody))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	assert.NotNil(t, capturedClaims)
	rpClaims, ok := capturedClaims["rp_claims"].(map[string]any)
	if assert.True(t, ok) {
		approvalLevel := rpClaims["approvalLevel"].(map[string]any)
		assert.Equal(t, "B", approvalLevel["value"])
		assert.Equal(t, "user_and_admin", approvalLevel["readPermission"])
		assert.Equal(t, "user_and_admin", approvalLevel["writePermission"])

		activeMember := rpClaims["activeMember"].(map[string]any)
		assert.Equal(t, false, activeMember["value"])

		score := rpClaims["score"].(map[string]any)
		assert.Equal(t, float64(42), score["value"])

		featureList := rpClaims["featureList"].(map[string]any)
		assert.Equal(t, []any{"sso", "claims"}, featureList["value"])

		preferences := rpClaims["preferences"].(map[string]any)
		assert.Equal(t, map[string]any{"theme": "dark", "density": "compact"}, preferences["value"])

		contractDate := rpClaims["contractDate"].(map[string]any)
		assert.Equal(t, float64(1781017200), contractDate["value"])

		approvedAt := rpClaims["approvedAt"].(map[string]any)
		assert.Equal(t, float64(1780968600), approvedAt["value"])

		assert.NotContains(t, rpClaims, "tenants")
		assert.NotContains(t, rpClaims, "internalMemo")
		assert.NotContains(t, rpClaims, "approvalLevel_permissions")
	}
	assert.NotContains(t, capturedClaims["joined_tenants"], "rp_custom_claims")
	tenants := capturedClaims["tenants"].(map[string]any)
	assert.Contains(t, tenants, "tenant-leaf")
	assert.NotEqual(t, "must-not-shadow-tenants", capturedClaims["tenants"])
	assertNoRPClaimDataInTenantClaims(t, tenants)
	assert.NotContains(t, capturedClaims, legacyProfileArrayClaimKeyForTest())
	repo.AssertExpectations(t)
}

func legacyProfileArrayClaimKeyForTest() string {
	return strings.Join([]string{"rp", "profiles"}, "_")
}

func assertNoRPClaimDataInTenantClaims(t *testing.T, value any) {
	t.Helper()

	switch typed := value.(type) {
	case map[string]any:
		for key, child := range typed {
			assert.False(t, strings.HasPrefix(key, "rp_"))
			assertNoRPClaimDataInTenantClaims(t, child)
		}
	case []any:
		for _, child := range typed {
			assertNoRPClaimDataInTenantClaims(t, child)
		}
	}
}
