package middleware

import (
	"context"
	"errors"
	"sync"
	"testing"
	"time"

	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
)

func TestNewPermissionStateWarmerRejectsLegacyOrIncompleteComposition(
	t *testing.T,
) {
	cache := newTestPermissionStateCache(t)
	provider := permissionStateProviderFunc(func(
		context.Context,
		permissionstatev1.Subject,
	) (permissionstatev1.EffectiveSnapshot, error) {
		return permissionstatev1.EffectiveSnapshot{}, nil
	})
	for name, inputs := range map[string]struct {
		cache    *PermissionStateCache
		provider hostruntime.EffectivePermissionSnapshotProvider
	}{
		"missing cache":    {provider: provider},
		"missing provider": {cache: cache},
	} {
		t.Run(name, func(t *testing.T) {
			warmer, err := NewPermissionStateWarmer(
				inputs.cache,
				inputs.provider,
				nil,
			)
			if err == nil || warmer != nil {
				t.Fatalf("warmer=%#v err=%v, want construction failure", warmer, err)
			}
		})
	}
}

func TestPermissionStateWarmerUsesAggregateProviderAndRequestContext(
	t *testing.T,
) {
	version := testPermissionStateVersion(t, 1, 5)
	installed := false
	requestContext := permissionStateWarmRequestContextFunc(
		func(ctx context.Context) context.Context {
			installed = true
			return ctx
		},
	)
	var gotSubject permissionstatev1.Subject
	provider := permissionStateProviderFunc(func(
		_ context.Context,
		subject permissionstatev1.Subject,
	) (permissionstatev1.EffectiveSnapshot, error) {
		gotSubject = subject
		return testPermissionStateSnapshot(t, version, "ApiEnabled"), nil
	})
	cache := newTestPermissionStateCache(t)
	warmer, err := NewPermissionStateWarmer(cache, provider, requestContext)
	if err != nil {
		t.Fatal(err)
	}

	if err := warmer.WarmUser(
		context.Background(),
		"user-1",
		"profile-1",
	); err != nil {
		t.Fatalf("WarmUser: %v", err)
	}
	if !installed ||
		gotSubject.UserID() != "user-1" ||
		gotSubject.ProfileID() != "profile-1" {
		t.Fatalf("installed=%t subject=%#v", installed, gotSubject)
	}
}

func TestPermissionStateWarmerAsyncCoalescesAndDropsAtBound(t *testing.T) {
	version := testPermissionStateVersion(t, 1, 8)
	started := make(chan permissionstatev1.Subject, 1)
	requestIDs := make(chan string, 1)
	release := make(chan struct{})
	var mu sync.Mutex
	calls := 0
	provider := permissionStateProviderFunc(func(
		ctx context.Context,
		subject permissionstatev1.Subject,
	) (permissionstatev1.EffectiveSnapshot, error) {
		mu.Lock()
		calls++
		mu.Unlock()
		requestIDs <- hostruntime.RequestLineageFromContext(ctx).RequestID
		started <- subject
		<-release
		return testPermissionStateSnapshot(t, version, "ApiEnabled"), nil
	})
	cache := newTestPermissionStateCache(t)
	warmer, err := NewPermissionStateWarmer(cache, provider, nil)
	if err != nil {
		t.Fatal(err)
	}
	warmer.slots = make(chan struct{}, 1)

	warmer.WarmUserAsync(
		context.Background(),
		"request-1",
		"user-1",
		"profile-1",
	)
	first := <-started
	if requestID := <-requestIDs; requestID != "request-1" {
		t.Fatalf("async request lineage = %q, want request-1", requestID)
	}
	warmer.WarmUserAsync(
		context.Background(),
		"request-1",
		"user-1",
		"profile-1",
	)
	warmer.WarmUserAsync(
		context.Background(),
		"request-2",
		"user-2",
		"profile-2",
	)
	close(release)
	waitForPermissionStateWarm(t, warmer, first)

	mu.Lock()
	defer mu.Unlock()
	if calls != 1 {
		t.Fatalf("aggregate provider calls = %d, want one bounded warm", calls)
	}
}

func TestPermissionStateWarmerAsyncPanicReleasesBound(t *testing.T) {
	version := testPermissionStateVersion(t, 1, 10)
	calls := 0
	completed := make(chan struct{}, 1)
	provider := permissionStateProviderFunc(func(
		context.Context,
		permissionstatev1.Subject,
	) (permissionstatev1.EffectiveSnapshot, error) {
		calls++
		if calls == 1 {
			panic("provider panic")
		}
		completed <- struct{}{}
		return testPermissionStateSnapshot(t, version, "ApiEnabled"), nil
	})
	cache := newTestPermissionStateCache(t)
	warmer, err := NewPermissionStateWarmer(cache, provider, nil)
	if err != nil {
		t.Fatal(err)
	}
	warmer.slots = make(chan struct{}, 1)
	first, err := permissionstatev1.NewSubject("user-1", "profile-1")
	if err != nil {
		t.Fatal(err)
	}
	warmer.WarmUserAsync(
		context.Background(),
		"request-1",
		first.UserID(),
		first.ProfileID(),
	)
	waitForPermissionStateWarm(t, warmer, first)

	second, err := permissionstatev1.NewSubject("user-2", "profile-2")
	if err != nil {
		t.Fatal(err)
	}
	warmer.WarmUserAsync(
		context.Background(),
		"request-2",
		second.UserID(),
		second.ProfileID(),
	)
	select {
	case <-completed:
	case <-time.After(time.Second):
		t.Fatal("warm slot remained occupied after recovered panic")
	}
	waitForPermissionStateWarm(t, warmer, second)
	if calls != 2 {
		t.Fatalf("provider calls = %d, want recovered panic plus next warm", calls)
	}
}

func TestPermissionStateWarmerFailureIsNotCached(t *testing.T) {
	subject := testPermissionStateSubject(t)
	version := testPermissionStateVersion(t, 1, 9)
	calls := 0
	provider := permissionStateProviderFunc(func(
		context.Context,
		permissionstatev1.Subject,
	) (permissionstatev1.EffectiveSnapshot, error) {
		calls++
		if calls == 1 {
			return permissionstatev1.EffectiveSnapshot{}, errors.New("unavailable")
		}
		return testPermissionStateSnapshot(t, version, "ApiEnabled"), nil
	})
	cache := newTestPermissionStateCache(t)
	warmer, err := NewPermissionStateWarmer(cache, provider, nil)
	if err != nil {
		t.Fatal(err)
	}

	if err := warmer.WarmUser(
		context.Background(),
		subject.UserID(),
		subject.ProfileID(),
	); err == nil {
		t.Fatal("WarmUser succeeded through provider failure")
	}
	if _, err := cache.Resolve(
		context.Background(),
		provider,
		subject,
		version,
	); err != nil {
		t.Fatalf("Resolve after failed warm: %v", err)
	}
	if calls != 2 {
		t.Fatalf("provider calls = %d, want failed warm plus resolve", calls)
	}
}

type permissionStateWarmRequestContextFunc func(context.Context) context.Context

func (function permissionStateWarmRequestContextFunc) Install(
	ctx context.Context,
) context.Context {
	return function(ctx)
}

func (permissionStateWarmRequestContextFunc) Snapshot(
	context.Context,
) hostruntime.PermissionDiagnosticsSnapshot {
	return hostruntime.PermissionDiagnosticsSnapshot{}
}

func waitForPermissionStateWarm(
	t *testing.T,
	warmer *PermissionStateWarmer,
	subject permissionstatev1.Subject,
) {
	t.Helper()
	deadline := time.Now().Add(time.Second)
	key := permissionStateSubjectKey(subject)
	for time.Now().Before(deadline) {
		if _, inFlight := warmer.warming.Load(key); !inFlight {
			return
		}
		time.Sleep(time.Millisecond)
	}
	t.Fatal("permission state warm did not complete")
}
