//go:build failpoint

package server

import (
	"bytes"
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"

	"github.com/c4pt0r/agfs/agfs-server/pkg/filesystem"
	"github.com/pingcap/failpoint"

	"github.com/mem9-ai/drive9/pkg/backend"
	"github.com/mem9-ai/drive9/pkg/datastore"
)

const (
	fsLayerCommitAfterPreflightFailpoint            = "github.com/mem9-ai/drive9/pkg/server/fsLayerCommitAfterPreflight"
	fsLayerCommitRollbackFailFailpoint              = "github.com/mem9-ai/drive9/pkg/server/fsLayerCommitRollbackFail"
	fsLayerCommitFinalizeFailFailpoint              = "github.com/mem9-ai/drive9/pkg/server/fsLayerCommitFinalizeFail"
	fsLayerApplyAfterEnsureFailpoint                = "github.com/mem9-ai/drive9/pkg/server/fsLayerApplyAfterEnsure"
	fsLayerApplyAfterSuccessFailpoint               = "github.com/mem9-ai/drive9/pkg/server/fsLayerApplyAfterSuccess"
	fsLayerRemoveOwnedBeforeDeleteFailpoint         = "github.com/mem9-ai/drive9/pkg/server/fsLayerRemoveOwnedBeforeDelete"
	fsLayerRenameNoReplaceAfterDestMissingFailpoint = "github.com/mem9-ai/drive9/pkg/server/fsLayerRenameNoReplaceAfterDestMissing"
	fsLayerRenameNoReplaceAfterSourceCheckFailpoint = "github.com/mem9-ai/drive9/pkg/server/fsLayerRenameNoReplaceAfterSourceCheck"
	centralFileOverwriteQuotaFailFailpoint          = "github.com/mem9-ai/drive9/pkg/backend/centralFileOverwriteQuotaFail"
)

func enableCallFailpoint(t *testing.T, name string, fn any) {
	t.Helper()
	if err := failpoint.EnableCall(name, fn); err != nil {
		t.Fatalf("enable failpoint %s: %v", name, err)
	}
	t.Cleanup(func() { _ = failpoint.Disable(name) })
}

func requireCommitHTTPStatus(t *testing.T, err error, minStatus int) *apiStatusError {
	t.Helper()
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode < minStatus {
		t.Fatalf("CommitFSLayer err=%v, want HTTP >= %d", err, minStatus)
	}
	return statusErr
}

func requireLayerState(t *testing.T, c *apiTestClient, layerID, want string) {
	t.Helper()
	layer, err := c.GetFSLayer(context.Background(), layerID)
	if err != nil {
		t.Fatalf("GetFSLayer: %v", err)
	}
	if layer.State != want {
		t.Fatalf("layer state=%s, want %s", layer.State, want)
	}
}

func requireFileBytes(t *testing.T, b *backend.Dat9Backend, path string, want []byte) {
	t.Helper()
	data, err := b.ReadCtx(context.Background(), path, 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx %s: %v", path, err)
	}
	if !bytes.Equal(data, want) {
		t.Fatalf("%s data = %q, want %q", path, data, want)
	}
}

func requirePathMode(t *testing.T, b *backend.Dat9Backend, path string, want uint32) {
	t.Helper()
	nf, err := b.StatNodeCtx(context.Background(), path)
	if err != nil {
		t.Fatalf("StatNodeCtx %s: %v", path, err)
	}
	got := nf.Mode
	if nf.File != nil && nf.File.Mode != 0 {
		got = nf.File.Mode
	}
	if got&0o777 != want&0o777 {
		t.Fatalf("%s mode=%o, want %o", path, got&0o777, want&0o777)
	}
}

func writeMainFile(t *testing.T, b *backend.Dat9Backend, path string, body []byte) {
	t.Helper()
	if _, _, err := b.WriteCtxIfRevisionWithTagsResult(context.Background(), path, body, 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write %s: %v", path, err)
	}
}

func TestFSLayerCommitConflictWhenInodeReplacedAfterPreflight(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	path := "/repo/race.txt"
	writeMainFile(t, s.fallback, path, []byte("base-v1"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-inode-race", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-inode-race", fsLayerEntryRequest{
		Path: path, Op: "upsert", Kind: "file", Content: []byte("layer-edit"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitAfterPreflightFailpoint, func(layerID string) {
		if layerID != "layer-inode-race" {
			return
		}
		_ = s.fallback.RemoveCtx(context.Background(), path)
		_, _, _ = s.fallback.WriteCtxIfRevisionWithTagsResult(context.Background(), path, []byte("replacement"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, "")
	})

	commit, err := c.CommitFSLayer(ctx, "layer-inode-race")
	if !errors.Is(err, errConflict) {
		t.Fatalf("CommitFSLayer err=%v, want conflict", err)
	}
	if commit == nil || commit.Status != "conflicted" {
		t.Fatalf("commit = %+v, want conflicted", commit)
	}
	if len(commit.Conflicts) != 1 || commit.Conflicts[0].Reason != "base inode changed" || commit.Conflicts[0].Path != path {
		t.Fatalf("conflicts = %+v, want base inode changed", commit.Conflicts)
	}
	requireFileBytes(t, s.fallback, path, []byte("replacement"))
}

func TestFSLayerCommitRollbackFailureReturns5xx(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	keepPath := "/repo/keep.txt"
	dirPath := "/repo/empty/"
	childPath := "/repo/empty/child.txt"
	writeMainFile(t, s.fallback, keepPath, []byte("keep-main"))
	if err := s.fallback.MkdirCtx(ctx, dirPath, 0o755); err != nil {
		t.Fatalf("MkdirCtx: %v", err)
	}

	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-rollback-5xx", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rollback-5xx", fsLayerEntryRequest{
		Path: keepPath, Op: "upsert", Kind: "file", Content: []byte("keep-layer"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry keep: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rollback-5xx", fsLayerEntryRequest{
		Path: dirPath, Op: "whiteout", Kind: "dir",
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry whiteout: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitAfterPreflightFailpoint, func(layerID string) {
		if layerID != "layer-rollback-5xx" {
			return
		}
		_, _, _ = s.fallback.WriteCtxIfRevisionWithTagsResult(context.Background(), childPath, []byte("child"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, "")
	})
	enableCallFailpoint(t, fsLayerCommitRollbackFailFailpoint, func(injected *error) {
		*injected = errors.New("forced rollback failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-rollback-5xx")
	if errors.Is(err, errConflict) {
		t.Fatalf("CommitFSLayer err=%v, want 5xx not conflict", err)
	}
	statusErr := requireCommitHTTPStatus(t, err, http.StatusInternalServerError)
	if statusErr.StatusCode != http.StatusInternalServerError {
		t.Fatalf("CommitFSLayer status=%d, want 500", statusErr.StatusCode)
	}
	requireLayerState(t, c, "layer-rollback-5xx", "conflicted")
	requireFileBytes(t, s.fallback, keepPath, []byte("keep-layer"))
	if _, err := s.fallback.StatNodeCtx(ctx, childPath); err != nil {
		t.Fatalf("child should remain after failed whiteout, stat err=%v", err)
	}
}

func TestFSLayerCommitPostCommitQuotaFailureRestoresAppliedPaths(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	keepPath := "/repo/keep.txt"
	laterPath := "/repo/later.txt"
	writeMainFile(t, s.fallback, keepPath, []byte("keep-main"))
	writeMainFile(t, s.fallback, laterPath, []byte("later-main"))
	later, err := s.fallback.StatNodeCtx(ctx, laterPath)
	if err != nil || later.File == nil || later.File.FileID == "" {
		t.Fatalf("stat later: nf=%+v err=%v", later, err)
	}
	laterFileID := later.File.FileID

	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-quota-restore", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-quota-restore", fsLayerEntryRequest{
		Path: keepPath, Op: "upsert", Kind: "file", Content: []byte("keep-layer"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry keep: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-quota-restore", fsLayerEntryRequest{
		Path: laterPath, Op: "upsert", Kind: "file", Content: []byte("later-layer"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry later: %v", err)
	}

	var injectedOnce atomic.Bool
	enableCallFailpoint(t, centralFileOverwriteQuotaFailFailpoint, func(fileID string, injected *error) {
		if fileID == laterFileID && injectedOnce.CompareAndSwap(false, true) {
			*injected = errors.New("quota handoff failed")
		}
	})

	_, err = c.CommitFSLayer(ctx, "layer-quota-restore")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want post-commit quota failure")
	}
	if errors.Is(err, errConflict) {
		t.Fatalf("CommitFSLayer err=%v, want backend error not conflict", err)
	}
	requireCommitHTTPStatus(t, err, http.StatusInternalServerError)
	requireLayerState(t, c, "layer-quota-restore", "conflicted")
	requireFileBytes(t, s.fallback, keepPath, []byte("keep-main"))
	requireFileBytes(t, s.fallback, laterPath, []byte("later-main"))
}

func TestFSLayerCommitWhiteoutActsOnReplacementAfterEnsure(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	path := "/repo/gone.txt"
	writeMainFile(t, s.fallback, path, []byte("base-v1"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-whiteout-gap", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-whiteout-gap", fsLayerEntryRequest{
		Path: path, Op: "whiteout", Kind: "file",
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}

	enableCallFailpoint(t, fsLayerApplyAfterEnsureFailpoint, func(gotPath, op string) {
		if gotPath != path || op != "whiteout" {
			return
		}
		_ = s.fallback.RemoveCtx(context.Background(), path)
		_, _, _ = s.fallback.WriteCtxIfRevisionWithTagsResult(context.Background(), path, []byte("replacement"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, "")
	})

	commit, err := c.CommitFSLayer(ctx, "layer-whiteout-gap")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit = %+v, want committed (V1 identity-then-mutate window)", commit)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, path); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("replacement stat err=%v, want ErrNotFound", err)
	}
}

func TestFSLayerCommitChmodActsOnReplacementAfterEnsure(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	path := "/repo/mode.txt"
	writeMainFile(t, s.fallback, path, []byte("base-v1"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-chmod-gap", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-chmod-gap", fsLayerEntryRequest{
		Path: path, Op: "chmod", Kind: "file", Mode: 0o600,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}

	enableCallFailpoint(t, fsLayerApplyAfterEnsureFailpoint, func(gotPath, op string) {
		if gotPath != path || op != "chmod" {
			return
		}
		_ = s.fallback.RemoveCtx(context.Background(), path)
		_, _, _ = s.fallback.WriteCtxIfRevisionWithTagsResult(context.Background(), path, []byte("replacement"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, "")
	})

	commit, err := c.CommitFSLayer(ctx, "layer-chmod-gap")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit = %+v, want committed (V1 identity-then-mutate window)", commit)
	}
	requireFileBytes(t, s.fallback, path, []byte("replacement"))
	nf, err := s.fallback.StatNodeCtx(ctx, path)
	if err != nil {
		t.Fatalf("StatNodeCtx: %v", err)
	}
	if nf.File == nil || nf.File.Mode&0o777 != 0o600 {
		t.Fatalf("mode = %+v, want 0600 on replacement", nf.File)
	}
}

func TestFSLayerCommitRenameActsOnReplacementAfterEnsure(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	src := "/repo/src.txt"
	dst := "/repo/dst.txt"
	writeMainFile(t, s.fallback, src, []byte("base-v1"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-rename-gap", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rename-gap", fsLayerEntryRequest{
		Path: src, Op: "rename", Kind: "file", ContentText: dst,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}

	enableCallFailpoint(t, fsLayerApplyAfterEnsureFailpoint, func(gotPath, op string) {
		if gotPath != src || op != "rename" {
			return
		}
		_ = s.fallback.RemoveCtx(context.Background(), src)
		_, _, _ = s.fallback.WriteCtxIfRevisionWithTagsResult(context.Background(), src, []byte("replacement"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, "")
	})

	commit, err := c.CommitFSLayer(ctx, "layer-rename-gap")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit = %+v, want committed (V1 identity-then-mutate window)", commit)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, src); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("src stat err=%v, want ErrNotFound", err)
	}
	requireFileBytes(t, s.fallback, dst, []byte("replacement"))
}

func TestFSLayerCommitRollbackRestoresRenameThenRecreateSource(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	src := "/repo/a.txt"
	dst := "/repo/b.txt"
	writeMainFile(t, s.fallback, src, []byte("original"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-rename-recreate-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rename-recreate-rb", fsLayerEntryRequest{
		Path: src, Op: "rename", Kind: "file", ContentText: dst,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry rename: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rename-recreate-rb", fsLayerEntryRequest{
		Path: src, Op: "upsert", Kind: "file", Content: []byte("new"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry recreate: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-rename-recreate-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-rename-recreate-rb", "conflicted")
	requireFileBytes(t, s.fallback, src, []byte("original"))
	if _, err := s.fallback.StatNodeCtx(ctx, dst); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("dest stat err=%v, want ErrNotFound", err)
	}
}

func TestFSLayerCommitRollbackRestoresDirectoryRename(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	srcDir := "/repo/old/"
	dstDir := "/repo/new/"
	child := "/repo/old/child.txt"
	writeMainFile(t, s.fallback, child, []byte("child-main"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-dir-rename-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-rename-rb", fsLayerEntryRequest{
		Path: srcDir, Op: "rename", Kind: "dir", ContentText: dstDir,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry rename: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-dir-rename-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-dir-rename-rb", "conflicted")
	requireFileBytes(t, s.fallback, child, []byte("child-main"))
	if _, err := s.fallback.StatNodeCtx(ctx, dstDir); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("dest dir stat err=%v, want ErrNotFound", err)
	}
}

func TestFSLayerCommitRollbackRestoresFileSwap(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/a.txt", []byte("A"))
	writeMainFile(t, s.fallback, "/repo/b.txt", []byte("B"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-swap-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	for _, e := range []fsLayerEntryRequest{
		{Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/tmp.txt"},
		{Path: "/repo/b.txt", Op: "rename", Kind: "file", ContentText: "/repo/a.txt"},
		{Path: "/repo/tmp.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt"},
	} {
		if _, err := c.UpsertFSLayerEntry(ctx, "layer-swap-rb", e); err != nil {
			t.Fatalf("rename %s→%s: %v", e.Path, e.ContentText, err)
		}
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-swap-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-swap-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/a.txt", []byte("A"))
	requireFileBytes(t, s.fallback, "/repo/b.txt", []byte("B"))
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/tmp.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("tmp stat err=%v, want ErrNotFound", err)
	}
}

func TestFSLayerCommitRollbackDropsDestChildAfterDirRename(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/old/child.txt", []byte("child-main"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-dir-dest-child-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-dest-child-rb", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-dest-child-rb", fsLayerEntryRequest{
		Path: "/repo/new/extra.txt", Op: "upsert", Kind: "file", Content: []byte("extra"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("dest child: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-dir-dest-child-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-dir-dest-child-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/old/child.txt", []byte("child-main"))
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/old/extra.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("leaked extra at source, stat err=%v", err)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/new/"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("dest dir stat err=%v, want ErrNotFound", err)
	}
}

func TestFSLayerCommitRollbackKeepsChildrenOnDirChmod(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/dir/child.txt", []byte("keep"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-dir-chmod-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-chmod-rb", fsLayerEntryRequest{
		Path: "/repo/dir/", Op: "chmod", Kind: "dir", Mode: 0o700,
	}); err != nil {
		t.Fatalf("chmod: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-dir-chmod-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-dir-chmod-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/dir/child.txt", []byte("keep"))
}

func TestFSLayerCommitRollbackKeepsSiblingWhenDirChmodAndChildEdit(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/dir/keep.txt", []byte("keep"))
	writeMainFile(t, s.fallback, "/repo/dir/edit.txt", []byte("old"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-dir-chmod-child-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-chmod-child-rb", fsLayerEntryRequest{
		Path: "/repo/dir/", Op: "chmod", Kind: "dir", Mode: 0o700,
	}); err != nil {
		t.Fatalf("chmod: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-chmod-child-rb", fsLayerEntryRequest{
		Path: "/repo/dir/edit.txt", Op: "upsert", Kind: "file", Content: []byte("new"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("edit: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-dir-chmod-child-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-dir-chmod-child-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/dir/keep.txt", []byte("keep"))
	requireFileBytes(t, s.fallback, "/repo/dir/edit.txt", []byte("old"))
}

func TestFSLayerCommitRollbackDropsDestChildAfterEmptyDirRename(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/empty/", 0o755); err != nil {
		t.Fatalf("MkdirCtx: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-empty-dir-dest-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-empty-dir-dest-rb", fsLayerEntryRequest{
		Path: "/repo/empty/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-empty-dir-dest-rb", fsLayerEntryRequest{
		Path: "/repo/new/extra.txt", Op: "upsert", Kind: "file", Content: []byte("extra"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("dest child: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-empty-dir-dest-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-empty-dir-dest-rb", "conflicted")
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/empty/extra.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("leaked extra at source, stat err=%v", err)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/new/"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("dest dir stat err=%v, want ErrNotFound", err)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/empty/"); err != nil {
		t.Fatalf("empty dir should remain, stat err=%v", err)
	}
}

func TestFSLayerCommitRollbackDropsNestedDestChildAfterDirRename(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/old/child.txt", []byte("child-main"))
	writeMainFile(t, s.fallback, "/repo/old/tmp/a.txt", []byte("keep-a"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-nested-dest-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-nested-dest-rb", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-nested-dest-rb", fsLayerEntryRequest{
		Path: "/repo/new/tmp/x.txt", Op: "upsert", Kind: "file", Content: []byte("extra"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("nested dest child: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-nested-dest-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-nested-dest-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/old/child.txt", []byte("child-main"))
	requireFileBytes(t, s.fallback, "/repo/old/tmp/a.txt", []byte("keep-a"))
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/old/tmp/x.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("leaked nested extra at source, stat err=%v", err)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/new/"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("dest dir stat err=%v, want ErrNotFound", err)
	}
}

func TestFSLayerCommitRollbackRestoresWhiteoutFileMode(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	path := "/repo/secret.txt"
	writeMainFile(t, s.fallback, path, []byte("secret"))
	if err := s.fallback.ChmodCtx(ctx, path, 0o600); err != nil {
		t.Fatalf("ChmodCtx: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-whiteout-mode-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-whiteout-mode-rb", fsLayerEntryRequest{
		Path: path, Op: "whiteout", Kind: "file",
	}); err != nil {
		t.Fatalf("whiteout: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-whiteout-mode-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-whiteout-mode-rb", "conflicted")
	requireFileBytes(t, s.fallback, path, []byte("secret"))
	requirePathMode(t, s.fallback, path, 0o600)
}

func TestFSLayerCommitRollbackLeavesForeignSourceReplacement(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/a.txt", []byte("original"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-foreign-src-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-foreign-src-rb", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		writeMainFile(t, s.fallback, "/repo/a.txt", []byte("foreign"))
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-foreign-src-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want rollback failure")
	}
	requireCommitHTTPStatus(t, err, http.StatusInternalServerError)
	requireLayerState(t, c, "layer-foreign-src-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/a.txt", []byte("foreign"))
	requireFileBytes(t, s.fallback, "/repo/b.txt", []byte("original"))
}

func TestFSLayerCommitRollbackLeavesForeignCreatedReplacement(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-foreign-create-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-foreign-create-rb", fsLayerEntryRequest{
		Path: "/repo/new.txt", Op: "upsert", Kind: "file", Content: []byte("layer"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("upsert: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		bg := context.Background()
		nf, err := s.fallback.StatNodeCtx(bg, "/repo/new.txt")
		if err != nil {
			t.Errorf("stat layer file: %v", err)
			*injected = errors.New("forced finalize failure")
			return
		}
		oldID := datastore.NodeFileID(nf)
		if err := s.fallback.RemoveCtx(bg, "/repo/new.txt"); err != nil {
			t.Errorf("remove layer file: %v", err)
			*injected = errors.New("forced finalize failure")
			return
		}
		writeMainFile(t, s.fallback, "/repo/new.txt", []byte("foreign"))
		nf, err = s.fallback.StatNodeCtx(bg, "/repo/new.txt")
		if err != nil {
			t.Errorf("stat foreign file: %v", err)
			*injected = errors.New("forced finalize failure")
			return
		}
		if datastore.NodeFileID(nf) == "" || datastore.NodeFileID(nf) == oldID {
			t.Errorf("replacement reused inode %q", oldID)
		}
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-foreign-create-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-foreign-create-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/new.txt", []byte("foreign"))
}

func TestFSLayerCommitRollbackLeavesForeignChildAfterDirRename(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/old/child.txt", []byte("child-main"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-foreign-dir-child-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-foreign-dir-child-rb", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		writeMainFile(t, s.fallback, "/repo/new/foreign.txt", []byte("foreign"))
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-foreign-dir-child-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-foreign-dir-child-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/old/child.txt", []byte("child-main"))
	requireFileBytes(t, s.fallback, "/repo/old/foreign.txt", []byte("foreign"))
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/new/"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("dest dir stat err=%v, want ErrNotFound", err)
	}
}

func TestFSLayerCommitRollbackRestoresDirRenameDestOverwrite(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/old/child.txt", []byte("child-main"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-dir-rename-dest-overwrite-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-rename-dest-overwrite-rb", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-rename-dest-overwrite-rb", fsLayerEntryRequest{
		Path: "/repo/new/child.txt", Op: "upsert", Kind: "file", Content: []byte("layer"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("dest overwrite: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-dir-rename-dest-overwrite-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-dir-rename-dest-overwrite-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/old/child.txt", []byte("child-main"))
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/new/"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("dest dir stat err=%v, want ErrNotFound", err)
	}
}

func TestFSLayerCommitRollbackLeavesForeignInRecreatedSourceDir(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/old/child.txt", []byte("child-main"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-foreign-recreate-dir-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-foreign-recreate-dir-rb", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-foreign-recreate-dir-rb", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "upsert", Kind: "dir", Mode: 0o755,
	}); err != nil {
		t.Fatalf("mkdir source: %v", err)
	}

	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		writeMainFile(t, s.fallback, "/repo/old/foreign.txt", []byte("foreign"))
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-foreign-recreate-dir-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want rollback failure")
	}
	requireCommitHTTPStatus(t, err, http.StatusInternalServerError)
	requireLayerState(t, c, "layer-foreign-recreate-dir-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/old/foreign.txt", []byte("foreign"))
}

func replacePathWithForeignFile(t *testing.T, b *backend.Dat9Backend, path string, body []byte) {
	t.Helper()
	bg := context.Background()
	old, err := b.StatNodeCtx(bg, path)
	if err != nil && !errors.Is(err, datastore.ErrNotFound) {
		t.Errorf("stat %s before replace: %v", path, err)
		return
	}
	oldID := datastore.NodeFileID(old)
	if err := b.RemoveCtx(bg, path); err != nil && !errors.Is(err, datastore.ErrNotFound) {
		t.Errorf("remove %s: %v", path, err)
		return
	}
	if _, _, err := b.WriteCtxIfRevisionWithTagsResult(bg, path, body, 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Errorf("write foreign %s: %v", path, err)
		return
	}
	nf, err := b.StatNodeCtx(bg, path)
	if err != nil {
		t.Errorf("stat %s after replace: %v", path, err)
		return
	}
	got := datastore.NodeFileID(nf)
	if got == "" || got == oldID {
		t.Errorf("%s replacement reused inode %q", path, oldID)
	}
}

func TestFSLayerCommitRollbackLeavesApplyReceiptReplacement(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-apply-receipt-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-apply-receipt-rb", fsLayerEntryRequest{
		Path: "/repo/new.txt", Op: "upsert", Kind: "file", Content: []byte("layer"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("upsert: %v", err)
	}

	enableCallFailpoint(t, fsLayerApplyAfterSuccessFailpoint, func(path string) {
		if path != "/repo/new.txt" {
			return
		}
		replacePathWithForeignFile(t, s.fallback, path, []byte("foreign"))
	})
	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-apply-receipt-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-apply-receipt-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/new.txt", []byte("foreign"))
}

func TestFSLayerCommitRollbackLeavesOwnedDeleteReplacement(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-owned-delete-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-owned-delete-rb", fsLayerEntryRequest{
		Path: "/repo/new.txt", Op: "upsert", Kind: "file", Content: []byte("layer"), Mode: 0o644,
	}); err != nil {
		t.Fatalf("upsert: %v", err)
	}

	enableCallFailpoint(t, fsLayerRemoveOwnedBeforeDeleteFailpoint, func(path, expectedFileID string) {
		if path != "/repo/new.txt" || expectedFileID == "" {
			return
		}
		replacePathWithForeignFile(t, s.fallback, path, []byte("foreign"))
	})
	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-owned-delete-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want finalize failure after rollback")
	}
	requireLayerState(t, c, "layer-owned-delete-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/new.txt", []byte("foreign"))
}

func TestFSLayerCommitRollbackLeavesForeignEmptyDirRenameDest(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/old/child.txt", []byte("child-main"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-dir-noreplace-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-noreplace-rb", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}

	var foreignID string
	enableCallFailpoint(t, fsLayerRenameNoReplaceAfterDestMissingFailpoint, func(from, to string) {
		if to != "/repo/old/" {
			return
		}
		bg := context.Background()
		if err := s.fallback.MkdirCtx(bg, "/repo/old/", 0o755); err != nil {
			t.Errorf("mkdir foreign dest: %v", err)
			return
		}
		nf, err := s.fallback.StatNodeCtx(bg, "/repo/old/")
		if err != nil {
			t.Errorf("stat foreign dest: %v", err)
			return
		}
		foreignID = datastore.NodeFileID(nf)
	})
	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-dir-noreplace-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want rollback failure")
	}
	requireCommitHTTPStatus(t, err, http.StatusInternalServerError)
	requireLayerState(t, c, "layer-dir-noreplace-rb", "conflicted")
	if foreignID == "" {
		t.Fatal("foreign dest inode was not recorded")
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/old/")
	if err != nil {
		t.Fatalf("stat foreign dest after rollback: %v", err)
	}
	if datastore.NodeFileID(nf) != foreignID {
		t.Fatalf("foreign dest inode = %q, want %q", datastore.NodeFileID(nf), foreignID)
	}
	requireFileBytes(t, s.fallback, "/repo/new/child.txt", []byte("child-main"))
}

func TestFSLayerCommitRollbackLeavesForeignSourceAfterSourceCheck(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/old.txt", []byte("original"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-rename-src-check-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rename-src-check-rb", fsLayerEntryRequest{
		Path: "/repo/old.txt", Op: "rename", Kind: "file", ContentText: "/repo/new.txt",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}

	enableCallFailpoint(t, fsLayerRenameNoReplaceAfterSourceCheckFailpoint, func(from, to string) {
		if from != "/repo/new.txt" {
			return
		}
		replacePathWithForeignFile(t, s.fallback, from, []byte("foreign"))
	})
	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-rename-src-check-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want rollback failure")
	}
	requireCommitHTTPStatus(t, err, http.StatusInternalServerError)
	requireLayerState(t, c, "layer-rename-src-check-rb", "conflicted")
	requireFileBytes(t, s.fallback, "/repo/new.txt", []byte("foreign"))
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/old.txt"); err == nil {
		data, readErr := s.fallback.ReadCtx(ctx, "/repo/old.txt", 0, -1)
		if readErr != nil {
			t.Fatalf("ReadCtx /repo/old.txt: %v", readErr)
		}
		if bytes.Equal(data, []byte("foreign")) {
			t.Fatal("foreign inode was relocated to /repo/old.txt")
		}
	}
}

func TestFSLayerCommitRollbackLeavesForeignDirAfterSourceCheck(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	defer ts.Close()

	ctx := context.Background()
	writeMainFile(t, s.fallback, "/repo/old/child.txt", []byte("child-main"))
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-rename-dir-src-check-rb", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rename-dir-src-check-rb", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}

	var foreignID string
	enableCallFailpoint(t, fsLayerRenameNoReplaceAfterSourceCheckFailpoint, func(from, to string) {
		if from != "/repo/new/" {
			return
		}
		bg := context.Background()
		if err := s.fallback.RemoveAllCtx(bg, from); err != nil {
			t.Errorf("remove source tree: %v", err)
			return
		}
		if err := s.fallback.MkdirCtx(bg, from, 0o755); err != nil {
			t.Errorf("mkdir foreign source: %v", err)
			return
		}
		nf, err := s.fallback.StatNodeCtx(bg, from)
		if err != nil {
			t.Errorf("stat foreign source: %v", err)
			return
		}
		foreignID = datastore.NodeFileID(nf)
	})
	enableCallFailpoint(t, fsLayerCommitFinalizeFailFailpoint, func(injected *error) {
		*injected = errors.New("forced finalize failure")
	})

	_, err := c.CommitFSLayer(ctx, "layer-rename-dir-src-check-rb")
	if err == nil {
		t.Fatal("CommitFSLayer err=nil, want rollback failure")
	}
	requireCommitHTTPStatus(t, err, http.StatusInternalServerError)
	requireLayerState(t, c, "layer-rename-dir-src-check-rb", "conflicted")
	if foreignID == "" {
		t.Fatal("foreign source inode was not recorded")
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/new/")
	if err != nil {
		t.Fatalf("stat foreign source after rollback: %v", err)
	}
	if datastore.NodeFileID(nf) != foreignID {
		t.Fatalf("foreign source inode = %q, want %q", datastore.NodeFileID(nf), foreignID)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/old/"); err == nil {
		old, statErr := s.fallback.StatNodeCtx(ctx, "/repo/old/")
		if statErr != nil {
			t.Fatalf("stat /repo/old/: %v", statErr)
		}
		if datastore.NodeFileID(old) == foreignID {
			t.Fatal("foreign dir inode was relocated to /repo/old/")
		}
	}
}
