package opstate

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

type fakeOp struct {
	id        string
	done      atomic.Bool
	updatedAt time.Time
	canceled  atomic.Bool
}

func (f *fakeOp) ID() string           { return f.id }
func (f *fakeOp) IsDone() bool         { return f.done.Load() }
func (f *fakeOp) UpdatedAt() time.Time { return f.updatedAt }
func (f *fakeOp) Cancel()              { f.canceled.Store(true) }

func TestRegistryInsertAndGet(t *testing.T) {
	var r Registry[*fakeOp]
	op := &fakeOp{id: "op-1", updatedAt: time.Now()}
	r.Insert(op)
	got, ok := r.Get("op-1")
	if !ok {
		t.Fatal("Get after Insert missed")
	}
	if got.ID() != "op-1" {
		t.Fatalf("Get().ID = %q", got.ID())
	}

	_, ok = r.Get("missing")
	if ok {
		t.Fatal("Get on missing key should miss")
	}
}

func TestRegistryPruneDropsCompletedOldOps(t *testing.T) {
	var r Registry[*fakeOp]
	now := time.Now()

	recent := &fakeOp{id: "recent", updatedAt: now}
	recent.done.Store(true)

	stale := &fakeOp{id: "stale", updatedAt: now.Add(-time.Hour)}
	stale.done.Store(true)

	pending := &fakeOp{id: "pending", updatedAt: now.Add(-time.Hour)}
	// NOT done → stays even though old.

	r.Insert(recent)
	r.Insert(stale)
	r.Insert(pending)

	cutoff := now.Add(-time.Minute)
	r.Prune(cutoff)

	if _, ok := r.Get("stale"); ok {
		t.Error("stale op should have been pruned")
	}
	if _, ok := r.Get("recent"); !ok {
		t.Error("recent op should survive (newer than cutoff)")
	}
	if _, ok := r.Get("pending"); !ok {
		t.Error("pending op should survive (not done)")
	}
}

func TestRegistryListReturnsSnapshot(t *testing.T) {
	var r Registry[*fakeOp]
	now := time.Now()

	a := &fakeOp{id: "a", updatedAt: now}
	b := &fakeOp{id: "b", updatedAt: now}
	c := &fakeOp{id: "c", updatedAt: now}
	c.done.Store(true)
	r.Insert(a)
	r.Insert(b)
	r.Insert(c)

	got := r.List()
	if len(got) != 3 {
		t.Fatalf("List() returned %d entries, want 3", len(got))
	}
	ids := map[string]bool{}
	for _, op := range got {
		ids[op.ID()] = true
	}
	for _, want := range []string{"a", "b", "c"} {
		if !ids[want] {
			t.Errorf("List() missing %q; got %v", want, ids)
		}
	}

	// Mutating the returned slice must not poison the registry.
	got[0] = &fakeOp{id: "tampered"}
	if _, ok := r.Get("tampered"); ok {
		t.Error("List() returned the registry's internal map, not a copy")
	}
}

func TestRegistryListEmpty(t *testing.T) {
	var r Registry[*fakeOp]
	if got := r.List(); len(got) != 0 {
		t.Fatalf("List() on empty registry returned %d entries, want 0", len(got))
	}
}

func TestRegistryPruneNoOpOnEmpty(t *testing.T) {
	var r Registry[*fakeOp]
	// Just shouldn't panic.
	r.Prune(time.Now())
}
