package cache

import (
	"errors"
	"path/filepath"
	"testing"
	"time"
)

func openTestCache(t *testing.T) (*Cache, func()) {
	t.Helper()
	path := filepath.Join(t.TempDir(), "orca_cache.db")
	c, err := Open(path)
	if err != nil {
		t.Fatalf("open cache: %v", err)
	}
	return c, func() { _ = c.Close() }
}

func TestCache_Hit(t *testing.T) {
	c, cleanup := openTestCache(t)
	defer cleanup()

	want := []byte("hello-orca")
	if err := c.Set("nodes", "list", want, 30*time.Second); err != nil {
		t.Fatalf("set: %v", err)
	}
	got, inserted, err := c.Get("nodes", "list")
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	if string(got) != string(want) {
		t.Errorf("get value = %q, want %q", got, want)
	}
	if inserted.IsZero() {
		t.Errorf("inserted time is zero")
	}
}

func TestCache_Miss(t *testing.T) {
	c, cleanup := openTestCache(t)
	defer cleanup()

	got, _, err := c.Get("nodes", "missing")
	if !errors.Is(err, ErrCacheMiss) {
		t.Fatalf("get miss: err = %v, want ErrCacheMiss", err)
	}
	if got != nil {
		t.Errorf("get miss value = %v, want nil", got)
	}
}

func TestCache_Invalidate(t *testing.T) {
	c, cleanup := openTestCache(t)
	defer cleanup()

	if err := c.Set("nodes", "list", []byte("a"), 30*time.Second); err != nil {
		t.Fatalf("set a: %v", err)
	}
	if err := c.Set("nodes", "other", []byte("b"), 30*time.Second); err != nil {
		t.Fatalf("set b: %v", err)
	}
	if err := c.Set("jobs", "list", []byte("c"), 30*time.Second); err != nil {
		t.Fatalf("set c: %v", err)
	}
	if err := c.Invalidate("nodes"); err != nil {
		t.Fatalf("invalidate: %v", err)
	}
	if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) {
		t.Errorf("nodes/list after invalidate: err = %v, want ErrCacheMiss", err)
	}
	if _, _, err := c.Get("nodes", "other"); !errors.Is(err, ErrCacheMiss) {
		t.Errorf("nodes/other after invalidate: err = %v, want ErrCacheMiss", err)
	}
	if _, _, err := c.Get("jobs", "list"); err != nil {
		t.Errorf("jobs/list after nodes invalidate: err = %v, want nil", err)
	}
}

func TestCache_InvalidateKey(t *testing.T) {
	c, cleanup := openTestCache(t)
	defer cleanup()

	if err := c.Set("nodes", "list", []byte("a"), 30*time.Second); err != nil {
		t.Fatalf("set: %v", err)
	}
	if err := c.InvalidateKey("nodes", "list"); err != nil {
		t.Fatalf("invalidate key: %v", err)
	}
	if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) {
		t.Errorf("get after invalidate key: err = %v, want ErrCacheMiss", err)
	}
}

func TestCache_TTLExpiry(t *testing.T) {
	c, cleanup := openTestCache(t)
	defer cleanup()

	if err := c.Set("jobs", "list", []byte("stale"), 1*time.Millisecond); err != nil {
		t.Fatalf("set: %v", err)
	}
	time.Sleep(10 * time.Millisecond)
	if _, _, err := c.Get("jobs", "list"); !errors.Is(err, ErrCacheMiss) {
		t.Errorf("get after ttl expiry: err = %v, want ErrCacheMiss", err)
	}
}

func TestCache_TTLZeroNeverExpires(t *testing.T) {
	c, cleanup := openTestCache(t)
	defer cleanup()

	if err := c.Set("namespaces", "list", []byte("forever"), 0); err != nil {
		t.Fatalf("set: %v", err)
	}
	time.Sleep(5 * time.Millisecond)
	got, _, err := c.Get("namespaces", "list")
	if err != nil {
		t.Fatalf("get ttl=0: %v", err)
	}
	if string(got) != "forever" {
		t.Errorf("get ttl=0 value = %q, want %q", got, "forever")
	}
}

func TestCache_Overwrite(t *testing.T) {
	c, cleanup := openTestCache(t)
	defer cleanup()

	if err := c.Set("nodes", "list", []byte("v1"), 30*time.Second); err != nil {
		t.Fatalf("set v1: %v", err)
	}
	if err := c.Set("nodes", "list", []byte("v2"), 30*time.Second); err != nil {
		t.Fatalf("set v2: %v", err)
	}
	got, _, err := c.Get("nodes", "list")
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	if string(got) != "v2" {
		t.Errorf("get after overwrite = %q, want %q", got, "v2")
	}
}

func TestCache_InvalidateAll(t *testing.T) {
	c, cleanup := openTestCache(t)
	defer cleanup()

	_ = c.Set("nodes", "list", []byte("a"), 30*time.Second)
	_ = c.Set("jobs", "list", []byte("b"), 30*time.Second)
	if err := c.InvalidateAll(); err != nil {
		t.Fatalf("invalidate all: %v", err)
	}
	if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) {
		t.Errorf("nodes/list after invalidate-all: err = %v, want ErrCacheMiss", err)
	}
	if _, _, err := c.Get("jobs", "list"); !errors.Is(err, ErrCacheMiss) {
		t.Errorf("jobs/list after invalidate-all: err = %v, want ErrCacheMiss", err)
	}
}

func TestCache_Stats(t *testing.T) {
	c, cleanup := openTestCache(t)
	defer cleanup()

	_ = c.Set("nodes", "list", []byte("aaaa"), 30*time.Second)
	_ = c.Set("jobs", "list", []byte("bb"), 30*time.Second)
	stats, err := c.Stats()
	if err != nil {
		t.Fatalf("stats: %v", err)
	}
	if len(stats) != 2 {
		t.Fatalf("stats len = %d, want 2", len(stats))
	}
	var nodes, jobs *ClassStats
	for i := range stats {
		switch stats[i].Class {
		case "nodes":
			nodes = &stats[i]
		case "jobs":
			jobs = &stats[i]
		}
	}
	if nodes == nil || nodes.Count != 1 || nodes.Bytes != 4 {
		t.Errorf("nodes stats = %+v, want count=1 bytes=4", nodes)
	}
	if jobs == nil || jobs.Count != 1 || jobs.Bytes != 2 {
		t.Errorf("jobs stats = %+v, want count=1 bytes=2", jobs)
	}
}

func TestCache_OpenDefaultPath(t *testing.T) {
	dir := t.TempDir()
	t.Setenv("ORCA_HOME", dir)
	c, err := Open("")
	if err != nil {
		t.Fatalf("open default path: %v", err)
	}
	defer c.Close()
	if err := c.Set("nodes", "list", []byte("ok"), 0); err != nil {
		t.Fatalf("set: %v", err)
	}
	got, _, err := c.Get("nodes", "list")
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	if string(got) != "ok" {
		t.Errorf("get = %q, want %q", got, "ok")
	}
}

func BenchmarkCacheHit(b *testing.B) {
	path := filepath.Join(b.TempDir(), "orca_cache.db")
	c, err := Open(path)
	if err != nil {
		b.Fatalf("open: %v", err)
	}
	defer c.Close()
	if err := c.Set("nodes", "list", []byte("bench"), 0); err != nil {
		b.Fatalf("set: %v", err)
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		if _, _, err := c.Get("nodes", "list"); err != nil {
			b.Fatalf("get: %v", err)
		}
	}
}
