package lspwatcher

import (
	"errors"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/microsoft/typescript-go/internal/bundled"
	"github.com/microsoft/typescript-go/internal/fswatch"
	"github.com/microsoft/typescript-go/internal/lsp/lsproto"
	"github.com/microsoft/typescript-go/internal/project/logging"
	"github.com/microsoft/typescript-go/internal/tspath"
	"github.com/microsoft/typescript-go/internal/vfs/osvfs"
)

func waitFor(t *testing.T, cond func() bool, msg string) {
	t.Helper()
	deadline := time.Now().Add(5 * time.Second)
	for time.Now().Before(deadline) {
		if cond() {
			return
		}
		time.Sleep(20 * time.Millisecond)
	}
	t.Fatalf("timed out waiting for %s", msg)
}

func TestWatcher_CreateChangeDelete(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()

	var (
		mu      sync.Mutex
		batches [][]*lsproto.FileEvent
	)
	w := New(bundled.WrapFS(osvfs.FS()), func(changes []*lsproto.FileEvent) {
		mu.Lock()
		defer mu.Unlock()
		batches = append(batches, changes)
	}, logging.NewLogger(os.Stderr))
	t.Cleanup(w.Close)

	pattern := tspath.NormalizeSlashes(dir) + "/**/*"
	kind := lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete
	if err := w.WatchFiles("test", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
		Kind:        &kind,
	}}); err != nil {
		t.Fatal(err)
	}

	time.Sleep(200 * time.Millisecond)

	file := filepath.Join(dir, "a.ts")
	if err := os.WriteFile(file, []byte("export {}"), 0o644); err != nil {
		t.Fatal(err)
	}

	collected := func() []*lsproto.FileEvent {
		mu.Lock()
		defer mu.Unlock()
		var all []*lsproto.FileEvent
		for _, b := range batches {
			all = append(all, b...)
		}
		return all
	}

	waitFor(t, func() bool {
		for _, e := range collected() {
			if e.Type == lsproto.FileChangeTypeChanged {
				return true
			}
		}
		return false
	}, "update event")

	if err := os.Remove(file); err != nil {
		t.Fatal(err)
	}
	waitFor(t, func() bool {
		for _, e := range collected() {
			if e.Type == lsproto.FileChangeTypeDeleted {
				return true
			}
		}
		return false
	}, "delete event")

	if err := w.UnwatchFiles("test"); err != nil {
		t.Fatal(err)
	}
}

func TestWatcher_KindFilter(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	dirNorm := tspath.NormalizeSlashes(dir)

	var (
		mu  sync.Mutex
		got []*lsproto.FileEvent
	)
	backend := newFakeBackend()
	w := newWithBackend(bundled.WrapFS(osvfs.FS()), backend, func(changes []*lsproto.FileEvent) {
		mu.Lock()
		defer mu.Unlock()
		got = append(got, changes...)
	}, logging.NewLogger(os.Stderr))
	t.Cleanup(w.Close)

	pattern := dirNorm + "/**/*"
	kind := lsproto.WatchKindDelete
	if err := w.WatchFiles("test", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
		Kind:        &kind,
	}}); err != nil {
		t.Fatal(err)
	}
	backend.emitAll([]fswatch.Event{
		{Kind: fswatch.EventUpdate, Path: filepath.FromSlash(filepath.Join(dirNorm, "x.ts"))},
		{Kind: fswatch.EventDelete, Path: filepath.FromSlash(filepath.Join(dirNorm, "x.ts"))},
	}, nil)

	waitFor(t, func() bool {
		mu.Lock()
		defer mu.Unlock()
		for _, e := range got {
			if e.Type == lsproto.FileChangeTypeDeleted {
				return true
			}
		}
		return false
	}, "delete event")

	mu.Lock()
	for _, e := range got {
		if e.Type != lsproto.FileChangeTypeDeleted {
			t.Errorf("unexpected non-delete event: %+v", e)
		}
	}
	mu.Unlock()
}

func TestRootFromGlob(t *testing.T) {
	t.Parallel()
	cases := []struct {
		pattern string
		want    string
	}{
		{"/abs/path/**/*", "/abs/path"},
		{"/abs/path/", "/abs/path"},
		{"/abs/path/?.ts", "/abs/path"},
		{"/abs/path/{a,b}/*", "/abs/path"},
	}
	for _, c := range cases {
		if got := rootFromGlob(c.pattern); got != c.want {
			t.Errorf("rootFromGlob(%q) = %q, want %q", c.pattern, got, c.want)
		}
	}
}

type fakeBackend struct {
	mu       sync.Mutex
	byDir    map[string]fswatch.WatchCallback
	closed   map[string]int
	optCount map[string]int
	failDirs map[string]error
}

func newFakeBackend() *fakeBackend {
	return &fakeBackend{
		byDir:    make(map[string]fswatch.WatchCallback),
		closed:   make(map[string]int),
		optCount: make(map[string]int),
		failDirs: make(map[string]error),
	}
}

func (f *fakeBackend) WatchDirectory(dir string, fn fswatch.WatchCallback, opts ...fswatch.WatchOption) (io.Closer, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if err := f.failDirs[dir]; err != nil {
		return nil, err
	}
	f.byDir[dir] = fn
	f.optCount[dir] = len(opts)
	return fakeWatch{closeFn: func() error {
		f.mu.Lock()
		defer f.mu.Unlock()
		delete(f.byDir, dir)
		f.closed[dir]++
		return nil
	}}, nil
}

// watchedDirs returns the directories currently subscribed, for assertions.
func (f *fakeBackend) watchedDirs() []string {
	f.mu.Lock()
	defer f.mu.Unlock()
	dirs := make([]string, 0, len(f.byDir))
	for d := range f.byDir {
		dirs = append(dirs, d)
	}
	return dirs
}

func (f *fakeBackend) isWatching(dir string) bool {
	f.mu.Lock()
	defer f.mu.Unlock()
	_, ok := f.byDir[dir]
	return ok
}

func (f *fakeBackend) emit(dir string, events []fswatch.Event, err error) {
	f.mu.Lock()
	cb := f.byDir[dir]
	f.mu.Unlock()
	if cb != nil {
		cb(events, err)
	}
}

func (f *fakeBackend) emitAll(events []fswatch.Event, err error) {
	f.mu.Lock()
	cbs := make([]fswatch.WatchCallback, 0, len(f.byDir))
	for _, cb := range f.byDir {
		cbs = append(cbs, cb)
	}
	f.mu.Unlock()
	for _, cb := range cbs {
		cb(events, err)
	}
}

type fakeWatch struct{ closeFn func() error }

func (w fakeWatch) Close() error { return w.closeFn() }

func TestWatcher_BookkeepingAndOverflow(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	dirNorm := tspath.NormalizeSlashes(dir)
	pattern := dirNorm + "/**/*"

	fs := bundled.WrapFS(osvfs.FS())
	backend := newFakeBackend()
	var (
		mu  sync.Mutex
		got []*lsproto.FileEvent
	)
	w := newWithBackend(fs, backend, func(changes []*lsproto.FileEvent) {
		mu.Lock()
		defer mu.Unlock()
		got = append(got, changes...)
	}, logging.NewLogger(os.Stderr))

	if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
	}}); err != nil {
		t.Fatal(err)
	}
	if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
	}}); err == nil {
		t.Fatal("expected duplicate-id error")
	}

	backend.emitAll([]fswatch.Event{
		{Kind: fswatch.EventUpdate, Path: filepath.FromSlash(filepath.Join(dirNorm, "a.ts"))},
	}, fswatch.ErrOverflow)
	waitFor(t, func() bool {
		mu.Lock()
		defer mu.Unlock()
		return len(got) > 0
	}, "events after overflow")

	if err := w.UnwatchFiles("missing"); err == nil {
		t.Fatal("expected unknown-id error")
	}
	if err := w.UnwatchFiles("id"); err != nil {
		t.Fatal(err)
	}
	if err := w.WatchFiles("id2", nil); err != nil {
		t.Fatal(err)
	}
	w.Close()
	if err := w.WatchFiles("id3", nil); err == nil {
		t.Fatal("expected closed error")
	}
}

func TestWatcher_NonRecursiveGlobIsNotRecursive(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	dirNorm := tspath.NormalizeSlashes(dir)
	if err := os.MkdirAll(filepath.Join(dir, "sub"), 0o755); err != nil {
		t.Fatal(err)
	}
	subNorm := tspath.NormalizeSlashes(filepath.Join(dir, "sub"))

	fs := bundled.WrapFS(osvfs.FS())
	backend := newFakeBackend()
	w := newWithBackend(fs, backend, func([]*lsproto.FileEvent) {}, logging.NewLogger(os.Stderr))
	t.Cleanup(w.Close)

	recursive := dirNorm + "/**/*"
	nonRecursive := subNorm + "/*"
	if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{
		{GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &recursive}},
		{GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &nonRecursive}},
	}); err != nil {
		t.Fatal(err)
	}

	backend.mu.Lock()
	defer backend.mu.Unlock()
	if got := backend.optCount[dirNorm]; got != 1 {
		t.Errorf("recursive glob %q: expected 1 watch option (WithRecursive), got %d", recursive, got)
	}
	if got := backend.optCount[subNorm]; got != 0 {
		t.Errorf("non-recursive glob %q: expected 0 watch options, got %d", nonRecursive, got)
	}
}

func TestWatcher_RealBackend_MissingThenCreate(t *testing.T) {
	t.Parallel()
	base := t.TempDir()
	fs := bundled.WrapFS(osvfs.FS())

	var (
		mu      sync.Mutex
		batches [][]*lsproto.FileEvent
	)
	w := New(fs, func(changes []*lsproto.FileEvent) {
		mu.Lock()
		defer mu.Unlock()
		batches = append(batches, changes)
	}, logging.NewLogger(os.Stderr))
	t.Cleanup(w.Close)

	// Watch a directory that does not exist yet.
	target := tspath.NormalizeSlashes(filepath.Join(base, "pkg"))
	pattern := target + "/*"
	kind := lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete
	if err := w.WatchFiles("test", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
		Kind:        &kind,
	}}); err != nil {
		t.Fatal(err)
	}

	// Give the ancestor watch time to install.
	time.Sleep(200 * time.Millisecond)

	// Create the target directory and a file inside it. The real backend's
	// ancestor watch should fire, promote to the target, and
	// the file should ultimately surface.
	if err := os.MkdirAll(filepath.Join(base, "pkg"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(base, "pkg", "index.ts"), []byte("export {}"), 0o644); err != nil {
		t.Fatal(err)
	}

	collected := func() []*lsproto.FileEvent {
		mu.Lock()
		defer mu.Unlock()
		var all []*lsproto.FileEvent
		for _, b := range batches {
			all = append(all, b...)
		}
		return all
	}

	waitFor(t, func() bool {
		for _, e := range collected() {
			if strings.HasSuffix(string(e.Uri), "/pkg/index.ts") {
				return true
			}
		}
		return false
	}, "event for file created in a previously-missing directory")
}

func TestWatcher_MissingDirectoryTracksAncestor(t *testing.T) {
	t.Parallel()

	fs := bundled.WrapFS(osvfs.FS())
	backend := newFakeBackend()
	w := newWithBackend(fs, backend, func([]*lsproto.FileEvent) {}, logging.NewLogger(os.Stderr))
	t.Cleanup(w.Close)

	base := t.TempDir()
	baseNorm := tspath.NormalizeSlashes(base)
	target := tspath.NormalizeSlashes(filepath.Join(base, "pkg"))
	pattern := target + "/*"

	if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
	}}); err != nil {
		t.Fatal(err)
	}

	// A missing target directory installs an ancestor watch on the nearest
	// existing ancestor (the base dir), not on the target.
	if !backend.isWatching(baseNorm) {
		t.Fatalf("expected ancestor watch on ancestor %q, watched: %v", baseNorm, backend.watchedDirs())
	}
	if dirs := backend.watchedDirs(); len(dirs) != 1 {
		t.Fatalf("expected exactly one (ancestor) watch, got %v", dirs)
	}

	if err := w.UnwatchFiles("id"); err != nil {
		t.Fatal(err)
	}
	if dirs := backend.watchedDirs(); len(dirs) != 0 {
		t.Fatalf("expected all watches closed after unwatch, got %v", dirs)
	}
}

func TestWatcher_MissingDirectoryPromotesOnCreate(t *testing.T) {
	t.Parallel()

	fs := bundled.WrapFS(osvfs.FS())
	backend := newFakeBackend()
	var (
		mu  sync.Mutex
		got []*lsproto.FileEvent
	)
	w := newWithBackend(fs, backend, func(changes []*lsproto.FileEvent) {
		mu.Lock()
		got = append(got, changes...)
		mu.Unlock()
	}, logging.NewLogger(os.Stderr))
	t.Cleanup(w.Close)

	base := t.TempDir()
	baseNorm := tspath.NormalizeSlashes(base)
	target := tspath.NormalizeSlashes(filepath.Join(base, "pkg"))
	pattern := target + "/*"
	kind := lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete

	if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
		Kind:        &kind,
	}}); err != nil {
		t.Fatal(err)
	}

	// Create the target directory with a file, then notify the ancestor watch.
	if err := os.MkdirAll(filepath.Join(base, "pkg"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(base, "pkg", "index.ts"), []byte("export {}"), 0o644); err != nil {
		t.Fatal(err)
	}
	backend.emit(baseNorm, []fswatch.Event{
		{Kind: fswatch.EventUpdate, Path: filepath.Join(base, "pkg")},
	}, nil)

	waitFor(t, func() bool { return backend.isWatching(target) }, "promotion to target watch")

	// Synthetic creates must cover the target dir and its immediate child so
	// the session re-resolves files created before the watch was installed.
	waitFor(t, func() bool {
		mu.Lock()
		defer mu.Unlock()
		var sawDir, sawChild bool
		for _, e := range got {
			if e.Type != lsproto.FileChangeTypeCreated {
				continue
			}
			s := string(e.Uri)
			if strings.HasSuffix(s, "/pkg") {
				sawDir = true
			}
			if strings.HasSuffix(s, "/pkg/index.ts") {
				sawChild = true
			}
		}
		return sawDir && sawChild
	}, "synthetic create events for target and child")
}

func TestWatcher_MultiLevelDescend(t *testing.T) {
	t.Parallel()

	fs := bundled.WrapFS(osvfs.FS())
	backend := newFakeBackend()
	w := newWithBackend(fs, backend, func([]*lsproto.FileEvent) {}, logging.NewLogger(os.Stderr))
	t.Cleanup(w.Close)

	base := t.TempDir()
	baseNorm := tspath.NormalizeSlashes(base)
	target := tspath.NormalizeSlashes(filepath.Join(base, "a", "b", "c"))
	pattern := target + "/*"

	if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
	}}); err != nil {
		t.Fatal(err)
	}
	if !backend.isWatching(baseNorm) {
		t.Fatalf("expected initial ancestor watch on %q, got %v", baseNorm, backend.watchedDirs())
	}

	// Reveal one path component at a time; the ancestor watch should descend.
	mkdirAndPath := func(rel string) string {
		p := filepath.Join(base, rel)
		if err := os.MkdirAll(p, 0o755); err != nil {
			t.Fatal(err)
		}
		return tspath.NormalizeSlashes(p)
	}

	aDir := mkdirAndPath("a")
	backend.emit(baseNorm, []fswatch.Event{{Kind: fswatch.EventUpdate, Path: filepath.Join(base, "a")}}, nil)
	waitFor(t, func() bool { return backend.isWatching(aDir) }, "descend to a")

	abDir := mkdirAndPath(filepath.Join("a", "b"))
	backend.emit(aDir, []fswatch.Event{{Kind: fswatch.EventUpdate, Path: filepath.Join(base, "a", "b")}}, nil)
	waitFor(t, func() bool { return backend.isWatching(abDir) }, "descend to a/b")

	abcDir := mkdirAndPath(filepath.Join("a", "b", "c"))
	backend.emit(abDir, []fswatch.Event{{Kind: fswatch.EventUpdate, Path: filepath.Join(base, "a", "b", "c")}}, nil)
	waitFor(t, func() bool { return backend.isWatching(abcDir) }, "promote to target a/b/c")
}

func TestWatcher_AtomicTreeCreateRace(t *testing.T) {
	t.Parallel()

	fs := bundled.WrapFS(osvfs.FS())
	backend := newFakeBackend()
	w := newWithBackend(fs, backend, func([]*lsproto.FileEvent) {}, logging.NewLogger(os.Stderr))
	t.Cleanup(w.Close)

	base := t.TempDir()
	baseNorm := tspath.NormalizeSlashes(base)
	target := tspath.NormalizeSlashes(filepath.Join(base, "a", "b", "c"))
	pattern := target + "/*"

	if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
	}}); err != nil {
		t.Fatal(err)
	}

	// The whole tree appears at once (e.g. an extraction/symlink). A single
	// notification on the base ancestor watch must descend all the way and
	// promote to the target in one reconcile pass.
	if err := os.MkdirAll(filepath.Join(base, "a", "b", "c"), 0o755); err != nil {
		t.Fatal(err)
	}
	backend.emit(baseNorm, []fswatch.Event{{Kind: fswatch.EventUpdate, Path: filepath.Join(base, "a")}}, nil)

	waitFor(t, func() bool { return backend.isWatching(target) }, "promote to target in one pass")
}

func TestWatcher_SyntheticCreateDepth(t *testing.T) {
	t.Parallel()

	for _, recursive := range []bool{false, true} {
		t.Run(map[bool]string{false: "non-recursive", true: "recursive"}[recursive], func(t *testing.T) {
			t.Parallel()
			fs := bundled.WrapFS(osvfs.FS())
			backend := newFakeBackend()
			var (
				mu  sync.Mutex
				got []*lsproto.FileEvent
			)
			w := newWithBackend(fs, backend, func(changes []*lsproto.FileEvent) {
				mu.Lock()
				got = append(got, changes...)
				mu.Unlock()
			}, logging.NewLogger(os.Stderr))
			t.Cleanup(w.Close)

			base := t.TempDir()
			baseNorm := tspath.NormalizeSlashes(base)
			target := tspath.NormalizeSlashes(filepath.Join(base, "pkg"))
			kind := lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete
			var pattern string
			if recursive {
				pattern = target + "/**/*"
			} else {
				pattern = target + "/*"
			}

			if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{
				GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
				Kind:        &kind,
			}}); err != nil {
				t.Fatal(err)
			}

			// Materialize the target with a nested file under a subdirectory.
			if err := os.MkdirAll(filepath.Join(base, "pkg", "sub"), 0o755); err != nil {
				t.Fatal(err)
			}
			if err := os.WriteFile(filepath.Join(base, "pkg", "top.ts"), []byte("export {}"), 0o644); err != nil {
				t.Fatal(err)
			}
			if err := os.WriteFile(filepath.Join(base, "pkg", "sub", "deep.ts"), []byte("export {}"), 0o644); err != nil {
				t.Fatal(err)
			}
			backend.emit(baseNorm, []fswatch.Event{{Kind: fswatch.EventUpdate, Path: filepath.Join(base, "pkg")}}, nil)

			waitFor(t, func() bool { return backend.isWatching(target) }, "promotion")

			created := func() map[string]bool {
				mu.Lock()
				defer mu.Unlock()
				m := map[string]bool{}
				for _, e := range got {
					if e.Type == lsproto.FileChangeTypeCreated {
						m[string(e.Uri)] = true
					}
				}
				return m
			}

			// Both modes must synthesize the immediate child.
			waitFor(t, func() bool {
				for s := range created() {
					if strings.HasSuffix(s, "/pkg/top.ts") {
						return true
					}
				}
				return false
			}, "synthetic create for immediate child")

			// Only the recursive watch should synthesize the deep descendant.
			deadline := time.Now().Add(1 * time.Second)
			sawDeep := func() bool {
				for s := range created() {
					if strings.HasSuffix(s, "/pkg/sub/deep.ts") {
						return true
					}
				}
				return false
			}
			if recursive {
				for time.Now().Before(deadline) && !sawDeep() {
					time.Sleep(20 * time.Millisecond)
				}
				if !sawDeep() {
					t.Errorf("recursive watch should synthesize deep descendant; got %v", created())
				}
			} else {
				time.Sleep(300 * time.Millisecond) // allow any erroneous deep event to arrive
				if sawDeep() {
					t.Errorf("non-recursive watch must not synthesize deep descendant; got %v", created())
				}
			}
		})
	}
}

func TestWatcher_TerminatedFallsBackAndRecovers(t *testing.T) {
	t.Parallel()

	fs := bundled.WrapFS(osvfs.FS())
	backend := newFakeBackend()
	var (
		mu  sync.Mutex
		got []*lsproto.FileEvent
	)
	w := newWithBackend(fs, backend, func(changes []*lsproto.FileEvent) {
		mu.Lock()
		got = append(got, changes...)
		mu.Unlock()
	}, logging.NewLogger(os.Stderr))
	t.Cleanup(w.Close)

	base := t.TempDir()
	baseNorm := tspath.NormalizeSlashes(base)
	target := tspath.NormalizeSlashes(filepath.Join(base, "pkg"))
	if err := os.MkdirAll(filepath.Join(base, "pkg"), 0o755); err != nil {
		t.Fatal(err)
	}
	pattern := target + "/*"
	kind := lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete

	if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
		Kind:        &kind,
	}}); err != nil {
		t.Fatal(err)
	}
	if !backend.isWatching(target) {
		t.Fatalf("expected target watch on %q, got %v", target, backend.watchedDirs())
	}

	// Delete the directory and deliver ErrWatchTerminated together with the
	// directory's own delete event (as the real backends do).
	if err := os.RemoveAll(filepath.Join(base, "pkg")); err != nil {
		t.Fatal(err)
	}
	backend.emit(target, []fswatch.Event{
		{Kind: fswatch.EventDelete, Path: filepath.Join(base, "pkg")},
	}, errors.Join(fswatch.ErrWatchTerminated, errors.New("removed")))

	// The delete must be forwarded, and the watch must fall back to watching
	// the ancestor.
	waitFor(t, func() bool {
		mu.Lock()
		defer mu.Unlock()
		for _, e := range got {
			if e.Type == lsproto.FileChangeTypeDeleted && strings.HasSuffix(string(e.Uri), "/pkg") {
				return true
			}
		}
		return false
	}, "forwarded delete of terminated dir")
	waitFor(t, func() bool { return backend.isWatching(baseNorm) && !backend.isWatching(target) }, "fallback to ancestor watch")

	// Recreate the directory; the ancestor watch must promote back to target.
	if err := os.MkdirAll(filepath.Join(base, "pkg"), 0o755); err != nil {
		t.Fatal(err)
	}
	backend.emit(baseNorm, []fswatch.Event{{Kind: fswatch.EventUpdate, Path: filepath.Join(base, "pkg")}}, nil)
	waitFor(t, func() bool { return backend.isWatching(target) }, "recovery to target watch after recreation")
}

func TestWatcher_GenuineFailureRollsBackForRetry(t *testing.T) {
	t.Parallel()

	fs := bundled.WrapFS(osvfs.FS())
	backend := newFakeBackend()
	w := newWithBackend(fs, backend, func([]*lsproto.FileEvent) {}, logging.NewLogger(os.Stderr))
	t.Cleanup(w.Close)

	dir := t.TempDir()
	dirNorm := tspath.NormalizeSlashes(dir)
	pattern := dirNorm + "/*"

	// Inject a genuine backend failure for the existing directory.
	backend.mu.Lock()
	backend.failDirs[dirNorm] = errors.New("too many open files")
	backend.mu.Unlock()

	err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
	}})
	if err == nil {
		t.Fatal("expected error from genuine backend failure")
	}

	// The id must have been rolled back so a retry can re-register cleanly
	// (rather than hitting the duplicate-id error). Clear the injected failure
	// to simulate the resource pressure easing on retry.
	backend.mu.Lock()
	delete(backend.failDirs, dirNorm)
	backend.mu.Unlock()

	if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
	}}); err != nil {
		t.Fatalf("retry after rollback should succeed, got %v", err)
	}
	if !backend.isWatching(dirNorm) {
		t.Fatalf("expected watch on %q after successful retry, got %v", dirNorm, backend.watchedDirs())
	}
}

func TestWatcher_WatchTerminatedDoesNotDropEvents(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	dirNorm := tspath.NormalizeSlashes(dir)
	fs := bundled.WrapFS(osvfs.FS())
	backend := newFakeBackend()
	var got []*lsproto.FileEvent
	var mu sync.Mutex
	w := newWithBackend(fs, backend, func(changes []*lsproto.FileEvent) {
		mu.Lock()
		got = append(got, changes...)
		mu.Unlock()
	}, logging.NewLogger(os.Stderr))

	pattern := dirNorm + "/**/*"
	if err := w.WatchFiles("id", []*lsproto.FileSystemWatcher{{
		GlobPattern: lsproto.PatternOrRelativePattern{Pattern: &pattern},
	}}); err != nil {
		t.Fatal(err)
	}

	backend.emitAll([]fswatch.Event{
		{Kind: fswatch.EventUpdate, Path: filepath.FromSlash(filepath.Join(dirNorm, "b.ts"))},
	}, errors.Join(fswatch.ErrWatchTerminated, errors.New("simulated")))

	waitFor(t, func() bool {
		mu.Lock()
		defer mu.Unlock()
		return len(got) > 0
	}, "events with watch-terminated error")
}
