package workspace

import (
	"errors"
	"sync"
	"sync/atomic"
	"testing"
)

func TestBranchSignatureCacheLookupMissThenHit(t *testing.T) {
	cache := newBranchSignatureCache(4)
	key := branchSignatureKey{baseCommit: "b1", headCommit: "h1"}

	if _, ok := cache.lookup(key); ok {
		t.Fatalf("lookup before store returned hit")
	}

	sig := branchSignature{token: "tok-1", fileCount: 7}
	cache.store(key, sig)

	got, ok := cache.lookup(key)
	if !ok {
		t.Fatalf("lookup after store returned miss")
	}
	if got != sig {
		t.Fatalf("lookup returned %#v, want %#v", got, sig)
	}
}

func TestBranchSignatureCacheLRUEviction(t *testing.T) {
	cache := newBranchSignatureCache(2)
	cache.store(branchSignatureKey{baseCommit: "b1", headCommit: "h1"}, branchSignature{token: "1"})
	cache.store(branchSignatureKey{baseCommit: "b2", headCommit: "h2"}, branchSignature{token: "2"})
	cache.store(branchSignatureKey{baseCommit: "b3", headCommit: "h3"}, branchSignature{token: "3"})

	if cache.len() != 2 {
		t.Fatalf("len after eviction = %d, want 2", cache.len())
	}
	if _, ok := cache.lookup(branchSignatureKey{baseCommit: "b1", headCommit: "h1"}); ok {
		t.Fatalf("oldest entry should have been evicted")
	}
	if _, ok := cache.lookup(branchSignatureKey{baseCommit: "b2", headCommit: "h2"}); !ok {
		t.Fatalf("second entry should still be present")
	}
	if _, ok := cache.lookup(branchSignatureKey{baseCommit: "b3", headCommit: "h3"}); !ok {
		t.Fatalf("newest entry should be present")
	}
}

func TestBranchSignatureCacheLRURefreshOnHit(t *testing.T) {
	cache := newBranchSignatureCache(2)
	k1 := branchSignatureKey{baseCommit: "b1", headCommit: "h1"}
	k2 := branchSignatureKey{baseCommit: "b2", headCommit: "h2"}
	cache.store(k1, branchSignature{token: "1"})
	cache.store(k2, branchSignature{token: "2"})

	// Touch k1 so k2 becomes LRU.
	if _, ok := cache.lookup(k1); !ok {
		t.Fatalf("lookup k1 miss")
	}

	cache.store(branchSignatureKey{baseCommit: "b3", headCommit: "h3"}, branchSignature{token: "3"})
	if _, ok := cache.lookup(k1); !ok {
		t.Fatalf("k1 should still be present after it was refreshed")
	}
	if _, ok := cache.lookup(k2); ok {
		t.Fatalf("k2 should have been evicted as LRU")
	}
}

func TestBranchSignatureCacheStoreOverwrites(t *testing.T) {
	cache := newBranchSignatureCache(2)
	key := branchSignatureKey{baseCommit: "b1", headCommit: "h1"}
	cache.store(key, branchSignature{token: "old"})
	cache.store(key, branchSignature{token: "new"})

	got, ok := cache.lookup(key)
	if !ok {
		t.Fatalf("lookup after overwrite returned miss")
	}
	if got.token != "new" {
		t.Fatalf("token = %q, want %q", got.token, "new")
	}
	if cache.len() != 1 {
		t.Fatalf("len = %d, want 1 after overwrite", cache.len())
	}
}

func TestBranchSignatureCacheGetOrCreateCaches(t *testing.T) {
	cache := newBranchSignatureCache(2)
	key := branchSignatureKey{baseCommit: "b1", headCommit: "h1"}

	var loads atomic.Int32
	load := func() (branchSignature, error) {
		loads.Add(1)
		return branchSignature{token: "tok", fileCount: 3}, nil
	}

	for i := 0; i < 3; i++ {
		got, err := cache.getOrCreate(key, load)
		if err != nil {
			t.Fatalf("getOrCreate returned error: %v", err)
		}
		if got.token != "tok" || got.fileCount != 3 {
			t.Fatalf("got = %#v", got)
		}
	}
	if loads.Load() != 1 {
		t.Fatalf("load called %d times, want 1", loads.Load())
	}
}

func TestBranchSignatureCacheGetOrCreateDoesNotCacheError(t *testing.T) {
	cache := newBranchSignatureCache(2)
	key := branchSignatureKey{baseCommit: "b1", headCommit: "h1"}

	var loads atomic.Int32
	load := func() (branchSignature, error) {
		n := loads.Add(1)
		if n == 1 {
			return branchSignature{}, errors.New("transient")
		}
		return branchSignature{token: "tok"}, nil
	}

	if _, err := cache.getOrCreate(key, load); err == nil {
		t.Fatalf("first call should fail")
	}
	got, err := cache.getOrCreate(key, load)
	if err != nil {
		t.Fatalf("second call returned error: %v", err)
	}
	if got.token != "tok" {
		t.Fatalf("token = %q, want %q", got.token, "tok")
	}
	if loads.Load() != 2 {
		t.Fatalf("load should be called once per failure (no caching of errors)")
	}
}

func TestBranchSignatureCacheGetOrCreateSingleflightDedupes(t *testing.T) {
	cache := newBranchSignatureCache(4)
	key := branchSignatureKey{baseCommit: "b1", headCommit: "h1"}

	var loads atomic.Int32
	ready := make(chan struct{})
	done := make(chan struct{})
	load := func() (branchSignature, error) {
		loads.Add(1)
		// Block the first load so concurrent callers pile up on the flight.
		if loads.Load() == 1 {
			close(ready)
			<-done
		}
		return branchSignature{token: "tok"}, nil
	}

	const callers = 4
	var wg sync.WaitGroup
	wg.Add(callers)
	start := make(chan struct{})
	for i := 0; i < callers; i++ {
		go func() {
			defer wg.Done()
			<-start
			_, _ = cache.getOrCreate(key, load)
		}()
	}
	close(start)

	// Wait until the in-flight load signals it has callers queued.
	<-ready
	close(done)
	wg.Wait()

	if loads.Load() != 1 {
		t.Fatalf("load invoked %d times, want 1 (singleflight collapsed concurrent miss)", loads.Load())
	}
}

func TestBranchSignatureCacheNilSafe(t *testing.T) {
	var cache *branchSignatureCache
	key := branchSignatureKey{baseCommit: "b1", headCommit: "h1"}

	if _, ok := cache.lookup(key); ok {
		t.Fatalf("nil cache lookup should miss")
	}

	cache.store(key, branchSignature{token: "x"})
	if cache != nil {
		t.Fatalf("nil cache should stay nil after store")
	}

	called := false
	_, err := cache.getOrCreate(key, func() (branchSignature, error) {
		called = true
		return branchSignature{token: "x"}, nil
	})
	if err != nil {
		t.Fatalf("nil cache getOrCreate error: %v", err)
	}
	if !called {
		t.Fatalf("nil cache should invoke load directly")
	}
}
