// License: GPLv3 Copyright: 2025, Kovid Goyal, <kovid at kovidgoyal.net>

package utils

import (
	"context"
	"errors"
	"io/fs"
	"os"
	"path/filepath"
	"runtime"
	"syscall"
	"testing"
)

// openTestDir opens the directory at path for the test, closing it on cleanup.
func openTestDir(t *testing.T, path string) *os.File {
	t.Helper()
	d, err := os.Open(path)
	if err != nil {
		t.Fatalf("openTestDir %s: %v", path, err)
	}
	t.Cleanup(func() { d.Close() })
	return d
}

// mustReadFile returns the contents of path or fatals the test.
func mustReadFile(t *testing.T, path string) string {
	t.Helper()
	b, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("ReadFile %s: %v", path, err)
	}
	return string(b)
}

// countOpenFDs returns the number of open file descriptors in the process.
// It uses /proc/self/fd and is only meaningful on Linux; returns -1 elsewhere.
func countOpenFDs() int {
	if runtime.GOOS != "linux" {
		return -1
	}
	entries, err := os.ReadDir("/proc/self/fd")
	if err != nil {
		return -1
	}
	return len(entries)
}

// --- Tests for individual API functions ---

func TestMkdirAt(t *testing.T) {
	tmp := t.TempDir()
	d := openTestDir(t, tmp)

	if err := MkdirAt(d, "newdir", 0755); err != nil {
		t.Fatalf("MkdirAt: %v", err)
	}
	info, err := os.Stat(filepath.Join(tmp, "newdir"))
	if err != nil {
		t.Fatalf("stat after MkdirAt: %v", err)
	}
	if !info.IsDir() {
		t.Error("expected a directory")
	}

	// Creating same directory again should return a PathError wrapping EEXIST.
	err = MkdirAt(d, "newdir", 0755)
	if err == nil {
		t.Fatal("expected error for duplicate MkdirAt")
	}
	var pe *fs.PathError
	if !errors.As(err, &pe) {
		t.Errorf("expected *fs.PathError, got %T: %v", err, err)
	}
}

func TestOpenAt(t *testing.T) {
	tmp := t.TempDir()
	os.WriteFile(filepath.Join(tmp, "hello.txt"), []byte("world"), 0644)
	d := openTestDir(t, tmp)

	f, err := OpenAt(d, "hello.txt")
	if err != nil {
		t.Fatalf("OpenAt: %v", err)
	}
	defer f.Close()

	buf := make([]byte, 5)
	n, _ := f.Read(buf)
	if got := string(buf[:n]); got != "world" {
		t.Errorf("OpenAt read: got %q, want %q", got, "world")
	}

	if _, err = OpenAt(d, "nonexistent.txt"); err == nil {
		t.Error("expected error for non-existent file")
	}
}

func TestOpenDirAt(t *testing.T) {
	tmp := t.TempDir()
	os.Mkdir(filepath.Join(tmp, "subdir"), 0755)
	os.WriteFile(filepath.Join(tmp, "file.txt"), []byte("x"), 0644)
	d := openTestDir(t, tmp)

	sub, err := OpenDirAt(d, "subdir")
	if err != nil {
		t.Fatalf("OpenDirAt: %v", err)
	}
	defer sub.Close()
	info, _ := sub.Stat()
	if !info.IsDir() {
		t.Error("OpenDirAt: expected directory")
	}

	// Opening a regular file as a directory should fail.
	if _, err = OpenDirAt(d, "file.txt"); err == nil {
		t.Error("expected error opening regular file as directory")
	}
}

func TestCreateAt(t *testing.T) {
	tmp := t.TempDir()
	d := openTestDir(t, tmp)

	f, err := CreateAt(d, "new.txt", 0644)
	if err != nil {
		t.Fatalf("CreateAt: %v", err)
	}
	f.WriteString("hello")
	f.Close()

	if got := mustReadFile(t, filepath.Join(tmp, "new.txt")); got != "hello" {
		t.Errorf("CreateAt content: got %q, want %q", got, "hello")
	}

	// CreateAt on an existing file should truncate it.
	f2, err := CreateAt(d, "new.txt", 0644)
	if err != nil {
		t.Fatalf("CreateAt (truncate): %v", err)
	}
	f2.WriteString("bye")
	f2.Close()

	if got := mustReadFile(t, filepath.Join(tmp, "new.txt")); got != "bye" {
		t.Errorf("CreateAt truncate: got %q, want %q", got, "bye")
	}
}

func TestCreateDirAt(t *testing.T) {
	tmp := t.TempDir()
	d := openTestDir(t, tmp)

	sub, err := CreateDirAt(d, "mydir", 0755)
	if err != nil {
		t.Fatalf("CreateDirAt new: %v", err)
	}
	sub.Close()

	// Should succeed when the directory already exists.
	sub2, err := CreateDirAt(d, "mydir", 0700)
	if err != nil {
		t.Fatalf("CreateDirAt existing: %v", err)
	}
	sub2.Close()
}

func TestSymlinkAt(t *testing.T) {
	tmp := t.TempDir()
	os.WriteFile(filepath.Join(tmp, "target.txt"), []byte("data"), 0644)
	d := openTestDir(t, tmp)

	if err := SymlinkAt(d, "link.txt", "target.txt"); err != nil {
		t.Fatalf("SymlinkAt: %v", err)
	}
	got, err := os.Readlink(filepath.Join(tmp, "link.txt"))
	if err != nil {
		t.Fatalf("Readlink: %v", err)
	}
	if got != "target.txt" {
		t.Errorf("symlink target: got %q, want %q", got, "target.txt")
	}

	// Duplicate symlink should fail.
	if err = SymlinkAt(d, "link.txt", "target.txt"); err == nil {
		t.Error("expected error for duplicate symlink")
	}
}

func TestStatAt(t *testing.T) {
	tmp := t.TempDir()
	os.WriteFile(filepath.Join(tmp, "file.txt"), []byte("content"), 0644)
	os.Symlink("file.txt", filepath.Join(tmp, "link.txt"))
	d := openTestDir(t, tmp)

	// StatAt follows symlinks.
	info, err := StatAt(d, "link.txt")
	if err != nil {
		t.Fatalf("StatAt: %v", err)
	}
	if info.Mode()&os.ModeSymlink != 0 {
		t.Error("StatAt should dereference symlinks")
	}
	if !info.Mode().IsRegular() {
		t.Error("StatAt: expected regular file after following symlink")
	}

	if _, err = StatAt(d, "nonexistent"); err == nil {
		t.Error("expected error for non-existent file")
	}
}

func TestLstatAt(t *testing.T) {
	tmp := t.TempDir()
	os.WriteFile(filepath.Join(tmp, "file.txt"), []byte("content"), 0644)
	os.Symlink("file.txt", filepath.Join(tmp, "link.txt"))
	d := openTestDir(t, tmp)

	// LstatAt must not follow symlinks.
	info, err := LstatAt(d, "link.txt")
	if err != nil {
		t.Fatalf("LstatAt symlink: %v", err)
	}
	if info.Mode()&os.ModeSymlink == 0 {
		t.Error("LstatAt should not dereference symlinks")
	}

	info2, err := LstatAt(d, "file.txt")
	if err != nil {
		t.Fatalf("LstatAt file: %v", err)
	}
	if !info2.Mode().IsRegular() {
		t.Error("LstatAt file: expected regular file")
	}
}

func TestUnlinkAt(t *testing.T) {
	tmp := t.TempDir()
	os.WriteFile(filepath.Join(tmp, "todelete.txt"), []byte("x"), 0644)
	d := openTestDir(t, tmp)

	if err := UnlinkAt(d, "todelete.txt"); err != nil {
		t.Fatalf("UnlinkAt: %v", err)
	}
	if _, err := os.Stat(filepath.Join(tmp, "todelete.txt")); !os.IsNotExist(err) {
		t.Error("file should be removed after UnlinkAt")
	}

	if err := UnlinkAt(d, "nonexistent"); err == nil {
		t.Error("expected error for non-existent file")
	}
}

func TestRemoveDirAt(t *testing.T) {
	tmp := t.TempDir()
	os.Mkdir(filepath.Join(tmp, "empty"), 0755)
	os.Mkdir(filepath.Join(tmp, "nonempty"), 0755)
	os.WriteFile(filepath.Join(tmp, "nonempty", "f"), []byte(""), 0644)
	d := openTestDir(t, tmp)

	if err := RemoveDirAt(d, "empty"); err != nil {
		t.Fatalf("RemoveDirAt empty: %v", err)
	}
	if _, err := os.Stat(filepath.Join(tmp, "empty")); !os.IsNotExist(err) {
		t.Error("empty dir should be removed")
	}

	// Non-empty directory must fail.
	if err := RemoveDirAt(d, "nonempty"); err == nil {
		t.Error("expected error removing non-empty directory")
	}
}

func TestLinkAt(t *testing.T) {
	tmp := t.TempDir()
	os.WriteFile(filepath.Join(tmp, "original.txt"), []byte("data"), 0644)
	d := openTestDir(t, tmp)

	if err := LinkAt(d, "original.txt", d, "hardlink.txt", false); err != nil {
		t.Fatalf("LinkAt: %v", err)
	}
	info1, _ := os.Stat(filepath.Join(tmp, "original.txt"))
	info2, _ := os.Stat(filepath.Join(tmp, "hardlink.txt"))
	s1 := info1.Sys().(*syscall.Stat_t)
	s2 := info2.Sys().(*syscall.Stat_t)
	if s1.Ino != s2.Ino {
		t.Error("hard link should share the same inode")
	}
}

func TestReadLinkAt(t *testing.T) {
	tmp := t.TempDir()
	os.Symlink("/absolute/path", filepath.Join(tmp, "abslink"))
	os.Symlink("relative/path", filepath.Join(tmp, "rellink"))
	os.WriteFile(filepath.Join(tmp, "regular"), []byte("x"), 0644)
	d := openTestDir(t, tmp)

	abs, err := ReadLinkAt(d, "abslink")
	if err != nil {
		t.Fatalf("ReadLinkAt abs: %v", err)
	}
	if abs != "/absolute/path" {
		t.Errorf("abs link: got %q, want %q", abs, "/absolute/path")
	}

	rel, err := ReadLinkAt(d, "rellink")
	if err != nil {
		t.Fatalf("ReadLinkAt rel: %v", err)
	}
	if rel != "relative/path" {
		t.Errorf("rel link: got %q, want %q", rel, "relative/path")
	}

	// ReadLinkAt on a regular file must fail.
	if _, err = ReadLinkAt(d, "regular"); err == nil {
		t.Error("expected error for ReadLinkAt on regular file")
	}
}

func TestDupFile(t *testing.T) {
	tmp := t.TempDir()
	f, err := os.CreateTemp(tmp, "dup")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	f.WriteString("original")

	dup, err := DupFile(f)
	if err != nil {
		t.Fatalf("DupFile: %v", err)
	}
	defer dup.Close()

	// Read from the duplicate.
	dup.Seek(0, 0)
	buf := make([]byte, 8)
	n, _ := dup.Read(buf)
	if got := string(buf[:n]); got != "original" {
		t.Errorf("dup read: got %q, want %q", got, "original")
	}

	// Closing the dup should not affect the original.
	dup.Close()
	f.Seek(0, 0)
	buf2 := make([]byte, 8)
	n2, _ := f.Read(buf2)
	if got := string(buf2[:n2]); got != "original" {
		t.Errorf("original after dup close: got %q, want %q", got, "original")
	}
}

func TestRenameAt(t *testing.T) {
	tmp := t.TempDir()
	os.WriteFile(filepath.Join(tmp, "src.txt"), []byte("hello"), 0644)
	d := openTestDir(t, tmp)

	// Rename within the same directory.
	if err := RenameAt(d, "src.txt", d, "dst.txt"); err != nil {
		t.Fatalf("RenameAt same dir: %v", err)
	}
	if _, err := os.Stat(filepath.Join(tmp, "src.txt")); !os.IsNotExist(err) {
		t.Error("src.txt should no longer exist after rename")
	}
	if got := mustReadFile(t, filepath.Join(tmp, "dst.txt")); got != "hello" {
		t.Errorf("dst.txt after rename: got %q, want %q", got, "hello")
	}

	// Rename to a different directory.
	sub := filepath.Join(tmp, "subdir")
	os.Mkdir(sub, 0755)
	subDir := openTestDir(t, sub)
	if err := RenameAt(d, "dst.txt", subDir, "moved.txt"); err != nil {
		t.Fatalf("RenameAt cross-dir: %v", err)
	}
	if _, err := os.Stat(filepath.Join(tmp, "dst.txt")); !os.IsNotExist(err) {
		t.Error("dst.txt should no longer exist after cross-dir rename")
	}
	if got := mustReadFile(t, filepath.Join(sub, "moved.txt")); got != "hello" {
		t.Errorf("moved.txt: got %q, want %q", got, "hello")
	}

	// Renaming a non-existent file should return an error.
	if err := RenameAt(d, "nonexistent.txt", d, "out.txt"); err == nil {
		t.Error("expected error when renaming non-existent file")
	}

	// Rename overwrites an existing destination file.
	os.WriteFile(filepath.Join(tmp, "a.txt"), []byte("aaa"), 0644)
	os.WriteFile(filepath.Join(tmp, "b.txt"), []byte("bbb"), 0644)
	if err := RenameAt(d, "a.txt", d, "b.txt"); err != nil {
		t.Fatalf("RenameAt overwrite: %v", err)
	}
	if _, err := os.Stat(filepath.Join(tmp, "a.txt")); !os.IsNotExist(err) {
		t.Error("a.txt should be gone after overwrite rename")
	}
	if got := mustReadFile(t, filepath.Join(tmp, "b.txt")); got != "aaa" {
		t.Errorf("b.txt after overwrite: got %q, want %q", got, "aaa")
	}

	// Renaming a symlink should move the symlink itself, not the target.
	os.WriteFile(filepath.Join(tmp, "target.txt"), []byte("target"), 0644)
	os.Symlink("target.txt", filepath.Join(tmp, "link.txt"))
	if err := RenameAt(d, "link.txt", d, "renamedlink.txt"); err != nil {
		t.Fatalf("RenameAt symlink: %v", err)
	}
	linkTarget, err := os.Readlink(filepath.Join(tmp, "renamedlink.txt"))
	if err != nil {
		t.Fatalf("Readlink after rename: %v", err)
	}
	if linkTarget != "target.txt" {
		t.Errorf("renamed symlink target: got %q, want %q", linkTarget, "target.txt")
	}
}

// --- RemoveChildren tests (pre-existing, kept for reference) ---

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

	subDir := filepath.Join(tmpDir, "subdir")
	os.Mkdir(subDir, 0755)
	os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("data"), 0644)
	os.WriteFile(filepath.Join(subDir, "file2.txt"), []byte("data"), 0644)

	d, err := os.Open(tmpDir)
	if err != nil {
		t.Fatal(err)
	}
	defer d.Close()

	if err := RemoveChildren(d); err != nil {
		t.Errorf("expected no error, got %v", err)
	}
	entries, _ := os.ReadDir(tmpDir)
	if len(entries) != 0 {
		t.Errorf("expected 0 entries after RemoveChildren, got %d", len(entries))
	}
}

func TestRemoveChildren_FirstError(t *testing.T) {
	tmpDir := t.TempDir()
	lockedFile := filepath.Join(tmpDir, "locked")
	os.WriteFile(lockedFile, nil, 0000)

	d, _ := os.Open(tmpDir)
	defer d.Close()

	err := RemoveChildren(d)
	if err != nil {
		if _, ok := err.(*os.PathError); !ok {
			t.Errorf("expected *os.PathError, got %T", err)
		}
	}
}

// --- CopyFolderContents tests ---

// buildDeepTree creates the following 3-level structure under base:
//
// base/
//
//	file1.txt        ("level1")
//	link_to_file1 -> file1.txt
//	subdir1/
//	  file2.txt      ("level2")
//	  subdir2/
//	    file3.txt    ("level3")
//	    link_to_top -> ../../file1.txt
//	    subdir3/
//	      file4.txt  ("deepest")
func buildDeepTree(t *testing.T, base string) {
	t.Helper()
	must := func(err error) {
		t.Helper()
		if err != nil {
			t.Fatal(err)
		}
	}
	must(os.WriteFile(filepath.Join(base, "file1.txt"), []byte("level1"), 0644))
	must(os.Symlink("file1.txt", filepath.Join(base, "link_to_file1")))
	sub1 := filepath.Join(base, "subdir1")
	must(os.Mkdir(sub1, 0755))
	must(os.WriteFile(filepath.Join(sub1, "file2.txt"), []byte("level2"), 0644))
	sub2 := filepath.Join(sub1, "subdir2")
	must(os.Mkdir(sub2, 0755))
	must(os.WriteFile(filepath.Join(sub2, "file3.txt"), []byte("level3"), 0644))
	must(os.Symlink("../../file1.txt", filepath.Join(sub2, "link_to_top")))
	sub3 := filepath.Join(sub2, "subdir3")
	must(os.Mkdir(sub3, 0755))
	must(os.WriteFile(filepath.Join(sub3, "file4.txt"), []byte("deepest"), 0644))
}

func TestCopyFolderContents_NoHardlinks_NoFollowSymlinks(t *testing.T) {
	tmp := t.TempDir()
	src := filepath.Join(tmp, "src")
	dst := filepath.Join(tmp, "dst")
	os.MkdirAll(src, 0755)
	os.MkdirAll(dst, 0755)
	buildDeepTree(t, src)

	srcDir := openTestDir(t, src)
	dstDir := openTestDir(t, dst)

	err := CopyFolderContents(context.Background(), srcDir, dstDir, CopyFolderOptions{
		Disallow_hardlinks: true,
		Follow_symlinks:    false,
	})
	if err != nil {
		t.Fatalf("CopyFolderContents: %v", err)
	}

	// All files should be present with correct contents.
	cases := []struct{ path, want string }{
		{"file1.txt", "level1"},
		{"subdir1/file2.txt", "level2"},
		{"subdir1/subdir2/file3.txt", "level3"},
		{"subdir1/subdir2/subdir3/file4.txt", "deepest"},
	}
	for _, c := range cases {
		got := mustReadFile(t, filepath.Join(dst, c.path))
		if got != c.want {
			t.Errorf("%s: got %q, want %q", c.path, got, c.want)
		}
	}

	// Symlinks should be copied verbatim (not resolved).
	symlinks := []struct{ path, want string }{
		{"link_to_file1", "file1.txt"},
		{"subdir1/subdir2/link_to_top", "../../file1.txt"},
	}
	for _, s := range symlinks {
		got, err := os.Readlink(filepath.Join(dst, s.path))
		if err != nil {
			t.Fatalf("readlink %s: %v", s.path, err)
		}
		if got != s.want {
			t.Errorf("symlink %s: got target %q, want %q", s.path, got, s.want)
		}
	}

	// With Disallow_hardlinks, files must have different inodes from source.
	srcInfo, _ := os.Stat(filepath.Join(src, "file1.txt"))
	dstInfo, _ := os.Stat(filepath.Join(dst, "file1.txt"))
	if srcInfo.Sys().(*syscall.Stat_t).Ino == dstInfo.Sys().(*syscall.Stat_t).Ino {
		t.Error("files should not share inodes when Disallow_hardlinks=true")
	}
}

func TestCopyFolderContents_WithHardlinks(t *testing.T) {
	tmp := t.TempDir()
	src := filepath.Join(tmp, "src")
	dst := filepath.Join(tmp, "dst")
	os.MkdirAll(src, 0755)
	os.MkdirAll(dst, 0755)
	os.WriteFile(filepath.Join(src, "file.txt"), []byte("data"), 0644)
	os.Mkdir(filepath.Join(src, "sub"), 0755)
	os.WriteFile(filepath.Join(src, "sub", "file2.txt"), []byte("data2"), 0644)

	srcDir := openTestDir(t, src)
	dstDir := openTestDir(t, dst)

	err := CopyFolderContents(context.Background(), srcDir, dstDir, CopyFolderOptions{
		Disallow_hardlinks: false,
		Follow_symlinks:    false,
	})
	if err != nil {
		t.Fatalf("CopyFolderContents: %v", err)
	}

	// Regular files must share inodes (hardlinked).
	for _, name := range []string{"file.txt", "sub/file2.txt"} {
		sInfo, _ := os.Stat(filepath.Join(src, name))
		dInfo, _ := os.Stat(filepath.Join(dst, name))
		sIno := sInfo.Sys().(*syscall.Stat_t).Ino
		dIno := dInfo.Sys().(*syscall.Stat_t).Ino
		if sIno != dIno {
			t.Errorf("%s: expected same inode (hardlink), got src=%d dst=%d", name, sIno, dIno)
		}
	}
}

// TestCopyFolderContents_SymlinkLoop verifies that a symlink creating a
// directory loop (a -> .) is correctly handled: the already-copied directory
// appears as a relative symlink in the destination.
func TestCopyFolderContents_SymlinkLoop(t *testing.T) {
	tmp := t.TempDir()
	src := filepath.Join(tmp, "src")
	dst := filepath.Join(tmp, "dst")
	os.MkdirAll(src, 0755)
	os.MkdirAll(dst, 0755)

	// src/loop -> . (points back to src itself, creating a loop)
	os.Symlink(".", filepath.Join(src, "loop"))
	os.WriteFile(filepath.Join(src, "file.txt"), []byte("hello"), 0644)

	srcDir := openTestDir(t, src)
	dstDir := openTestDir(t, dst)

	err := CopyFolderContents(context.Background(), srcDir, dstDir, CopyFolderOptions{
		Disallow_hardlinks: true,
		Follow_symlinks:    true,
	})
	if err != nil {
		t.Fatalf("CopyFolderContents with loop: %v", err)
	}

	// file.txt must be present with correct content.
	if got := mustReadFile(t, filepath.Join(dst, "file.txt")); got != "hello" {
		t.Errorf("file.txt: got %q, want %q", got, "hello")
	}

	// loop must exist and be a symlink (not cause infinite recursion).
	linfo, err := os.Lstat(filepath.Join(dst, "loop"))
	if err != nil {
		t.Fatalf("lstat dst/loop: %v", err)
	}
	if linfo.Mode()&os.ModeSymlink == 0 {
		t.Error("dst/loop should be a symlink (loop broken by relative back-reference)")
	}
}

// TestCopyFolderContents_FollowSymlinks_DirectorySymlink tests that when
// Follow_symlinks is true and a symlink points to a subdirectory, the
// directory contents are copied (and the underlying fd is kept alive long
// enough for next_dir to process the queued item — the key bug being tested).
func TestCopyFolderContents_FollowSymlinks_DirectorySymlink(t *testing.T) {
	tmp := t.TempDir()
	src := filepath.Join(tmp, "src")
	dst := filepath.Join(tmp, "dst")
	os.MkdirAll(src, 0755)
	os.MkdirAll(dst, 0755)

	// src/subdir/ with a file, and src/link_to_subdir -> subdir
	subdir := filepath.Join(src, "subdir")
	os.Mkdir(subdir, 0755)
	os.WriteFile(filepath.Join(subdir, "inner.txt"), []byte("inner"), 0644)
	os.Symlink("subdir", filepath.Join(src, "link_to_subdir"))

	srcDir := openTestDir(t, src)
	dstDir := openTestDir(t, dst)

	err := CopyFolderContents(context.Background(), srcDir, dstDir, CopyFolderOptions{
		Disallow_hardlinks: true,
		Follow_symlinks:    true,
	})
	if err != nil {
		t.Fatalf("CopyFolderContents (follow symlink to dir): %v", err)
	}

	// The real subdir must be copied.
	if got := mustReadFile(t, filepath.Join(dst, "subdir", "inner.txt")); got != "inner" {
		t.Errorf("subdir/inner.txt: got %q, want %q", got, "inner")
	}
}

// TestCopyFolderContents_DeepTree_FollowSymlinks copies the 3-level tree
// with Follow_symlinks=true. Symlinks that cross directory boundaries are
// resolved; symlinks whose targets resolve to the same source file become a
// relative symlink in the destination pointing at the already-copied file.
func TestCopyFolderContents_DeepTree_FollowSymlinks(t *testing.T) {
	tmp := t.TempDir()
	src := filepath.Join(tmp, "src")
	dst := filepath.Join(tmp, "dst")
	os.MkdirAll(src, 0755)
	os.MkdirAll(dst, 0755)
	buildDeepTree(t, src)

	srcDir := openTestDir(t, src)
	dstDir := openTestDir(t, dst)

	err := CopyFolderContents(context.Background(), srcDir, dstDir, CopyFolderOptions{
		Disallow_hardlinks: true,
		Follow_symlinks:    true,
	})
	if err != nil {
		t.Fatalf("CopyFolderContents (follow): %v", err)
	}

	// Deep regular files must be present.
	for _, c := range []struct{ path, want string }{
		{"file1.txt", "level1"},
		{"subdir1/file2.txt", "level2"},
		{"subdir1/subdir2/file3.txt", "level3"},
		{"subdir1/subdir2/subdir3/file4.txt", "deepest"},
	} {
		got := mustReadFile(t, filepath.Join(dst, c.path))
		if got != c.want {
			t.Errorf("%s: got %q, want %q", c.path, got, c.want)
		}
	}
}

// TestCopyFolderContents_FilterFiles checks that Filter_files can exclude entries.
func TestCopyFolderContents_FilterFiles(t *testing.T) {
	tmp := t.TempDir()
	src := filepath.Join(tmp, "src")
	dst := filepath.Join(tmp, "dst")
	os.MkdirAll(src, 0755)
	os.MkdirAll(dst, 0755)
	os.WriteFile(filepath.Join(src, "keep.txt"), []byte("keep"), 0644)
	os.WriteFile(filepath.Join(src, "skip.txt"), []byte("skip"), 0644)

	srcDir := openTestDir(t, src)
	dstDir := openTestDir(t, dst)

	err := CopyFolderContents(context.Background(), srcDir, dstDir, CopyFolderOptions{
		Disallow_hardlinks: true,
		Filter_files: func(_ *os.File, fi os.FileInfo) bool {
			return fi.Name() != "skip.txt"
		},
	})
	if err != nil {
		t.Fatalf("CopyFolderContents (filter): %v", err)
	}
	if _, err := os.Stat(filepath.Join(dst, "keep.txt")); err != nil {
		t.Error("keep.txt should be present")
	}
	if _, err := os.Stat(filepath.Join(dst, "skip.txt")); !os.IsNotExist(err) {
		t.Error("skip.txt should have been filtered out")
	}
}

// TestCopyFolderContents_CancelledContext verifies that cancellation is
// respected and does not leak file descriptors.
func TestCopyFolderContents_CancelledContext(t *testing.T) {
	if runtime.GOOS != "linux" {
		t.Skip("FD leak check requires Linux /proc/self/fd")
	}

	tmp := t.TempDir()
	src := filepath.Join(tmp, "src")
	dst := filepath.Join(tmp, "dst")
	os.MkdirAll(src, 0755)
	os.MkdirAll(dst, 0755)
	buildDeepTree(t, src)

	srcDir := openTestDir(t, src)
	dstDir := openTestDir(t, dst)

	ctx, cancel := context.WithCancel(context.Background())
	cancel() // pre-cancel

	before := countOpenFDs()
	_ = CopyFolderContents(ctx, srcDir, dstDir, CopyFolderOptions{
		Disallow_hardlinks: true,
	})
	after := countOpenFDs()

	if after > before {
		t.Errorf("FD leak after cancelled copy: before=%d after=%d", before, after)
	}
}

// TestCopyFolderContents_FDLeaks_Normal verifies that a complete, successful
// copy does not leak file descriptors.
func TestCopyFolderContents_FDLeaks_Normal(t *testing.T) {
	if runtime.GOOS != "linux" {
		t.Skip("FD leak check requires Linux /proc/self/fd")
	}

	tmp := t.TempDir()
	src := filepath.Join(tmp, "src")
	dst := filepath.Join(tmp, "dst")
	os.MkdirAll(src, 0755)
	os.MkdirAll(dst, 0755)
	buildDeepTree(t, src)

	srcDir := openTestDir(t, src)
	dstDir := openTestDir(t, dst)

	before := countOpenFDs()
	err := CopyFolderContents(context.Background(), srcDir, dstDir, CopyFolderOptions{
		Disallow_hardlinks: true,
		Follow_symlinks:    false,
	})
	after := countOpenFDs()

	if err != nil {
		t.Fatalf("CopyFolderContents: %v", err)
	}
	if after > before {
		t.Errorf("FD leak in normal copy: before=%d after=%d", before, after)
	}
}

// TestCopyFolderContents_FDLeaks_FollowSymlinks_DirSymlink exercises the
// code path that was previously buggy: a symlink pointing to a directory when
// Follow_symlinks=true. Without the fix, the directory fd would be closed
// before next_dir processed the queue item. This test verifies both
// correctness (copy succeeds) and the absence of FD leaks.
func TestCopyFolderContents_FDLeaks_FollowSymlinks_DirSymlink(t *testing.T) {
	if runtime.GOOS != "linux" {
		t.Skip("FD leak check requires Linux /proc/self/fd")
	}

	tmp := t.TempDir()
	src := filepath.Join(tmp, "src")
	dst := filepath.Join(tmp, "dst")
	os.MkdirAll(src, 0755)
	os.MkdirAll(dst, 0755)

	subdir := filepath.Join(src, "subdir")
	os.Mkdir(subdir, 0755)
	os.WriteFile(filepath.Join(subdir, "deep.txt"), []byte("deep"), 0644)
	// Symlink pointing to the subdirectory — the key bug scenario.
	os.Symlink("subdir", filepath.Join(src, "link_to_subdir"))

	srcDir := openTestDir(t, src)
	dstDir := openTestDir(t, dst)

	before := countOpenFDs()
	err := CopyFolderContents(context.Background(), srcDir, dstDir, CopyFolderOptions{
		Disallow_hardlinks: true,
		Follow_symlinks:    true,
	})
	after := countOpenFDs()

	if err != nil {
		t.Fatalf("CopyFolderContents (dir symlink): %v", err)
	}
	if after > before {
		t.Errorf("FD leak with dir symlink: before=%d after=%d", before, after)
	}
	// The real subdir contents must appear in the destination.
	if got := mustReadFile(t, filepath.Join(dst, "subdir", "deep.txt")); got != "deep" {
		t.Errorf("subdir/deep.txt: got %q, want %q", got, "deep")
	}
}
