package zddc

import (
	"os"
	"path/filepath"
	"testing"
)

// scanFixture lays down a small tree with a few .zddc files and reserved-
// prefix directories that should be pruned. Returns the fsRoot.
func scanFixture(t *testing.T) string {
	t.Helper()
	root := t.TempDir()
	mk := func(rel, body string) {
		path := filepath.Join(root, filepath.FromSlash(rel))
		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
			t.Fatalf("mkdir: %v", err)
		}
		if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
			t.Fatalf("write: %v", err)
		}
	}
	mk(".zddc", "admins:\n  - alice@example.com\n")
	mk("ProjectA/.zddc", "title: A\n")
	mk("ProjectA/Sub/.zddc", "title: A-sub\n")
	mk("ProjectB/.zddc", "title: B\n")
	// Reserved-prefix subtrees must be pruned.
	mk(".devshell/.zddc", "title: hidden\n")
	mk("_template/.zddc", "title: scaffold\n")
	return root
}

func TestScanZddcFiles_FindsAllAndPrunesReservedPrefixes(t *testing.T) {
	InvalidateScanCache()
	root := scanFixture(t)

	dirs, err := ScanZddcFiles(root)
	if err != nil {
		t.Fatalf("ScanZddcFiles: %v", err)
	}

	want := []string{
		root,
		filepath.Join(root, "ProjectA"),
		filepath.Join(root, "ProjectA", "Sub"),
		filepath.Join(root, "ProjectB"),
	}
	if len(dirs) != len(want) {
		t.Fatalf("got %d dirs, want %d: %v", len(dirs), len(want), dirs)
	}
	for i, w := range want {
		if dirs[i] != w {
			t.Errorf("dirs[%d] = %q, want %q", i, dirs[i], w)
		}
	}
}

// Cache hit returns the same slice header as the first call (proves the
// walk wasn't repeated). Cache miss after invalidation returns a freshly-
// allocated slice.
func TestScanZddcFiles_CachesAndInvalidates(t *testing.T) {
	InvalidateScanCache()
	root := scanFixture(t)

	first, err := ScanZddcFiles(root)
	if err != nil {
		t.Fatalf("first call: %v", err)
	}
	second, err := ScanZddcFiles(root)
	if err != nil {
		t.Fatalf("second call: %v", err)
	}
	if &first[0] != &second[0] {
		t.Errorf("expected cached slice reuse — got fresh allocation")
	}

	// Add a new .zddc file. Without invalidation the cache returns stale data.
	newDir := filepath.Join(root, "ProjectC")
	if err := os.MkdirAll(newDir, 0o755); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	if err := os.WriteFile(filepath.Join(newDir, ".zddc"), []byte("title: C\n"), 0o644); err != nil {
		t.Fatalf("write .zddc: %v", err)
	}

	stale, _ := ScanZddcFiles(root)
	if len(stale) != len(first) {
		t.Errorf("expected stale cached result before invalidation; got %d entries (first had %d)", len(stale), len(first))
	}

	InvalidateScanCache()
	fresh, err := ScanZddcFiles(root)
	if err != nil {
		t.Fatalf("post-invalidate: %v", err)
	}
	if len(fresh) != len(first)+1 {
		t.Errorf("expected fresh result with new entry; got %v", fresh)
	}
	wantNew := filepath.Join(root, "ProjectC")
	found := false
	for _, d := range fresh {
		if d == wantNew {
			found = true
			break
		}
	}
	if !found {
		t.Errorf("fresh scan missing %q; got %v", wantNew, fresh)
	}
}

// Walk errors must NOT poison the cache — a transient permissions blip
// during the walk should leave the cache empty so the next call retries.
func TestScanZddcFiles_DoesNotCacheOnError(t *testing.T) {
	InvalidateScanCache()
	bogus := filepath.Join(t.TempDir(), "does-not-exist")
	_, err := ScanZddcFiles(bogus)
	if err == nil {
		t.Fatalf("expected error walking nonexistent path")
	}
	// Cache should not have stored anything.
	if _, ok := scanCache.Load(filepath.Clean(bogus)); ok {
		t.Errorf("cache populated despite walk error")
	}
}
