package rpc

import (
	"fmt"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/steveyegge/beads/internal/storage/sqlite"
)

func TestStorageCacheEviction_TTL(t *testing.T) {
	tmpDir := t.TempDir()

	// Create main DB
	mainDB := filepath.Join(tmpDir, "main.db")
	mainStore, err := sqlite.New(mainDB)
	if err != nil {
		t.Fatal(err)
	}
	defer mainStore.Close()

	// Create server with short TTL for testing
	socketPath := filepath.Join(tmpDir, "test.sock")
	server := NewServer(socketPath, mainStore)
	server.cacheTTL = 100 * time.Millisecond // Short TTL for testing
	defer server.Stop()

	// Create two test databases
	db1 := filepath.Join(tmpDir, "repo1", ".beads", "issues.db")
	os.MkdirAll(filepath.Dir(db1), 0755)
	store1, err := sqlite.New(db1)
	if err != nil {
		t.Fatal(err)
	}
	store1.Close()

	db2 := filepath.Join(tmpDir, "repo2", ".beads", "issues.db")
	os.MkdirAll(filepath.Dir(db2), 0755)
	store2, err := sqlite.New(db2)
	if err != nil {
		t.Fatal(err)
	}
	store2.Close()

	// Access both repos to populate cache
	req1 := &Request{Cwd: filepath.Join(tmpDir, "repo1")}
	_, err = server.getStorageForRequest(req1)
	if err != nil {
		t.Fatal(err)
	}

	req2 := &Request{Cwd: filepath.Join(tmpDir, "repo2")}
	_, err = server.getStorageForRequest(req2)
	if err != nil {
		t.Fatal(err)
	}

	// Verify both are cached
	server.cacheMu.RLock()
	cacheSize := len(server.storageCache)
	server.cacheMu.RUnlock()
	if cacheSize != 2 {
		t.Fatalf("expected 2 cached entries, got %d", cacheSize)
	}

	// Wait for TTL to expire
	time.Sleep(150 * time.Millisecond)

	// Run eviction
	server.evictStaleStorage()

	// Verify both entries were evicted
	server.cacheMu.RLock()
	cacheSize = len(server.storageCache)
	server.cacheMu.RUnlock()
	if cacheSize != 0 {
		t.Fatalf("expected 0 cached entries after TTL eviction, got %d", cacheSize)
	}
}

func TestStorageCacheEviction_LRU(t *testing.T) {
	tmpDir := t.TempDir()

	// Create main DB
	mainDB := filepath.Join(tmpDir, "main.db")
	mainStore, err := sqlite.New(mainDB)
	if err != nil {
		t.Fatal(err)
	}
	defer mainStore.Close()

	// Create server with small cache size
	socketPath := filepath.Join(tmpDir, "test.sock")
	server := NewServer(socketPath, mainStore)
	server.maxCacheSize = 2         // Only keep 2 entries
	server.cacheTTL = 1 * time.Hour // Long TTL so we test LRU
	defer server.Stop()

	// Create three test databases
	for i := 1; i <= 3; i++ {
		dbPath := filepath.Join(tmpDir, "repo"+string(rune('0'+i)), ".beads", "issues.db")
		os.MkdirAll(filepath.Dir(dbPath), 0755)
		store, err := sqlite.New(dbPath)
		if err != nil {
			t.Fatal(err)
		}
		store.Close()
	}

	// Access repos 1 and 2
	req1 := &Request{Cwd: filepath.Join(tmpDir, "repo1")}
	_, err = server.getStorageForRequest(req1)
	if err != nil {
		t.Fatal(err)
	}
	time.Sleep(10 * time.Millisecond) // Ensure different timestamps

	req2 := &Request{Cwd: filepath.Join(tmpDir, "repo2")}
	_, err = server.getStorageForRequest(req2)
	if err != nil {
		t.Fatal(err)
	}

	// Verify 2 entries cached
	server.cacheMu.RLock()
	cacheSize := len(server.storageCache)
	server.cacheMu.RUnlock()
	if cacheSize != 2 {
		t.Fatalf("expected 2 cached entries, got %d", cacheSize)
	}

	// Access repo 3, which should trigger LRU eviction of repo1 (oldest)
	req3 := &Request{Cwd: filepath.Join(tmpDir, "repo3")}
	_, err = server.getStorageForRequest(req3)
	if err != nil {
		t.Fatal(err)
	}

	// Run eviction to enforce max cache size
	server.evictStaleStorage()

	// Should still have 2 entries
	server.cacheMu.RLock()
	cacheSize = len(server.storageCache)
	_, hasRepo1 := server.storageCache[filepath.Join(tmpDir, "repo1")]
	_, hasRepo2 := server.storageCache[filepath.Join(tmpDir, "repo2")]
	_, hasRepo3 := server.storageCache[filepath.Join(tmpDir, "repo3")]
	server.cacheMu.RUnlock()

	if cacheSize != 2 {
		t.Fatalf("expected 2 cached entries after LRU eviction, got %d", cacheSize)
	}

	// Repo1 should be evicted (oldest), repo2 and repo3 should remain
	if hasRepo1 {
		t.Error("repo1 should have been evicted (oldest)")
	}
	if !hasRepo2 {
		t.Error("repo2 should still be cached")
	}
	if !hasRepo3 {
		t.Error("repo3 should be cached")
	}
}

func TestStorageCacheEviction_LastAccessUpdate(t *testing.T) {
	tmpDir := t.TempDir()

	// Create main DB
	mainDB := filepath.Join(tmpDir, "main.db")
	mainStore, err := sqlite.New(mainDB)
	if err != nil {
		t.Fatal(err)
	}
	defer mainStore.Close()

	// Create server
	socketPath := filepath.Join(tmpDir, "test.sock")
	server := NewServer(socketPath, mainStore)
	defer server.Stop()

	// Create test database
	dbPath := filepath.Join(tmpDir, "repo1", ".beads", "issues.db")
	os.MkdirAll(filepath.Dir(dbPath), 0755)
	store, err := sqlite.New(dbPath)
	if err != nil {
		t.Fatal(err)
	}
	store.Close()

	// First access
	req := &Request{Cwd: filepath.Join(tmpDir, "repo1")}
	_, err = server.getStorageForRequest(req)
	if err != nil {
		t.Fatal(err)
	}

	// Get initial lastAccess time
	server.cacheMu.RLock()
	entry := server.storageCache[filepath.Join(tmpDir, "repo1")]
	initialTime := entry.lastAccess
	server.cacheMu.RUnlock()

	// Wait a bit
	time.Sleep(50 * time.Millisecond)

	// Access again
	_, err = server.getStorageForRequest(req)
	if err != nil {
		t.Fatal(err)
	}

	// Verify lastAccess was updated
	server.cacheMu.RLock()
	entry = server.storageCache[filepath.Join(tmpDir, "repo1")]
	updatedTime := entry.lastAccess
	server.cacheMu.RUnlock()

	if !updatedTime.After(initialTime) {
		t.Errorf("lastAccess should be updated on cache hit, initial: %v, updated: %v", initialTime, updatedTime)
	}
}

func TestStorageCacheEviction_EnvVars(t *testing.T) {
	tmpDir := t.TempDir()

	// Create main DB
	mainDB := filepath.Join(tmpDir, "main.db")
	mainStore, err := sqlite.New(mainDB)
	if err != nil {
		t.Fatal(err)
	}
	defer mainStore.Close()

	// Set env vars
	os.Setenv("BEADS_DAEMON_MAX_CACHE_SIZE", "100")
	os.Setenv("BEADS_DAEMON_CACHE_TTL", "1h30m")
	defer os.Unsetenv("BEADS_DAEMON_MAX_CACHE_SIZE")
	defer os.Unsetenv("BEADS_DAEMON_CACHE_TTL")

	// Create server
	socketPath := filepath.Join(tmpDir, "test.sock")
	server := NewServer(socketPath, mainStore)
	defer server.Stop()

	// Verify config was parsed
	if server.maxCacheSize != 100 {
		t.Errorf("expected maxCacheSize=100, got %d", server.maxCacheSize)
	}
	expectedTTL := 90 * time.Minute
	if server.cacheTTL != expectedTTL {
		t.Errorf("expected cacheTTL=%v, got %v", expectedTTL, server.cacheTTL)
	}
}

func TestStorageCacheEviction_CleanupOnStop(t *testing.T) {
	tmpDir := t.TempDir()

	// Create main DB
	mainDB := filepath.Join(tmpDir, "main.db")
	mainStore, err := sqlite.New(mainDB)
	if err != nil {
		t.Fatal(err)
	}
	defer mainStore.Close()

	// Create server
	socketPath := filepath.Join(tmpDir, "test.sock")
	server := NewServer(socketPath, mainStore)

	// Create test database and populate cache
	dbPath := filepath.Join(tmpDir, "repo1", ".beads", "issues.db")
	os.MkdirAll(filepath.Dir(dbPath), 0755)
	store, err := sqlite.New(dbPath)
	if err != nil {
		t.Fatal(err)
	}
	store.Close()

	req := &Request{Cwd: filepath.Join(tmpDir, "repo1")}
	_, err = server.getStorageForRequest(req)
	if err != nil {
		t.Fatal(err)
	}

	// Verify cached
	server.cacheMu.RLock()
	cacheSize := len(server.storageCache)
	server.cacheMu.RUnlock()
	if cacheSize != 1 {
		t.Fatalf("expected 1 cached entry, got %d", cacheSize)
	}

	// Stop server
	if err := server.Stop(); err != nil {
		t.Fatal(err)
	}

	// Verify cache was cleared
	server.cacheMu.RLock()
	cacheSize = len(server.storageCache)
	server.cacheMu.RUnlock()
	if cacheSize != 0 {
		t.Errorf("expected cache to be cleared on stop, got %d entries", cacheSize)
	}
}

func TestStorageCacheEviction_CanonicalKey(t *testing.T) {
	tmpDir := t.TempDir()

	// Create main DB
	mainDB := filepath.Join(tmpDir, "main.db")
	mainStore, err := sqlite.New(mainDB)
	if err != nil {
		t.Fatal(err)
	}
	defer mainStore.Close()

	// Create server
	socketPath := filepath.Join(tmpDir, "test.sock")
	server := NewServer(socketPath, mainStore)
	defer server.Stop()

	// Create test database
	dbPath := filepath.Join(tmpDir, "repo1", ".beads", "issues.db")
	os.MkdirAll(filepath.Dir(dbPath), 0755)
	store, err := sqlite.New(dbPath)
	if err != nil {
		t.Fatal(err)
	}
	store.Close()

	// Access from different subdirectories of the same repo
	req1 := &Request{Cwd: filepath.Join(tmpDir, "repo1")}
	_, err = server.getStorageForRequest(req1)
	if err != nil {
		t.Fatal(err)
	}

	req2 := &Request{Cwd: filepath.Join(tmpDir, "repo1", "subdir1")}
	_, err = server.getStorageForRequest(req2)
	if err != nil {
		t.Fatal(err)
	}

	req3 := &Request{Cwd: filepath.Join(tmpDir, "repo1", "subdir1", "subdir2")}
	_, err = server.getStorageForRequest(req3)
	if err != nil {
		t.Fatal(err)
	}

	// Should only have one cache entry (all pointing to same repo root)
	server.cacheMu.RLock()
	cacheSize := len(server.storageCache)
	server.cacheMu.RUnlock()
	if cacheSize != 1 {
		t.Errorf("expected 1 cached entry (canonical key), got %d", cacheSize)
	}
}

func TestStorageCacheEviction_ImmediateLRU(t *testing.T) {
	tmpDir := t.TempDir()

	// Create main DB
	mainDB := filepath.Join(tmpDir, "main.db")
	mainStore, err := sqlite.New(mainDB)
	if err != nil {
		t.Fatal(err)
	}
	defer mainStore.Close()

	// Create server with max cache size of 2
	socketPath := filepath.Join(tmpDir, "test.sock")
	server := NewServer(socketPath, mainStore)
	server.maxCacheSize = 2
	server.cacheTTL = 1 * time.Hour // Long TTL
	defer server.Stop()

	// Create 3 test databases
	for i := 1; i <= 3; i++ {
		dbPath := filepath.Join(tmpDir, fmt.Sprintf("repo%d", i), ".beads", "issues.db")
		os.MkdirAll(filepath.Dir(dbPath), 0755)
		store, err := sqlite.New(dbPath)
		if err != nil {
			t.Fatal(err)
		}
		store.Close()
	}

	// Access all 3 repos
	for i := 1; i <= 3; i++ {
		req := &Request{Cwd: filepath.Join(tmpDir, fmt.Sprintf("repo%d", i))}
		_, err = server.getStorageForRequest(req)
		if err != nil {
			t.Fatal(err)
		}
		time.Sleep(10 * time.Millisecond) // Ensure different timestamps
	}

	// Cache should never exceed maxCacheSize (immediate LRU enforcement)
	server.cacheMu.RLock()
	cacheSize := len(server.storageCache)
	server.cacheMu.RUnlock()
	if cacheSize > server.maxCacheSize {
		t.Errorf("cache size %d exceeds max %d (immediate LRU not enforced)", cacheSize, server.maxCacheSize)
	}
}

func TestStorageCacheEviction_InvalidTTL(t *testing.T) {
	tmpDir := t.TempDir()

	// Create main DB
	mainDB := filepath.Join(tmpDir, "main.db")
	mainStore, err := sqlite.New(mainDB)
	if err != nil {
		t.Fatal(err)
	}
	defer mainStore.Close()

	// Set invalid TTL
	os.Setenv("BEADS_DAEMON_CACHE_TTL", "-5m")
	defer os.Unsetenv("BEADS_DAEMON_CACHE_TTL")

	// Create server
	socketPath := filepath.Join(tmpDir, "test.sock")
	server := NewServer(socketPath, mainStore)
	defer server.Stop()

	// Should fall back to default (30 minutes)
	expectedTTL := 30 * time.Minute
	if server.cacheTTL != expectedTTL {
		t.Errorf("expected TTL to fall back to %v for invalid value, got %v", expectedTTL, server.cacheTTL)
	}
}

func TestStorageCacheEviction_ReopenAfterEviction(t *testing.T) {
	tmpDir := t.TempDir()

	// Create main DB
	mainDB := filepath.Join(tmpDir, "main.db")
	mainStore, err := sqlite.New(mainDB)
	if err != nil {
		t.Fatal(err)
	}
	defer mainStore.Close()

	// Create server with short TTL
	socketPath := filepath.Join(tmpDir, "test.sock")
	server := NewServer(socketPath, mainStore)
	server.cacheTTL = 50 * time.Millisecond
	defer server.Stop()

	// Create test database
	dbPath := filepath.Join(tmpDir, "repo1", ".beads", "issues.db")
	os.MkdirAll(filepath.Dir(dbPath), 0755)
	store, err := sqlite.New(dbPath)
	if err != nil {
		t.Fatal(err)
	}
	store.Close()

	// Access repo
	req := &Request{Cwd: filepath.Join(tmpDir, "repo1")}
	_, err = server.getStorageForRequest(req)
	if err != nil {
		t.Fatal(err)
	}

	// Wait for TTL to expire
	time.Sleep(100 * time.Millisecond)

	// Evict
	server.evictStaleStorage()

	// Verify evicted
	server.cacheMu.RLock()
	cacheSize := len(server.storageCache)
	server.cacheMu.RUnlock()
	if cacheSize != 0 {
		t.Fatalf("expected cache to be empty after eviction, got %d", cacheSize)
	}

	// Access again - should cleanly re-open
	_, err = server.getStorageForRequest(req)
	if err != nil {
		t.Fatalf("failed to re-open after eviction: %v", err)
	}

	// Verify re-cached
	server.cacheMu.RLock()
	cacheSize = len(server.storageCache)
	server.cacheMu.RUnlock()
	if cacheSize != 1 {
		t.Errorf("expected 1 cached entry after re-open, got %d", cacheSize)
	}
}

func TestStorageCacheEviction_StopIdempotent(t *testing.T) {
	tmpDir := t.TempDir()

	// Create main DB
	mainDB := filepath.Join(tmpDir, "main.db")
	mainStore, err := sqlite.New(mainDB)
	if err != nil {
		t.Fatal(err)
	}
	defer mainStore.Close()

	// Create server
	socketPath := filepath.Join(tmpDir, "test.sock")
	server := NewServer(socketPath, mainStore)

	// Stop multiple times - should not panic
	if err := server.Stop(); err != nil {
		t.Fatalf("first Stop failed: %v", err)
	}
	if err := server.Stop(); err != nil {
		t.Fatalf("second Stop failed: %v", err)
	}
	if err := server.Stop(); err != nil {
		t.Fatalf("third Stop failed: %v", err)
	}
}
