package store

import (
	"context"
	"database/sql"
	"path/filepath"
	"testing"
	"time"

	_ "modernc.org/sqlite"
)

func openCacheTestDB(t *testing.T) (*AllocHistoryRepo, *sql.DB, func()) {
	t.Helper()
	path := filepath.Join(t.TempDir(), "cache.db")
	db, err := sql.Open("sqlite", path+"?_pragma=journal_mode(WAL)")
	if err != nil {
		t.Fatalf("open cache sqlite: %v", err)
	}
	if err := db.Ping(); err != nil {
		_ = db.Close()
		t.Fatalf("ping cache sqlite: %v", err)
	}
	return NewAllocHistoryRepo(db), db, func() { _ = db.Close() }
}

func TestAllocHistory_RecordAndList(t *testing.T) {
	repo, _, cleanup := openCacheTestDB(t)
	defer cleanup()
	ctx := context.Background()

	e1 := AllocHistoryEntry{
		AllocID: "alloc-1", JobID: "job-1", NodeID: "node-1",
		Namespace: "default", FromState: "", ToState: "created",
		Timestamp: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC), Reason: "dispatch",
	}
	if err := repo.Record(ctx, e1); err != nil {
		t.Fatalf("record: %v", err)
	}
	e2 := AllocHistoryEntry{
		AllocID: "alloc-1", JobID: "job-1", NodeID: "node-1",
		Namespace: "default", FromState: "created", ToState: "started",
		Timestamp: time.Date(2026, 1, 1, 12, 1, 0, 0, time.UTC),
	}
	if err := repo.Record(ctx, e2); err != nil {
		t.Fatalf("record 2: %v", err)
	}

	got, err := repo.List(ctx, HistoryFilter{})
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(got) != 2 {
		t.Fatalf("expected 2 entries, got %d", len(got))
	}
	if got[0].ToState != "created" || got[1].ToState != "started" {
		t.Errorf("order/states wrong: %+v", got)
	}
	if got[0].FromState != "" {
		t.Errorf("first entry from_state = %q, want empty", got[0].FromState)
	}
	if got[1].FromState != "created" {
		t.Errorf("second entry from_state = %q, want created", got[1].FromState)
	}
	if !got[0].Timestamp.Equal(e1.Timestamp) {
		t.Errorf("ts = %v, want %v", got[0].Timestamp, e1.Timestamp)
	}
	if got[0].Reason != "dispatch" {
		t.Errorf("reason = %q, want dispatch", got[0].Reason)
	}
}

func TestAllocHistory_ListFilterByJob(t *testing.T) {
	repo, _, cleanup := openCacheTestDB(t)
	defer cleanup()
	ctx := context.Background()
	base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	entries := []AllocHistoryEntry{
		{AllocID: "a1", JobID: "jobA", NodeID: "n1", Namespace: "ns", ToState: "created", Timestamp: base},
		{AllocID: "a2", JobID: "jobB", NodeID: "n1", Namespace: "ns", ToState: "created", Timestamp: base.Add(1 * time.Second)},
		{AllocID: "a3", JobID: "jobA", NodeID: "n2", Namespace: "ns", ToState: "created", Timestamp: base.Add(2 * time.Second)},
	}
	for _, e := range entries {
		if err := repo.Record(ctx, e); err != nil {
			t.Fatalf("record: %v", err)
		}
	}
	got, err := repo.List(ctx, HistoryFilter{JobID: "jobA"})
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(got) != 2 {
		t.Fatalf("expected 2 entries for jobA, got %d", len(got))
	}
	for _, e := range got {
		if e.JobID != "jobA" {
			t.Errorf("job = %q, want jobA", e.JobID)
		}
	}
}

func TestAllocHistory_ListFilterByNode(t *testing.T) {
	repo, _, cleanup := openCacheTestDB(t)
	defer cleanup()
	ctx := context.Background()
	base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	entries := []AllocHistoryEntry{
		{AllocID: "a1", JobID: "jobA", NodeID: "n1", Namespace: "ns", ToState: "created", Timestamp: base},
		{AllocID: "a2", JobID: "jobB", NodeID: "n1", Namespace: "ns", ToState: "created", Timestamp: base.Add(1 * time.Second)},
		{AllocID: "a3", JobID: "jobA", NodeID: "n2", Namespace: "ns", ToState: "created", Timestamp: base.Add(2 * time.Second)},
	}
	for _, e := range entries {
		if err := repo.Record(ctx, e); err != nil {
			t.Fatalf("record: %v", err)
		}
	}
	got, err := repo.List(ctx, HistoryFilter{NodeID: "n2"})
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(got) != 1 {
		t.Fatalf("expected 1 entry for n2, got %d", len(got))
	}
	if got[0].NodeID != "n2" {
		t.Errorf("node = %q, want n2", got[0].NodeID)
	}
}

func TestAllocHistory_ListFilterBySince(t *testing.T) {
	repo, _, cleanup := openCacheTestDB(t)
	defer cleanup()
	ctx := context.Background()
	base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	entries := []AllocHistoryEntry{
		{AllocID: "a1", JobID: "j", NodeID: "n", Namespace: "ns", ToState: "created", Timestamp: base},
		{AllocID: "a2", JobID: "j", NodeID: "n", Namespace: "ns", ToState: "started", Timestamp: base.Add(10 * time.Second)},
		{AllocID: "a3", JobID: "j", NodeID: "n", Namespace: "ns", ToState: "stopped", Timestamp: base.Add(20 * time.Second)},
	}
	for _, e := range entries {
		if err := repo.Record(ctx, e); err != nil {
			t.Fatalf("record: %v", err)
		}
	}
	since := base.Add(5 * time.Second)
	got, err := repo.List(ctx, HistoryFilter{Since: since})
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(got) != 2 {
		t.Fatalf("expected 2 entries since %v, got %d", since, len(got))
	}
	if got[0].ToState != "started" || got[1].ToState != "stopped" {
		t.Errorf("expected started+stopped, got %+v", got)
	}
}

func TestAllocHistory_ListFilterByNamespace(t *testing.T) {
	repo, _, cleanup := openCacheTestDB(t)
	defer cleanup()
	ctx := context.Background()
	base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	entries := []AllocHistoryEntry{
		{AllocID: "a1", JobID: "j", NodeID: "n", Namespace: "prod", ToState: "created", Timestamp: base},
		{AllocID: "a2", JobID: "j", NodeID: "n", Namespace: "dev", ToState: "created", Timestamp: base.Add(1 * time.Second)},
	}
	for _, e := range entries {
		if err := repo.Record(ctx, e); err != nil {
			t.Fatalf("record: %v", err)
		}
	}
	got, err := repo.List(ctx, HistoryFilter{Namespace: "prod"})
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(got) != 1 {
		t.Fatalf("expected 1 entry for prod, got %d", len(got))
	}
	if got[0].Namespace != "prod" {
		t.Errorf("ns = %q, want prod", got[0].Namespace)
	}
}

func TestAllocHistory_Evict(t *testing.T) {
	repo, _, cleanup := openCacheTestDB(t)
	defer cleanup()
	ctx := context.Background()
	base := time.Now().UTC()
	old := base.Add(-30 * 24 * time.Hour)
	recent := base.Add(-1 * time.Hour)
	entries := []AllocHistoryEntry{
		{AllocID: "old1", JobID: "j", NodeID: "n", Namespace: "ns", ToState: "created", Timestamp: old},
		{AllocID: "old2", JobID: "j", NodeID: "n", Namespace: "ns", ToState: "started", Timestamp: old.Add(1 * time.Second)},
		{AllocID: "new1", JobID: "j", NodeID: "n", Namespace: "ns", ToState: "created", Timestamp: recent},
	}
	for _, e := range entries {
		if err := repo.Record(ctx, e); err != nil {
			t.Fatalf("record: %v", err)
		}
	}
	cutoff := base.Add(-7 * 24 * time.Hour)
	n, err := repo.Evict(ctx, cutoff)
	if err != nil {
		t.Fatalf("evict: %v", err)
	}
	if n != 2 {
		t.Errorf("evicted = %d, want 2", n)
	}
	got, err := repo.List(ctx, HistoryFilter{})
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(got) != 1 {
		t.Fatalf("after evict, expected 1 entry, got %d", len(got))
	}
	if got[0].AllocID != "new1" {
		t.Errorf("remaining alloc = %q, want new1", got[0].AllocID)
	}
}

func TestAllocHistory_EvictEmpty(t *testing.T) {
	repo, _, cleanup := openCacheTestDB(t)
	defer cleanup()
	ctx := context.Background()
	n, err := repo.Evict(ctx, time.Now().UTC())
	if err != nil {
		t.Fatalf("evict empty: %v", err)
	}
	if n != 0 {
		t.Errorf("evict empty = %d, want 0", n)
	}
}

func TestAllocHistory_RecordDefaultsTimestamp(t *testing.T) {
	repo, _, cleanup := openCacheTestDB(t)
	defer cleanup()
	ctx := context.Background()
	before := time.Now().UTC().Add(-1 * time.Second)
	if err := repo.Record(ctx, AllocHistoryEntry{
		AllocID: "a", JobID: "j", NodeID: "n", Namespace: "ns", ToState: "created",
	}); err != nil {
		t.Fatalf("record: %v", err)
	}
	got, err := repo.List(ctx, HistoryFilter{})
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(got) != 1 {
		t.Fatalf("expected 1, got %d", len(got))
	}
	if got[0].Timestamp.Before(before) {
		t.Errorf("default ts = %v, want >= %v", got[0].Timestamp, before)
	}
}

func TestAllocHistory_TTLEvictor(t *testing.T) {
	prev := DefaultEvictorInterval
	DefaultEvictorInterval = 20 * time.Millisecond
	defer func() { DefaultEvictorInterval = prev }()

	repo, _, cleanup := openCacheTestDB(t)
	defer cleanup()
	ctx := context.Background()

	old := time.Now().UTC().Add(-30 * 24 * time.Hour)
	if err := repo.Record(ctx, AllocHistoryEntry{
		AllocID: "old1", JobID: "j", NodeID: "n", Namespace: "ns",
		ToState: "created", Timestamp: old,
	}); err != nil {
		t.Fatalf("record old: %v", err)
	}
	if err := repo.Record(ctx, AllocHistoryEntry{
		AllocID: "new1", JobID: "j", NodeID: "n", Namespace: "ns",
		ToState: "created", Timestamp: time.Now().UTC(),
	}); err != nil {
		t.Fatalf("record new: %v", err)
	}

	rootCtx, rootCancel := context.WithCancel(context.Background())
	stop := repo.StartEvictor(rootCtx, 7*24*time.Hour, 20*time.Millisecond)

	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		got, err := repo.List(ctx, HistoryFilter{})
		if err != nil {
			t.Fatalf("list: %v", err)
		}
		if len(got) == 1 {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}

	stop()
	rootCancel()

	got, err := repo.List(ctx, HistoryFilter{})
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(got) != 1 {
		t.Fatalf("after evictor, expected 1 entry, got %d", len(got))
	}
	if got[0].AllocID != "new1" {
		t.Errorf("remaining = %q, want new1", got[0].AllocID)
	}
}

func TestAllocHistory_TTLEvictor_StopsOnCancel(t *testing.T) {
	prev := DefaultEvictorInterval
	DefaultEvictorInterval = 20 * time.Millisecond
	defer func() { DefaultEvictorInterval = prev }()

	repo, _, cleanup := openCacheTestDB(t)
	defer cleanup()
	ctx, cancel := context.WithCancel(context.Background())
	stop := repo.StartEvictor(ctx, 7*24*time.Hour, 20*time.Millisecond)

	done := make(chan struct{})
	go func() {
		stop()
		close(done)
	}()
	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatal("StartEvictor stop did not return within 2s")
	}
	cancel()
}

func TestAllocHistory_EnsureSchemaIdempotent(t *testing.T) {
	repo, _, cleanup := openCacheTestDB(t)
	defer cleanup()
	ctx := context.Background()
	for i := 0; i < 3; i++ {
		if err := repo.EnsureSchema(ctx); err != nil {
			t.Fatalf("ensureSchema[%d]: %v", i, err)
		}
	}
	if err := repo.Record(ctx, AllocHistoryEntry{
		AllocID: "x", JobID: "j", NodeID: "n", Namespace: "ns", ToState: "created",
	}); err != nil {
		t.Fatalf("record after repeated schema: %v", err)
	}
}
