package handler

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

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

type rpClaimsE2ERepo struct {
	mu      sync.Mutex
	rows    map[string]*domain.RPUserMetadata
	getKeys []string
}

func newRPClaimsE2ERepo() *rpClaimsE2ERepo {
	return &rpClaimsE2ERepo{rows: map[string]*domain.RPUserMetadata{}}
}

func (r *rpClaimsE2ERepo) Get(ctx context.Context, clientID, userID string) (*domain.RPUserMetadata, error) {
	r.mu.Lock()
	defer r.mu.Unlock()

	key := rpClaimsE2ERepoKey(clientID, userID)
	r.getKeys = append(r.getKeys, key)
	row, ok := r.rows[key]
	if !ok {
		return nil, fmt.Errorf("rp user metadata not found")
	}
	return cloneRPUserMetadata(row), nil
}

func (r *rpClaimsE2ERepo) Upsert(ctx context.Context, metadata *domain.RPUserMetadata) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	r.rows[rpClaimsE2ERepoKey(metadata.ClientID, metadata.UserID)] = cloneRPUserMetadata(metadata)
	return nil
}

func (r *rpClaimsE2ERepo) seenGet(clientID, userID string) bool {
	r.mu.Lock()
	defer r.mu.Unlock()

	key := rpClaimsE2ERepoKey(clientID, userID)
	for _, seen := range r.getKeys {
		if seen == key {
			return true
		}
	}
	return false
}

func rpClaimsE2ERepoKey(clientID, userID string) string {
	return strings.TrimSpace(clientID) + "\x00" + strings.TrimSpace(userID)
}

func cloneRPUserMetadata(row *domain.RPUserMetadata) *domain.RPUserMetadata {
	if row == nil {
		return nil
	}
	cloned := &domain.RPUserMetadata{
		ClientID: row.ClientID,
		UserID:   row.UserID,
		Metadata: domain.JSONMap{},
	}
	for key, value := range row.Metadata {
		cloned.Metadata[key] = value
	}
	return cloned
}

func TestRPClaimsE2E_UpdatedClaimsAreScopedToCurrentRP(t *testing.T) {
	const userID = "user-rp-e2e"
	const clientA = "client-rp-a"
	const clientB = "client-rp-b"

	clients := map[string]map[string]any{
		clientA: rpClaimsE2EClient(clientA, []map[string]any{
			rpClaimsE2EClaim("approvalLevel", "text", "A", "user_and_admin", "user_and_admin"),
			rpClaimsE2EClaim("activeMember", "boolean", "true", "user_and_admin", "user_and_admin"),
			rpClaimsE2EClaim("score", "number", "1", "user_and_admin", "user_and_admin"),
			rpClaimsE2EClaim("featureList", "array", `["default"]`, "user_and_admin", "user_and_admin"),
			rpClaimsE2EClaim("preferences", "object", `{"theme":"light","density":"comfortable"}`, "user_and_admin", "user_and_admin"),
			rpClaimsE2EClaim("contractDate", "date", float64(1780930800), "user_and_admin", "user_and_admin"),
			rpClaimsE2EClaim("approvedAt", "datetime", float64(1780965000), "user_and_admin", "user_and_admin"),
			rpClaimsE2EClaim("adminManagedNote", "text", "admin-default", "user_and_admin", "admin_only"),
		}),
		clientB: rpClaimsE2EClient(clientB, []map[string]any{
			rpClaimsE2EClaim("approvalLevel", "text", "B-default", "user_and_admin", "user_and_admin"),
			rpClaimsE2EClaim("activeMember", "boolean", "false", "user_and_admin", "user_and_admin"),
		}),
	}
	challenges := map[string]string{
		"challenge-client-a-default":      clientA,
		"challenge-client-a-admin-update": clientA,
		"challenge-client-a-self-update":  clientA,
		"challenge-client-b-default":      clientB,
		"challenge-client-b-update":       clientB,
	}
	capturedClaims := map[string]map[string]any{}
	hydraClient := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if strings.HasPrefix(r.URL.Path, "/clients/") {
			clientID := strings.TrimPrefix(r.URL.Path, "/clients/")
			client, ok := clients[clientID]
			if !ok {
				return httpJSONAny(r, http.StatusNotFound, nil), nil
			}
			return httpJSONAny(r, http.StatusOK, client), nil
		}
		if r.URL.Path == "/oauth2/auth/requests/consent" {
			challenge := r.URL.Query().Get("consent_challenge")
			clientID, ok := challenges[challenge]
			if !ok {
				return httpResponse(r, http.StatusNotFound, "not found"), nil
			}
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"challenge":       challenge,
				"requested_scope": []string{"openid", "profile"},
				"subject":         userID,
				"client":          clients[clientID],
			}), nil
		}
		if r.URL.Path == "/oauth2/auth/requests/consent/accept" {
			challenge := r.URL.Query().Get("consent_challenge")
			body, _ := io.ReadAll(r.Body)
			var acceptReq map[string]any
			_ = json.Unmarshal(body, &acceptReq)
			session, _ := acceptReq["session"].(map[string]any)
			idToken, _ := session["id_token"].(map[string]any)
			capturedClaims[challenge] = idToken
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"redirect_to": "http://rp/cb",
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})}

	repo := newRPClaimsE2ERepo()
	kratos := new(MockKratosAdminService)
	kratos.On("GetIdentity", mock.Anything, userID).Return(&service.KratosIdentity{
		ID:    userID,
		State: "active",
		Traits: map[string]any{
			"email": "rp-e2e@example.com",
			"name":  "RP E2E User",
		},
	}, nil)

	authHandler := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: hydraClient,
		},
		KratosAdmin:        kratos,
		RPUserMetadataRepo: repo,
	}
	devHandler := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: hydraClient,
		},
		RPUserMetadataRepo: repo,
	}

	app := fiber.New()
	app.Put("/api/v1/dev/clients/:id/users/me/metadata", func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: userID, Role: domain.RoleUser})
		return devHandler.SelfUpdateRPUserMetadata(c)
	})
	app.Put("/api/v1/dev/clients/:id/users/:userId/metadata", func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "admin", Role: domain.RoleSuperAdmin})
		return devHandler.UpsertRPUserMetadata(c)
	})
	app.Post("/api/v1/auth/consent/accept", authHandler.AcceptConsentRequest)

	initialA := acceptRPClaimsE2EConsent(t, app, capturedClaims, "challenge-client-a-default")
	assert.Equal(t, "A", rpClaimValue(t, initialA, "approvalLevel"))
	assert.Equal(t, "user_and_admin", rpClaimPermission(t, initialA, "approvalLevel", "readPermission"))
	assert.Equal(t, true, rpClaimValue(t, initialA, "activeMember"))
	assert.Equal(t, float64(1), rpClaimValue(t, initialA, "score"))
	assert.Equal(t, []any{"default"}, rpClaimValue(t, initialA, "featureList"))
	assert.Equal(t, map[string]any{"theme": "light", "density": "comfortable"}, rpClaimValue(t, initialA, "preferences"))
	assert.Equal(t, float64(1780930800), rpClaimValue(t, initialA, "contractDate"))
	assert.Equal(t, float64(1780965000), rpClaimValue(t, initialA, "approvedAt"))

	upsertRPClaimsE2EMetadata(t, app, clientA, userID, map[string]any{
		"approvalLevel":    "B",
		"activeMember":     false,
		"score":            42,
		"featureList":      []string{"sso", "claims"},
		"preferences":      map[string]any{"theme": "dark", "density": "compact"},
		"contractDate":     float64(1781017200),
		"approvedAt":       float64(1780968600),
		"adminManagedNote": "admin-updated",
		"approvalLevel_permissions": map[string]any{
			"writePermission": "user_and_admin",
		},
	})

	updatedA := acceptRPClaimsE2EConsent(t, app, capturedClaims, "challenge-client-a-admin-update")
	assert.Equal(t, "B", rpClaimValue(t, updatedA, "approvalLevel"))
	assert.Equal(t, false, rpClaimValue(t, updatedA, "activeMember"))
	assert.Equal(t, float64(42), rpClaimValue(t, updatedA, "score"))
	assert.Equal(t, []any{"sso", "claims"}, rpClaimValue(t, updatedA, "featureList"))
	assert.Equal(t, map[string]any{"theme": "dark", "density": "compact"}, rpClaimValue(t, updatedA, "preferences"))
	assert.Equal(t, float64(1781017200), rpClaimValue(t, updatedA, "contractDate"))
	assert.Equal(t, float64(1780968600), rpClaimValue(t, updatedA, "approvedAt"))
	assert.Equal(t, "admin-updated", rpClaimValue(t, updatedA, "adminManagedNote"))
	assert.NotContains(t, updatedA, "approvalLevel_permissions")
	assert.NotContains(t, updatedA, "adminManagedNote_permissions")

	rejectedSelfUpdate := putRPClaimsE2EMetadata(t, app, http.MethodPut, "/api/v1/dev/clients/"+clientA+"/users/me/metadata", map[string]any{
		"metadata": map[string]any{
			"adminManagedNote": "user-should-not-overwrite",
		},
	})
	assert.Equal(t, http.StatusForbidden, rejectedSelfUpdate.StatusCode)

	allowedSelfUpdate := putRPClaimsE2EMetadata(t, app, http.MethodPut, "/api/v1/dev/clients/"+clientA+"/users/me/metadata", map[string]any{
		"metadata": map[string]any{
			"approvalLevel": "C",
		},
	})
	assert.Equal(t, http.StatusOK, allowedSelfUpdate.StatusCode)

	selfUpdatedA := acceptRPClaimsE2EConsent(t, app, capturedClaims, "challenge-client-a-self-update")
	assert.Equal(t, "C", rpClaimValue(t, selfUpdatedA, "approvalLevel"))
	assert.Equal(t, "admin-updated", rpClaimValue(t, selfUpdatedA, "adminManagedNote"))

	defaultB := acceptRPClaimsE2EConsent(t, app, capturedClaims, "challenge-client-b-default")
	assert.Equal(t, "B-default", rpClaimValue(t, defaultB, "approvalLevel"))
	assert.Equal(t, false, rpClaimValue(t, defaultB, "activeMember"))
	assert.NotContains(t, defaultB, "score")
	assert.NotContains(t, defaultB, "featureList")
	assert.NotContains(t, defaultB, "adminManagedNote")

	upsertRPClaimsE2EMetadata(t, app, clientB, userID, map[string]any{
		"approvalLevel": "B-rp-only",
		"activeMember":  true,
	})
	updatedB := acceptRPClaimsE2EConsent(t, app, capturedClaims, "challenge-client-b-update")
	assert.Equal(t, "B-rp-only", rpClaimValue(t, updatedB, "approvalLevel"))
	assert.Equal(t, true, rpClaimValue(t, updatedB, "activeMember"))
	assert.NotEqual(t, rpClaimValue(t, selfUpdatedA, "approvalLevel"), rpClaimValue(t, updatedB, "approvalLevel"))
	assert.NotContains(t, updatedB, "score")
	assert.NotContains(t, updatedB, "featureList")

	require.True(t, repo.seenGet(clientA, userID))
	require.True(t, repo.seenGet(clientB, userID))
	require.Contains(t, capturedClaims, "challenge-client-a-admin-update")
	require.Contains(t, capturedClaims, "challenge-client-b-update")
	kratos.AssertExpectations(t)
}

func rpClaimsE2EClient(clientID string, claims []map[string]any) map[string]any {
	return map[string]any{
		"client_id":   clientID,
		"client_name": clientID,
		"metadata": map[string]any{
			"tenant_id":       "tenant-rp-e2e",
			"id_token_claims": claims,
		},
	}
}

func rpClaimsE2EClaim(key string, valueType string, value any, readPermission string, writePermission string) map[string]any {
	return map[string]any{
		"namespace":       "rp_claims",
		"key":             key,
		"valueType":       valueType,
		"value":           value,
		"readPermission":  readPermission,
		"writePermission": writePermission,
	}
}

func acceptRPClaimsE2EConsent(t *testing.T, app *fiber.App, capturedClaims map[string]map[string]any, challenge string) map[string]any {
	t.Helper()

	body, _ := json.Marshal(map[string]any{
		"consent_challenge": challenge,
		"grant_scope":       []string{"openid", "profile"},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req, -1)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)

	idToken, ok := capturedClaims[challenge]
	require.True(t, ok)
	rpClaims, ok := idToken["rp_claims"].(map[string]any)
	require.True(t, ok)
	return rpClaims
}

func rpClaimValue(t *testing.T, claims map[string]any, key string) any {
	t.Helper()

	payload, ok := claims[key].(map[string]any)
	require.Truef(t, ok, "rp_claims.%s must be an object payload", key)
	return payload["value"]
}

func rpClaimPermission(t *testing.T, claims map[string]any, key string, permissionKey string) string {
	t.Helper()

	payload, ok := claims[key].(map[string]any)
	require.Truef(t, ok, "rp_claims.%s must be an object payload", key)
	value, ok := payload[permissionKey].(string)
	require.Truef(t, ok, "rp_claims.%s.%s must be a string", key, permissionKey)
	return value
}

func upsertRPClaimsE2EMetadata(t *testing.T, app *fiber.App, clientID, userID string, metadata map[string]any) {
	t.Helper()

	resp := putRPClaimsE2EMetadata(t, app, http.MethodPut, "/api/v1/dev/clients/"+clientID+"/users/"+userID+"/metadata", map[string]any{
		"metadata": metadata,
	})
	require.Equal(t, http.StatusOK, resp.StatusCode)
}

func putRPClaimsE2EMetadata(t *testing.T, app *fiber.App, method, path string, body map[string]any) *http.Response {
	t.Helper()

	payload, _ := json.Marshal(body)
	req := httptest.NewRequest(method, path, bytes.NewReader(payload))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req, -1)
	require.NoError(t, err)
	return resp
}
