package media

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"
)

func createTempFile(t *testing.T, dir, name string) string {
	t.Helper()
	path := filepath.Join(dir, name)
	if err := os.WriteFile(path, []byte("test content"), 0o644); err != nil {
		t.Fatalf("failed to create temp file: %v", err)
	}
	return path
}

func TestStoreAndResolve(t *testing.T) {
	dir := t.TempDir()
	store := NewFileMediaStore()

	path := createTempFile(t, dir, "photo.jpg")

	ref, err := store.Store(path, MediaMeta{Filename: "photo.jpg", Source: "telegram"}, "scope1")
	if err != nil {
		t.Fatalf("Store failed: %v", err)
	}

	if !strings.HasPrefix(ref, "media://") {
		t.Errorf("ref should start with media://, got %q", ref)
	}

	resolved, err := store.Resolve(ref)
	if err != nil {
		t.Fatalf("Resolve failed: %v", err)
	}
	if resolved != path {
		t.Errorf("Resolve returned %q, want %q", resolved, path)
	}
}

func TestReleaseAll(t *testing.T) {
	dir := t.TempDir()
	store := NewFileMediaStore()

	paths := make([]string, 3)
	refs := make([]string, 3)
	for i := range 3 {
		paths[i] = createTempFile(t, dir, strings.Repeat("a", i+1)+".jpg")
		var err error
		refs[i], err = store.Store(paths[i], MediaMeta{Source: "test"}, "scope1")
		if err != nil {
			t.Fatalf("Store failed: %v", err)
		}
	}

	if err := store.ReleaseAll("scope1"); err != nil {
		t.Fatalf("ReleaseAll failed: %v", err)
	}

	// Files should be deleted
	for _, p := range paths {
		if _, err := os.Stat(p); !os.IsNotExist(err) {
			t.Errorf("file %q should have been deleted", p)
		}
	}

	// Refs should be unresolvable
	for _, ref := range refs {
		if _, err := store.Resolve(ref); err == nil {
			t.Errorf("Resolve(%q) should fail after ReleaseAll", ref)
		}
	}
}

func TestReleaseAllForgetOnlyKeepsFile(t *testing.T) {
	dir := t.TempDir()
	store := NewFileMediaStore()

	path := createTempFile(t, dir, "workspace.txt")
	ref, err := store.Store(path, MediaMeta{
		Source:        "test",
		CleanupPolicy: CleanupPolicyForgetOnly,
	}, "scope1")
	if err != nil {
		t.Fatalf("Store failed: %v", err)
	}

	if err := store.ReleaseAll("scope1"); err != nil {
		t.Fatalf("ReleaseAll failed: %v", err)
	}

	if _, err := store.Resolve(ref); err == nil {
		t.Error("forget-only ref should be unresolvable after release")
	}
	if _, err := os.Stat(path); err != nil {
		t.Errorf("forget-only file should remain on disk: %v", err)
	}
}

func TestReleaseAllSharedPathDeletesOnFinalRefOnly(t *testing.T) {
	dir := t.TempDir()
	store := NewFileMediaStore()

	path := createTempFile(t, dir, "shared.jpg")
	refA, err := store.Store(path, MediaMeta{
		Source:        "test",
		CleanupPolicy: CleanupPolicyDeleteOnCleanup,
	}, "scopeA")
	if err != nil {
		t.Fatalf("Store(scopeA) failed: %v", err)
	}
	refB, err := store.Store(path, MediaMeta{
		Source:        "test",
		CleanupPolicy: CleanupPolicyDeleteOnCleanup,
	}, "scopeB")
	if err != nil {
		t.Fatalf("Store(scopeB) failed: %v", err)
	}

	if err := store.ReleaseAll("scopeA"); err != nil {
		t.Fatalf("ReleaseAll(scopeA) failed: %v", err)
	}

	if _, err := store.Resolve(refA); err == nil {
		t.Error("refA should be unresolvable after ReleaseAll(scopeA)")
	}
	if _, err := store.Resolve(refB); err != nil {
		t.Fatalf("refB should still resolve: %v", err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Errorf("shared file should remain until final ref is released: %v", err)
	}

	if err := store.ReleaseAll("scopeB"); err != nil {
		t.Fatalf("ReleaseAll(scopeB) failed: %v", err)
	}
	if _, err := os.Stat(path); !os.IsNotExist(err) {
		t.Error("shared file should be deleted after final ref is released")
	}
}

func TestReleaseAllMixedPoliciesKeepsFile(t *testing.T) {
	dir := t.TempDir()
	store := NewFileMediaStore()

	path := createTempFile(t, dir, "shared.txt")
	if _, err := store.Store(path, MediaMeta{
		Source:        "test",
		CleanupPolicy: CleanupPolicyDeleteOnCleanup,
	}, "owned"); err != nil {
		t.Fatalf("Store(owned) failed: %v", err)
	}
	if _, err := store.Store(path, MediaMeta{
		Source:        "test",
		CleanupPolicy: CleanupPolicyForgetOnly,
	}, "borrowed"); err != nil {
		t.Fatalf("Store(borrowed) failed: %v", err)
	}

	if err := store.ReleaseAll("owned"); err != nil {
		t.Fatalf("ReleaseAll(owned) failed: %v", err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Fatalf("mixed-policy file should remain after owned ref release: %v", err)
	}

	if err := store.ReleaseAll("borrowed"); err != nil {
		t.Fatalf("ReleaseAll(borrowed) failed: %v", err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Errorf("mixed-policy path should not be auto-deleted: %v", err)
	}
}

func TestMultiScopeIsolation(t *testing.T) {
	dir := t.TempDir()
	store := NewFileMediaStore()

	pathA := createTempFile(t, dir, "fileA.jpg")
	pathB := createTempFile(t, dir, "fileB.jpg")

	refA, _ := store.Store(pathA, MediaMeta{Source: "test"}, "scopeA")
	refB, _ := store.Store(pathB, MediaMeta{Source: "test"}, "scopeB")

	// Release only scopeA
	if err := store.ReleaseAll("scopeA"); err != nil {
		t.Fatalf("ReleaseAll(scopeA) failed: %v", err)
	}

	// scopeA file should be gone
	if _, err := os.Stat(pathA); !os.IsNotExist(err) {
		t.Error("file A should have been deleted")
	}
	if _, err := store.Resolve(refA); err == nil {
		t.Error("refA should be unresolvable after release")
	}

	// scopeB file should still exist
	if _, err := os.Stat(pathB); err != nil {
		t.Error("file B should still exist")
	}
	resolved, err := store.Resolve(refB)
	if err != nil {
		t.Fatalf("refB should still resolve: %v", err)
	}
	if resolved != pathB {
		t.Errorf("resolved %q, want %q", resolved, pathB)
	}
}

func TestReleaseAllIdempotent(t *testing.T) {
	store := NewFileMediaStore()

	// ReleaseAll on non-existent scope should not error
	if err := store.ReleaseAll("nonexistent"); err != nil {
		t.Fatalf("ReleaseAll on empty scope should not error: %v", err)
	}

	// Create and release, then release again
	dir := t.TempDir()
	path := createTempFile(t, dir, "file.jpg")
	_, _ = store.Store(path, MediaMeta{Source: "test"}, "scope1")

	if err := store.ReleaseAll("scope1"); err != nil {
		t.Fatalf("first ReleaseAll failed: %v", err)
	}
	if err := store.ReleaseAll("scope1"); err != nil {
		t.Fatalf("second ReleaseAll should not error: %v", err)
	}
}

func TestReleaseAllCleansMappingsIfRefsMissing(t *testing.T) {
	dir := t.TempDir()
	store := NewFileMediaStore()

	path := createTempFile(t, dir, "file.jpg")
	ref, err := store.Store(path, MediaMeta{Source: "test"}, "scope1")
	if err != nil {
		t.Fatalf("Store failed: %v", err)
	}

	// Simulate internal inconsistency: scopeToRefs/refToScope contains ref but refs map doesn't.
	store.mu.Lock()
	delete(store.refs, ref)
	store.mu.Unlock()

	if err := store.ReleaseAll("scope1"); err != nil {
		t.Fatalf("ReleaseAll failed: %v", err)
	}

	// ReleaseAll should still clean mappings (even if it can't delete the file without the path).
	store.mu.RLock()
	defer store.mu.RUnlock()
	if _, ok := store.refToScope[ref]; ok {
		t.Error("refToScope should not contain ref after ReleaseAll")
	}
	if _, ok := store.scopeToRefs["scope1"]; ok {
		t.Error("scopeToRefs should not contain scope1 after ReleaseAll")
	}
}

func TestStoreNonexistentFile(t *testing.T) {
	store := NewFileMediaStore()

	_, err := store.Store("/nonexistent/path/file.jpg", MediaMeta{Source: "test"}, "scope1")
	if err == nil {
		t.Error("Store should fail for nonexistent file")
	}
	// Error message should include the underlying os error, not just "file does not exist"
	if !strings.Contains(err.Error(), "no such file or directory") &&
		!strings.Contains(err.Error(), "cannot find") {
		t.Errorf("Error should contain OS error detail, got: %v", err)
	}
}

func TestResolveWithMeta(t *testing.T) {
	dir := t.TempDir()
	store := NewFileMediaStore()

	path := createTempFile(t, dir, "image.png")
	meta := MediaMeta{
		Filename:    "image.png",
		ContentType: "image/png",
		Source:      "telegram",
	}

	ref, err := store.Store(path, meta, "scope1")
	if err != nil {
		t.Fatalf("Store failed: %v", err)
	}

	resolvedPath, resolvedMeta, err := store.ResolveWithMeta(ref)
	if err != nil {
		t.Fatalf("ResolveWithMeta failed: %v", err)
	}
	if resolvedPath != path {
		t.Errorf("ResolveWithMeta path = %q, want %q", resolvedPath, path)
	}
	if resolvedMeta.Filename != meta.Filename {
		t.Errorf("ResolveWithMeta Filename = %q, want %q", resolvedMeta.Filename, meta.Filename)
	}
	if resolvedMeta.ContentType != meta.ContentType {
		t.Errorf("ResolveWithMeta ContentType = %q, want %q", resolvedMeta.ContentType, meta.ContentType)
	}
	if resolvedMeta.Source != meta.Source {
		t.Errorf("ResolveWithMeta Source = %q, want %q", resolvedMeta.Source, meta.Source)
	}

	// Unknown ref should fail
	_, _, err = store.ResolveWithMeta("media://nonexistent")
	if err == nil {
		t.Error("ResolveWithMeta should fail for unknown ref")
	}
}

func TestConcurrentSafety(t *testing.T) {
	dir := t.TempDir()
	store := NewFileMediaStore()

	const goroutines = 20
	const filesPerGoroutine = 5

	var wg sync.WaitGroup
	wg.Add(goroutines)

	for g := range goroutines {
		go func(gIdx int) {
			defer wg.Done()
			scope := strings.Repeat("s", gIdx+1)

			for i := range filesPerGoroutine {
				path := createTempFile(t, dir, strings.Repeat("f", gIdx*filesPerGoroutine+i+1)+".tmp")
				ref, err := store.Store(path, MediaMeta{Source: "test"}, scope)
				if err != nil {
					t.Errorf("Store failed: %v", err)
					return
				}

				if _, err := store.Resolve(ref); err != nil {
					t.Errorf("Resolve failed: %v", err)
				}
			}

			if err := store.ReleaseAll(scope); err != nil {
				t.Errorf("ReleaseAll failed: %v", err)
			}
		}(g)
	}

	wg.Wait()
}

// --- TTL cleanup tests ---

func newTestStoreWithCleanup(maxAge time.Duration) *FileMediaStore {
	s := NewFileMediaStoreWithCleanup(MediaCleanerConfig{
		Enabled:  true,
		MaxAge:   maxAge,
		Interval: time.Hour, // won't tick in tests
	})
	return s
}

func TestCleanExpiredRemovesOldEntries(t *testing.T) {
	dir := t.TempDir()
	now := time.Now()
	store := newTestStoreWithCleanup(10 * time.Minute)
	store.nowFunc = func() time.Time { return now.Add(-20 * time.Minute) }

	path := createTempFile(t, dir, "old.jpg")
	ref, err := store.Store(path, MediaMeta{Source: "test"}, "scope1")
	if err != nil {
		t.Fatalf("Store failed: %v", err)
	}

	// Advance clock to present
	store.nowFunc = func() time.Time { return now }
	removed := store.CleanExpired()

	if removed != 1 {
		t.Errorf("expected 1 removed, got %d", removed)
	}
	if _, err := store.Resolve(ref); err == nil {
		t.Error("expired ref should be unresolvable")
	}
	if _, err := os.Stat(path); !os.IsNotExist(err) {
		t.Error("expired file should be deleted")
	}
}

func TestCleanExpiredForgetOnlyKeepsFile(t *testing.T) {
	dir := t.TempDir()
	now := time.Now()
	store := newTestStoreWithCleanup(10 * time.Minute)
	store.nowFunc = func() time.Time { return now.Add(-20 * time.Minute) }

	path := createTempFile(t, dir, "workspace.txt")
	ref, err := store.Store(path, MediaMeta{
		Source:        "test",
		CleanupPolicy: CleanupPolicyForgetOnly,
	}, "scope1")
	if err != nil {
		t.Fatalf("Store failed: %v", err)
	}

	store.nowFunc = func() time.Time { return now }
	removed := store.CleanExpired()

	if removed != 1 {
		t.Errorf("expected 1 removed, got %d", removed)
	}
	if _, err := store.Resolve(ref); err == nil {
		t.Error("expired forget-only ref should be unresolvable")
	}
	if _, err := os.Stat(path); err != nil {
		t.Errorf("forget-only file should remain on disk: %v", err)
	}
}

func TestCleanExpiredKeepsNonExpired(t *testing.T) {
	dir := t.TempDir()
	now := time.Now()
	store := newTestStoreWithCleanup(10 * time.Minute)
	store.nowFunc = func() time.Time { return now }

	path := createTempFile(t, dir, "fresh.jpg")
	ref, err := store.Store(path, MediaMeta{Source: "test"}, "scope1")
	if err != nil {
		t.Fatalf("Store failed: %v", err)
	}

	removed := store.CleanExpired()
	if removed != 0 {
		t.Errorf("expected 0 removed, got %d", removed)
	}

	if _, err := store.Resolve(ref); err != nil {
		t.Errorf("fresh ref should still resolve: %v", err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Error("fresh file should still exist")
	}
}

func TestCleanExpiredMixedAges(t *testing.T) {
	dir := t.TempDir()
	now := time.Now()
	store := newTestStoreWithCleanup(10 * time.Minute)

	// Store old entry
	store.nowFunc = func() time.Time { return now.Add(-20 * time.Minute) }
	oldPath := createTempFile(t, dir, "old.jpg")
	oldRef, _ := store.Store(oldPath, MediaMeta{Source: "test"}, "scope1")

	// Store fresh entry
	store.nowFunc = func() time.Time { return now }
	freshPath := createTempFile(t, dir, "fresh.jpg")
	freshRef, _ := store.Store(freshPath, MediaMeta{Source: "test"}, "scope1")

	removed := store.CleanExpired()
	if removed != 1 {
		t.Errorf("expected 1 removed, got %d", removed)
	}

	if _, err := store.Resolve(oldRef); err == nil {
		t.Error("old ref should be gone")
	}
	if _, err := store.Resolve(freshRef); err != nil {
		t.Errorf("fresh ref should still resolve: %v", err)
	}
}

func TestCleanExpiredSharedPathDeletesOnFinalRefOnly(t *testing.T) {
	dir := t.TempDir()
	now := time.Now()
	store := newTestStoreWithCleanup(10 * time.Minute)

	path := createTempFile(t, dir, "shared.jpg")

	store.nowFunc = func() time.Time { return now.Add(-20 * time.Minute) }
	oldRef, err := store.Store(path, MediaMeta{
		Source:        "test",
		CleanupPolicy: CleanupPolicyDeleteOnCleanup,
	}, "scope-old")
	if err != nil {
		t.Fatalf("Store(old) failed: %v", err)
	}

	store.nowFunc = func() time.Time { return now }
	freshRef, err := store.Store(path, MediaMeta{
		Source:        "test",
		CleanupPolicy: CleanupPolicyDeleteOnCleanup,
	}, "scope-fresh")
	if err != nil {
		t.Fatalf("Store(fresh) failed: %v", err)
	}

	removed := store.CleanExpired()
	if removed != 1 {
		t.Errorf("expected 1 removed, got %d", removed)
	}
	if _, err := store.Resolve(oldRef); err == nil {
		t.Error("old ref should be gone after cleanup")
	}
	if _, err := store.Resolve(freshRef); err != nil {
		t.Fatalf("fresh ref should still resolve: %v", err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Errorf("shared file should remain while fresh ref exists: %v", err)
	}

	if err := store.ReleaseAll("scope-fresh"); err != nil {
		t.Fatalf("ReleaseAll(scope-fresh) failed: %v", err)
	}
	if _, err := os.Stat(path); !os.IsNotExist(err) {
		t.Error("shared file should be deleted after final ref is released")
	}
}

func TestCleanExpiredCleansEmptyScopes(t *testing.T) {
	dir := t.TempDir()
	now := time.Now()
	store := newTestStoreWithCleanup(10 * time.Minute)

	// Store old entry as the only one in scope
	store.nowFunc = func() time.Time { return now.Add(-20 * time.Minute) }
	path := createTempFile(t, dir, "only.jpg")
	store.Store(path, MediaMeta{Source: "test"}, "lonely_scope")

	store.nowFunc = func() time.Time { return now }
	store.CleanExpired()

	store.mu.RLock()
	defer store.mu.RUnlock()
	if _, ok := store.scopeToRefs["lonely_scope"]; ok {
		t.Error("empty scope should be cleaned up")
	}
}

func TestStartStopLifecycle(t *testing.T) {
	store := NewFileMediaStoreWithCleanup(MediaCleanerConfig{
		Enabled:  true,
		MaxAge:   time.Minute,
		Interval: 50 * time.Millisecond,
	})

	// Start and stop should not panic
	store.Start()
	// Double start should not spawn a second goroutine
	store.Start()
	time.Sleep(100 * time.Millisecond)
	store.Stop()

	// Double stop should not panic
	store.Stop()
}

func TestCleanExpiredZeroMaxAge(t *testing.T) {
	store := NewFileMediaStoreWithCleanup(MediaCleanerConfig{
		Enabled:  true,
		MaxAge:   0,
		Interval: time.Hour,
	})

	dir := t.TempDir()
	path := createTempFile(t, dir, "file.jpg")
	ref, _ := store.Store(path, MediaMeta{Source: "test"}, "scope1")

	// Zero MaxAge should be a no-op
	removed := store.CleanExpired()
	if removed != 0 {
		t.Errorf("expected 0 removed with zero MaxAge, got %d", removed)
	}
	if _, err := store.Resolve(ref); err != nil {
		t.Errorf("ref should still resolve: %v", err)
	}
}

func TestStartDisabledIsNoop(t *testing.T) {
	store := NewFileMediaStoreWithCleanup(MediaCleanerConfig{
		Enabled:  false,
		MaxAge:   time.Minute,
		Interval: time.Minute,
	})
	// Should not start any goroutine or panic
	store.Start()
	store.Stop()
}

func TestStartZeroIntervalNoPanic(t *testing.T) {
	store := NewFileMediaStoreWithCleanup(MediaCleanerConfig{
		Enabled:  true,
		MaxAge:   time.Minute,
		Interval: 0,
	})
	// Zero interval should not panic (time.NewTicker panics on <= 0)
	store.Start()
	store.Stop()
}

func TestStartZeroMaxAgeNoPanic(t *testing.T) {
	store := NewFileMediaStoreWithCleanup(MediaCleanerConfig{
		Enabled:  true,
		MaxAge:   0,
		Interval: time.Minute,
	})
	store.Start()
	store.Stop()
}

func TestConcurrentCleanupSafety(t *testing.T) {
	dir := t.TempDir()
	store := newTestStoreWithCleanup(50 * time.Millisecond)
	store.nowFunc = time.Now

	const workers = 10
	const ops = 20
	var wg sync.WaitGroup
	wg.Add(workers * 4)

	// Store workers
	for w := range workers {
		go func(wIdx int) {
			defer wg.Done()
			scope := fmt.Sprintf("scope-%d", wIdx)
			for i := range ops {
				p := createTempFile(t, dir, fmt.Sprintf("w%d-f%d.tmp", wIdx, i))
				store.Store(p, MediaMeta{Source: "test"}, scope)
			}
		}(w)
	}

	// Resolve workers
	for range workers {
		go func() {
			defer wg.Done()
			for range ops {
				store.Resolve("media://nonexistent")
			}
		}()
	}

	// ReleaseAll workers
	for w := range workers {
		go func(wIdx int) {
			defer wg.Done()
			for range ops {
				store.ReleaseAll(fmt.Sprintf("scope-%d", wIdx))
			}
		}(w)
	}

	// CleanExpired workers
	for range workers {
		go func() {
			defer wg.Done()
			for range ops {
				store.CleanExpired()
			}
		}()
	}

	wg.Wait()
}

func TestRefToScopeConsistency(t *testing.T) {
	dir := t.TempDir()
	store := NewFileMediaStore()

	// Store entries in two scopes
	ref1, _ := store.Store(createTempFile(t, dir, "a.jpg"), MediaMeta{Source: "test"}, "s1")
	ref2, _ := store.Store(createTempFile(t, dir, "b.jpg"), MediaMeta{Source: "test"}, "s1")
	ref3, _ := store.Store(createTempFile(t, dir, "c.jpg"), MediaMeta{Source: "test"}, "s2")

	store.mu.RLock()
	checkRef := func(ref, expectedScope string) {
		t.Helper()
		if scope, ok := store.refToScope[ref]; !ok || scope != expectedScope {
			t.Errorf("refToScope[%s] = %q, want %q", ref, scope, expectedScope)
		}
	}
	checkRef(ref1, "s1")
	checkRef(ref2, "s1")
	checkRef(ref3, "s2")
	store.mu.RUnlock()

	// Release s1 and verify refToScope is cleaned
	store.ReleaseAll("s1")

	store.mu.RLock()
	defer store.mu.RUnlock()
	if _, ok := store.refToScope[ref1]; ok {
		t.Error("refToScope should not contain ref1 after ReleaseAll")
	}
	if _, ok := store.refToScope[ref2]; ok {
		t.Error("refToScope should not contain ref2 after ReleaseAll")
	}
	if _, ok := store.refToScope[ref3]; !ok {
		t.Error("refToScope should still contain ref3")
	}
}
