package mru

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

	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

type asyncMRUContextKey struct{}

type asyncMRUContextProbe struct {
	value                     interface{}
	traceparent               interface{}
	requestID                 interface{}
	afterCommitHookRegistered bool
	err                       error
	hasDeadline               bool
}

func TestTrackRecordAccessAsyncDropsUnsafeValuesAfterRequestCancel(t *testing.T) {
	repo := &asyncContractMRURepository{
		recent: make(chan asyncMRUContextProbe, 1),
		object: make(chan asyncMRUContextProbe, 1),
	}
	svc := NewService(repo)

	requestCtx, cancel := context.WithCancel(context.WithValue(context.Background(), asyncMRUContextKey{}, "request-value"))
	requestCtx = context.WithValue(requestCtx, constants.ContextKeyTraceparent, "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01")
	requestCtx = context.WithValue(requestCtx, constants.ContextKeyRequestID, "req-123")
	requestCtx = ports.WithAfterCommitHooks(requestCtx)
	cancel()

	svc.TrackRecordAccessAsync(requestCtx, "005-user", "Account", "001-record")

	recent := waitForAsyncMRUProbe(t, repo.recent)
	if recent.value != nil {
		t.Fatalf("recent-item context leaked unsafe value = %v, want nil", recent.value)
	}
	if recent.traceparent != "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01" {
		t.Fatalf("recent-item traceparent = %v, want parent traceparent", recent.traceparent)
	}
	if recent.requestID != "req-123" {
		t.Fatalf("recent-item request_id = %v, want parent request ID", recent.requestID)
	}
	if recent.afterCommitHookRegistered {
		t.Fatal("recent-item context inherited after-commit hook collector")
	}
	if recent.err != nil {
		t.Fatalf("recent-item context err = %v, want nil despite request cancellation", recent.err)
	}
	if !recent.hasDeadline {
		t.Fatal("recent-item context has no deadline; async MRU writes must stay bounded")
	}

	object := waitForAsyncMRUProbe(t, repo.object)
	if object.value != nil {
		t.Fatalf("object-interaction context leaked unsafe value = %v, want nil", object.value)
	}
	if object.traceparent != "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01" {
		t.Fatalf("object-interaction traceparent = %v, want parent traceparent", object.traceparent)
	}
	if object.requestID != "req-123" {
		t.Fatalf("object-interaction request_id = %v, want parent request ID", object.requestID)
	}
	if object.afterCommitHookRegistered {
		t.Fatal("object-interaction context inherited after-commit hook collector")
	}
	if object.err != nil {
		t.Fatalf("object-interaction context err = %v, want nil despite request cancellation", object.err)
	}
	if !object.hasDeadline {
		t.Fatal("object-interaction context has no deadline; async MRU writes must stay bounded")
	}
}

func TestTrackRecordAccessAsyncRecoversRepositoryPanic(t *testing.T) {
	repo := &panicMRURepository{
		called: make(chan struct{}),
	}
	svc := NewService(repo)

	svc.TrackRecordAccessAsync(context.Background(), "005-user", "Account", "001-record")

	select {
	case <-repo.called:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for async MRU panic path")
	}

	// If TrackRecordAccessAsync does not recover, the goroutine panic crashes
	// the test binary before this sleep completes.
	time.Sleep(20 * time.Millisecond)
}

func TestTrackRecordAccessAsyncCoalescesObjectInteractions(t *testing.T) {
	repo := &countingMRURepository{
		recent: make(chan asyncMRUContextProbe, 5),
		counts: make(chan objectInteractionCountProbe, 1),
	}
	svc := NewService(repo)
	svc.objectInteractions.flushDelay = time.Millisecond

	ctx := context.WithValue(context.Background(), constants.ContextKeyTraceparent, "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01")
	ctx = context.WithValue(ctx, constants.ContextKeyRequestID, "req-456")

	for i := 0; i < 5; i++ {
		svc.TrackRecordAccessAsync(ctx, "005-user", "Lead", "00Q-record-"+string(rune('a'+i)))
	}

	for i := 0; i < 5; i++ {
		_ = waitForAsyncMRUProbe(t, repo.recent)
	}

	count := waitForObjectInteractionCount(t, repo.counts)
	if count.userID != "005-user" || count.objectAPIName != "Lead" || count.count != 5 {
		t.Fatalf("coalesced object interaction = %#v, want user Lead count 5", count)
	}
	if count.traceparent != "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01" {
		t.Fatalf("coalesced object interaction traceparent = %v, want parent traceparent", count.traceparent)
	}
	if count.requestID != "req-456" {
		t.Fatalf("coalesced object interaction request_id = %v, want parent request ID", count.requestID)
	}
	if !count.hasDeadline {
		t.Fatal("coalesced object interaction context has no deadline")
	}
}

func waitForAsyncMRUProbe(t *testing.T, ch <-chan asyncMRUContextProbe) asyncMRUContextProbe {
	t.Helper()

	select {
	case probe := <-ch:
		return probe
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for async MRU write")
		return asyncMRUContextProbe{}
	}
}

func waitForObjectInteractionCount(t *testing.T, ch <-chan objectInteractionCountProbe) objectInteractionCountProbe {
	t.Helper()

	select {
	case probe := <-ch:
		return probe
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for coalesced object interaction")
		return objectInteractionCountProbe{}
	}
}

type asyncContractMRURepository struct {
	recent chan asyncMRUContextProbe
	object chan asyncMRUContextProbe
}

func (r *asyncContractMRURepository) TrackObjectInteraction(ctx context.Context, _, _ string) error {
	r.object <- probeAsyncMRUContext(ctx)
	return nil
}

func (r *asyncContractMRURepository) TrackRecentItem(ctx context.Context, _, _, _ string) error {
	r.recent <- probeAsyncMRUContext(ctx)
	return nil
}

func probeAsyncMRUContext(ctx context.Context) asyncMRUContextProbe {
	_, hasDeadline := ctx.Deadline()
	return asyncMRUContextProbe{
		value:                     ctx.Value(asyncMRUContextKey{}),
		traceparent:               ctx.Value(constants.ContextKeyTraceparent),
		requestID:                 ctx.Value(constants.ContextKeyRequestID),
		afterCommitHookRegistered: ports.RegisterAfterCommitHook(ctx, func() {}),
		err:                       ctx.Err(),
		hasDeadline:               hasDeadline,
	}
}

func (r *asyncContractMRURepository) GetScopeOrder(context.Context, string) ([]ports.MRUObjectInteraction, error) {
	return nil, nil
}

func (r *asyncContractMRURepository) SetObjectPinned(context.Context, string, string, bool) error {
	return nil
}

func (r *asyncContractMRURepository) GetRelevantItems(context.Context, string, []string, int64) ([]ports.MRURecentItem, int64, error) {
	return nil, 0, nil
}

func (r *asyncContractMRURepository) GetObjectMeta(context.Context, string) (*models.ObjectMetadata, error) {
	return nil, nil
}

func (r *asyncContractMRURepository) GetObjectMetaBatch(context.Context, []string) (map[string]*models.ObjectMetadata, error) {
	return nil, nil
}

func (r *asyncContractMRURepository) GetRecordNamesBatch(context.Context, map[string][]string) (map[string]string, error) {
	return nil, nil
}

type objectInteractionCountProbe struct {
	userID        string
	objectAPIName string
	count         int
	traceparent   interface{}
	requestID     interface{}
	hasDeadline   bool
}

type countingMRURepository struct {
	recent chan asyncMRUContextProbe
	counts chan objectInteractionCountProbe
}

func (r *countingMRURepository) TrackObjectInteraction(context.Context, string, string) error {
	return errors.New("TrackObjectInteraction fallback should not be used when TrackObjectInteractionCount is available")
}

func (r *countingMRURepository) TrackObjectInteractionCount(ctx context.Context, userID, objectAPIName string, count int) error {
	_, hasDeadline := ctx.Deadline()
	r.counts <- objectInteractionCountProbe{
		userID:        userID,
		objectAPIName: objectAPIName,
		count:         count,
		traceparent:   ctx.Value(constants.ContextKeyTraceparent),
		requestID:     ctx.Value(constants.ContextKeyRequestID),
		hasDeadline:   hasDeadline,
	}
	return nil
}

func (r *countingMRURepository) TrackRecentItem(ctx context.Context, _, _, _ string) error {
	r.recent <- probeAsyncMRUContext(ctx)
	return nil
}

func (r *countingMRURepository) GetScopeOrder(context.Context, string) ([]ports.MRUObjectInteraction, error) {
	return nil, nil
}

func (r *countingMRURepository) SetObjectPinned(context.Context, string, string, bool) error {
	return nil
}

func (r *countingMRURepository) GetRelevantItems(context.Context, string, []string, int64) ([]ports.MRURecentItem, int64, error) {
	return nil, 0, nil
}

func (r *countingMRURepository) GetObjectMeta(context.Context, string) (*models.ObjectMetadata, error) {
	return nil, nil
}

func (r *countingMRURepository) GetObjectMetaBatch(context.Context, []string) (map[string]*models.ObjectMetadata, error) {
	return nil, nil
}

func (r *countingMRURepository) GetRecordNamesBatch(context.Context, map[string][]string) (map[string]string, error) {
	return nil, nil
}

type panicMRURepository struct {
	called chan struct{}
}

func (r *panicMRURepository) TrackObjectInteraction(context.Context, string, string) error {
	return errors.New("unexpected object interaction after panic")
}

func (r *panicMRURepository) TrackRecentItem(context.Context, string, string, string) error {
	close(r.called)
	panic("async MRU test panic")
}

func (r *panicMRURepository) GetScopeOrder(context.Context, string) ([]ports.MRUObjectInteraction, error) {
	return nil, nil
}

func (r *panicMRURepository) SetObjectPinned(context.Context, string, string, bool) error {
	return nil
}

func (r *panicMRURepository) GetRelevantItems(context.Context, string, []string, int64) ([]ports.MRURecentItem, int64, error) {
	return nil, 0, nil
}

func (r *panicMRURepository) GetObjectMeta(context.Context, string) (*models.ObjectMetadata, error) {
	return nil, nil
}

func (r *panicMRURepository) GetObjectMetaBatch(context.Context, []string) (map[string]*models.ObjectMetadata, error) {
	return nil, nil
}

func (r *panicMRURepository) GetRecordNamesBatch(context.Context, map[string][]string) (map[string]string, error) {
	return nil, nil
}
