package runtimecache

import (
	"context"
	"errors"
	"sync"
	"testing"
)

func TestMapGetOrLoadCachesAndClones(t *testing.T) {
	ctx := context.Background()
	cache := NewMap[string, []string](WithClone[string, []string](func(values []string) []string {
		return append([]string(nil), values...)
	}))

	loads := 0
	loader := func(context.Context, string) ([]string, error) {
		loads++
		return []string{"active"}, nil
	}

	first, err := cache.GetOrLoad(ctx, "lead:create", loader)
	if err != nil {
		t.Fatalf("GetOrLoad(first) error = %v", err)
	}
	first[0] = "mutated"

	second, err := cache.GetOrLoad(ctx, "lead:create", loader)
	if err != nil {
		t.Fatalf("GetOrLoad(second) error = %v", err)
	}
	if loads != 1 {
		t.Fatalf("loader calls = %d, want 1", loads)
	}
	if second[0] != "active" {
		t.Fatalf("cached value = %q after caller mutation, want active", second[0])
	}

	cache.Invalidate()
	_, err = cache.GetOrLoad(ctx, "lead:create", loader)
	if err != nil {
		t.Fatalf("GetOrLoad(after invalidate) error = %v", err)
	}
	if loads != 2 {
		t.Fatalf("loader calls after invalidate = %d, want 2", loads)
	}
}

func TestMapGetOrLoadCollapsesConcurrentMisses(t *testing.T) {
	ctx := context.Background()
	cache := NewMap[string, string]()

	started := make(chan struct{})
	release := make(chan struct{})
	var once sync.Once
	loads := 0
	loader := func(context.Context, string) (string, error) {
		loads++
		once.Do(func() { close(started) })
		<-release
		return "value", nil
	}

	const workers = 8
	errs := make(chan error, workers)
	for i := 0; i < workers; i++ {
		go func() {
			value, err := cache.GetOrLoad(ctx, "same-key", loader)
			if err != nil {
				errs <- err
				return
			}
			if value != "value" {
				errs <- errors.New("unexpected value")
				return
			}
			errs <- nil
		}()
	}
	<-started
	close(release)

	for i := 0; i < workers; i++ {
		if err := <-errs; err != nil {
			t.Fatalf("worker error = %v", err)
		}
	}
	if loads != 1 {
		t.Fatalf("loader calls = %d, want 1", loads)
	}
}

func TestMapGetOrLoadDoesNotPublishLoadAcrossInvalidation(t *testing.T) {
	ctx := context.Background()
	cache := NewMap[string, string]()

	started := make(chan struct{})
	release := make(chan struct{})
	loads := 0
	loader := func(context.Context, string) (string, error) {
		loads++
		if loads == 1 {
			close(started)
			<-release
			return "old", nil
		}
		return "new", nil
	}

	done := make(chan string, 1)
	go func() {
		value, err := cache.GetOrLoad(ctx, "flow-plan", loader)
		if err != nil {
			done <- err.Error()
			return
		}
		done <- value
	}()

	<-started
	cache.Invalidate()
	close(release)

	if value := <-done; value != "new" {
		t.Fatalf("value after invalidation race = %q, want new", value)
	}
	if loads != 2 {
		t.Fatalf("loader calls = %d, want 2", loads)
	}

	cached, err := cache.GetOrLoad(ctx, "flow-plan", loader)
	if err != nil {
		t.Fatalf("GetOrLoad(cached) error = %v", err)
	}
	if cached != "new" {
		t.Fatalf("cached value = %q, want new", cached)
	}
	if loads != 2 {
		t.Fatalf("loader calls after cached read = %d, want 2", loads)
	}
}
