package handler

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

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

type devMockRPUserMetadataRepo struct {
	mock.Mock
}

func (m *devMockRPUserMetadataRepo) Get(ctx context.Context, clientID, userID string) (*domain.RPUserMetadata, error) {
	args := m.Called(ctx, clientID, userID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.RPUserMetadata), args.Error(1)
}

func (m *devMockRPUserMetadataRepo) Upsert(ctx context.Context, metadata *domain.RPUserMetadata) error {
	args := m.Called(ctx, metadata)
	return args.Error(0)
}

func TestDevHandler_RPUserMetadataRoundTrip(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "Client One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"id_token_claims": []map[string]any{
						{
							"namespace": "rp_claims",
							"key":       "approvalLevel",
							"valueType": "text",
							"value":     "A",
						},
						{
							"namespace": "rp_claims",
							"key":       "activeMember",
							"valueType": "boolean",
							"value":     "true",
						},
						{
							"namespace": "rp_claims",
							"key":       "score",
							"valueType": "number",
							"value":     "1",
						},
						{
							"namespace": "rp_claims",
							"key":       "featureList",
							"valueType": "array",
							"value":     `["default"]`,
						},
						{
							"namespace": "rp_claims",
							"key":       "preferences",
							"valueType": "object",
							"value":     `{"theme":"light","density":"comfortable"}`,
						},
						{
							"namespace": "rp_claims",
							"key":       "contractDate",
							"valueType": "date",
							"value":     "2026-06-09",
						},
						{
							"namespace": "rp_claims",
							"key":       "approvedAt",
							"valueType": "datetime",
							"value":     "2026-06-09T09:30",
						},
					},
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	repo := new(devMockRPUserMetadataRepo)
	repo.On("Upsert", mock.Anything, mock.MatchedBy(func(row *domain.RPUserMetadata) bool {
		return row.ClientID == "client-1" &&
			row.UserID == "user-1" &&
			row.Metadata["approvalLevel"] == "A" &&
			row.Metadata["activeMember"] == false &&
			row.Metadata["score"] == float64(42) &&
			assert.ObjectsAreEqual([]any{"sso", "claims"}, row.Metadata["featureList"]) &&
			assert.ObjectsAreEqual(map[string]any{"theme": "dark", "density": "compact"}, row.Metadata["preferences"]) &&
			rpMetadataNumberEquals(row.Metadata["contractDate"], 1781017200) &&
			rpMetadataNumberEquals(row.Metadata["approvedAt"], 1780968600) &&
			row.Metadata["approvalLevel_permissions"].(map[string]any)["readPermission"] == "admin_only" &&
			row.Metadata["approvalLevel_permissions"].(map[string]any)["writePermission"] == "user_and_admin" &&
			row.Metadata["featureList_permissions"].(map[string]any)["readPermission"] == "admin_only" &&
			row.Metadata["featureList_permissions"].(map[string]any)["writePermission"] == "admin_only"
	})).Return(nil).Once()
	repo.On("Get", mock.Anything, "client-1", "user-1").Return(&domain.RPUserMetadata{
		ClientID: "client-1",
		UserID:   "user-1",
		Metadata: domain.JSONMap{"approvalLevel": "A"},
	}, nil).Once()

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		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.Put("/api/v1/dev/clients/:id/users/:userId/metadata", h.UpsertRPUserMetadata)
	app.Get("/api/v1/dev/clients/:id/users/:userId/metadata", h.GetRPUserMetadata)

	body, _ := json.Marshal(map[string]any{
		"metadata": map[string]any{
			"approvalLevel": "A",
			"activeMember":  false,
			"score":         42,
			"featureList":   []string{"sso", "claims"},
			"preferences": map[string]any{
				"theme":   "dark",
				"density": "compact",
			},
			"contractDate": float64(1781017200),
			"approvedAt":   float64(1780968600),
			"approvalLevel_permissions": map[string]any{
				"writePermission": "user_and_admin",
			},
		},
	})
	putReq := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1/users/user-1/metadata", bytes.NewReader(body))
	putReq.Header.Set("Content-Type", "application/json")
	putResp, _ := app.Test(putReq, -1)
	assert.Equal(t, http.StatusOK, putResp.StatusCode)

	getReq := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/client-1/users/user-1/metadata", nil)
	getResp, _ := app.Test(getReq, -1)
	assert.Equal(t, http.StatusOK, getResp.StatusCode)

	var got map[string]any
	assert.NoError(t, json.NewDecoder(getResp.Body).Decode(&got))
	assert.Equal(t, "client-1", got["clientId"])
	assert.Equal(t, "user-1", got["userId"])
	assert.Equal(t, "A", got["metadata"].(map[string]any)["approvalLevel"])
	repo.AssertExpectations(t)
}

func TestDevHandler_RPUserMetadataAdminUpsertRequiresRPManage(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "Client One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"id_token_claims": []map[string]any{
						{
							"namespace":       "rp_claims",
							"key":             "approvalLevel",
							"valueType":       "text",
							"value":           "A",
							"readPermission":  "user_and_admin",
							"writePermission": "user_and_admin",
						},
					},
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	t.Run("tenant grant does not allow rp user metadata admin upsert", func(t *testing.T) {
		repo := new(devMockRPUserMetadataRepo)
		repo.On("Upsert", mock.Anything, mock.AnythingOfType("*domain.RPUserMetadata")).Return(nil).Maybe()
		keto := new(devMockKetoService)
		keto.On("CheckPermission", mock.Anything, "User:operator-1", "RelyingParty", "client-1", "manage").Return(false, nil)
		keto.On("CheckPermission", mock.Anything, "User:operator-1", "Tenant", "tenant-1", "grant_dev_permissions").Return(true, nil).Maybe()
		h := &DevHandler{
			Hydra: &service.HydraAdminService{
				AdminURL:   "http://hydra.test",
				HTTPClient: &http.Client{Transport: transport},
			},
			Keto:               keto,
			RPUserMetadataRepo: repo,
		}
		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_profile", &domain.UserProfileResponse{ID: "operator-1", Role: domain.RoleUser})
			return c.Next()
		})
		app.Put("/api/v1/dev/clients/:id/users/:userId/metadata", h.UpsertRPUserMetadata)

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

		require.Equal(t, http.StatusForbidden, resp.StatusCode)
		repo.AssertNotCalled(t, "Upsert", mock.Anything, mock.Anything)
		keto.AssertExpectations(t)
	})

	t.Run("rp manage allows rp user metadata admin upsert", func(t *testing.T) {
		repo := new(devMockRPUserMetadataRepo)
		repo.On("Upsert", mock.Anything, mock.MatchedBy(func(row *domain.RPUserMetadata) bool {
			return row.ClientID == "client-1" &&
				row.UserID == "user-1" &&
				row.Metadata["approvalLevel"] == "B"
		})).Return(nil).Once()
		keto := new(devMockKetoService)
		keto.On("CheckPermission", mock.Anything, "User:operator-1", "RelyingParty", "client-1", "manage").Return(true, nil)
		h := &DevHandler{
			Hydra: &service.HydraAdminService{
				AdminURL:   "http://hydra.test",
				HTTPClient: &http.Client{Transport: transport},
			},
			Keto:               keto,
			RPUserMetadataRepo: repo,
		}
		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_profile", &domain.UserProfileResponse{ID: "operator-1", Role: domain.RoleUser})
			return c.Next()
		})
		app.Put("/api/v1/dev/clients/:id/users/:userId/metadata", h.UpsertRPUserMetadata)

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

		require.Equal(t, http.StatusOK, resp.StatusCode)
		repo.AssertExpectations(t)
		keto.AssertExpectations(t)
	})
}

func TestDevHandler_RPUserMetadataMirrorsToKratosTraits(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "Client One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"id_token_claims": []map[string]any{
						{
							"namespace":       "rp_claims",
							"key":             "approvalLevel",
							"valueType":       "text",
							"value":           "A",
							"readPermission":  "user_and_admin",
							"writePermission": "admin_only",
						},
					},
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	repo := new(devMockRPUserMetadataRepo)
	repo.On("Upsert", mock.Anything, mock.AnythingOfType("*domain.RPUserMetadata")).Return(nil).Once()
	kratos := new(MockKratosAdmin)
	kratos.On("GetIdentity", mock.Anything, "user-1").Return(&service.KratosIdentity{
		ID:    "user-1",
		State: "active",
		Traits: map[string]any{
			"email": "user@example.com",
			"name":  "User One",
		},
	}, nil).Once()
	var capturedTraits map[string]any
	kratos.On("UpdateIdentity", mock.Anything, "user-1", mock.Anything, "active").Run(func(args mock.Arguments) {
		capturedTraits = args.Get(2).(map[string]any)
	}).Return(&service.KratosIdentity{ID: "user-1", State: "active", Traits: map[string]any{}}, nil).Once()
	identityWriter := service.NewIdentityWriteService(kratos, nil)

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		KratosAdmin:        kratos,
		IdentityWriter:     identityWriter,
		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.Put("/api/v1/dev/clients/:id/users/:userId/metadata", h.UpsertRPUserMetadata)

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

	require.Equal(t, http.StatusOK, resp.StatusCode)
	rpClaims := capturedTraits["rp_custom_claims"].(map[string]any)
	clientClaims := rpClaims["client-1"].(domain.JSONMap)
	require.Equal(t, "B", clientClaims["approvalLevel"])
	require.Equal(t, map[string]any{
		"readPermission":  "user_and_admin",
		"writePermission": "admin_only",
	}, clientClaims["approvalLevel_permissions"])
	repo.AssertExpectations(t)
	kratos.AssertExpectations(t)
}

func rpMetadataNumberEquals(value any, want int64) bool {
	switch typed := value.(type) {
	case int64:
		return typed == want
	case int:
		return int64(typed) == want
	case float64:
		return typed == float64(want)
	case float32:
		return float64(typed) == float64(want)
	default:
		return false
	}
}

func TestDevHandler_SelfUpdateRPUserMetadataHonorsWritePermission(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "Client One",
				"metadata": map[string]any{
					"tenant_id": "tenant-1",
					"id_token_claims": []map[string]any{
						{
							"namespace":       "rp_claims",
							"key":             "approvalLevel",
							"valueType":       "text",
							"value":           "A",
							"readPermission":  "user_and_admin",
							"writePermission": "user_and_admin",
						},
						{
							"namespace":       "rp_claims",
							"key":             "internalRank",
							"valueType":       "text",
							"value":           "S",
							"readPermission":  "admin_only",
							"writePermission": "admin_only",
						},
					},
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	t.Run("rejects admin_only claim", func(t *testing.T) {
		repo := new(devMockRPUserMetadataRepo)
		repo.On("Upsert", mock.Anything, mock.AnythingOfType("*domain.RPUserMetadata")).Return(nil).Maybe()
		h := &DevHandler{
			Hydra: &service.HydraAdminService{
				AdminURL:   "http://hydra.test",
				HTTPClient: &http.Client{Transport: transport},
			},
			RPUserMetadataRepo: repo,
		}
		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleUser})
			return c.Next()
		})
		app.Put("/api/v1/dev/clients/:id/users/me/metadata", h.SelfUpdateRPUserMetadata)

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

		require.Equal(t, http.StatusForbidden, resp.StatusCode)
		repo.AssertNotCalled(t, "Upsert", mock.Anything, mock.Anything)
	})

	t.Run("allows user_and_admin claim for self", func(t *testing.T) {
		repo := new(devMockRPUserMetadataRepo)
		repo.On("Get", mock.Anything, "client-1", "user-1").Return(&domain.RPUserMetadata{
			ClientID: "client-1",
			UserID:   "user-1",
			Metadata: domain.JSONMap{
				"internalRank": "S",
				"internalRank_permissions": map[string]any{
					"readPermission":  "admin_only",
					"writePermission": "admin_only",
				},
			},
		}, nil).Once()
		repo.On("Upsert", mock.Anything, mock.MatchedBy(func(row *domain.RPUserMetadata) bool {
			return row.ClientID == "client-1" &&
				row.UserID == "user-1" &&
				row.Metadata["approvalLevel"] == "B" &&
				row.Metadata["internalRank"] == "S"
		})).Return(nil).Once()
		kratos := new(MockKratosAdmin)
		kratos.On("GetIdentity", mock.Anything, "user-1").Return(&service.KratosIdentity{
			ID:    "user-1",
			State: "active",
			Traits: map[string]any{
				"email": "user@example.com",
			},
		}, nil).Once()
		var capturedTraits map[string]any
		kratos.On("UpdateIdentity", mock.Anything, "user-1", mock.Anything, "active").Run(func(args mock.Arguments) {
			capturedTraits = args.Get(2).(map[string]any)
		}).Return(&service.KratosIdentity{ID: "user-1", State: "active", Traits: map[string]any{}}, nil).Once()
		h := &DevHandler{
			Hydra: &service.HydraAdminService{
				AdminURL:   "http://hydra.test",
				HTTPClient: &http.Client{Transport: transport},
			},
			KratosAdmin:        kratos,
			IdentityWriter:     service.NewIdentityWriteService(kratos, nil),
			RPUserMetadataRepo: repo,
		}
		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleUser})
			return c.Next()
		})
		app.Put("/api/v1/dev/clients/:id/users/me/metadata", h.SelfUpdateRPUserMetadata)

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

		require.Equal(t, http.StatusOK, resp.StatusCode)
		rpClaims := capturedTraits["rp_custom_claims"].(map[string]any)
		clientClaims := rpClaims["client-1"].(domain.JSONMap)
		require.Equal(t, "B", clientClaims["approvalLevel"])
		require.Equal(t, "S", clientClaims["internalRank"])
		repo.AssertExpectations(t)
		kratos.AssertExpectations(t)
	})
}

func TestDevHandler_RPUserMetadataRejectsUndefinedClaimKey(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "Client One",
				"metadata": map[string]any{
					"id_token_claims": []map[string]any{
						{
							"namespace": "rp_claims",
							"key":       "contract_date",
							"valueType": "date",
							"value":     "2026-06-09",
						},
					},
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	repo := new(devMockRPUserMetadataRepo)
	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		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.Put("/api/v1/dev/clients/:id/users/:userId/metadata", h.UpsertRPUserMetadata)

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

	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
	repo.AssertNotCalled(t, "Upsert", mock.Anything, mock.Anything)
}

func TestDevHandler_RPUserMetadataRejectsInvalidTypedClaimValue(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/clients/client-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":   "client-1",
				"client_name": "Client One",
				"metadata": map[string]any{
					"id_token_claims": []map[string]any{
						{
							"namespace": "rp_claims",
							"key":       "contract_date",
							"valueType": "date",
							"value":     "2026-06-09",
						},
					},
				},
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	repo := new(devMockRPUserMetadataRepo)
	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		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.Put("/api/v1/dev/clients/:id/users/:userId/metadata", h.UpsertRPUserMetadata)

	body, _ := json.Marshal(map[string]any{
		"metadata": map[string]any{"contract_date": "2026/06/09"},
	})
	req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1/users/user-1/metadata", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, _ := app.Test(req, -1)

	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
	repo.AssertNotCalled(t, "Upsert", mock.Anything, mock.Anything)
}

func TestNormalizeIDTokenClaimsMetadataAcceptsUnixDateDefaults(t *testing.T) {
	metadata, err := normalizeIDTokenClaimsMetadata(map[string]any{
		"id_token_claims": []any{
			map[string]any{
				"namespace": "rp_claims",
				"key":       "contract_date",
				"valueType": "date",
				"value":     float64(1781020800),
			},
		},
	})

	require.NoError(t, err)
	claims := metadata["id_token_claims"].([]normalizedIDTokenClaim)
	require.Len(t, claims, 1)
	require.Equal(t, "1781020800", claims[0].Value)
}
