package middleware

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"reflect"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
)

const (
	testPermManageUsers          = "ManageUsers"
	testPermModifyAllData        = "ModifyAllData"
	testPermViewSetup            = "ViewSetup"
	testPermCustomizeApplication = "CustomizeApplication"
)

type testPermissionSessionContext struct {
	systemPerms map[string]bool
	session     SessionLookup
	permLookup  PermissionLookup
}

func newTestPermissionSessionContext(perms map[string]bool) testPermissionSessionContext {
	return testPermissionSessionContext{
		systemPerms: perms,
		session: SessionLookup{
			User:   hostruntime.UserSession{ID: "005TEST", ProfileID: "00eTEST"},
			Exists: true,
			Valid:  true,
		},
		permLookup: PermissionLookup{Permissions: perms, Exists: true, Valid: true},
	}
}

func (ctx testPermissionSessionContext) ReadSession(*gin.Context) SessionLookup {
	return ctx.session
}

func (ctx testPermissionSessionContext) WriteCurrentSession(*gin.Context, hostruntime.UserSession) {}

func (ctx testPermissionSessionContext) WritePermissions(*gin.Context, map[string]bool, map[string]bool) {
}

func (ctx testPermissionSessionContext) SystemPermissions(*gin.Context) PermissionLookup {
	return ctx.permLookup
}

func (ctx testPermissionSessionContext) RequestID(*gin.Context) string {
	return "req-test"
}

func servePermissionMiddleware(perms map[string]bool, guard gin.HandlerFunc) *httptest.ResponseRecorder {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.GET("/guarded",
		guard,
		func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{"ok": true})
		},
	)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/guarded", nil))
	return w
}

func TestRequirePermissionDoesNotTreatModifyAllDataAsAdminWildcard(t *testing.T) {
	session := newTestPermissionSessionContext(map[string]bool{testPermModifyAllData: true})
	w := servePermissionMiddleware(
		session.systemPerms,
		RequirePermissionWithSessionContext(session, testPermManageUsers),
	)
	if w.Code != http.StatusForbidden {
		t.Fatalf("ModifyAllData-only caller = %d, want 403 for ManageUsers gate; body=%s", w.Code, w.Body.String())
	}
}

func TestRequireAnyPermissionDoesNotTreatModifyAllDataAsAdminWildcard(t *testing.T) {
	session := newTestPermissionSessionContext(map[string]bool{testPermModifyAllData: true})
	w := servePermissionMiddleware(
		session.systemPerms,
		RequireAnyPermissionWithSessionContext(session, testPermViewSetup, testPermCustomizeApplication),
	)
	if w.Code != http.StatusForbidden {
		t.Fatalf("ModifyAllData-only caller = %d, want 403 for setup gate; body=%s", w.Code, w.Body.String())
	}
}

func TestRequirePermissionAllowsExplicitGrant(t *testing.T) {
	session := newTestPermissionSessionContext(map[string]bool{testPermManageUsers: true})
	w := servePermissionMiddleware(
		session.systemPerms,
		RequirePermissionWithSessionContext(session, testPermManageUsers),
	)
	if w.Code != http.StatusOK {
		t.Fatalf("ManageUsers caller = %d, want 200; body=%s", w.Code, w.Body.String())
	}
}

func TestPermissionCacheWarmEpochSkipsPublishAfterInvalidation(t *testing.T) {
	cache := NewPermissionCache(time.Hour)

	globalEpoch := cache.captureWarmEpoch("005USER")
	cache.Invalidate()
	if cache.setIfWarmEpoch("005USER", "00ePROFILE", globalEpoch, map[string]bool{"stale": true}, nil) {
		t.Fatal("stale global warm epoch published system/custom permissions after Invalidate")
	}
	if _, _, hit := cache.get("005USER", "00ePROFILE"); hit {
		t.Fatal("stale global warm epoch populated system/custom cache after Invalidate")
	}

	userEpoch := cache.captureWarmEpoch("005USER")
	cache.InvalidateUser("005USER")
	if cache.setIfWarmEpoch("005USER", "00ePROFILE", userEpoch, map[string]bool{"stale": true}, nil) {
		t.Fatal("stale user warm epoch published system/custom permissions after InvalidateUser")
	}

	freshEpoch := cache.captureWarmEpoch("005USER")
	if !cache.setIfWarmEpoch("005USER", "00ePROFILE", freshEpoch, map[string]bool{testPermManageUsers: true}, nil) {
		t.Fatal("fresh warm epoch did not publish system/custom permissions")
	}
	if sys, _, hit := cache.get("005USER", "00ePROFILE"); !hit || !sys[testPermManageUsers] {
		t.Fatalf("fresh warm epoch cache hit = %v, sys = %#v; want ManageUsers", hit, sys)
	}
}

func TestPermissionCacheWarmUserAsyncDropsWhenGlobalGateFull(t *testing.T) {
	cache := NewPermissionCache(time.Hour)
	cache.asyncWarmSlots = make(chan struct{}, 1)
	cache.asyncWarmTimeout = time.Second
	release, ok := cache.acquireAsyncWarmSlot()
	if !ok {
		t.Fatal("failed to occupy async warm gate")
	}
	defer release()

	cache.WarmUserAsync(context.Background(), "req-1", "005OTHER", "00ePROFILE", nil, nil)
	if _, ok := cache.warming.Load(cache.cacheKey("005OTHER", "00ePROFILE")); ok {
		t.Fatal("WarmUserAsync leaked per-user in-flight marker after global gate drop")
	}
}

func TestPermissionWarmAsyncContextDropsUnsafeValuesAfterCancel(t *testing.T) {
	type unsafeKey struct{}
	sourceCtx := hostruntime.ContextWithRequestLineage(context.Background(), hostruntime.RequestLineage{
		RequestID:   "req-from-context",
		Traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
	})
	sourceCtx = context.WithValue(sourceCtx, unsafeKey{}, "request-scoped")
	sourceCtx, cancelSource := context.WithCancel(sourceCtx)
	cancelSource()

	asyncCtx, cancel := permissionWarmAsyncContext(sourceCtx, "req-explicit", time.Second)
	defer cancel()

	if err := asyncCtx.Err(); err != nil {
		t.Fatalf("async context inherited source cancellation: %v", err)
	}
	if _, ok := asyncCtx.Deadline(); !ok {
		t.Fatal("async context is missing timeout deadline")
	}
	lineage := hostruntime.RequestLineageFromContext(asyncCtx)
	if lineage.RequestID != "req-explicit" {
		t.Fatalf("request id = %q, want explicit request id", lineage.RequestID)
	}
	if lineage.Traceparent == "" {
		t.Fatal("traceparent was not preserved")
	}
	if got := asyncCtx.Value(unsafeKey{}); got != nil {
		t.Fatalf("unsafe request-scoped value leaked into async context: %#v", got)
	}
}

func TestPermissionWarmAsyncContextFallsBackToContextRequestID(t *testing.T) {
	sourceCtx := hostruntime.ContextWithRequestLineage(context.Background(), hostruntime.RequestLineage{RequestID: "req-from-context"})
	asyncCtx, cancel := permissionWarmAsyncContext(sourceCtx, "", time.Second)
	defer cancel()

	if got := hostruntime.RequestLineageFromContext(asyncCtx).RequestID; got != "req-from-context" {
		t.Fatalf("request id = %q, want source context request id", got)
	}
}

type warmTestPermissionProvider struct {
	systemPerms map[string]bool
	customPerms map[string]bool
	systemErr   error
	customErr   error
	systemCalls int
	customCalls int
}

func (p *warmTestPermissionProvider) GetEffectiveSystemPermissions(context.Context, string, string) (map[string]bool, error) {
	p.systemCalls++
	return p.systemPerms, p.systemErr
}

func (p *warmTestPermissionProvider) GetEffectiveCustomPermissions(context.Context, string, string) (map[string]bool, error) {
	p.customCalls++
	return p.customPerms, p.customErr
}

func TestPermissionProviderContractExcludesAllObjectProjections(t *testing.T) {
	providerType := reflect.TypeOf((*hostruntime.PermissionProvider)(nil)).Elem()
	if got := providerType.NumMethod(); got != 2 {
		t.Fatalf("PermissionProvider method count = %d, want exactly 2 bounded methods", got)
	}
	for _, method := range []string{
		"GetEffectiveSystemPermissions",
		"GetEffectiveCustomPermissions",
	} {
		if _, ok := providerType.MethodByName(method); !ok {
			t.Fatalf("PermissionProvider is missing bounded method %s", method)
		}
	}
	for _, method := range []string{
		"ListEffectiveObjectPermissionsForUser",
		"ListEffectiveFieldPermissionsForUser",
	} {
		if _, ok := providerType.MethodByName(method); ok {
			t.Fatalf("PermissionProvider exposes request-unsafe all-object method %s", method)
		}
	}
}

func TestPermissionLoaderColdAndWarmRequestsDoNotSchedulePermissionWarm(t *testing.T) {
	gin.SetMode(gin.TestMode)
	cache := NewPermissionCache(time.Hour)
	provider := &warmTestPermissionProvider{
		systemPerms: map[string]bool{testPermManageUsers: true},
		customPerms: map[string]bool{"CustomGrant": true},
	}
	session := newTestPermissionSessionContext(nil)
	router := gin.New()
	router.Use(PermissionLoaderWithSessionContext(provider, cache, nil, session))
	router.GET("/product/notifications", func(c *gin.Context) {
		c.Status(http.StatusNoContent)
	})

	for i := 0; i < 2; i++ {
		recorder := httptest.NewRecorder()
		request := httptest.NewRequest(http.MethodGet, "/product/notifications", nil)
		router.ServeHTTP(recorder, request)
		if recorder.Code != http.StatusNoContent {
			t.Fatalf("request %d status = %d, want %d", i+1, recorder.Code, http.StatusNoContent)
		}
	}

	if provider.systemCalls != 1 || provider.customCalls != 1 {
		t.Fatalf("cold+warm system/custom calls = %d/%d, want 1/1", provider.systemCalls, provider.customCalls)
	}
	if _, scheduled := cache.warming.Load(cache.cacheKey("005TEST", "00eTEST")); scheduled {
		t.Fatal("ordinary request scheduled detached permission warm work")
	}
}

func TestPermissionCacheWarmUserLoadsOnlySystemCustomPermissions(t *testing.T) {
	cache := NewPermissionCache(time.Hour)
	provider := &warmTestPermissionProvider{
		systemPerms: map[string]bool{testPermManageUsers: true},
		customPerms: map[string]bool{"CustomGrant": true},
	}

	cache.warmUserWithEpoch(context.Background(), "005USER", "00ePROFILE", provider, nil, cache.captureWarmEpoch("005USER"))

	if provider.systemCalls != 1 || provider.customCalls != 1 {
		t.Fatalf("system/custom warm calls = %d/%d, want 1/1", provider.systemCalls, provider.customCalls)
	}
	if sys, custom, hit := cache.get("005USER", "00ePROFILE"); !hit || !sys[testPermManageUsers] || !custom["CustomGrant"] {
		t.Fatalf("system/custom permissions were not cached: hit=%v sys=%#v custom=%#v", hit, sys, custom)
	}
}

func TestPermissionCacheWarmUserAsyncRecoversRootPanicAndReleasesGate(t *testing.T) {
	cache := NewPermissionCache(time.Hour)
	cache.asyncWarmSlots = make(chan struct{}, 1)
	cache.asyncWarmTimeout = time.Second

	cache.WarmUserAsync(context.Background(), "req-1", "005USER", "00ePROFILE", nil, nil)

	key := cache.cacheKey("005USER", "00ePROFILE")
	deadline := time.After(time.Second)
	ticker := time.NewTicker(10 * time.Millisecond)
	defer ticker.Stop()
	for {
		if _, ok := cache.warming.Load(key); !ok {
			release, ok := cache.acquireAsyncWarmSlot()
			if !ok {
				t.Fatal("async warm gate was not released after recovered root panic")
			}
			release()
			return
		}
		select {
		case <-deadline:
			t.Fatal("async warm in-flight marker was not cleared after recovered root panic")
		case <-ticker.C:
		}
	}
}

func TestPermissionCacheWarmUserSkipsPublishOnSystemCustomLoadFailure(t *testing.T) {
	cache := NewPermissionCache(time.Hour)
	cache.set("005USER", "00ePROFILE",
		map[string]bool{testPermManageUsers: true},
		map[string]bool{"ExistingCustom": true},
	)

	provider := &warmTestPermissionProvider{
		systemErr: errors.New("system permissions unavailable"),
		customErr: errors.New("custom permissions unavailable"),
	}

	cache.warmUserWithEpoch(context.Background(), "005USER", "00ePROFILE", provider, nil, cache.captureWarmEpoch("005USER"))

	sys, custom, hit := cache.get("005USER", "00ePROFILE")
	if !hit || !sys[testPermManageUsers] || !custom["ExistingCustom"] {
		t.Fatalf("warm load failure overwrote existing system/custom cache: hit=%v sys=%#v custom=%#v", hit, sys, custom)
	}
}
