package engine

import (
	"context"
	"errors"
	"slices"
	"testing"
	"time"
)

func TestPruneCachesExpiryPolicies(t *testing.T) {
	ctx := context.Background()
	now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC)
	type spec struct {
		id    string
		state string
		age   time.Duration
	}
	tests := []struct {
		name        string
		retention   time.Duration
		pendingMax  time.Duration
		entries     []spec
		wantPruned  int
		wantSurvive []string
	}{
		{
			name:        "ready entries expire by last use",
			retention:   time.Hour,
			pendingMax:  15 * time.Minute,
			entries:     []spec{{"old", "ready", 2 * time.Hour}, {"fresh", "ready", 30 * time.Minute}},
			wantPruned:  1,
			wantSurvive: []string{"fresh"},
		},
		{
			name:        "zero retention keeps ready entries",
			retention:   0,
			pendingMax:  time.Hour,
			entries:     []spec{{"ready", "ready", 24 * time.Hour}, {"pending", "pending", 2 * time.Hour}},
			wantPruned:  1,
			wantSurvive: []string{"ready"},
		},
		{
			name:        "pending entries expire by age",
			retention:   time.Hour,
			pendingMax:  time.Hour,
			entries:     []spec{{"old", "pending", 2 * time.Hour}, {"fresh", "pending", 10 * time.Minute}},
			wantPruned:  1,
			wantSurvive: []string{"fresh"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			d := newCacheTestDB(t)
			store := &fakeStorage{objects: map[string][]byte{}}
			keys := map[string]string{}
			for _, e := range tt.entries {
				entry := cacheTestEntry(e.id, "did:plc:repo", "microvm", "deps", "hash", e.state, now.Add(-e.age))
				insertCacheTestEntry(t, d, entry)
				store.objects[entry.StorageKey] = []byte(e.id)
				keys[e.id] = entry.StorageKey
			}

			pruned, err := PruneCaches(ctx, d, store, now, tt.retention, tt.pendingMax, 10)
			if err != nil {
				t.Fatalf("PruneCaches: %v", err)
			}
			if pruned != tt.wantPruned {
				t.Fatalf("pruned %d entries, want %d", pruned, tt.wantPruned)
			}
			for _, e := range tt.entries {
				survives := slices.Contains(tt.wantSurvive, e.id)
				if store.has(keys[e.id]) != survives || cacheTestEntryExists(t, d, e.id) != survives {
					t.Fatalf("entry %q survived = %v, want %v", e.id, !survives, survives)
				}
			}
		})
	}
}

func TestPruneCachesSkipsEntryRefreshedAfterScan(t *testing.T) {
	ctx := context.Background()
	d := newCacheTestDB(t)
	now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC)
	first := cacheTestEntry("first", "did:plc:repo", "microvm", "deps", "first", "ready", now.Add(-3*time.Hour))
	refreshed := cacheTestEntry("refreshed", "did:plc:repo", "microvm", "deps", "refreshed", "ready", now.Add(-2*time.Hour))
	insertCacheTestEntry(t, d, first)
	insertCacheTestEntry(t, d, refreshed)
	store := &fakeStorage{objects: map[string][]byte{
		first.StorageKey:     []byte("first"),
		refreshed.StorageKey: []byte("refreshed"),
	}}
	store.onDelete = func(key string) {
		if key != first.StorageKey {
			return
		}
		if err := d.TouchCacheEntry(ctx, refreshed.ID, now); err != nil {
			t.Fatalf("TouchCacheEntry: %v", err)
		}
	}

	pruned, err := PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10)
	if err != nil {
		t.Fatalf("PruneCaches: %v", err)
	}
	if pruned != 1 {
		t.Fatalf("pruned %d entries, want 1", pruned)
	}
	if !store.has(refreshed.StorageKey) || !cacheTestEntryExists(t, d, refreshed.ID) {
		t.Fatal("entry refreshed after expiry scan was pruned")
	}
}

func TestPruneCachesDeleteFailureRemainsRetryable(t *testing.T) {
	for _, initialState := range []string{"ready", "deleting"} {
		t.Run(initialState, func(t *testing.T) {
			ctx := context.Background()
			d := newCacheTestDB(t)
			now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC)
			entry := cacheTestEntry("retry", "did:plc:repo", "microvm", "deps", "hash", initialState, now.Add(-2*time.Hour))
			insertCacheTestEntry(t, d, entry)
			deleteErr := errors.New("delete failed")
			store := &fakeStorage{
				objects:   map[string][]byte{entry.StorageKey: []byte("archive")},
				deleteErr: deleteErr,
			}

			pruned, err := PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10)
			if !errors.Is(err, deleteErr) {
				t.Fatalf("first PruneCaches error = %v, want %v", err, deleteErr)
			}
			if pruned != 0 {
				t.Fatalf("first prune count = %d, want 0", pruned)
			}
			state, _, _ := cacheTestEntryState(t, d, entry.ID)
			if state != initialState {
				t.Fatalf("state after delete failure = %q, want %q", state, initialState)
			}
			if !store.has(entry.StorageKey) {
				t.Fatal("failed delete removed object")
			}

			pruned, err = PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10)
			if err != nil {
				t.Fatalf("retry PruneCaches: %v", err)
			}
			if pruned != 1 || store.has(entry.StorageKey) || cacheTestEntryExists(t, d, entry.ID) {
				t.Fatalf("retry result = pruned %d, object %v, metadata %v", pruned, store.has(entry.StorageKey), cacheTestEntryExists(t, d, entry.ID))
			}
		})
	}
}
