package handler

import (
	"baron-sso-backend/internal/domain"
	auditmw "baron-sso-backend/internal/middleware"
	"baron-sso-backend/internal/service"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

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

// --- Mocks with Unique Names to Avoid Collisions ---

type devMockKetoService struct {
	mock.Mock
}

func (m *devMockKetoService) CheckPermission(ctx context.Context, subject, namespace, object, relation string) (bool, error) {
	args := m.Called(ctx, subject, namespace, object, relation)
	return args.Bool(0), args.Error(1)
}

func (m *devMockKetoService) CreateRelation(ctx context.Context, ns, obj, rel, sub string) error {
	return m.Called(ctx, ns, obj, rel, sub).Error(0)
}

func (m *devMockKetoService) DeleteRelation(ctx context.Context, ns, obj, rel, sub string) error {
	return m.Called(ctx, ns, obj, rel, sub).Error(0)
}

func (m *devMockKetoService) ListRelations(ctx context.Context, ns, obj, rel, sub string) ([]service.RelationTuple, error) {
	if len(m.ExpectedCalls) == 0 {
		return []service.RelationTuple{}, nil
	}
	hasListRelationsExpectation := false
	for _, call := range m.ExpectedCalls {
		if call.Method == "ListRelations" {
			hasListRelationsExpectation = true
			break
		}
	}
	if !hasListRelationsExpectation {
		return []service.RelationTuple{}, nil
	}
	args := m.Called(ctx, ns, obj, rel, sub)
	return args.Get(0).([]service.RelationTuple), args.Error(1)
}

func (m *devMockKetoService) ListObjects(ctx context.Context, ns, rel, sub string) ([]string, error) {
	args := m.Called(ctx, ns, rel, sub)
	return args.Get(0).([]string), args.Error(1)
}

type devMockDeveloperService struct {
	mock.Mock
}

func (m *devMockDeveloperService) RequestAccess(ctx context.Context, req domain.DeveloperRequest) error {
	args := m.Called(ctx, req)
	return args.Error(0)
}

func (m *devMockDeveloperService) GetRequestStatus(ctx context.Context, userID, tenantID string) (*domain.DeveloperRequest, error) {
	args := m.Called(ctx, userID, tenantID)
	if req, ok := args.Get(0).(*domain.DeveloperRequest); ok {
		return req, args.Error(1)
	}
	return nil, args.Error(1)
}

func (m *devMockDeveloperService) GetRequestByID(ctx context.Context, id uint) (*domain.DeveloperRequest, error) {
	args := m.Called(ctx, id)
	if req, ok := args.Get(0).(*domain.DeveloperRequest); ok {
		return req, args.Error(1)
	}
	return nil, args.Error(1)
}

func (m *devMockDeveloperService) ListRequests(ctx context.Context, userID, status string) ([]domain.DeveloperRequest, error) {
	args := m.Called(ctx, userID, status)
	if requests, ok := args.Get(0).([]domain.DeveloperRequest); ok {
		return requests, args.Error(1)
	}
	return nil, args.Error(1)
}

func (m *devMockDeveloperService) ApproveRequest(ctx context.Context, id uint, adminNotes string) error {
	args := m.Called(ctx, id, adminNotes)
	return args.Error(0)
}

func (m *devMockDeveloperService) RejectRequest(ctx context.Context, id uint, adminNotes string) error {
	args := m.Called(ctx, id, adminNotes)
	return args.Error(0)
}

func (m *devMockDeveloperService) CancelApprovedRequest(ctx context.Context, id uint, adminNotes string) error {
	args := m.Called(ctx, id, adminNotes)
	return args.Error(0)
}

type devMockRedisRepo struct {
	data map[string]string
}

type devMockKratosAdmin struct {
	mock.Mock
}

func (m *devMockKratosAdmin) ListIdentities(ctx context.Context) ([]service.KratosIdentity, error) {
	args := m.Called(ctx)
	return args.Get(0).([]service.KratosIdentity), args.Error(1)
}

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

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

func (m *devMockKratosAdmin) UpdateIdentity(ctx context.Context, identityID string, traits map[string]any, state string) (*service.KratosIdentity, error) {
	args := m.Called(ctx, identityID, traits, state)
	if identity, ok := args.Get(0).(*service.KratosIdentity); ok {
		return identity, args.Error(1)
	}
	return nil, args.Error(1)
}

func (m *devMockKratosAdmin) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error {
	return m.Called(ctx, identityID, newPassword).Error(0)
}

func (m *devMockKratosAdmin) DeleteIdentity(ctx context.Context, identityID string) error {
	return m.Called(ctx, identityID).Error(0)
}

func (m *devMockKratosAdmin) CreateUser(ctx context.Context, user *domain.BrokerUser, password string) (string, error) {
	args := m.Called(ctx, user, password)
	return args.String(0), args.Error(1)
}

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

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

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

type devMockKetoOutboxRepository struct {
	mock.Mock
}

type devMockAuthProvider struct {
	mock.Mock
}

func (m *devMockAuthProvider) GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
	args := m.Called(c)
	if profile, ok := args.Get(0).(*domain.UserProfileResponse); ok {
		return profile, args.Error(1)
	}
	return nil, args.Error(1)
}

func (m *devMockKetoOutboxRepository) Create(ctx context.Context, entry *domain.KetoOutbox) error {
	return m.Called(ctx, entry).Error(0)
}

func (m *devMockKetoOutboxRepository) CreateWithTx(tx *gorm.DB, entry *domain.KetoOutbox) error {
	return m.Called(tx, entry).Error(0)
}

func (m *devMockKetoOutboxRepository) FindPending(ctx context.Context, limit int) ([]domain.KetoOutbox, error) {
	args := m.Called(ctx, limit)
	return args.Get(0).([]domain.KetoOutbox), args.Error(1)
}

func (m *devMockKetoOutboxRepository) ListCurrentBySubject(ctx context.Context, namespace, subject string) ([]domain.KetoOutbox, error) {
	args := m.Called(ctx, namespace, subject)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]domain.KetoOutbox), args.Error(1)
}

func (m *devMockKetoOutboxRepository) UpdateStatus(ctx context.Context, id string, status string, retryCount int, lastError string) error {
	return m.Called(ctx, id, status, retryCount, lastError).Error(0)
}

func (m *devMockKetoOutboxRepository) MarkProcessed(ctx context.Context, id string) error {
	return m.Called(ctx, id).Error(0)
}

func (m *devMockRedisRepo) Set(key, value string, exp time.Duration) error {
	if m.data == nil {
		m.data = make(map[string]string)
	}
	m.data[key] = value
	return nil
}

func (m *devMockRedisRepo) Get(key string) (string, error) {
	v, ok := m.data[key]
	if !ok {
		return "", fmt.Errorf("not found")
	}
	return v, nil
}

func (m *devMockRedisRepo) Delete(key string) error {
	delete(m.data, key)
	return nil
}
func (m *devMockRedisRepo) StoreVerificationCode(p, c string) error      { return nil }
func (m *devMockRedisRepo) GetVerificationCode(p string) (string, error) { return "", nil }
func (m *devMockRedisRepo) DeleteVerificationCode(p string) error        { return nil }

type devEnhancedMockAuditRepo struct {
	mockAuditRepo
	countFailures int64
	countSessions int64
}

func (m *devEnhancedMockAuditRepo) CountFailuresSince(ctx context.Context, s time.Time, t string) (int64, error) {
	return m.countFailures, nil
}

func (m *devEnhancedMockAuditRepo) CountActiveSessionsSince(ctx context.Context, s time.Time, t string) (int64, error) {
	return m.countSessions, nil
}

func devTestJWKSFirstKeyString(t *testing.T, jwks map[string]any, field string) string {
	t.Helper()

	keys, ok := jwks["keys"].([]any)
	if !ok || len(keys) == 0 {
		t.Fatalf("expected jwks keys")
	}
	key, ok := keys[0].(map[string]any)
	if !ok {
		t.Fatalf("expected jwks key object")
	}
	value, ok := key[field].(string)
	if !ok {
		t.Fatalf("expected jwks field %s", field)
	}
	return value
}

// --- Tests ---

func TestGetCurrentProfile_SetsAuditUserContext(t *testing.T) {
	mockAuth := new(devMockAuthProvider)
	handler := &DevHandler{Auth: mockAuth}
	app := fiber.New()

	mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{
		ID:   "0a5b7284-e88a-4fdf-b56f-98d0435b24f5",
		Role: domain.RoleUser,
	}, nil)

	app.Get("/test", func(c *fiber.Ctx) error {
		profile := handler.getCurrentProfile(c)
		return c.JSON(fiber.Map{
			"profile_id": profile.ID,
			"user_id":    c.Locals("user_id"),
		})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	resp, _ := app.Test(req)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var body map[string]string
	assert.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
	assert.NoError(t, resp.Body.Close())
	assert.Equal(t, "0a5b7284-e88a-4fdf-b56f-98d0435b24f5", body["profile_id"])
	assert.Equal(t, "0a5b7284-e88a-4fdf-b56f-98d0435b24f5", body["user_id"])
}

func TestGetCurrentProfile_PreservesExistingAuditUserContext(t *testing.T) {
	handler := &DevHandler{}
	app := fiber.New()

	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:   "profile-user",
			Role: domain.RoleUser,
		})
		c.Locals("user_id", "existing-user")
		return c.Next()
	})
	app.Get("/test", func(c *fiber.Ctx) error {
		profile := handler.getCurrentProfile(c)
		return c.JSON(fiber.Map{
			"profile_id": profile.ID,
			"user_id":    c.Locals("user_id"),
		})
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	resp, _ := app.Test(req)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var body map[string]string
	assert.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
	assert.NoError(t, resp.Body.Close())
	assert.Equal(t, "profile-user", body["profile_id"])
	assert.Equal(t, "existing-user", body["user_id"])
}

func TestListClients_Success(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients" {
			return httpJSONAny(r, http.StatusOK, []map[string]any{
				{"client_id": "client-1", "client_name": "App One", "metadata": map[string]any{"status": "active"}},
				{"client_id": "client-2", "client_name": "App Two", "metadata": map[string]any{"status": "inactive"}},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(true, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Get("/api/v1/dev/clients", h.ListClients)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
	resp, _ := app.Test(req, -1)

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

func TestListClients_UserSeesOnlyClientsAllowedByReBAC(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients" {
			return httpJSONAny(r, http.StatusOK, []map[string]any{
				{"client_id": "client-denied", "client_name": "Denied App", "metadata": map[string]any{"tenant_id": "tenant-a", "status": "active"}},
				{"client_id": "client-allowed", "client_name": "Allowed App", "metadata": map[string]any{"tenant_id": "tenant-b", "status": "active"}},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-denied", "view").Return(false, nil)
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-allowed", "view").Return(true, nil)
	mockKeto.On(
		"ListRelations",
		mock.Anything,
		"RelyingParty",
		mock.Anything,
		mock.Anything,
		mock.Anything,
	).Return([]service.RelationTuple{}, nil).Maybe()

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		tenantID := "tenant-a"
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/clients", h.ListClients)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var result clientListResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	if assert.Len(t, result.Items, 1) {
		assert.Equal(t, "client-allowed", result.Items[0].ID)
	}
	mockKeto.AssertExpectations(t)
}

func TestCreateClient_ReservedSystemNameForbidden(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		t.Fatalf("hydra should not be called when reserved system name is rejected")
		return nil, nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"name":         "AdminFront",
		"type":         "pkce",
		"redirectUris": []string{"http://localhost/cb"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusForbidden, resp.StatusCode)
}

func TestUpdateClient_ReservedSystemNameForbidden(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"redirect_uris": []string{
					"http://localhost/cb",
				},
				"grant_types":                []string{"authorization_code", "refresh_token"},
				"response_types":             []string{"code"},
				"scope":                      "openid profile email offline_access",
				"token_endpoint_auth_method": "none",
				"metadata":                   map[string]any{"status": "active"},
			}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
			t.Fatalf("hydra update should not be called when reserved system name is rejected")
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

	body, _ := json.Marshal(map[string]any{
		"name": "DevFront",
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusForbidden, resp.StatusCode)
}

func TestUpdateClient_PrivateClientAllowedByEditConfigPermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"redirect_uris": []string{
					"http://localhost/cb",
				},
				"grant_types":                []string{"authorization_code", "refresh_token"},
				"response_types":             []string{"code"},
				"scope":                      "openid profile email offline_access",
				"token_endpoint_auth_method": "client_secret_basic",
				"metadata": map[string]any{
					"status":    "active",
					"tenant_id": "tenant-1",
				},
			}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One Updated",
				"redirect_uris": []string{
					"http://localhost/cb",
				},
				"grant_types":                []string{"authorization_code", "refresh_token"},
				"response_types":             []string{"code"},
				"scope":                      "openid profile email offline_access",
				"token_endpoint_auth_method": "client_secret_basic",
				"metadata": map[string]any{
					"status":    "active",
					"tenant_id": "tenant-1",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "edit_config").Return(true, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}

	app := fiber.New()
	tenantID := "tenant-1"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

	body, _ := json.Marshal(map[string]any{
		"name": "App One Updated",
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result clientDetailResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	assert.Equal(t, "App One Updated", result.Client.Name)
	mockKeto.AssertExpectations(t)
}

func TestUpdateClient_ManagedRPAdminRequiresEditConfigPermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"redirect_uris": []string{
					"http://localhost/cb",
				},
				"grant_types":                []string{"authorization_code", "refresh_token"},
				"response_types":             []string{"code"},
				"scope":                      "openid profile email offline_access",
				"token_endpoint_auth_method": "none",
				"metadata": map[string]any{
					"status":    "active",
					"tenant_id": "tenant-1",
				},
			}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
			t.Fatalf("hydra update should not be called without edit_config permission")
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "edit_config").Return(false, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}

	app := fiber.New()
	tenantID := "tenant-1"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
			Metadata: map[string]any{
				"managed_client_ids": []any{"client-1"},
			},
		})
		return c.Next()
	})
	app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

	body, _ := json.Marshal(map[string]any{
		"name": "App One Updated",
		"metadata": map[string]any{
			"tenant_access_restricted": true,
			"allowed_tenants":          []string{"tenant-1"},
		},
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusForbidden, resp.StatusCode)
	mockKeto.AssertExpectations(t)
}

func TestUpdateClient_SuperAdminBypassesEditConfigPermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"redirect_uris": []string{
					"http://localhost/cb",
				},
				"grant_types":                []string{"authorization_code", "refresh_token"},
				"response_types":             []string{"code"},
				"scope":                      "openid profile email offline_access",
				"token_endpoint_auth_method": "client_secret_basic",
				"metadata": map[string]any{
					"status":    "active",
					"tenant_id": "tenant-1",
				},
			}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One Updated",
				"redirect_uris": []string{
					"http://localhost/cb",
				},
				"grant_types":                []string{"authorization_code", "refresh_token"},
				"response_types":             []string{"code"},
				"scope":                      "openid profile email offline_access",
				"token_endpoint_auth_method": "client_secret_basic",
				"metadata": map[string]any{
					"status":    "active",
					"tenant_id": "tenant-1",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
	}

	app := fiber.New()
	tenantID := "tenant-1"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleSuperAdmin,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

	body, _ := json.Marshal(map[string]any{
		"name": "App One Updated",
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result clientDetailResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	assert.Equal(t, "App One Updated", result.Client.Name)
}

func TestUpdateClient_AuditDetailsIncludeGeneralSettingChanges(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"redirect_uris": []string{
					"http://localhost/cb",
				},
				"grant_types":                []string{"authorization_code", "refresh_token"},
				"response_types":             []string{"code"},
				"scope":                      "openid profile email",
				"token_endpoint_auth_method": "client_secret_basic",
				"metadata": map[string]any{
					"status":                              "active",
					"tenant_id":                           "tenant-1",
					"tenant_access_restricted":            false,
					"allowed_tenants":                     []any{},
					"id_token_claims":                     []any{},
					"headless_login_enabled":              false,
					"headless_jwks_uri":                   "",
					"headless_token_endpoint_auth_method": "",
				},
			}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One Updated",
				"redirect_uris": []string{
					"http://localhost/cb",
				},
				"grant_types":                []string{"authorization_code", "refresh_token"},
				"response_types":             []string{"code"},
				"scope":                      "openid profile email tenant",
				"token_endpoint_auth_method": "private_key_jwt",
				"metadata": map[string]any{
					"status":                              "active",
					"tenant_id":                           "tenant-1",
					"tenant_access_restricted":            true,
					"allowed_tenants":                     []any{"tenant-1", "tenant-2"},
					"id_token_claims":                     []any{map[string]any{"namespace": "rp_claims", "key": "locale", "valueType": "text", "value": "ko-KR"}},
					"headless_login_enabled":              true,
					"headless_jwks_uri":                   "https://rp.example.com/jwks.json",
					"headless_token_endpoint_auth_method": "private_key_jwt",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	auditRepo := &mockAuditRepo{}
	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		AuditRepo: auditRepo,
	}

	app := fiber.New()
	app.Use(auditmw.AuditMiddleware(auditmw.AuditConfig{Repo: auditRepo}))
	tenantID := "tenant-1"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleSuperAdmin,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

	body, _ := json.Marshal(map[string]any{
		"name":   "App One Updated",
		"scopes": []string{"openid", "profile", "email", "tenant"},
		"metadata": map[string]any{
			"tenant_access_restricted": true,
			"allowed_tenants":          []string{"tenant-1", "tenant-2"},
			"id_token_claims": []map[string]any{
				{
					"namespace": "rp_claims",
					"key":       "locale",
					"valueType": "text",
					"value":     "ko-KR",
				},
			},
			"headless_login_enabled":              true,
			"headless_jwks_uri":                   "https://rp.example.com/jwks.json",
			"headless_token_endpoint_auth_method": "private_key_jwt",
			"backchannel_logout_uri":              "https://rp.example.com/logout",
			"backchannel_logout_session_required": true,
		},
		"tokenEndpointAuthMethod":          "private_key_jwt",
		"jwksUri":                          "https://rp.example.com/jwks.json",
		"backchannelLogoutUri":             "https://rp.example.com/logout",
		"backchannelLogoutSessionRequired": true,
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	if assert.NotEmpty(t, auditRepo.logs) {
		var details map[string]any
		assert.NoError(t, json.Unmarshal([]byte(auditRepo.logs[0].Details), &details))
		before, _ := details["before"].(map[string]any)
		after, _ := details["after"].(map[string]any)
		assert.NotNil(t, before)
		assert.NotNil(t, after)
		assert.Contains(t, after, "scopes")
		assert.Contains(t, after, "tenant_access_restricted")
		assert.Contains(t, after, "allowed_tenants")
		assert.Contains(t, after, "id_token_claims")
		assert.Contains(t, after, "headless_login_enabled")
		assert.Contains(t, after, "headless_jwks_uri")
		assert.Contains(t, after, "backchannel_logout_uri")
		assert.Contains(t, after, "backchannel_logout_session_required")
	}
}

func TestListClients_ProtectedSystemClientHidden(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients" {
			return httpJSONAny(r, http.StatusOK, []map[string]any{
				{"client_id": "oathkeeper-introspect", "client_name": "Internal Client"},
				{"client_id": "client-1", "client_name": "App One", "metadata": map[string]any{"status": "active"}},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(true, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Get("/api/v1/dev/clients", h.ListClients)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var res clientListResponse
	_ = json.NewDecoder(resp.Body).Decode(&res)
	assert.Len(t, res.Items, 1)
	assert.Equal(t, "client-1", res.Items[0].ID)
}

func TestListClients_ReservedSystemNameAliasHidden(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients" {
			return httpJSONAny(r, http.StatusOK, []map[string]any{
				{"client_id": "adminfront", "client_name": "AdminFront", "metadata": map[string]any{"status": "active"}},
				{"client_id": "4f2c9fd6-1111-2222-3333-444444444444", "client_name": "AdminFront", "metadata": map[string]any{"status": "active"}},
				{"client_id": "devfront", "client_name": "DevFront", "metadata": map[string]any{"status": "active"}},
				{"client_id": "7d2c9fd6-1111-2222-3333-444444444444", "client_name": "DevFront", "metadata": map[string]any{"status": "active"}},
				{"client_id": "client-1", "client_name": "App One", "metadata": map[string]any{"status": "active"}},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(true, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Get("/api/v1/dev/clients", h.ListClients)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result clientListResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	assert.Len(t, result.Items, 3)
	assert.Equal(t, "adminfront", result.Items[0].ID)
	assert.Equal(t, "devfront", result.Items[1].ID)
	assert.Equal(t, "client-1", result.Items[2].ID)
}

func TestGetClient_ReservedSystemNameAliasHidden(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients/4f2c9fd6-1111-2222-3333-444444444444" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "4f2c9fd6-1111-2222-3333-444444444444",
				"client_name": "AdminFront",
				"metadata":    map[string]any{"status": "active"},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Get("/api/v1/dev/clients/:id", h.GetClient)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/4f2c9fd6-1111-2222-3333-444444444444", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}

func TestUpdateClientStatus_Success(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id": "client-1", "metadata": map[string]any{"status": "active"},
			}), nil
		}
		if r.Method == http.MethodPatch && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id": "client-1", "metadata": map[string]any{"status": "inactive"},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Patch("/api/v1/dev/clients/:id/status", h.UpdateClientStatus)

	body, _ := json.Marshal(map[string]any{"status": "inactive"})
	req := httptest.NewRequest(http.MethodPatch, "/api/v1/dev/clients/client-1/status", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var res clientDetailResponse
	json.NewDecoder(resp.Body).Decode(&res)
	assert.Equal(t, "inactive", res.Client.Status)
}

func TestUpdateClientStatus_UserAllowedByStatusPermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		if r.Method == http.MethodPatch && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "inactive",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "change_status").Return(true, nil)
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "edit_config").Return(false, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}
	app := fiber.New()
	tenantID := "tenant-1"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Patch("/api/v1/dev/clients/:id/status", h.UpdateClientStatus)

	body, _ := json.Marshal(map[string]any{"status": "inactive"})
	req := httptest.NewRequest(http.MethodPatch, "/api/v1/dev/clients/client-1/status", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var res clientDetailResponse
	_ = json.NewDecoder(resp.Body).Decode(&res)
	assert.Equal(t, "inactive", res.Client.Status)
	mockKeto.AssertExpectations(t)
}

func TestUpdateClientStatus_UserAllowedByEditConfigPermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		if r.Method == http.MethodPatch && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "inactive",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "change_status").Return(false, nil)
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "edit_config").Return(true, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}
	app := fiber.New()
	tenantID := "tenant-1"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Patch("/api/v1/dev/clients/:id/status", h.UpdateClientStatus)

	body, _ := json.Marshal(map[string]any{"status": "inactive"})
	req := httptest.NewRequest(http.MethodPatch, "/api/v1/dev/clients/client-1/status", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var res clientDetailResponse
	_ = json.NewDecoder(resp.Body).Decode(&res)
	assert.Equal(t, "inactive", res.Client.Status)
	mockKeto.AssertExpectations(t)
}

func TestUpdateClientStatus_ProtectedSystemClientForbidden(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/oathkeeper-introspect" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id": "oathkeeper-introspect",
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Patch("/api/v1/dev/clients/:id/status", h.UpdateClientStatus)

	body, _ := json.Marshal(map[string]any{"status": "inactive"})
	req := httptest.NewRequest(http.MethodPatch, "/api/v1/dev/clients/oathkeeper-introspect/status", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusForbidden, resp.StatusCode)
}

func TestDeleteClient_Success(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{"client_id": "client-1"}), nil
		}
		if r.Method == http.MethodDelete && r.URL.Path == "/clients/client-1" {
			return &http.Response{StatusCode: http.StatusNoContent, Body: http.NoBody}, nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	secretRepo := &mockSecretRepo{secrets: map[string]string{"client-1": "secret"}}
	redisRepo := &devMockRedisRepo{data: map[string]string{"client_secret:client-1": "secret"}}

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		SecretRepo: secretRepo,
		Redis:      redisRepo,
		Keto:       new(devMockKetoService),
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Delete("/api/v1/dev/clients/:id", h.DeleteClient)

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/dev/clients/client-1", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusNoContent, resp.StatusCode)
	s, _ := secretRepo.GetByID(nil, "client-1")
	assert.Empty(t, s)
	_, err := redisRepo.Get("client_secret:client-1")
	assert.Error(t, err)
}

func TestDeleteClient_ProtectedSystemClientForbidden(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/oathkeeper-introspect" {
			return httpJSONAny(r, http.StatusOK, map[string]any{"client_id": "oathkeeper-introspect"}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		SecretRepo: &mockSecretRepo{secrets: map[string]string{"oathkeeper-introspect": "secret"}},
		Redis:      &devMockRedisRepo{data: map[string]string{"client_secret:oathkeeper-introspect": "secret"}},
		Keto:       new(devMockKetoService),
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Delete("/api/v1/dev/clients/:id", h.DeleteClient)

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/dev/clients/oathkeeper-introspect", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusForbidden, resp.StatusCode)
}

func TestGetClient_ProtectedSystemClientHidden(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/oathkeeper-introspect" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "oathkeeper-introspect",
				"client_name": "Internal Client",
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Get("/api/v1/dev/clients/:id", h.GetClient)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/oathkeeper-introspect", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}

func TestGetClient_RPAdminAllowedByKetoViewPermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-b",
					"status":    "active",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:rp-1", "RelyingParty", "client-1", "view").Return(true, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}

	app := fiber.New()
	tenantID := "tenant-a"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "rp-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/clients/:id", h.GetClient)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/client-1", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	mockKeto.AssertExpectations(t)
}

func TestGetClient_RedactsSecretWithoutViewSecretPermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":     "client-1",
				"client_name":   "App One",
				"client_secret": "stored-secret",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "view").Return(true, nil)
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "view_secret").Return(false, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}
	app := fiber.New()
	tenantID := "tenant-1"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/clients/:id", h.GetClient)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/client-1", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var result clientDetailResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	assert.Empty(t, result.Client.ClientSecret)
	mockKeto.AssertExpectations(t)
}

func TestGetClient_UserAllowedToViewSecretByPermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":     "client-1",
				"client_name":   "App One",
				"client_secret": "stored-secret",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "view").Return(true, nil)
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "view_secret").Return(true, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: mockKeto,
	}
	app := fiber.New()
	tenantID := "tenant-1"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/clients/:id", h.GetClient)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/client-1", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var result clientDetailResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	assert.Equal(t, "stored-secret", result.Client.ClientSecret)
	mockKeto.AssertExpectations(t)
}

func TestRotateClientSecret_Success(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{"client_id": "client-1"}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
			var body map[string]any
			json.NewDecoder(r.Body).Decode(&body)
			return httpJSONAny(r, http.StatusOK, body), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	secretRepo := &mockSecretRepo{secrets: make(map[string]string)}
	redisRepo := &devMockRedisRepo{data: make(map[string]string)}

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		SecretRepo: secretRepo,
		Redis:      redisRepo,
		Keto:       new(devMockKetoService),
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients/:id/secret/rotate", h.RotateClientSecret)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients/client-1/secret/rotate", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var res clientDetailResponse
	json.NewDecoder(resp.Body).Decode(&res)
	assert.NotEmpty(t, res.Client.ClientSecret)

	dbS, _ := secretRepo.GetByID(nil, "client-1")
	assert.Equal(t, res.Client.ClientSecret, dbS)
}

func TestCreateClient_RPAdminAllowedByTenantGrantPermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodPost && r.URL.Path == "/clients" {
			var body map[string]any
			_ = json.NewDecoder(r.Body).Decode(&body)
			body["client_secret"] = "generated-secret"
			return httpJSONAny(r, http.StatusCreated, body), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:rp-1", "Tenant", "tenant-a", "grant_dev_permissions").Return(true, nil)

	secretRepo := &mockSecretRepo{secrets: make(map[string]string)}
	redisRepo := &devMockRedisRepo{data: make(map[string]string)}

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		SecretRepo: secretRepo,
		Redis:      redisRepo,
		Keto:       mockKeto,
	}

	app := fiber.New()
	tenantID := "tenant-a"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "rp-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"id":           "client-1",
		"name":         "App One",
		"type":         "pkce",
		"redirectUris": []string{"http://localhost/cb"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)
	mockKeto.AssertExpectations(t)
}

func TestCreateClient_ApprovedDeveloperCanCreatePrivateClient(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodPost && r.URL.Path == "/clients" {
			var body map[string]any
			_ = json.NewDecoder(r.Body).Decode(&body)
			body["client_secret"] = "generated-secret"
			return httpJSONAny(r, http.StatusCreated, body), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "Tenant", "tenant-a", "grant_dev_permissions").Return(true, nil)
	mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "client-1", "admins", "User:user-1").Return([]service.RelationTuple{}, nil)
	mockKeto.On("CreateRelation", mock.Anything, "RelyingParty", "client-1", "admins", "User:user-1").Return(nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		SecretRepo: &mockSecretRepo{secrets: make(map[string]string)},
		Redis:      &devMockRedisRepo{data: make(map[string]string)},
		Keto:       mockKeto,
		KetoOutbox: new(devMockKetoOutboxRepository),
	}

	app := fiber.New()
	tenantID := "tenant-a"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"id":           "client-1",
		"name":         "App One",
		"type":         "private",
		"redirectUris": []string{"http://localhost/cb"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)
	mockKeto.AssertExpectations(t)
}

func TestCreateClient_ApprovedDeveloperRequestAllowsCreateWhenTenantGrantNotVisible(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodPost && r.URL.Path == "/clients" {
			var body map[string]any
			_ = json.NewDecoder(r.Body).Decode(&body)
			body["client_secret"] = "generated-secret"
			return httpJSONAny(r, http.StatusCreated, body), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "Tenant", "tenant-a", "grant_dev_permissions").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "System", "global", "manage_all").Return(false, nil).Maybe()

	developerSvc := new(devMockDeveloperService)
	developerSvc.On("GetRequestStatus", mock.Anything, "user-1", "tenant-a").Return(&domain.DeveloperRequest{
		UserID:   "user-1",
		TenantID: "tenant-a",
		Status:   domain.DeveloperRequestStatusApproved,
	}, nil).Maybe()

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		SecretRepo:   &mockSecretRepo{secrets: make(map[string]string)},
		Redis:        &devMockRedisRepo{data: make(map[string]string)},
		Keto:         mockKeto,
		DeveloperSvc: developerSvc,
	}

	app := fiber.New()
	tenantID := "tenant-a"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"id":           "client-1",
		"name":         "App One",
		"type":         "private",
		"redirectUris": []string{"http://localhost/cb"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)
	mockKeto.AssertExpectations(t)
	developerSvc.AssertExpectations(t)
}

func TestGrantCreatorAdminRelation_FallsBackToOutboxOnImmediateFailure(t *testing.T) {
	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "client-1", "admins", "User:user-1").Return([]service.RelationTuple{}, nil).Maybe()
	mockKeto.On("CreateRelation", mock.Anything, "RelyingParty", "client-1", "admins", "User:user-1").Return(assert.AnError).Maybe()

	mockOutbox := new(devMockKetoOutboxRepository)
	mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
		return entry.Namespace == "RelyingParty" &&
			entry.Object == "client-1" &&
			entry.Relation == "admins" &&
			entry.Subject == "User:user-1" &&
			entry.Action == domain.KetoOutboxActionCreate
	})).Return(nil).Maybe()

	h := &DevHandler{
		Keto:       mockKeto,
		KetoOutbox: mockOutbox,
	}

	app := fiber.New()
	app.Get("/test", func(c *fiber.Ctx) error {
		assert.NoError(t, h.grantCreatorAdminRelation(c, "client-1", "User:user-1"))
		return c.SendStatus(fiber.StatusOK)
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	mockKeto.AssertExpectations(t)
	mockOutbox.AssertExpectations(t)
}

func TestEnsureDeveloperGrantRelation_CreatesRequiredTenantRelations(t *testing.T) {
	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	for _, relation := range []string{"developer_console_grant_manager", "view_dev_console", "grant_dev_permissions"} {
		mockKeto.On("ListRelations", mock.Anything, "Tenant", "tenant-a", relation, "User:user-1").Return([]service.RelationTuple{}, nil).Maybe()
		mockKeto.On("CreateRelation", mock.Anything, "Tenant", "tenant-a", relation, "User:user-1").Return(nil).Maybe()
	}

	mockOutbox := new(devMockKetoOutboxRepository)
	for _, relation := range []string{"developer_console_grant_manager", "view_dev_console", "grant_dev_permissions"} {
		expectedRelation := relation
		mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
			return entry.Namespace == "Tenant" &&
				entry.Object == "tenant-a" &&
				entry.Relation == expectedRelation &&
				entry.Subject == "User:user-1" &&
				entry.Action == domain.KetoOutboxActionCreate
		})).Return(nil).Maybe()
	}

	h := &DevHandler{
		Keto:       mockKeto,
		KetoOutbox: mockOutbox,
	}

	app := fiber.New()
	app.Get("/test", func(c *fiber.Ctx) error {
		h.ensureDeveloperGrantRelation(c, "user-1", "tenant-a")
		return c.SendStatus(fiber.StatusOK)
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	mockKeto.AssertExpectations(t)
	mockOutbox.AssertExpectations(t)
}

func TestEnsureDeveloperGrantRelation_SkipsExistingTenantRelations(t *testing.T) {
	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	for _, relation := range []string{"developer_console_grant_manager", "view_dev_console", "grant_dev_permissions"} {
		mockKeto.On("ListRelations", mock.Anything, "Tenant", "tenant-a", relation, "User:user-1").
			Return([]service.RelationTuple{{Namespace: "Tenant", Object: "tenant-a", Relation: relation, SubjectID: "User:user-1"}}, nil).Maybe()
	}

	h := &DevHandler{
		Keto:       mockKeto,
		KetoOutbox: new(devMockKetoOutboxRepository),
	}

	app := fiber.New()
	app.Get("/test", func(c *fiber.Ctx) error {
		h.ensureDeveloperGrantRelation(c, "user-1", "tenant-a")
		return c.SendStatus(fiber.StatusOK)
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	mockKeto.AssertExpectations(t)
}

func TestRevokeDeveloperGrantRelation_DeletesRequiredTenantRelations(t *testing.T) {
	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	for _, relation := range []string{"developer_console_grant_manager", "view_dev_console", "grant_dev_permissions"} {
		mockKeto.On("DeleteRelation", mock.Anything, "Tenant", "tenant-a", relation, "User:user-1").Return(nil).Maybe()
	}

	mockOutbox := new(devMockKetoOutboxRepository)
	for _, relation := range []string{"developer_console_grant_manager", "view_dev_console", "grant_dev_permissions"} {
		expectedRelation := relation
		mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
			return entry.Namespace == "Tenant" &&
				entry.Object == "tenant-a" &&
				entry.Relation == expectedRelation &&
				entry.Subject == "User:user-1" &&
				entry.Action == domain.KetoOutboxActionDelete
		})).Return(nil).Maybe()
	}

	h := &DevHandler{
		Keto:       mockKeto,
		KetoOutbox: mockOutbox,
	}

	app := fiber.New()
	app.Get("/test", func(c *fiber.Ctx) error {
		h.revokeDeveloperGrantRelation(c, "user-1", "tenant-a")
		return c.SendStatus(fiber.StatusOK)
	})

	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	mockKeto.AssertExpectations(t)
	mockOutbox.AssertExpectations(t)
}

func TestGetStats_Success(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients" {
			return httpJSONAny(r, http.StatusOK, []map[string]any{
				{"client_id": "c1", "metadata": map[string]any{"tenant_id": "t1"}},
				{"client_id": "c2", "metadata": map[string]any{"tenant_id": "t1"}},
				{"client_id": "oathkeeper-introspect", "metadata": map[string]any{"tenant_id": "t1"}},
				{"client_id": "c3", "metadata": map[string]any{"tenant_id": "t2"}},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	auditRepo := &devEnhancedMockAuditRepo{
		countFailures: 7,
		countSessions: 3,
	}

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On(
		"CheckPermission",
		mock.Anything,
		"User:u1",
		"RelyingParty",
		mock.Anything,
		"view",
	).Return(false, nil).Maybe()
	mockKeto.On(
		"ListRelations",
		mock.Anything,
		"RelyingParty",
		mock.Anything,
		mock.Anything,
		mock.Anything,
	).Return([]service.RelationTuple{}, nil).Maybe()

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		AuditRepo: auditRepo,
		Keto:      mockKeto,
	}
	app := fiber.New()
	tenantID := "t1"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID: "u1", Role: domain.RoleSuperAdmin, TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/stats", h.GetStats)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/stats", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var res devStatsResponse
	json.NewDecoder(resp.Body).Decode(&res)

	assert.Equal(t, int64(3), res.TotalClients)
	assert.Equal(t, int64(7), res.AuthFailures)
	assert.Equal(t, int64(3), res.ActiveSessions)
}

func TestGetStats_UserScopesAuditMetricsToVisibleClients(t *testing.T) {
	now := time.Now()
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients" {
			return httpJSONAny(r, http.StatusOK, []map[string]any{
				{"client_id": "client-owned", "metadata": map[string]any{"tenant_id": "tenant-a"}},
				{"client_id": "client-other", "metadata": map[string]any{"tenant_id": "tenant-a"}},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	auditRepo := &mockAuditRepo{
		logs: []domain.AuditLog{
			{
				EventID:   "evt-1",
				Timestamp: now.Add(-15 * time.Minute),
				SessionID: "sess-owned",
				Status:    "success",
				EventType: "GET /api/v1/dev/clients/client-owned",
				Details:   `{"client_id":"client-owned","tenant_id":"tenant-a"}`,
			},
			{
				EventID:   "evt-2",
				Timestamp: now.Add(-20 * time.Minute),
				Status:    "failure",
				EventType: "GET /api/v1/dev/clients/client-owned",
				Details:   `{"client_id":"client-owned","tenant_id":"tenant-a"}`,
			},
			{
				EventID:   "evt-3",
				Timestamp: now.Add(-10 * time.Minute),
				SessionID: "sess-other",
				Status:    "success",
				EventType: "GET /api/v1/dev/clients/client-other",
				Details:   `{"client_id":"client-other","tenant_id":"tenant-a"}`,
			},
			{
				EventID:   "evt-4",
				Timestamp: now.Add(-30 * time.Minute),
				Status:    "failure",
				EventType: "GET /api/v1/dev/clients/client-other",
				Details:   `{"client_id":"client-other","tenant_id":"tenant-a"}`,
			},
		},
	}

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-owned", "view").Return(true, nil)
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-other", "view").Return(false, nil)
	mockKeto.On(
		"ListRelations",
		mock.Anything,
		"RelyingParty",
		mock.Anything,
		mock.Anything,
		mock.Anything,
	).Return([]service.RelationTuple{}, nil).Maybe()

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		AuditRepo: auditRepo,
		Keto:      mockKeto,
	}

	app := fiber.New()
	tenantID := "tenant-a"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/stats", h.GetStats)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/stats", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var res devStatsResponse
	_ = json.NewDecoder(resp.Body).Decode(&res)

	assert.Equal(t, int64(1), res.TotalClients)
	assert.Equal(t, int64(1), res.AuthFailures)
	assert.Equal(t, int64(1), res.ActiveSessions)
	mockKeto.AssertExpectations(t)
}

func TestGetRPUsageDaily_UserScopesItemsToVisibleClients(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients" {
			return httpJSONAny(r, http.StatusOK, []map[string]any{
				{"client_id": "client-owned", "client_name": "Owned App", "metadata": map[string]any{"tenant_id": "tenant-a"}},
				{"client_id": "client-other", "client_name": "Other App", "metadata": map[string]any{"tenant_id": "tenant-a"}},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-owned", "view").Return(true, nil)
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-other", "view").Return(false, nil)
	mockKeto.On(
		"ListRelations",
		mock.Anything,
		"RelyingParty",
		mock.Anything,
		mock.Anything,
		mock.Anything,
	).Return([]service.RelationTuple{}, nil).Maybe()

	usageRepo := &fakeRPUsageQueryRepo{
		items: []domain.RPUsageDailyMetric{
			{Date: "2026-05-12", TenantID: "tenant-a", ClientID: "client-owned", ClientName: "Owned App", LoginRequests: 3},
			{Date: "2026-05-12", TenantID: "tenant-a", ClientID: "client-other", ClientName: "Other App", LoginRequests: 9},
		},
	}

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto:           mockKeto,
		RPUsageQueries: usageRepo,
	}

	app := fiber.New()
	tenantID := "tenant-a"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/rp-usage/daily", h.GetRPUsageDaily)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/rp-usage/daily?days=14&period=day", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var res devRPUsageDailyResponse
	_ = json.NewDecoder(resp.Body).Decode(&res)

	if assert.Len(t, res.Items, 1) {
		assert.Equal(t, "client-owned", res.Items[0].ClientID)
	}
	assert.Equal(t, "tenant-a", usageRepo.query.TenantID)
	mockKeto.AssertExpectations(t)
}

func TestDevHandler_NoAuditNoAction(t *testing.T) {
	h := &DevHandler{
		Hydra:     &service.HydraAdminService{AdminURL: "http://hydra.test"},
		AuditRepo: nil, // Missing
		Keto:      new(devMockKetoService),
	}

	t.Run("Mutating action fails when audit log is unavailable", func(t *testing.T) {
		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			if h.AuditRepo == nil {
				return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Audit service unavailable"})
			}
			return c.Next()
		})
		app.Post("/api/v1/dev/clients", h.CreateClient)

		req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader([]byte("{}")))
		resp, _ := app.Test(req, -1)

		assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
	})
}

func TestCreateClient_HeadlessLoginPayloadMapping(t *testing.T) {
	var captured domain.HydraClient

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodPost && r.URL.Path == "/clients" {
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			err = json.Unmarshal(body, &captured)
			assert.NoError(t, err)

			return httpJSONAny(r, http.StatusCreated, map[string]any{
				"client_id":                  captured.ClientID,
				"client_name":                captured.ClientName,
				"redirect_uris":              captured.RedirectURIs,
				"grant_types":                captured.GrantTypes,
				"response_types":             captured.ResponseTypes,
				"scope":                      captured.Scope,
				"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
				"jwks":                       captured.JWKS,
				"metadata":                   captured.Metadata,
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			PublicURL:  "http://hydra.public",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"name":                    "Headless Login App",
		"type":                    "private",
		"redirectUris":            []string{"https://rp.example.com/callback"},
		"scopes":                  []string{"openid", "profile"},
		"tokenEndpointAuthMethod": "private_key_jwt",
		"jwksUri":                 "https://rp.example.com/.well-known/jwks.json",
		"metadata": map[string]any{
			"headless_login_enabled":     true,
			"request_object_signing_alg": "RS256",
		},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)
	assert.Equal(t, "private_key_jwt", captured.TokenEndpointAuthMethod)
	assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.JWKSUri)
	assert.Nil(t, captured.JWKS)
	assert.Equal(t, "private_key_jwt", captured.Metadata["headless_token_endpoint_auth_method"])
	assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.Metadata["headless_jwks_uri"])
	assert.True(t, captured.IsHeadlessLoginEnabled())
	assert.Equal(t, true, captured.Metadata["headless_login_enabled"])
	_, hasRequestObjectAlg := captured.Metadata["request_object_signing_alg"]
	assert.False(t, hasRequestObjectAlg)
}

func TestCreateClient_DefaultsSkipConsentToTrue(t *testing.T) {
	var captured domain.HydraClient

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodPost && r.URL.Path == "/clients" {
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			err = json.Unmarshal(body, &captured)
			assert.NoError(t, err)

			return httpJSONAny(r, http.StatusCreated, map[string]any{
				"client_id":                  captured.ClientID,
				"client_name":                captured.ClientName,
				"redirect_uris":              captured.RedirectURIs,
				"grant_types":                captured.GrantTypes,
				"response_types":             captured.ResponseTypes,
				"scope":                      captured.Scope,
				"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
				"skip_consent":               captured.SkipConsent,
				"metadata":                   captured.Metadata,
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			PublicURL:  "http://hydra.public",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"name":         "Trusted App",
		"type":         "pkce",
		"redirectUris": []string{"https://rp.example.com/callback"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)
	assert.NotNil(t, captured.SkipConsent)
	assert.True(t, *captured.SkipConsent)
}

func TestNormalizeClientAutoLoginMetadata(t *testing.T) {
	t.Run("keeps supported flag and URL", func(t *testing.T) {
		metadata, err := normalizeClientAutoLoginMetadata(map[string]any{
			"auto_login_supported": true,
			"auto_login_url":       "https://rp.example.com/login?auto=1",
		})
		assert.NoError(t, err)
		assert.Equal(t, true, metadata["auto_login_supported"])
		assert.Equal(t, "https://rp.example.com/login?auto=1", metadata["auto_login_url"])
	})

	t.Run("requires URL when supported", func(t *testing.T) {
		_, err := normalizeClientAutoLoginMetadata(map[string]any{
			"auto_login_supported": true,
		})
		assert.Error(t, err)
	})

	t.Run("removes URL when unsupported", func(t *testing.T) {
		metadata, err := normalizeClientAutoLoginMetadata(map[string]any{
			"auto_login_supported": false,
			"auto_login_url":       "https://rp.example.com/login?auto=1",
		})
		assert.NoError(t, err)
		assert.Equal(t, false, metadata["auto_login_supported"])
		_, exists := metadata["auto_login_url"]
		assert.False(t, exists)
	})
}

func TestCreateClient_AllowsExplicitSkipConsentFalse(t *testing.T) {
	var captured domain.HydraClient

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodPost && r.URL.Path == "/clients" {
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			err = json.Unmarshal(body, &captured)
			assert.NoError(t, err)

			return httpJSONAny(r, http.StatusCreated, map[string]any{
				"client_id":                  captured.ClientID,
				"client_name":                captured.ClientName,
				"redirect_uris":              captured.RedirectURIs,
				"grant_types":                captured.GrantTypes,
				"response_types":             captured.ResponseTypes,
				"scope":                      captured.Scope,
				"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
				"skip_consent":               captured.SkipConsent,
				"metadata":                   captured.Metadata,
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			PublicURL:  "http://hydra.public",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"name":         "Consent Required App",
		"type":         "pkce",
		"skipConsent":  false,
		"redirectUris": []string{"https://rp.example.com/callback"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)
	assert.NotNil(t, captured.SkipConsent)
	assert.False(t, *captured.SkipConsent)
}

func TestCreateClient_LegacyPKCEHeadlessInputIsNormalizedToPrivate(t *testing.T) {
	var captured domain.HydraClient

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodPost && r.URL.Path == "/clients" {
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			err = json.Unmarshal(body, &captured)
			assert.NoError(t, err)

			return httpJSONAny(r, http.StatusCreated, map[string]any{
				"client_id":                  captured.ClientID,
				"client_name":                captured.ClientName,
				"redirect_uris":              captured.RedirectURIs,
				"grant_types":                captured.GrantTypes,
				"response_types":             captured.ResponseTypes,
				"scope":                      captured.Scope,
				"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
				"jwks_uri":                   captured.JWKSUri,
				"jwks":                       captured.JWKS,
				"metadata":                   captured.Metadata,
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			PublicURL:  "http://hydra.public",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"name":         "Legacy Headless Login App",
		"type":         "pkce",
		"redirectUris": []string{"https://rp.example.com/callback"},
		"metadata": map[string]any{
			"headless_login_enabled": true,
			"headless_jwks_uri":      "https://rp.example.com/.well-known/jwks.json",
		},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)
	assert.Equal(t, "private_key_jwt", captured.TokenEndpointAuthMethod)
	assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.JWKSUri)
	assert.Nil(t, captured.JWKS)
	assert.Equal(t, "private_key_jwt", captured.Metadata["headless_token_endpoint_auth_method"])
	assert.True(t, captured.IsHeadlessLoginEnabled())
}

func TestMapClientSummary_ClassifiesHeadlessLoginAsPrivate(t *testing.T) {
	h := &DevHandler{}

	summary := h.mapClientSummary(domain.HydraClient{
		ClientID:                "client-headless-login",
		ClientName:              "Headless Login App",
		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",
		},
	})

	assert.Equal(t, "private", summary.Type)
}

func TestCreateClient_HeadlessLoginRejectsInlineJWKS(t *testing.T) {
	var hydraCalled bool
	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:  "http://hydra.test",
			PublicURL: "http://hydra.public",
			HTTPClient: &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
				hydraCalled = true
				return httpJSONAny(r, http.StatusCreated, map[string]any{
					"client_id":                  "client-headless-login",
					"client_name":                "Headless Login App",
					"redirect_uris":              []string{"https://rp.example.com/callback"},
					"grant_types":                []string{"authorization_code", "refresh_token"},
					"response_types":             []string{"code"},
					"scope":                      "openid profile",
					"token_endpoint_auth_method": "none",
					"metadata": map[string]any{
						"headless_login_enabled": true,
					},
				}), nil
			})},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"name":                    "Headless Login App",
		"type":                    "pkce",
		"redirectUris":            []string{"https://rp.example.com/callback"},
		"scopes":                  []string{"openid", "profile"},
		"tokenEndpointAuthMethod": "private_key_jwt",
		"jwks": map[string]any{
			"keys": []map[string]any{{
				"kty": "RSA",
				"alg": "RS256",
				"n":   "AQIDBAUGBw",
				"e":   "AQAB",
			}},
		},
		"metadata": map[string]any{
			"headless_login_enabled":     true,
			"request_object_signing_alg": "RS256",
		},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
	defer resp.Body.Close()

	bodyBytes, _ := io.ReadAll(resp.Body)
	assert.Contains(t, string(bodyBytes), "headless login supports jwksUri only")
	assert.False(t, hydraCalled)
}

func TestCreateClient_NormalizesIDTokenClaimsMetadata(t *testing.T) {
	var captured domain.HydraClient

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodPost && r.URL.Path == "/clients" {
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			assert.NoError(t, json.Unmarshal(body, &captured))

			return httpJSONAny(r, http.StatusCreated, map[string]any{
				"client_id":                  captured.ClientID,
				"client_name":                captured.ClientName,
				"redirect_uris":              captured.RedirectURIs,
				"grant_types":                captured.GrantTypes,
				"response_types":             captured.ResponseTypes,
				"scope":                      captured.Scope,
				"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
				"metadata":                   captured.Metadata,
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			PublicURL:  "http://hydra.public",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"name":         "Claims App",
		"type":         "pkce",
		"redirectUris": []string{"https://rp.example.com/callback"},
		"metadata": map[string]any{
			"id_token_claims": []map[string]any{
				{
					"id":        "claim-1",
					"namespace": "rp_claims",
					"key":       "locale",
					"value":     " ko-KR ",
					"valueType": "text",
				},
				{
					"id":        "claim-2",
					"namespace": "rp_claims",
					"key":       "tier",
					"value":     "2",
					"valueType": "number",
				},
			},
		},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)

	claims, ok := captured.Metadata[domain.MetadataIDTokenClaims].([]any)
	if assert.True(t, ok) && assert.Len(t, claims, 2) {
		first, ok := claims[0].(map[string]any)
		if assert.True(t, ok) {
			assert.Equal(t, "rp_claims", first["namespace"])
			assert.Equal(t, "locale", first["key"])
			assert.Equal(t, "ko-KR", first["value"])
			assert.Equal(t, "text", first["valueType"])
			_, hasID := first["id"]
			assert.False(t, hasID)
		}

		second, ok := claims[1].(map[string]any)
		if assert.True(t, ok) {
			assert.Equal(t, "rp_claims", second["namespace"])
			assert.Equal(t, "tier", second["key"])
			assert.Equal(t, "2", second["value"])
			assert.Equal(t, "number", second["valueType"])
		}
	}
}

func TestCreateClient_RejectsInvalidIDTokenClaimsMetadata(t *testing.T) {
	hydraCalled := false
	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:  "http://hydra.test",
			PublicURL: "http://hydra.public",
			HTTPClient: &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
				hydraCalled = true
				return httpJSONAny(r, http.StatusCreated, map[string]any{}), nil
			})},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"name":         "Claims App",
		"type":         "pkce",
		"redirectUris": []string{"https://rp.example.com/callback"},
		"metadata": map[string]any{
			"id_token_claims": []map[string]any{
				{
					"namespace": "top_level",
					"key":       "rp_claims",
					"value":     "forbidden",
					"valueType": "text",
				},
			},
		},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
	defer resp.Body.Close()

	bodyBytes, _ := io.ReadAll(resp.Body)
	assert.Contains(t, string(bodyBytes), "top_level namespace is managed from admin user custom claims")
	assert.False(t, hydraCalled)
}

func TestUpdateClient_HeadlessLoginPayloadMapping(t *testing.T) {
	var captured domain.HydraClient

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-headless-login" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":                  "client-headless-login",
				"client_name":                "Headless Login Before",
				"redirect_uris":              []string{"https://before.example.com/callback"},
				"grant_types":                []string{"authorization_code", "refresh_token"},
				"response_types":             []string{"code"},
				"scope":                      "openid profile",
				"token_endpoint_auth_method": "none",
				"metadata": map[string]any{
					"status":                     "active",
					"headless_jwks":              map[string]any{"keys": []map[string]any{}},
					"headless_jwks_uri":          "https://stale.example.com/old.json",
					"headless_login_enabled":     true,
					"request_object_signing_alg": "RS256",
				},
			}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-headless-login" {
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			err = json.Unmarshal(body, &captured)
			assert.NoError(t, err)

			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":                  captured.ClientID,
				"client_name":                captured.ClientName,
				"redirect_uris":              captured.RedirectURIs,
				"grant_types":                captured.GrantTypes,
				"response_types":             captured.ResponseTypes,
				"scope":                      captured.Scope,
				"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
				"jwks_uri":                   captured.JWKSUri,
				"metadata":                   captured.Metadata,
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			PublicURL:  "http://hydra.public",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

	body, _ := json.Marshal(map[string]any{
		"name":                    "Headless Login After",
		"type":                    "private",
		"tokenEndpointAuthMethod": "private_key_jwt",
		"jwksUri":                 "https://rp.example.com/.well-known/jwks.json",
		"metadata": map[string]any{
			"headless_login_enabled":     true,
			"request_object_signing_alg": "RS256",
		},
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-headless-login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Equal(t, "private_key_jwt", captured.TokenEndpointAuthMethod)
	assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.JWKSUri)
	assert.Equal(t, "private_key_jwt", captured.Metadata["headless_token_endpoint_auth_method"])
	assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.Metadata["headless_jwks_uri"])
	_, hasInlineJWKS := captured.Metadata["headless_jwks"]
	assert.False(t, hasInlineJWKS)
	_, hasRequestObjectAlg := captured.Metadata["request_object_signing_alg"]
	assert.False(t, hasRequestObjectAlg)
	assert.True(t, captured.IsHeadlessLoginEnabled())
	assert.Equal(t, true, captured.Metadata["headless_login_enabled"])
}

func TestUpdateClient_AllowsExplicitSkipConsentFalse(t *testing.T) {
	var captured domain.HydraClient
	currentSkipConsent := true

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, domain.HydraClient{
				ClientID:                "client-1",
				ClientName:              "Trusted Before",
				RedirectURIs:            []string{"https://rp.example.com/callback"},
				GrantTypes:              []string{"authorization_code", "refresh_token"},
				ResponseTypes:           []string{"code"},
				Scope:                   "openid profile",
				TokenEndpointAuthMethod: "none",
				SkipConsent:             &currentSkipConsent,
				Metadata:                map[string]any{"status": "active"},
			}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			err = json.Unmarshal(body, &captured)
			assert.NoError(t, err)

			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":                  captured.ClientID,
				"client_name":                captured.ClientName,
				"redirect_uris":              captured.RedirectURIs,
				"grant_types":                captured.GrantTypes,
				"response_types":             captured.ResponseTypes,
				"scope":                      captured.Scope,
				"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
				"skip_consent":               captured.SkipConsent,
				"metadata":                   captured.Metadata,
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			PublicURL:  "http://hydra.public",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

	body, _ := json.Marshal(map[string]any{
		"name":        "Consent Required After",
		"type":        "pkce",
		"skipConsent": false,
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.NotNil(t, captured.SkipConsent)
	assert.False(t, *captured.SkipConsent)
}

func TestUpdateClient_HeadlessLoginIgnoresExistingTopLevelJWKS(t *testing.T) {
	var captured domain.HydraClient

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-headless-login" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":                  "client-headless-login",
				"client_name":                "Headless Login Before",
				"redirect_uris":              []string{"https://before.example.com/callback"},
				"grant_types":                []string{"authorization_code", "refresh_token"},
				"response_types":             []string{"code"},
				"scope":                      "openid profile",
				"token_endpoint_auth_method": "none",
				"jwks": map[string]any{
					"keys": []map[string]any{{
						"kty": "RSA",
						"alg": "RS256",
						"n":   "AQIDBAUGBw",
						"e":   "AQAB",
					}},
				},
				"metadata": map[string]any{
					"status":                     "active",
					"headless_login_enabled":     true,
					"headless_jwks_uri":          "https://stale.example.com/old.json",
					"request_object_signing_alg": "RS256",
				},
			}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-headless-login" {
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			err = json.Unmarshal(body, &captured)
			assert.NoError(t, err)

			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":                  captured.ClientID,
				"client_name":                captured.ClientName,
				"redirect_uris":              captured.RedirectURIs,
				"grant_types":                captured.GrantTypes,
				"response_types":             captured.ResponseTypes,
				"scope":                      captured.Scope,
				"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
				"jwks_uri":                   captured.JWKSUri,
				"metadata":                   captured.Metadata,
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			PublicURL:  "http://hydra.public",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

	body, _ := json.Marshal(map[string]any{
		"name":                    "Headless Login After",
		"type":                    "pkce",
		"tokenEndpointAuthMethod": "private_key_jwt",
		"jwksUri":                 "https://rp.example.com/.well-known/jwks.json",
		"metadata": map[string]any{
			"headless_login_enabled":     true,
			"request_object_signing_alg": "RS256",
		},
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-headless-login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Nil(t, captured.JWKS)
	assert.Equal(t, "private_key_jwt", captured.TokenEndpointAuthMethod)
	assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.JWKSUri)
	assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.Metadata["headless_jwks_uri"])
	_, hasRequestObjectAlg := captured.Metadata["request_object_signing_alg"]
	assert.False(t, hasRequestObjectAlg)
}

func TestUpdateClient_RevokesExistingConsentsWhenTenantPolicyChanges(t *testing.T) {
	var revokedSubjects []string

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, domain.HydraClient{
				ClientID:                "client-1",
				ClientName:              "Tenant Guarded App",
				RedirectURIs:            []string{"https://rp.example.com/callback"},
				GrantTypes:              []string{"authorization_code", "refresh_token"},
				ResponseTypes:           []string{"code"},
				Scope:                   "openid tenant profile email",
				TokenEndpointAuthMethod: "none",
				Metadata: map[string]any{
					"tenant_access_restricted": true,
					"allowed_tenants":          []string{"tenant-a"},
				},
			}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
			var updated domain.HydraClient
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			assert.NoError(t, json.Unmarshal(body, &updated))
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":                  updated.ClientID,
				"client_name":                updated.ClientName,
				"redirect_uris":              updated.RedirectURIs,
				"grant_types":                updated.GrantTypes,
				"response_types":             updated.ResponseTypes,
				"scope":                      updated.Scope,
				"token_endpoint_auth_method": updated.TokenEndpointAuthMethod,
				"metadata":                   updated.Metadata,
			}), nil
		}
		if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
			revokedSubjects = append(revokedSubjects, r.URL.Query().Get("subject"))
			assert.Equal(t, "client-1", r.URL.Query().Get("client"))
			return httpResponse(r, http.StatusNoContent, ""), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	consentRepo := &mockConsentRepo{
		consents: []domain.ClientConsent{
			{ClientID: "client-1", Subject: "user-1"},
			{ClientID: "client-1", Subject: "user-2"},
			{ClientID: "other-client", Subject: "user-3"},
		},
	}

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			PublicURL:  "http://hydra.public",
			HTTPClient: &http.Client{Transport: transport},
		},
		ConsentRepo: consentRepo,
		Keto:        new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

	body, _ := json.Marshal(map[string]any{
		"metadata": map[string]any{
			"tenant_access_restricted": true,
			"allowed_tenants":          []string{"tenant-b"},
		},
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.ElementsMatch(t, []string{"user-1", "user-2"}, revokedSubjects)
	assert.Len(t, consentRepo.consents, 1)
	assert.Equal(t, "other-client", consentRepo.consents[0].ClientID)
}

func TestUpdateClient_DoesNotRevokeConsentsWhenTenantPolicyUnchanged(t *testing.T) {
	revoked := false

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, domain.HydraClient{
				ClientID:                "client-1",
				ClientName:              "Tenant Guarded App",
				RedirectURIs:            []string{"https://rp.example.com/callback"},
				GrantTypes:              []string{"authorization_code", "refresh_token"},
				ResponseTypes:           []string{"code"},
				Scope:                   "openid tenant profile email",
				TokenEndpointAuthMethod: "none",
				Metadata: map[string]any{
					"tenant_access_restricted": true,
					"allowed_tenants":          []string{"tenant-a"},
				},
			}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
			var updated domain.HydraClient
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			assert.NoError(t, json.Unmarshal(body, &updated))
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":                  updated.ClientID,
				"client_name":                updated.ClientName,
				"redirect_uris":              updated.RedirectURIs,
				"grant_types":                updated.GrantTypes,
				"response_types":             updated.ResponseTypes,
				"scope":                      updated.Scope,
				"token_endpoint_auth_method": updated.TokenEndpointAuthMethod,
				"metadata":                   updated.Metadata,
			}), nil
		}
		if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
			revoked = true
			return httpResponse(r, http.StatusNoContent, ""), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	consentRepo := &mockConsentRepo{
		consents: []domain.ClientConsent{
			{ClientID: "client-1", Subject: "user-1"},
		},
	}

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			PublicURL:  "http://hydra.public",
			HTTPClient: &http.Client{Transport: transport},
		},
		ConsentRepo: consentRepo,
		Keto:        new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

	body, _ := json.Marshal(map[string]any{
		"name": "Renamed App",
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.False(t, revoked)
	assert.Len(t, consentRepo.consents, 1)
}

func TestRefreshHeadlessJWKSCache_ReturnsUpdatedCacheState(t *testing.T) {
	privateKey, jwks := mustHeadlessRSAJWK(t)
	_ = privateKey
	jwksBody, _ := json.Marshal(jwks)
	expectedN := devTestJWKSFirstKeyString(t, jwks, "n")
	redisRepo := &devMockRedisRepo{data: map[string]string{}}
	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:  "http://hydra.test",
			PublicURL: "http://hydra.public",
			HTTPClient: &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
				if r.Method == http.MethodGet && r.URL.Path == "/clients/client-headless-login" {
					return httpJSONAny(r, http.StatusOK, domain.HydraClient{
						ClientID: "client-headless-login",
						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",
						},
					}), nil
				}
				return httpJSONAny(r, http.StatusNotFound, nil), nil
			})},
		},
		Redis: redisRepo,
		HeadlessJWKS: service.NewHeadlessJWKSCacheService(redisRepo, &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
			assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", r.URL.String())
			var payload map[string]any
			_ = json.Unmarshal(jwksBody, &payload)
			return httpJSONAny(r, http.StatusOK, payload), nil
		})}),
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients/:id/headless-jwks/refresh", h.RefreshHeadlessJWKSCache)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients/client-headless-login/headless-jwks/refresh", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var got clientDetailResponse
	err := json.NewDecoder(resp.Body).Decode(&got)
	assert.NoError(t, err)
	if assert.NotNil(t, got.HeadlessJWKSCache) {
		assert.Equal(t, "success", got.HeadlessJWKSCache.LastRefreshStatus)
		assert.Equal(t, []string{"test-kid"}, got.HeadlessJWKSCache.CachedKids)
		if assert.Len(t, got.HeadlessJWKSCache.ParsedKeys, 1) {
			assert.Equal(t, "test-kid", got.HeadlessJWKSCache.ParsedKeys[0].Kid)
			assert.Equal(t, "RSA", got.HeadlessJWKSCache.ParsedKeys[0].Kty)
			assert.Equal(t, "sig", got.HeadlessJWKSCache.ParsedKeys[0].Use)
			assert.Equal(t, "RS256", got.HeadlessJWKSCache.ParsedKeys[0].Alg)
			assert.Equal(t, expectedN, got.HeadlessJWKSCache.ParsedKeys[0].N)
		}
	}
}

func TestRevokeHeadlessJWKSCache_DeletesCachedState(t *testing.T) {
	redisRepo := &devMockRedisRepo{data: map[string]string{}}
	cacheService := service.NewHeadlessJWKSCacheService(redisRepo, nil)
	now := time.Now()
	expiresAt := now.Add(30 * time.Minute)
	err := cacheService.SaveState("client-headless-login", domain.HeadlessJWKSCacheState{
		ClientID:            "client-headless-login",
		JWKSURI:             "https://rp.example.com/.well-known/jwks.json",
		CachedAt:            &now,
		ExpiresAt:           &expiresAt,
		LastRefreshStatus:   "success",
		ConsecutiveFailures: 0,
		RawJWKS:             `{"keys":[{"kid":"cached-key","kty":"RSA","n":"AQIDBAUGBw","e":"AQAB"}]}`,
	})
	assert.NoError(t, err)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:  "http://hydra.test",
			PublicURL: "http://hydra.public",
			HTTPClient: &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
				if r.Method == http.MethodGet && r.URL.Path == "/clients/client-headless-login" {
					return httpJSONAny(r, http.StatusOK, domain.HydraClient{
						ClientID: "client-headless-login",
						Metadata: map[string]any{
							"status":                 "active",
							"headless_login_enabled": true,
						},
					}), nil
				}
				return httpJSONAny(r, http.StatusNotFound, nil), nil
			})},
		},
		Redis:        redisRepo,
		HeadlessJWKS: cacheService,
		Keto:         new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Delete("/api/v1/dev/clients/:id/headless-jwks/cache", h.RevokeHeadlessJWKSCache)

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/dev/clients/client-headless-login/headless-jwks/cache", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusNoContent, resp.StatusCode)

	stored, err := cacheService.GetState("client-headless-login")
	assert.Error(t, err)
	assert.Nil(t, stored)
}

func TestListAuditLogs_TenantMemberWithoutAuditPermissionReturnsEmpty(t *testing.T) {
	h := &DevHandler{
		Hydra:     &service.HydraAdminService{AdminURL: "http://hydra.test"},
		AuditRepo: &mockAuditRepo{},
		Keto:      new(devMockKetoService),
	}

	app := fiber.New()
	tenantID := "tenant-a"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "u-member",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/audit-logs", h.ListAuditLogs)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/audit-logs", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result devAuditListResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	assert.Empty(t, result.Items)
}

func TestListAuditLogs_RPAdminScope(t *testing.T) {
	auditRepo := &mockAuditRepo{
		logs: []domain.AuditLog{
			{
				EventID:   "evt-1",
				EventType: "POST /api/v1/dev/clients",
				Status:    "success",
				Timestamp: time.Now().UTC(),
				Details:   `{"target_id":"client-allowed","tenant_id":"tenant-a","action":"CREATE_CLIENT"}`,
			},
			{
				EventID:   "evt-2",
				EventType: "POST /api/v1/dev/clients",
				Status:    "success",
				Timestamp: time.Now().UTC().Add(-time.Minute),
				Details:   `{"target_id":"client-other","tenant_id":"tenant-a","action":"CREATE_CLIENT"}`,
			},
		},
	}

	h := &DevHandler{
		Hydra:     &service.HydraAdminService{AdminURL: "http://hydra.test"},
		AuditRepo: auditRepo,
		Keto:      new(devMockKetoService),
	}

	app := fiber.New()
	tenantID := "tenant-a"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "u-rp-admin",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
			Metadata: map[string]any{
				"managed_client_ids": []any{"client-allowed"},
			},
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/audit-logs", h.ListAuditLogs)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/audit-logs?limit=50", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result devAuditListResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	assert.Len(t, result.Items, 1)
	assert.Equal(t, "evt-1", result.Items[0].EventID)
}

func TestListAuditLogs_UserAllowedByRPAuditPermission(t *testing.T) {
	auditRepo := &mockAuditRepo{
		logs: []domain.AuditLog{
			{
				EventID:   "evt-allowed",
				EventType: "POST /api/v1/dev/clients/client-allowed/secret/rotate",
				Status:    "success",
				Timestamp: time.Now().UTC(),
				Details:   `{"target_id":"client-allowed","tenant_id":"tenant-a","action":"ROTATE_SECRET"}`,
			},
			{
				EventID:   "evt-allowed-path",
				EventType: "GET /api/v1/dev/clients/client-allowed/relations",
				Status:    "success",
				Timestamp: time.Now().UTC().Add(-30 * time.Second),
				Details:   `{"request_id":"req-1"}`,
			},
			{
				EventID:   "evt-denied",
				EventType: "POST /api/v1/dev/clients/client-denied/secret/rotate",
				Status:    "success",
				Timestamp: time.Now().UTC().Add(-time.Minute),
				Details:   `{"target_id":"client-denied","tenant_id":"tenant-b","action":"ROTATE_SECRET"}`,
			},
		},
	}
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients" {
			return httpJSONAny(r, http.StatusOK, []map[string]any{
				{"client_id": "client-allowed", "client_name": "Allowed App", "metadata": map[string]any{"tenant_id": "tenant-a"}},
				{"client_id": "client-denied", "client_name": "Denied App", "metadata": map[string]any{"tenant_id": "tenant-b"}},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-allowed", "audit_viewer").Return(true, nil)
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-denied", "audit_viewer").Return(false, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		AuditRepo: auditRepo,
		Keto:      mockKeto,
	}

	app := fiber.New()
	tenantID := "tenant-a"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/audit-logs", h.ListAuditLogs)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/audit-logs?limit=50", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result devAuditListResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	if assert.Len(t, result.Items, 2) {
		assert.Equal(t, "evt-allowed", result.Items[0].EventID)
		assert.Equal(t, "evt-allowed-path", result.Items[1].EventID)
	}
	mockKeto.AssertExpectations(t)
}

func TestListConsents_UserAllowedByRPAdminsRelation(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "view_consents").Return(true, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		ConsentRepo: &mockConsentRepo{
			consents: []domain.ClientConsent{
				{
					ClientID:      "client-1",
					Subject:       "subject-1",
					GrantedScopes: []string{"openid", "profile"},
					CreatedAt:     time.Now().UTC(),
				},
			},
		},
		Keto: mockKeto,
	}

	app := fiber.New()
	tenantID := "tenant-1"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/consents", h.ListConsents)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/consents?client_id=client-1", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result consentListResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	if assert.Len(t, result.Items, 1) {
		assert.Equal(t, "client-1", result.Items[0].ClientID)
		assert.Equal(t, "subject-1", result.Items[0].Subject)
	}
	mockKeto.AssertExpectations(t)
}

func TestListConsents_IncludesRPUserMetadata(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	repo := new(devMockRPUserMetadataRepo)
	repo.On("Get", mock.Anything, "client-1", "subject-1").Return(&domain.RPUserMetadata{
		ClientID: "client-1",
		UserID:   "subject-1",
		Metadata: domain.JSONMap{
			"approvalLevel": "A",
			"reviewedAt":    "2026-06-09T09:30:00+09:00",
		},
	}, nil).Once()

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		ConsentRepo: &mockConsentRepo{
			consents: []domain.ClientConsent{
				{
					ClientID:      "client-1",
					Subject:       "subject-1",
					GrantedScopes: []string{"openid", "profile"},
					CreatedAt:     time.Now().UTC(),
				},
			},
		},
		RPUserMetadataRepo: repo,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "admin", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Get("/api/v1/dev/consents", h.ListConsents)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/consents?client_id=client-1", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var result consentListResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	if assert.Len(t, result.Items, 1) {
		assert.Equal(t, domain.JSONMap{
			"approvalLevel": "A",
			"reviewedAt":    "2026-06-09T09:30:00+09:00",
		}, result.Items[0].RPMetadata)
	}
	repo.AssertExpectations(t)
}

func TestNormalizeIDTokenClaimsMetadata_AllowsDateAndDatetime(t *testing.T) {
	metadata, err := normalizeIDTokenClaimsMetadata(map[string]any{
		domain.MetadataIDTokenClaims: []any{
			map[string]any{
				"namespace": "rp_claims",
				"key":       "contract_date",
				"value":     "2026-06-09",
				"valueType": "date",
			},
			map[string]any{
				"namespace": "rp_claims",
				"key":       "approved_at",
				"value":     "2026-06-09T09:30:00+09:00",
				"valueType": "datetime",
			},
		},
	})

	assert.NoError(t, err)
	claims := metadata[domain.MetadataIDTokenClaims].([]normalizedIDTokenClaim)
	assert.Equal(t, "date", claims[0].ValueType)
	assert.Equal(t, "datetime", claims[1].ValueType)
}

func TestUpdateClient_RejectsTopLevelIDTokenClaimsFromDevConsole(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":                  "client-1",
				"client_name":                "App One",
				"redirect_uris":              []string{"http://localhost/cb"},
				"grant_types":                []string{"authorization_code"},
				"response_types":             []string{"code"},
				"scope":                      "openid profile",
				"token_endpoint_auth_method": "none",
				"metadata":                   map[string]any{"status": "active"},
			}), nil
		}
		if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
			t.Fatalf("hydra update should not be called for top-level id token claims")
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "admin", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

	body, _ := json.Marshal(map[string]any{
		"metadata": map[string]any{
			domain.MetadataIDTokenClaims: []any{
				map[string]any{
					"namespace": "top_level",
					"key":       "employee_id",
					"value":     "EMP001",
					"valueType": "text",
				},
			},
		},
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}

func TestListClientRelations_RPAdminAllowedByViewRelationshipsPermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "view_relationships").Return(true, nil)
	mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "client-1", "config_editor", "").Return([]service.RelationTuple{
		{Object: "client-1", Relation: "config_editor", SubjectID: "User:user-2"},
	}, nil)
	for _, relation := range []string{"admins", "creator", "secret_viewer", "secret_rotator", "jwks_viewer", "jwks_operator", "consent_viewer", "consent_revoker", "relationship_viewer", "audit_viewer", "status_operator"} {
		mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "client-1", relation, "").Return([]service.RelationTuple{}, nil)
	}
	mockKratos := new(devMockKratosAdmin)
	mockKratos.On("GetIdentity", mock.Anything, "user-2").Return(&service.KratosIdentity{
		ID: "user-2",
		Traits: map[string]any{
			"name":  "김용연",
			"email": "kyy@example.com",
			"id":    "kyy01",
		},
	}, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto:        mockKeto,
		KratosAdmin: mockKratos,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		tenantID := "tenant-1"
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/clients/:id/relations", h.ListClientRelations)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/client-1/relations", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var result clientRelationListResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	assert.Len(t, result.Items, 1)
	assert.Equal(t, "config_editor", result.Items[0].Relation)
	assert.Equal(t, "User", result.Items[0].SubjectType)
	assert.Equal(t, "user-2", result.Items[0].SubjectID)
	assert.Equal(t, "김용연", result.Items[0].UserName)
	assert.Equal(t, "kyy@example.com", result.Items[0].UserEmail)
	assert.Equal(t, "kyy01", result.Items[0].UserLoginID)
}

func TestListClientRelations_DedupesDuplicateRelations(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "view_relationships").Return(true, nil)
	mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "client-1", "admins", "").Return([]service.RelationTuple{
		{Namespace: "RelyingParty", Object: "client-1", Relation: "admins", SubjectID: "User:user-1"},
		{Namespace: "RelyingParty", Object: "client-1", Relation: "admins", SubjectID: "User:user-1"},
	}, nil)
	for _, relation := range []string{"creator", "config_editor", "secret_viewer", "secret_rotator", "jwks_viewer", "jwks_operator", "consent_viewer", "consent_revoker", "relationship_viewer", "audit_viewer", "status_operator"} {
		mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "client-1", relation, "").Return([]service.RelationTuple{}, nil)
	}

	mockKratos := new(devMockKratosAdmin)
	mockKratos.On("GetIdentity", mock.Anything, "user-1").Return(&service.KratosIdentity{
		ID: "user-1",
		Traits: map[string]any{
			"name":  "Tester",
			"email": "tester@example.com",
		},
	}, nil).Maybe()

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto:        mockKeto,
		KratosAdmin: mockKratos,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		tenantID := "tenant-1"
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/clients/:id/relations", h.ListClientRelations)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/client-1/relations", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var result clientRelationListResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	if assert.Len(t, result.Items, 1) {
		assert.Equal(t, "admins", result.Items[0].Relation)
		assert.Equal(t, "User:user-1", result.Items[0].Subject)
	}
	mockKeto.AssertExpectations(t)
	mockKratos.AssertExpectations(t)
}

func TestAddClientRelation_RPAdminAllowedByTenantGrantPermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "manage").Return(false, nil)
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "Tenant", "tenant-1", "grant_dev_permissions").Return(true, nil)
	mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "client-1", "config_editor", "User:user-2").Return([]service.RelationTuple{}, nil)

	mockOutbox := new(devMockKetoOutboxRepository)
	mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
		return entry.Namespace == "RelyingParty" &&
			entry.Object == "client-1" &&
			entry.Relation == "config_editor" &&
			entry.Subject == "User:user-2" &&
			entry.Action == domain.KetoOutboxActionCreate
	})).Return(nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto:       mockKeto,
		KetoOutbox: mockOutbox,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		tenantID := "tenant-1"
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients/:id/relations", h.AddClientRelation)

	body, _ := json.Marshal(map[string]any{
		"relation": "config_editor",
		"userId":   "user-2",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients/client-1/relations", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)
	mockOutbox.AssertExpectations(t)
}

func TestRemoveClientRelation_RPAdminAllowedByManagePermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "manage").Return(true, nil)

	mockOutbox := new(devMockKetoOutboxRepository)
	mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(entry *domain.KetoOutbox) bool {
		return entry.Namespace == "RelyingParty" &&
			entry.Object == "client-1" &&
			entry.Relation == "config_editor" &&
			entry.Subject == "User:user-2" &&
			entry.Action == domain.KetoOutboxActionDelete
	})).Return(nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto:       mockKeto,
		KetoOutbox: mockOutbox,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		tenantID := "tenant-1"
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Delete("/api/v1/dev/clients/:id/relations", h.RemoveClientRelation)

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/dev/clients/client-1/relations?relation=config_editor&subject=User:user-2", nil)
	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusNoContent, resp.StatusCode)
	mockOutbox.AssertExpectations(t)
}

func TestSearchUsers_RPAdminSearchByNameOrEmailWithinTenantScope(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKratos := new(devMockKratosAdmin)
	mockKratos.On("ListIdentities", mock.Anything).Return([]service.KratosIdentity{
		{
			ID: "user-1",
			Traits: map[string]any{
				"name":      "Alice Kim",
				"email":     "alice@example.com",
				"id":        "alice01",
				"tenant_id": "tenant-1",
			},
		},
		{
			ID: "user-2",
			Traits: map[string]any{
				"name":      "Bob Lee",
				"email":     "bob@example.com",
				"id":        "bob01",
				"tenant_id": "tenant-2",
			},
		},
	}, nil)

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-9", "RelyingParty", "client-1", "manage").Return(true, nil).Maybe()

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		KratosAdmin: mockKratos,
		Keto:        mockKeto,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		tenantID := "tenant-1"
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-9",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
			ManageableTenants: []domain.Tenant{
				{ID: "tenant-1", Slug: "tenant-one"},
			},
			Metadata: map[string]any{
				"managed_client_ids": []any{"client-1"},
			},
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/users", h.SearchUsers)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/users?clientId=client-1&search=alice", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var result devUserListResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	if assert.Len(t, result.Items, 1) {
		assert.Equal(t, "user-1", result.Items[0].ID)
		assert.Equal(t, "Alice Kim", result.Items[0].Name)
		assert.Equal(t, "alice@example.com", result.Items[0].Email)
	}
	mockKratos.AssertExpectations(t)
}

func TestSearchUsers_UserAllowedByRPAdminRelation(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "App One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"status":    "active",
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	mockKeto := new(devMockKetoService)
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(false, nil).Maybe()
	mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "manage").Return(true, nil)

	mockKratos := new(devMockKratosAdmin)
	mockKratos.On("ListIdentities", mock.Anything).Return([]service.KratosIdentity{
		{
			ID: "target-user",
			Traits: map[string]any{
				"name":      "김용연",
				"email":     "kyy@example.com",
				"id":        "kyy01",
				"tenant_id": "tenant-1",
			},
		},
	}, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto:        mockKeto,
		KratosAdmin: mockKratos,
	}

	app := fiber.New()
	tenantID := "tenant-1"
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleUser,
			TenantID: &tenantID,
		})
		return c.Next()
	})
	app.Get("/api/v1/dev/users", h.SearchUsers)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/users?clientId=client-1&search=김용연", nil)
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	var result devUserListResponse
	_ = json.NewDecoder(resp.Body).Decode(&result)
	if assert.Len(t, result.Items, 1) {
		assert.Equal(t, "target-user", result.Items[0].ID)
		assert.Equal(t, "김용연", result.Items[0].Name)
	}
	mockKeto.AssertExpectations(t)
	mockKratos.AssertExpectations(t)
}
