package server

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"testing"

	"github.com/c4pt0r/agfs/agfs-server/pkg/filesystem"
	"github.com/mem9-ai/drive9/pkg/backend"
	"github.com/mem9-ai/drive9/pkg/datastore"
	"github.com/mem9-ai/drive9/pkg/metrics"
	"github.com/mem9-ai/drive9/pkg/s3client"
)

func TestFSLayerApplyMutationCommitted(t *testing.T) {
	if fsLayerApplyMutationCommitted(nil) {
		t.Fatal("nil should not be a committed mutation")
	}
	if fsLayerApplyMutationCommitted(errors.New("quota handoff failed")) {
		t.Fatal("plain error should not be a committed mutation")
	}
	postCommit := &backend.PostCommitQuotaMutationError{Op: "file_overwrite", Err: errors.New("quota handoff failed")}
	if !fsLayerApplyMutationCommitted(postCommit) {
		t.Fatal("PostCommitQuotaMutationError should be a committed mutation")
	}
	if !fsLayerApplyMutationCommitted(fmt.Errorf("apply: %w", postCommit)) {
		t.Fatal("wrapped PostCommitQuotaMutationError should be a committed mutation")
	}
	chmodErr := fsLayerMarkApplyCommitted(fmt.Errorf("chmod fs layer path /p: %w", datastore.ErrNotFound))
	if !fsLayerApplyMutationCommitted(chmodErr) {
		t.Fatal("chmod after a committed write should be a committed mutation")
	}
	if fsLayerMarkApplyCommitted(postCommit) != postCommit {
		t.Fatal("already-committed error should not be re-wrapped")
	}
}

func TestFSLayerRestoreMustRecreate(t *testing.T) {
	if !fsLayerRestoreMustRecreate(nil, datastore.ErrNotFound) {
		t.Fatal("missing path should recreate")
	}
	dir := &datastore.NodeWithFile{Node: datastore.FileNode{IsDirectory: true}}
	if !fsLayerRestoreMustRecreate(dir, nil) {
		t.Fatal("directory occupant should recreate")
	}
	pending := &datastore.NodeWithFile{Node: datastore.FileNode{Path: "/p"}}
	if !fsLayerRestoreMustRecreate(pending, nil) {
		t.Fatal("occupant without a file entity should recreate")
	}
	link := &datastore.NodeWithFile{
		Node: datastore.FileNode{Path: "/p"},
		File: &datastore.File{Mode: 0o120777},
	}
	if !fsLayerRestoreMustRecreate(link, nil) {
		t.Fatal("symlink occupant should recreate")
	}
	regular := &datastore.NodeWithFile{
		Node: datastore.FileNode{Path: "/p"},
		File: &datastore.File{FileID: "inode-a", Mode: 0o100644},
	}
	if fsLayerRestoreMustRecreate(regular, nil) {
		t.Fatal("regular file occupant should restore in place")
	}
	same := &fsLayerBaseSnapshot{FileID: "inode-a"}
	ours := map[string]struct{}{"inode-a": {}}
	if fsLayerRestoreLeaveReplacement(regular, same, ours) {
		t.Fatal("same inode should restore in place")
	}
	moved := &datastore.NodeWithFile{
		Node: datastore.FileNode{Path: "/p"},
		File: &datastore.File{FileID: "inode-a", Mode: 0o100644},
	}
	destSnap := &fsLayerBaseSnapshot{FileID: "inode-dest"}
	if fsLayerRestoreLeaveReplacement(moved, destSnap, ours) {
		t.Fatal("our inode moved onto a dest path should restore that dest snapshot")
	}
	other := &datastore.NodeWithFile{
		Node: datastore.FileNode{Path: "/p"},
		File: &datastore.File{FileID: "inode-b", Mode: 0o100644},
	}
	if !fsLayerRestoreLeaveReplacement(other, same, ours) {
		t.Fatal("foreign inode should leave the replacement")
	}
	foreignDir := &datastore.NodeWithFile{
		Node: datastore.FileNode{Path: "/d/", IsDirectory: true, InodeID: "inode-x"},
	}
	if !fsLayerRestoreLeaveReplacement(foreignDir, same, ours) {
		t.Fatal("foreign dir inode should leave the replacement")
	}
}

func TestFSLayerCommitSnapshotRenameTreeSkipsChildBody(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/child.txt", []byte("keep-body"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write child: %v", err)
	}
	snapshots, err := snapshotFSLayerCommit(ctx, s.fallback, []datastore.FSLayerEntry{{
		Path:        "/repo/old/",
		Op:          datastore.FSLayerEntryOpRename,
		Kind:        datastore.FSLayerEntryKindDir,
		ContentText: "/repo/new/",
	}})
	if err != nil {
		t.Fatalf("snapshotFSLayerCommit: %v", err)
	}
	var child *fsLayerBaseSnapshot
	for i := range snapshots {
		if snapshots[i].Path == "/repo/old/child.txt" {
			child = &snapshots[i]
			break
		}
	}
	if child == nil {
		t.Fatal("missing child snapshot")
	}
	if !child.Exists || child.FileID == "" {
		t.Fatalf("child snapshot = %+v, want exists with FileID", child)
	}
	if child.BodyOK || len(child.Data) != 0 {
		t.Fatalf("rename-tree child captured body (%d bytes)", len(child.Data))
	}
}

func TestFSLayerCommitSnapshotCapturesRemappedDestUpsertBody(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/child.txt", []byte("keep-body"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write child: %v", err)
	}
	snapshots, err := snapshotFSLayerCommit(ctx, s.fallback, []datastore.FSLayerEntry{
		{Path: "/repo/old/", Op: datastore.FSLayerEntryOpRename, Kind: datastore.FSLayerEntryKindDir, ContentText: "/repo/new/"},
		{Path: "/repo/new/child.txt", Op: datastore.FSLayerEntryOpUpsert, Kind: datastore.FSLayerEntryKindFile, ContentBlob: []byte("layer")},
	})
	if err != nil {
		t.Fatalf("snapshotFSLayerCommit: %v", err)
	}
	var child *fsLayerBaseSnapshot
	for i := range snapshots {
		if snapshots[i].Path == "/repo/old/child.txt" {
			child = &snapshots[i]
			break
		}
	}
	if child == nil {
		t.Fatal("missing remapped child snapshot")
	}
	if !child.BodyOK || !bytes.Equal(child.Data, []byte("keep-body")) {
		t.Fatalf("remapped dest upsert child body = %q BodyOK=%v, want keep-body", child.Data, child.BodyOK)
	}
}

func TestFSLayerSnapshotBodyLivePathNestedDirHops(t *testing.T) {
	entries := []datastore.FSLayerEntry{
		{Path: "/a/", Op: datastore.FSLayerEntryOpRename, Kind: datastore.FSLayerEntryKindDir, ContentText: "/b/"},
		{Path: "/d/", Op: datastore.FSLayerEntryOpRename, Kind: datastore.FSLayerEntryKindDir, ContentText: "/b/sub/"},
	}
	got := fsLayerSnapshotBodyLivePath(entries, "/b/sub/x.txt")
	if got != "/d/x.txt" {
		t.Fatalf("nested dest remap = %q, want /d/x.txt", got)
	}
	got = fsLayerSnapshotBodyLivePath(entries, "/b/y.txt")
	if got != "/a/y.txt" {
		t.Fatalf("outer dest remap = %q, want /a/y.txt", got)
	}
}

func TestFSLayerApplyOCCConflictReasons(t *testing.T) {
	cases := []struct {
		err    error
		reason string
		ok     bool
	}{
		{err: fmt.Errorf("%w: %w", datastore.ErrRevisionConflict, datastore.ErrFSLayerBasePathExists), reason: "base path exists", ok: true},
		{err: fmt.Errorf("%w: %w", datastore.ErrRevisionConflict, datastore.ErrFSLayerBasePathMissing), reason: "base path missing", ok: true},
		{err: fmt.Errorf("%w: %w", datastore.ErrRevisionConflict, datastore.ErrFSLayerBaseInodeChanged), reason: "base inode changed", ok: true},
		{err: fmt.Errorf("%w: %w", datastore.ErrRevisionConflict, datastore.ErrFSLayerBaseRevisionChanged), reason: "base revision changed", ok: true},
		{err: datastore.ErrNotFound, ok: false},
	}
	for _, tc := range cases {
		got, ok := fsLayerApplyOCCConflict("/p", tc.err)
		if ok != tc.ok {
			t.Fatalf("err=%v ok=%v, want %v", tc.err, ok, tc.ok)
		}
		if !tc.ok {
			continue
		}
		if got.Path != "/p" || got.Reason != tc.reason {
			t.Fatalf("conflict=%+v, want path=/p reason=%q", got, tc.reason)
		}
	}
}

func tenantFileBytesMetricValue(t *testing.T, direction, trafficPath string) int64 {
	t.Helper()
	recorder := httptest.NewRecorder()
	metrics.WritePrometheus(recorder)
	prefix := `drive9_tenant_file_bytes_total{direction="` + direction +
		`",tenant_id="local",tidbcloud_org_id="guest",traffic_path="` + trafficPath + `"} `
	for _, line := range strings.Split(recorder.Body.String(), "\n") {
		if !strings.HasPrefix(line, prefix) {
			continue
		}
		value, err := strconv.ParseInt(strings.TrimPrefix(line, prefix), 10, 64)
		if err != nil {
			t.Fatalf("parse tenant file bytes metric %q: %v", line, err)
		}
		return value
	}
	return 0
}

func requireTenantFileBytesDelta(t *testing.T, before, want int64, direction, trafficPath string) {
	t.Helper()
	after := tenantFileBytesMetricValue(t, direction, trafficPath)
	if got := after - before; got != want {
		t.Fatalf("tenant file bytes delta direction=%q traffic_path=%q = %d, want %d", direction, trafficPath, got, want)
	}
}

func tenantFileOpsMetricValue(t *testing.T, direction, trafficPath string) int64 {
	t.Helper()
	recorder := httptest.NewRecorder()
	metrics.WritePrometheus(recorder)
	prefix := `drive9_tenant_file_ops_total{direction="` + direction +
		`",tenant_id="local",tidbcloud_org_id="guest",traffic_path="` + trafficPath + `"} `
	for _, line := range strings.Split(recorder.Body.String(), "\n") {
		if !strings.HasPrefix(line, prefix) {
			continue
		}
		value, err := strconv.ParseInt(strings.TrimPrefix(line, prefix), 10, 64)
		if err != nil {
			t.Fatalf("parse tenant file ops metric %q: %v", line, err)
		}
		return value
	}
	return 0
}

func requireTenantFileOpsDelta(t *testing.T, before, want int64, direction, trafficPath string) {
	t.Helper()
	after := tenantFileOpsMetricValue(t, direction, trafficPath)
	if got := after - before; got != want {
		t.Fatalf("tenant file ops delta direction=%q traffic_path=%q = %d, want %d", direction, trafficPath, got, want)
	}
}

func TestFSLayerPathQueryPreservesControlWhitespace(t *testing.T) {
	const path = "/repo/line\nbreak\tname\r"
	req := httptest.NewRequest(http.MethodGet, "/v1/layers/layer/entries?path="+url.QueryEscape(path), nil)
	if got := fsLayerPathQuery(req); got != path {
		t.Fatalf("path query = %q, want %q", got, path)
	}
}

func TestFSLayerPathQueryPreservesEdgeSpaces(t *testing.T) {
	for _, path := range []string{"/repo/ leading", "/repo/trailing ", "/repo/ both "} {
		req := httptest.NewRequest(http.MethodGet, "/v1/layers/layer/entries?path="+url.QueryEscape(path), nil)
		if got := fsLayerPathQuery(req); got != path {
			t.Fatalf("path query = %q, want %q", got, path)
		}
	}
}

func TestFSLayerRenameTargetPreservesControlWhitespace(t *testing.T) {
	entry := &datastore.FSLayerEntry{ContentText: "/repo/target\t\r\n"}
	if got := fsLayerRenameTarget(entry); got != entry.ContentText {
		t.Fatalf("rename target = %q, want %q", got, entry.ContentText)
	}
}

func TestFSLayerRenameTargetPreservesSpaces(t *testing.T) {
	entry := &datastore.FSLayerEntry{ContentText: "  "}
	if got := fsLayerRenameTarget(entry); got != entry.ContentText {
		t.Fatalf("rename target = %q, want %q", got, entry.ContentText)
	}
}

func newFSLayerRouteAliasFixture(t *testing.T) (*Server, *apiTestClient, string, string) {
	t.Helper()
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)
	c := newAPITestClient(ts.URL)
	layerA := strings.Repeat("a", 48) + "b"
	layerB := strings.Repeat("a", 48) + "%62"
	for _, layer := range []struct {
		id   string
		name string
	}{
		{id: layerA, name: "route A"},
		{id: layerB, name: "route B"},
	} {
		if _, err := c.CreateFSLayer(context.Background(), fsLayerCreateRequest{
			LayerID:      layer.id,
			BaseRootPath: "/route",
			Name:         layer.name,
		}); err != nil {
			t.Fatalf("CreateFSLayer %q: %v", layer.id, err)
		}
	}
	return s, c, layerA, layerB
}

func TestFSLayerAPIRouteSegmentsDecodeOnce(t *testing.T) {
	ctx := context.Background()

	t.Run("get layer", func(t *testing.T) {
		_, c, _, layerB := newFSLayerRouteAliasFixture(t)
		got, err := c.GetFSLayer(ctx, layerB)
		if err != nil {
			t.Fatalf("GetFSLayer percent id: %v", err)
		}
		if got.LayerID != layerB || got.Name != "route B" {
			t.Fatalf("GetFSLayer percent id = %+v, want layer B", got)
		}
	})

	t.Run("read object", func(t *testing.T) {
		s, c, layerA, layerB := newFSLayerRouteAliasFixture(t)
		store := s.fallback.Store()
		for _, entry := range []datastore.FSLayerEntry{
			{LayerID: layerA, Path: "/route/file.txt", Op: datastore.FSLayerEntryOpUpsert, Kind: datastore.FSLayerEntryKindFile, ContentBlob: []byte("A"), SizeBytes: 1},
			{LayerID: layerB, Path: "/route/file.txt", Op: datastore.FSLayerEntryOpUpsert, Kind: datastore.FSLayerEntryKindFile, ContentBlob: []byte("B"), SizeBytes: 1},
		} {
			if err := store.UpsertFSLayerEntry(ctx, &entry); err != nil {
				t.Fatalf("UpsertFSLayerEntry %q: %v", entry.LayerID, err)
			}
		}
		got, err := c.ReadFSLayerFile(ctx, layerB, "/route/file.txt", nil)
		if err != nil {
			t.Fatalf("ReadFSLayerFile percent id: %v", err)
		}
		if string(got) != "B" {
			t.Fatalf("ReadFSLayerFile percent id=%q, want B", got)
		}
	})

	t.Run("create checkpoint", func(t *testing.T) {
		_, c, _, layerB := newFSLayerRouteAliasFixture(t)
		checkpoint, err := c.CheckpointFSLayer(ctx, layerB, fsLayerCheckpointRequest{CheckpointID: "checkpoint-percent-layer"})
		if err != nil {
			t.Fatalf("CheckpointFSLayer percent id: %v", err)
		}
		if checkpoint.LayerID != layerB {
			t.Fatalf("CheckpointFSLayer layer_id=%q, want %q", checkpoint.LayerID, layerB)
		}
	})

	t.Run("get checkpoint", func(t *testing.T) {
		s, c, layerA, layerB := newFSLayerRouteAliasFixture(t)
		checkpointA := strings.Repeat("c", 48) + "b"
		checkpointB := strings.Repeat("c", 48) + "%62"
		store := s.fallback.Store()
		for _, checkpoint := range []datastore.FSLayerCheckpoint{
			{CheckpointID: checkpointA, LayerID: layerA},
			{CheckpointID: checkpointB, LayerID: layerB},
		} {
			if err := store.CreateFSLayerCheckpoint(ctx, &checkpoint); err != nil {
				t.Fatalf("CreateFSLayerCheckpoint %q: %v", checkpoint.CheckpointID, err)
			}
		}
		got, err := c.GetFSLayerCheckpoint(ctx, checkpointB)
		if err != nil {
			t.Fatalf("GetFSLayerCheckpoint percent id: %v", err)
		}
		if got.CheckpointID != checkpointB || got.LayerID != layerB {
			t.Fatalf("GetFSLayerCheckpoint percent id = %+v, want checkpoint B", got)
		}
	})

	t.Run("commit", func(t *testing.T) {
		s, c, layerA, layerB := newFSLayerRouteAliasFixture(t)
		committed, err := c.CommitFSLayer(ctx, layerB)
		if err != nil {
			t.Fatalf("CommitFSLayer percent id: %v", err)
		}
		if committed.LayerID != layerB || committed.Status != "committed" {
			t.Fatalf("CommitFSLayer percent id = %+v, want layer B committed", committed)
		}
		store := s.fallback.Store()
		gotA, err := store.GetFSLayer(ctx, layerA)
		if err != nil {
			t.Fatalf("GetFSLayer layer A: %v", err)
		}
		gotB, err := store.GetFSLayer(ctx, layerB)
		if err != nil {
			t.Fatalf("GetFSLayer layer B: %v", err)
		}
		if gotA.State != datastore.FSLayerStateActive || gotB.State != datastore.FSLayerStateCommitted {
			t.Fatalf("states after commit A=%q B=%q", gotA.State, gotB.State)
		}
	})

	t.Run("rollback", func(t *testing.T) {
		s, c, layerA, layerB := newFSLayerRouteAliasFixture(t)
		if err := c.RollbackFSLayer(ctx, layerB); err != nil {
			t.Fatalf("RollbackFSLayer percent id: %v", err)
		}
		store := s.fallback.Store()
		gotA, err := store.GetFSLayer(ctx, layerA)
		if err != nil {
			t.Fatalf("GetFSLayer layer A: %v", err)
		}
		gotB, err := store.GetFSLayer(ctx, layerB)
		if err != nil {
			t.Fatalf("GetFSLayer layer B: %v", err)
		}
		if gotA.State != datastore.FSLayerStateActive || gotB.State != datastore.FSLayerStateAbandoned {
			t.Fatalf("states after rollback A=%q B=%q", gotA.State, gotB.State)
		}
	})
}

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

	ctx := context.Background()
	c := newAPITestClient(ts.URL)
	for i, size := range []int{49, 50, 51, datastore.FSLayerIDMaxBytes} {
		layerID := strings.Repeat(string(rune('a'+i)), size)
		layer, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
			LayerID:      layerID,
			BaseRootPath: "/repo",
		})
		if err != nil {
			t.Fatalf("CreateFSLayer %d bytes: %v", size, err)
		}
		if layer.LayerID != layerID {
			t.Fatalf("CreateFSLayer %d bytes returned id=%q", size, layer.LayerID)
		}
	}

	auto, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{BaseRootPath: "/repo"})
	if err != nil {
		t.Fatalf("CreateFSLayer generated id: %v", err)
	}
	if auto.LayerID == "" || len(auto.LayerID) > datastore.FSLayerIDMaxBytes {
		t.Fatalf("generated layer id = %q", auto.LayerID)
	}

	for _, tt := range []struct {
		name    string
		layerID string
		message string
	}{
		{
			name:    "oversized",
			layerID: strings.Repeat("z", datastore.FSLayerIDMaxBytes+1),
			message: fmt.Sprintf("layer_id exceeds %d bytes", datastore.FSLayerIDMaxBytes),
		},
		{name: "colon", layerID: "layer:invalid", message: "layer_id must not contain"},
	} {
		t.Run(tt.name, func(t *testing.T) {
			_, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: tt.layerID, BaseRootPath: "/repo"})
			var statusErr *apiStatusError
			if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusBadRequest {
				t.Fatalf("CreateFSLayer err=%v, want HTTP 400", err)
			}
			if !strings.Contains(statusErr.Message, tt.message) {
				t.Fatalf("CreateFSLayer message=%q, want %q", statusErr.Message, tt.message)
			}
		})
	}

	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-checkpoint-boundary",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer checkpoint boundary: %v", err)
	}
	checkpointID := strings.Repeat("c", datastore.FSLayerCheckpointIDMaxBytes)
	if _, err := c.CheckpointFSLayer(ctx, "layer-checkpoint-boundary", fsLayerCheckpointRequest{CheckpointID: checkpointID}); err != nil {
		t.Fatalf("CheckpointFSLayer %d bytes: %v", datastore.FSLayerCheckpointIDMaxBytes, err)
	}
	if got, err := c.GetFSLayerCheckpoint(ctx, checkpointID); err != nil || got.CheckpointID != checkpointID {
		t.Fatalf("GetFSLayerCheckpoint %d bytes got=%+v err=%v", datastore.FSLayerCheckpointIDMaxBytes, got, err)
	}
	_, err = c.CheckpointFSLayer(ctx, "layer-checkpoint-boundary", fsLayerCheckpointRequest{
		CheckpointID: strings.Repeat("c", datastore.FSLayerCheckpointIDMaxBytes+1),
	})
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusBadRequest {
		t.Fatalf("CheckpointFSLayer oversized err=%v, want HTTP 400", err)
	}
	checkpointMessage := fmt.Sprintf("checkpoint_id exceeds %d bytes", datastore.FSLayerCheckpointIDMaxBytes)
	if !strings.Contains(statusErr.Message, checkpointMessage) {
		t.Fatalf("CheckpointFSLayer message=%q", statusErr.Message)
	}
	for _, invalidID := range []string{
		strings.Repeat("c", datastore.FSLayerCheckpointIDMaxBytes+1),
		strings.Repeat("界", 22),
	} {
		_, err := c.GetFSLayerCheckpoint(ctx, invalidID)
		if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusBadRequest {
			t.Fatalf("GetFSLayerCheckpoint %d-byte id err=%v, want HTTP 400", len(invalidID), err)
		}
		if !strings.Contains(statusErr.Message, checkpointMessage) {
			t.Fatalf("GetFSLayerCheckpoint message=%q", statusErr.Message)
		}
	}
}

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

	ctx := context.Background()
	c := newAPITestClient(ts.URL)
	for _, tt := range []struct {
		name    string
		request fsLayerCreateRequest
		message string
	}{
		{
			name: "durability",
			request: fsLayerCreateRequest{
				LayerID:        "layer-invalid-durability",
				BaseRootPath:   "/repo",
				DurabilityMode: "invalid",
			},
			message: "invalid fs layer durability mode",
		},
		{
			name: "tags",
			request: fsLayerCreateRequest{
				LayerID:      "layer-invalid-tags",
				BaseRootPath: "/repo",
				Tags:         map[string]string{"invalid=key": "value"},
			},
			message: "must not contain '='",
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			_, err := c.CreateFSLayer(ctx, tt.request)
			var statusErr *apiStatusError
			if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusBadRequest {
				t.Fatalf("CreateFSLayer err=%v, want HTTP 400", err)
			}
			if !strings.Contains(statusErr.Message, tt.message) {
				t.Fatalf("CreateFSLayer message=%q, want %q", statusErr.Message, tt.message)
			}
		})
	}
}

func TestFSLayerObjectPathPreservesTrailingControlWhitespace(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-control-whitespace",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	const path = "/repo/name\t"
	if _, err := c.UploadFSLayerFile(ctx, "layer-control-whitespace", path, strings.NewReader("payload"), 7, 0, 0o644, true); err != nil {
		t.Fatalf("UploadFSLayerFile: %v", err)
	}
	entry, err := c.GetFSLayerEntry(ctx, "layer-control-whitespace", path)
	if err != nil {
		t.Fatalf("GetFSLayerEntry: %v", err)
	}
	if entry.Path != path {
		t.Fatalf("entry path = %q, want %q", entry.Path, path)
	}
	data, err := c.ReadFSLayerFile(ctx, "layer-control-whitespace", path, nil)
	if err != nil {
		t.Fatalf("ReadFSLayerFile: %v", err)
	}
	if string(data) != "payload" {
		t.Fatalf("payload = %q, want payload", data)
	}
}

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

	ctx := context.Background()
	c := newAPITestClient(ts.URL)
	layer, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:        "layer-api",
		BaseRootPath:   "/repo",
		Name:           "api layer",
		Tags:           map[string]string{"task": "api", "env": "test"},
		DurabilityMode: "restore-safe",
		ActorID:        "actor-api",
	})
	if err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if layer.LayerID != "layer-api" || layer.BaseRootPath != "/repo/" || layer.Name != "api layer" || layer.State != "active" {
		t.Fatalf("created layer = %+v", layer)
	}
	if layer.Origin != datastore.FSLayerOriginCreate || layer.RootLayerID != "layer-api" || layer.ParentLayerID != "" {
		t.Fatalf("created layer chain fields = %+v", layer)
	}
	if layer.Tags["task"] != "api" || layer.Tags["env"] != "test" {
		t.Fatalf("created layer tags = %+v", layer.Tags)
	}

	if _, err := c.GetFSLayer(ctx, "api layer"); err != nil {
		t.Fatalf("GetFSLayer by name: %v", err)
	}
	if _, err := c.GetFSLayer(ctx, "tag:task=api"); err != nil {
		t.Fatalf("GetFSLayer by tag: %v", err)
	}

	entry, err := c.UpsertFSLayerEntry(ctx, "api layer", fsLayerEntryRequest{
		Path:    "/repo/new.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("layered"),
		Mode:    0o644,
	})
	if err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}
	if entry.LayerID != "layer-api" || entry.Path != "/repo/new.txt" || entry.SizeBytes != 7 {
		t.Fatalf("entry = %+v", entry)
	}
	gotEntry, err := c.GetFSLayerEntry(ctx, "task=api", "/repo/new.txt")
	if err != nil {
		t.Fatalf("GetFSLayerEntry: %v", err)
	}
	if !bytes.Equal(gotEntry.Content, []byte("layered")) {
		t.Fatalf("entry content = %q, want layered", gotEntry.Content)
	}
	entries, err := c.DiffFSLayer(ctx, "task=api")
	if err != nil {
		t.Fatalf("DiffFSLayer: %v", err)
	}
	if len(entries) != 1 || entries[0].Path != "/repo/new.txt" || entries[0].Op != "upsert" {
		t.Fatalf("entries = %+v", entries)
	}

	checkpoint, err := c.CheckpointFSLayer(ctx, "task=api", fsLayerCheckpointRequest{
		CheckpointID: "ckpt-api",
		Label:        "restore point",
	})
	if err != nil {
		t.Fatalf("CheckpointFSLayer: %v", err)
	}
	if checkpoint.CheckpointID != "ckpt-api" || checkpoint.DurableSeq != 1 {
		t.Fatalf("checkpoint = %+v", checkpoint)
	}
	gotCheckpoint, err := c.GetFSLayerCheckpoint(ctx, "ckpt-api")
	if err != nil {
		t.Fatalf("GetFSLayerCheckpoint: %v", err)
	}
	if gotCheckpoint.LayerID != "layer-api" || gotCheckpoint.DurableSeq != 1 {
		t.Fatalf("got checkpoint = %+v", gotCheckpoint)
	}

	layers, err := c.ListFSLayers(ctx)
	if err != nil {
		t.Fatalf("ListFSLayers: %v", err)
	}
	if len(layers) != 1 || layers[0].LayerID != "layer-api" {
		t.Fatalf("layers = %+v", layers)
	}

	commit, err := c.CommitFSLayer(ctx, "tag:task=api")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" || commit.LayerID != "layer-api" || commit.Applied != 1 {
		t.Fatalf("commit = %+v", commit)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/new.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx committed file: %v", err)
	}
	if !bytes.Equal(data, []byte("layered")) {
		t.Fatalf("committed data = %q, want layered", data)
	}

	rollbackLayer, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-rollback",
		BaseRootPath: "/repo",
		Name:         "rollback layer",
		Tags:         map[string]string{"task": "rollback"},
	})
	if err != nil {
		t.Fatalf("CreateFSLayer rollback: %v", err)
	}
	if rollbackLayer.LayerID != "layer-rollback" {
		t.Fatalf("rollback layer = %+v", rollbackLayer)
	}
	if err := c.RollbackFSLayer(ctx, "task=rollback"); err != nil {
		t.Fatalf("RollbackFSLayer: %v", err)
	}
	got, err := c.GetFSLayer(ctx, "layer-rollback")
	if err != nil {
		t.Fatalf("GetFSLayer: %v", err)
	}
	if got.State != "abandoned" {
		t.Fatalf("layer state=%s, want abandoned", got.State)
	}
}

func TestFSLayerObjectUploadReadAndCommitLargeFile(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-large-object",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	payload := bytes.Repeat([]byte("x"), 2<<20)
	payload = append(payload, []byte("tail")...)
	writeBefore := tenantFileBytesMetricValue(t, "write", "db9")
	writeOpsBefore := tenantFileOpsMetricValue(t, "write", "db9")
	entry, err := c.UploadFSLayerFile(ctx, "layer-large-object", "/repo/large.bin", bytes.NewReader(payload), int64(len(payload)), 0, 0o644, true)
	if err != nil {
		t.Fatalf("UploadFSLayerFile: %v", err)
	}
	if entry.StorageType != "s3" || entry.StorageRef == "" || entry.SizeBytes != int64(len(payload)) {
		t.Fatalf("uploaded entry = %+v", entry)
	}
	requireTenantFileBytesDelta(t, writeBefore, int64(len(payload)), "write", "db9")
	requireTenantFileOpsDelta(t, writeOpsBefore, 1, "write", "db9")
	readBefore := tenantFileBytesMetricValue(t, "read", "s3")
	readOpsBefore := tenantFileOpsMetricValue(t, "read", "s3")
	got, err := c.ReadFSLayerFile(ctx, "layer-large-object", "/repo/large.bin", nil)
	if err != nil {
		t.Fatalf("ReadFSLayerFile: %v", err)
	}
	if !bytes.Equal(got, payload) {
		t.Fatalf("ReadFSLayerFile payload mismatch: got %d bytes want %d", len(got), len(payload))
	}
	requireTenantFileBytesDelta(t, readBefore, int64(len(payload)), "read", "s3")
	requireTenantFileOpsDelta(t, readOpsBefore, 1, "read", "s3")
	grep, err := c.GrepWithLayer(ctx, "tail", "/repo", 10, "layer-large-object")
	if err != nil {
		t.Fatalf("GrepWithLayer large object: %v", err)
	}
	if len(grep) != 1 || grep[0].Path != "/repo/large.bin" {
		t.Fatalf("GrepWithLayer large object = %+v, want /repo/large.bin", grep)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-large-object")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" || commit.Applied != 1 {
		t.Fatalf("commit = %+v, want committed applied=1", commit)
	}
	committed, err := s.fallback.ReadCtx(ctx, "/repo/large.bin", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx committed: %v", err)
	}
	if !bytes.Equal(committed, payload) {
		t.Fatalf("committed payload mismatch: got %d bytes want %d", len(committed), len(payload))
	}
}

func TestFSLayerCommitReplaysUpsertBeforeChmod(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-upsert-chmod",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-upsert-chmod", fsLayerEntryRequest{
		Path:    "/repo/chmod.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("layered chmod"),
		Mode:    0o644,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry upsert: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-upsert-chmod", fsLayerEntryRequest{
		Path: "/repo/chmod.txt",
		Op:   "chmod",
		Kind: "file",
		Mode: 0o600,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry chmod: %v", err)
	}
	current, err := c.DiffFSLayer(ctx, "layer-upsert-chmod")
	if err != nil {
		t.Fatalf("DiffFSLayer: %v", err)
	}
	if len(current) != 1 || current[0].Op != "chmod" {
		t.Fatalf("current diff = %+v, want latest chmod entry", current)
	}
	replay, err := c.ReplayFSLayer(ctx, "layer-upsert-chmod")
	if err != nil {
		t.Fatalf("ReplayFSLayer: %v", err)
	}
	if len(replay) != 2 || replay[0].Op != "upsert" || replay[1].Op != "chmod" {
		t.Fatalf("replay = %+v, want upsert then chmod", replay)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-upsert-chmod")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" || commit.Applied != 1 {
		t.Fatalf("commit = %+v, want committed applied=1 (folded upsert+chmod)", commit)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/chmod.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx: %v", err)
	}
	if !bytes.Equal(data, []byte("layered chmod")) {
		t.Fatalf("data = %q, want layered chmod", data)
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/chmod.txt")
	if err != nil {
		t.Fatalf("StatNodeCtx: %v", err)
	}
	if nf.File == nil || nf.File.Mode&0o777 != 0o600 {
		t.Fatalf("mode = %+v, want 0600", nf.File)
	}
}

func TestFSLayerCommitReplaysSamePathUpsertsWithoutBaseRevConflict(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-same-path-upserts",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-same-path-upserts", fsLayerEntryRequest{
		Path:    "/repo/repeated.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("first"),
		Mode:    0o644,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry first: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-same-path-upserts", fsLayerEntryRequest{
		Path:    "/repo/repeated.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("second"),
		Mode:    0o600,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry second: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-same-path-upserts")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" || commit.Applied != 1 {
		t.Fatalf("commit = %+v, want committed applied=1 (folded last upsert)", commit)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/repeated.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx: %v", err)
	}
	if !bytes.Equal(data, []byte("second")) {
		t.Fatalf("data = %q, want second", data)
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/repeated.txt")
	if err != nil {
		t.Fatalf("StatNodeCtx: %v", err)
	}
	if nf.File == nil || nf.File.Mode&0o777 != 0o600 {
		t.Fatalf("mode = %+v, want 0600", nf.File)
	}
}

func TestFSLayerCommitAllowsSupersededUpsertModeBeforeWhiteout(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-upsert-whiteout",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-upsert-whiteout", fsLayerEntryRequest{
		Path:    "/repo/temp.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("temp"),
		Mode:    0o600,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry upsert: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-upsert-whiteout", fsLayerEntryRequest{
		Path: "/repo/temp.txt",
		Op:   "whiteout",
		Kind: "file",
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry whiteout: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-upsert-whiteout")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit = %+v, want committed", commit)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/temp.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("temp stat err=%v, want ErrNotFound", err)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/move.txt", []byte("base"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write base: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-upsert-rename",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-upsert-rename", fsLayerEntryRequest{
		Path:    "/repo/move.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("layered move"),
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry upsert: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-upsert-rename", fsLayerEntryRequest{
		Path:        "/repo/move.txt",
		Op:          "rename",
		Kind:        "file",
		ContentText: "/repo/moved.txt",
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry rename: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-upsert-rename")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" || commit.Applied != 2 {
		t.Fatalf("commit = %+v, want committed applied=2", commit)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/move.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("source stat err=%v, want ErrNotFound", err)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/moved.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx moved: %v", err)
	}
	if !bytes.Equal(data, []byte("layered move")) {
		t.Fatalf("moved data = %q, want layered move", data)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-chmod-swap", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-chmod-swap", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "chmod", Kind: "file", Mode: 0o600,
	}); err != nil {
		t.Fatalf("chmod a: %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-chmod-swap", e); err != nil {
			t.Fatalf("rename %s→%s: %v", e.Path, e.ContentText, err)
		}
	}
	commit, err := c.CommitFSLayer(ctx, "layer-chmod-swap")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	gotA, err := s.fallback.ReadCtx(ctx, "/repo/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx a: %v", err)
	}
	gotB, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(gotA, []byte("B")) || !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("main a=%q b=%q, want swapped B/A", gotA, gotB)
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/b.txt")
	if err != nil {
		t.Fatalf("StatNodeCtx b: %v", err)
	}
	if nf.File == nil || nf.File.Mode&0o777 != 0o600 {
		t.Fatalf("b mode=%+v, want 0600", nf.File)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	if err := s.fallback.ChmodCtx(ctx, "/repo/b.txt", 0o600); err != nil {
		t.Fatalf("chmod live b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-chmod-swap-same", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-chmod-swap-same", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "chmod", Kind: "file", Mode: 0o600,
	}); err != nil {
		t.Fatalf("chmod a: %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-chmod-swap-same", e); err != nil {
			t.Fatalf("rename %s→%s: %v", e.Path, e.ContentText, err)
		}
	}
	commit, err := c.CommitFSLayer(ctx, "layer-chmod-swap-same")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	gotB, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("b data=%q, want A", gotB)
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/b.txt")
	if err != nil {
		t.Fatalf("StatNodeCtx b: %v", err)
	}
	if nf.File == nil || nf.File.Mode&0o777 != 0o600 {
		t.Fatalf("b mode=%+v, want 0600 (chmod of swapped-in a)", nf.File)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("aaa"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-chmod-land-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-chmod-land-parent", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-chmod-land-parent", fsLayerForkRequest{LayerID: "layer-chmod-land-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-chmod-land-parent", fsLayerEntryRequest{
		Path: "/repo/b.txt", Op: "chmod", Kind: "file", Mode: 0o600,
	}); err != nil {
		t.Fatalf("parent dest chmod: %v", err)
	}
	child, err := c.CommitFSLayer(ctx, "layer-chmod-land-child")
	if err != nil {
		t.Fatalf("child commit: %v commit=%+v", err, child)
	}
	if child.Status != "committed" {
		t.Fatalf("child=%+v, want committed", child)
	}
	parent, err := c.CommitFSLayer(ctx, "layer-chmod-land-parent")
	if err != nil {
		t.Fatalf("parent commit: %v commit=%+v", err, parent)
	}
	if parent.Status != "committed" {
		t.Fatalf("parent=%+v, want committed", parent)
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/b.txt")
	if err != nil {
		t.Fatalf("StatNodeCtx b: %v", err)
	}
	if nf.File == nil || nf.File.Mode&0o777 != 0o600 {
		t.Fatalf("b mode=%+v, want 0600", nf.File)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("aaa"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-chmod-land-occ-p", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-chmod-land-occ-p", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-chmod-land-occ-p", fsLayerForkRequest{LayerID: "layer-chmod-land-occ-c"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-chmod-land-occ-p", fsLayerEntryRequest{
		Path: "/repo/b.txt", Op: "chmod", Kind: "file", Mode: 0o600,
	}); err != nil {
		t.Fatalf("parent dest chmod: %v", err)
	}
	child, err := c.CommitFSLayer(ctx, "layer-chmod-land-occ-c")
	if err != nil {
		t.Fatalf("child commit: %v commit=%+v", err, child)
	}
	if child.Status != "committed" {
		t.Fatalf("child=%+v, want committed", child)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("external"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("external write dest: %v", err)
	}
	parent, err := c.CommitFSLayer(ctx, "layer-chmod-land-occ-p")
	if !errors.Is(err, errConflict) {
		t.Fatalf("parent commit err=%v commit=%+v, want conflict", err, parent)
	}
	if parent == nil || parent.Status != "conflicted" {
		t.Fatalf("parent=%+v, want conflicted", parent)
	}
	if len(parent.Conflicts) != 1 || parent.Conflicts[0].Path != "/repo/b.txt" || parent.Conflicts[0].Reason != "base revision changed" {
		t.Fatalf("conflicts=%+v, want base revision changed at /repo/b.txt", parent.Conflicts)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(got, []byte("external")) {
		t.Fatalf("main b=%q, want external", got)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-fold-exist-b", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-fold-exist-b", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("rename a→b: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-fold-exist-b", fsLayerEntryRequest{
		Path: "/repo/b.txt", Op: "rename", Kind: "file", ContentText: "/repo/c.txt",
	}); err != nil {
		t.Fatalf("rename b→c: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-fold-exist-b")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	gotC, err := s.fallback.ReadCtx(ctx, "/repo/c.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx c: %v", err)
	}
	if !bytes.Equal(gotC, []byte("A")) {
		t.Fatalf("c=%q, want A", gotC)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/a.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("a stat err=%v, want ErrNotFound", err)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/b.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("b stat err=%v, want ErrNotFound", err)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-rename-dest-upsert", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rename-dest-upsert", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("rename a→b: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rename-dest-upsert", fsLayerEntryRequest{
		Path: "/repo/b.txt", Op: "upsert", Kind: "file", Content: []byte("new"), SizeBytes: 3, Mode: 0o644,
	}); err != nil {
		t.Fatalf("upsert dest: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-rename-dest-upsert")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(got, []byte("new")) {
		t.Fatalf("b=%q, want new", got)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/a.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("a stat err=%v, want ErrNotFound", err)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-rename-dest-upsert-occ", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rename-dest-upsert-occ", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("rename a→b: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rename-dest-upsert-occ", fsLayerEntryRequest{
		Path: "/repo/b.txt", Op: "upsert", Kind: "file", Content: []byte("new"), SizeBytes: 3, Mode: 0o644,
	}); err != nil {
		t.Fatalf("upsert dest: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("external"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("external write a: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-rename-dest-upsert-occ")
	if !errors.Is(err, errConflict) {
		t.Fatalf("CommitFSLayer err=%v commit=%+v, want conflict", err, commit)
	}
	if commit == nil || commit.Status != "conflicted" {
		t.Fatalf("commit=%+v, want conflicted", commit)
	}
	if len(commit.Conflicts) != 1 || commit.Conflicts[0].Path != "/repo/a.txt" {
		t.Fatalf("conflicts=%+v, want /repo/a.txt", commit.Conflicts)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx a: %v", err)
	}
	if !bytes.Equal(got, []byte("external")) {
		t.Fatalf("main a=%q, want external", got)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-body-ren-p", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-body-ren-p", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-body-ren-p", fsLayerEntryRequest{
		Path: "/repo/b.txt", Op: "upsert", Kind: "file", Content: []byte("new"), SizeBytes: 3, Mode: 0o644,
	}); err != nil {
		t.Fatalf("parent dest upsert: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-body-ren-p", fsLayerForkRequest{LayerID: "layer-body-ren-c"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-body-ren-p"); err != nil {
		t.Fatalf("parent commit: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-body-ren-c", fsLayerEntryRequest{
		Path: "/repo/b.txt", Op: "rename", Kind: "file", ContentText: "/repo/c.txt",
	}); err != nil {
		t.Fatalf("child rename: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-body-ren-c")
	if err != nil {
		t.Fatalf("child commit: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/c.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx c: %v", err)
	}
	if !bytes.Equal(got, []byte("new")) {
		t.Fatalf("c=%q, want new", got)
	}
}

func TestFSLayerCommitReplaysMkdirBeforeChmod(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-mkdir-chmod",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-mkdir-chmod", fsLayerEntryRequest{
		Path: "/repo/mode-dir/",
		Op:   "mkdir",
		Kind: "dir",
		Mode: 0o755,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry mkdir: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-mkdir-chmod", fsLayerEntryRequest{
		Path: "/repo/mode-dir/",
		Op:   "chmod",
		Kind: "dir",
		Mode: 0o700,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry chmod: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-mkdir-chmod")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" || commit.Applied != 1 {
		t.Fatalf("commit = %+v, want committed applied=1 (folded mkdir+chmod)", commit)
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/mode-dir/")
	if err != nil {
		t.Fatalf("StatNodeCtx mode-dir: %v", err)
	}
	if !nf.Node.IsDirectory {
		t.Fatalf("dir node = %+v, want directory", nf.Node)
	}
}

func TestFSLayerCommitRejectsCommittingWithoutFence(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-retry",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-retry", fsLayerEntryRequest{
		Path:    "/repo/retry.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("pending"),
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}
	if err := s.fallback.Store().SetFSLayerState(ctx, "layer-retry", datastore.FSLayerStateCommitting); err != nil {
		t.Fatalf("SetFSLayerState committing: %v", err)
	}
	if commit, err := c.CommitFSLayer(ctx, "layer-retry"); !errors.Is(err, errConflict) {
		t.Fatalf("CommitFSLayer committing commit=%+v err=%v, want conflict", commit, err)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("MkdirCtx old: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/child.txt", []byte("child"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write child: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-dir-rename",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-rename", fsLayerEntryRequest{
		Path:        "/repo/old/",
		Op:          "rename",
		Kind:        "dir",
		ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry rename dir: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-dir-rename")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" || commit.Applied != 1 {
		t.Fatalf("commit = %+v, want committed applied=1", commit)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/old/child.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("old child stat err=%v, want ErrNotFound", err)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/new/child.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx new child: %v", err)
	}
	if !bytes.Equal(got, []byte("child")) {
		t.Fatalf("new child content = %q, want child", got)
	}
}

func TestFSLayerSearchOverlayMergesLayerEntries(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/base.txt", []byte("needle base"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write base: %v", err)
	}
	if err := s.fallback.Store().CreateFSLayer(ctx, &datastore.FSLayer{
		LayerID:      "layer-search",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if err := s.fallback.Store().UpsertFSLayerEntry(ctx, &datastore.FSLayerEntry{
		LayerID:     "layer-search",
		Path:        "/repo/base.txt",
		Op:          datastore.FSLayerEntryOpUpsert,
		Kind:        datastore.FSLayerEntryKindFile,
		ContentBlob: []byte("overlay without match"),
		SizeBytes:   int64(len("overlay without match")),
	}); err != nil {
		t.Fatalf("Upsert base overlay: %v", err)
	}
	if err := s.fallback.Store().UpsertFSLayerEntry(ctx, &datastore.FSLayerEntry{
		LayerID:     "layer-search",
		Path:        "/repo/layer.txt",
		Op:          datastore.FSLayerEntryOpUpsert,
		Kind:        datastore.FSLayerEntryKindFile,
		ContentBlob: []byte("needle layer"),
		SizeBytes:   int64(len("needle layer")),
	}); err != nil {
		t.Fatalf("Upsert layer file: %v", err)
	}
	base := []datastore.SearchResult{{Path: "/repo/base.txt", Name: "base.txt", SizeBytes: 11}}
	grep, err := overlayFSLayerGrep(ctx, s.fallback, "layer-search", "needle", "/repo/", 20, base)
	if err != nil {
		t.Fatalf("overlayFSLayerGrep: %v", err)
	}
	if len(grep) != 1 || grep[0].Path != "/repo/layer.txt" {
		t.Fatalf("grep = %+v, want only layer file", grep)
	}
	find, err := overlayFSLayerFind(ctx, s.fallback, "layer-search", &datastore.FindFilter{PathPrefix: "/repo/"}, base)
	if err != nil {
		t.Fatalf("overlayFSLayerFind: %v", err)
	}
	gotFind := map[string]bool{}
	for _, result := range find {
		gotFind[result.Path] = true
	}
	if len(find) != 2 || !gotFind["/repo/base.txt"] || !gotFind["/repo/layer.txt"] {
		t.Fatalf("find = %+v, want overlaid base and layer file", find)
	}
}

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

	ctx := context.Background()
	req, err := http.NewRequest(http.MethodPut, ts.URL+"/v1/fs/repo/delete-dir/gone.txt", bytes.NewReader([]byte("gone")))
	if err != nil {
		t.Fatal(err)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	_ = resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("write baseline file status=%d, want 200", resp.StatusCode)
	}

	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-dir-whiteout",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-whiteout", fsLayerEntryRequest{
		Path: "/repo/delete-dir/",
		Op:   "whiteout",
		Kind: "dir",
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry dir whiteout: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-dir-whiteout")
	if err == nil {
		t.Fatalf("CommitFSLayer err=nil commit=%+v, want conflict", commit)
	}
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("CommitFSLayer err=%v, want conflict", err)
	}
	if commit == nil || len(commit.Conflicts) != 1 || commit.Conflicts[0].Reason != "directory whiteout requires empty directory" {
		t.Fatalf("commit conflict = %+v", commit)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/delete-dir/gone.txt"); err != nil {
		t.Fatalf("baseline child should remain, stat err=%v", err)
	}
}

func TestFSLayerCommitAllowsEmptyDirectoryWhiteout(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-whiteout",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-empty-whiteout", fsLayerEntryRequest{
		Path: "/repo/empty/",
		Op:   "whiteout",
		Kind: "dir",
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry dir whiteout: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-empty-whiteout")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" || commit.Applied != 1 {
		t.Fatalf("commit = %+v, want committed applied=1", commit)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/empty/"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("deleted dir stat err=%v, want ErrNotFound", err)
	}
}

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

	c := newAPITestClient(ts.URL)
	if _, err := c.DiffFSLayer(context.Background(), "missing"); !apiIsNotFound(err) {
		t.Fatalf("DiffFSLayer missing err=%v, want not found", err)
	}
}

func TestFSLayerRejectsEntryOutsideBaseRoot(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-scope",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	_, err := c.UpsertFSLayerEntry(ctx, "layer-scope", fsLayerEntryRequest{
		Path:    "/other/owned.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("owned"),
	})
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusBadRequest {
		t.Fatalf("UpsertFSLayerEntry outside root err=%v, want 400", err)
	}
}

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

	ctx := context.Background()
	c := newAPITestClient(ts.URL)
	const layerID = "layer-inline-storage-facts"
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      layerID,
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	payload := []byte("billing-storage")
	entry, err := c.UpsertFSLayerEntry(ctx, layerID, fsLayerEntryRequest{
		Path:           "/repo/data.bin",
		Op:             "upsert",
		Kind:           "file",
		Content:        payload,
		StorageType:    "s3",
		StorageRef:     "layers/" + layerID + "/client-supplied",
		StorageRefHash: "client-supplied-hash",
		SizeBytes:      1,
	})
	if err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}
	if entry.StorageType != string(datastore.StorageDB9) {
		t.Fatalf("storage_type=%q, want db9", entry.StorageType)
	}
	if entry.SizeBytes != int64(len(payload)) {
		t.Fatalf("size_bytes=%d, want %d", entry.SizeBytes, len(payload))
	}
	if entry.StorageRef != "" || entry.StorageRefHash != "" {
		t.Fatalf("storage_ref=%q storage_ref_hash=%q, want inline storage without object reference", entry.StorageRef, entry.StorageRefHash)
	}
}

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

	ctx := context.Background()
	c := newAPITestClient(ts.URL)
	const layerID = "layer-client-object-metadata"
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      layerID,
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	_, err := c.UpsertFSLayerEntry(ctx, layerID, fsLayerEntryRequest{
		Path:        "/repo/data.bin",
		Op:          "upsert",
		Kind:        "file",
		StorageType: "s3",
		StorageRef:  "layers/" + layerID + "/client-supplied",
		SizeBytes:   1024,
	})
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusBadRequest {
		t.Fatalf("UpsertFSLayerEntry client object metadata err=%v, want HTTP 400", err)
	}
}

func TestFSLayerCreateRequiresScopedWriteOnBaseRoot(t *testing.T) {
	s := newTestServer(t)
	req := httptest.NewRequest(http.MethodPost, "/v1/layers", bytes.NewReader([]byte(`{"layer_id":"layer-denied","base_root_path":"/repo"}`)))
	scope := &TenantScope{
		IsScoped: true,
		FSScopes: []FSScope{{
			Prefix: "/other",
			Ops:    map[FSOp]bool{FSOpRead: true, FSOpWrite: true},
		}},
	}
	req = req.WithContext(withScope(req.Context(), scope))
	rr := httptest.NewRecorder()
	s.handleFSLayerCreate(rr, req, s.fallback.Store())
	if rr.Code != http.StatusForbidden {
		t.Fatalf("status=%d body=%s, want 403", rr.Code, rr.Body.String())
	}
	if _, err := s.fallback.Store().GetFSLayer(context.Background(), "layer-denied"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("layer-denied err=%v, want ErrNotFound", err)
	}
}

func TestFSLayerEntryMutationRequiresScopedPathPermission(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if err := s.fallback.Store().CreateFSLayer(ctx, &datastore.FSLayer{
		LayerID:      "layer-auth",
		BaseRootPath: "/",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	layer, err := s.fallback.Store().GetFSLayer(ctx, "layer-auth")
	if err != nil {
		t.Fatalf("GetFSLayer: %v", err)
	}
	req := httptest.NewRequest(http.MethodPost, "/v1/layers/layer-auth/entries", bytes.NewReader([]byte(`{
		"path": "/repo/source.txt",
		"op": "rename",
		"kind": "file",
		"content_text": "/other/target.txt"
	}`)))
	scope := &TenantScope{
		IsScoped: true,
		FSScopes: []FSScope{{
			Prefix: "/repo",
			Ops:    map[FSOp]bool{FSOpRead: true, FSOpWrite: true, FSOpDelete: true},
		}},
	}
	req = req.WithContext(withScope(req.Context(), scope))
	rr := httptest.NewRecorder()
	s.handleFSLayerEntryUpsert(rr, req, s.fallback, s.fallback.Store(), layer)
	if rr.Code != http.StatusForbidden {
		t.Fatalf("status=%d body=%s, want 403", rr.Code, rr.Body.String())
	}
	entries, err := s.fallback.Store().ListFSLayerEntryLog(ctx, "layer-auth")
	if err != nil {
		t.Fatalf("ListFSLayerEntryLog: %v", err)
	}
	if len(entries) != 0 {
		t.Fatalf("entries=%+v, want none after denied mutation", entries)
	}
}

func TestFSLayerAcceptsDirectoryRenameEntry(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-dir-rename",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	entry, err := c.UpsertFSLayerEntry(ctx, "layer-dir-rename", fsLayerEntryRequest{
		Path:        "/repo/old-dir/",
		Op:          "rename",
		Kind:        "dir",
		ContentText: "/repo/new-dir/",
	})
	if err != nil {
		t.Fatalf("UpsertFSLayerEntry directory rename: %v", err)
	}
	if entry.Path != "/repo/old-dir/" || entry.ContentText != "/repo/new-dir/" {
		t.Fatalf("directory rename entry path=%q target=%q", entry.Path, entry.ContentText)
	}
}

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

	ctx := context.Background()
	if _, err := s.fallback.WriteCtx(ctx, "/repo/source.txt", []byte("source"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate); err != nil {
		t.Fatalf("write source: %v", err)
	}
	if _, err := s.fallback.WriteCtx(ctx, "/repo/target.txt", []byte("target"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate); err != nil {
		t.Fatalf("write target: %v", err)
	}

	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-rename-conflict",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rename-conflict", fsLayerEntryRequest{
		Path:        "/repo/source.txt",
		Op:          "rename",
		Kind:        "file",
		ContentText: "/repo/target.txt",
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry rename: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-rename-conflict")
	if !errors.Is(err, errConflict) {
		t.Fatalf("CommitFSLayer err=%v, want conflict", err)
	}
	if commit == nil || len(commit.Conflicts) != 1 || commit.Conflicts[0].Path != "/repo/target.txt" || commit.Conflicts[0].Reason != "rename target exists" {
		t.Fatalf("commit conflicts = %+v, want rename target exists", commit)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/target.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx target: %v", err)
	}
	if !bytes.Equal(data, []byte("target")) {
		t.Fatalf("target data = %q, want target", data)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/conflict.txt", []byte("base-v1"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write base-v1: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-stale-base",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	entry, err := c.UpsertFSLayerEntry(ctx, "layer-stale-base", fsLayerEntryRequest{
		Path:    "/repo/conflict.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("layer-edit"),
		Mode:    0o644,
	})
	if err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}
	if entry.BaseRevision <= 0 || entry.BaseInodeID == "" {
		t.Fatalf("copy-up snapshot rev=%d inode=%q, want stamped from main", entry.BaseRevision, entry.BaseInodeID)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/conflict.txt", []byte("main-v2"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main-v2: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-stale-base")
	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 revision changed" || commit.Conflicts[0].Path != "/repo/conflict.txt" {
		t.Fatalf("conflicts = %+v, want base revision changed", commit.Conflicts)
	}
	layer, err := c.GetFSLayer(ctx, "layer-stale-base")
	if err != nil {
		t.Fatalf("GetFSLayer: %v", err)
	}
	if layer.State != "conflicted" {
		t.Fatalf("layer state=%s, want conflicted", layer.State)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/conflict.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx: %v", err)
	}
	if !bytes.Equal(data, []byte("main-v2")) {
		t.Fatalf("main data = %q, want main-v2", data)
	}
}

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

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/inode.txt", []byte("base-v1"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write base-v1: %v", err)
	}
	before, err := s.fallback.StatNodeCtx(ctx, "/repo/inode.txt")
	if err != nil || before.File == nil {
		t.Fatalf("stat before: nf=%+v err=%v", before, err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-stale-inode", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	entry, err := c.UpsertFSLayerEntry(ctx, "layer-stale-inode", fsLayerEntryRequest{
		Path: "/repo/inode.txt", Op: "upsert", Kind: "file", Content: []byte("layer-edit"), Mode: 0o644,
	})
	if err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}
	if entry.BaseInodeID != before.File.FileID || entry.BaseRevision != before.File.Revision {
		t.Fatalf("copy-up snapshot inode=%q rev=%d, want %q/%d", entry.BaseInodeID, entry.BaseRevision, before.File.FileID, before.File.Revision)
	}
	replaceMainFileAtPath(t, s.fallback, "/repo/inode.txt", []byte("replacement"))
	after, err := s.fallback.StatNodeCtx(ctx, "/repo/inode.txt")
	if err != nil || after.File == nil {
		t.Fatalf("stat after: nf=%+v err=%v", after, err)
	}
	if after.File.FileID == before.File.FileID {
		t.Fatalf("recreate kept file_id %q", after.File.FileID)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-stale-inode")
	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 != "/repo/inode.txt" {
		t.Fatalf("conflicts = %+v, want base inode changed", commit.Conflicts)
	}
	layer, err := c.GetFSLayer(ctx, "layer-stale-inode")
	if err != nil {
		t.Fatalf("GetFSLayer: %v", err)
	}
	if layer.State != "conflicted" {
		t.Fatalf("layer state=%s, want conflicted", layer.State)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/inode.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx: %v", err)
	}
	if !bytes.Equal(data, []byte("replacement")) {
		t.Fatalf("main data = %q, want replacement", data)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/cas.txt", []byte("base-v1"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write base-v1: %v", err)
	}
	before, err := s.fallback.StatNodeCtx(ctx, "/repo/cas.txt")
	if err != nil || before.File == nil {
		t.Fatalf("stat before: nf=%+v err=%v", before, err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-client-rev-inode", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	entry, err := c.UpsertFSLayerEntry(ctx, "layer-client-rev-inode", fsLayerEntryRequest{
		Path: "/repo/cas.txt", Op: "upsert", Kind: "file", Content: []byte("layer-edit"), Mode: 0o644,
		BaseRevision: before.File.Revision,
	})
	if err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}
	if entry.BaseInodeID != before.File.FileID || entry.BaseRevision != before.File.Revision {
		t.Fatalf("stamped snapshot inode=%q rev=%d, want %q/%d", entry.BaseInodeID, entry.BaseRevision, before.File.FileID, before.File.Revision)
	}
	replaceMainFileAtPath(t, s.fallback, "/repo/cas.txt", []byte("replacement"))
	commit, err := c.CommitFSLayer(ctx, "layer-client-rev-inode")
	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 != "/repo/cas.txt" {
		t.Fatalf("conflicts = %+v, want base inode changed", commit.Conflicts)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/cas.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx: %v", err)
	}
	if !bytes.Equal(data, []byte("replacement")) {
		t.Fatalf("main data = %q, want replacement", data)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/obj.bin", []byte("base-v1"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write base-v1: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-stale-inode-obj", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	payload := bytes.Repeat([]byte("x"), 2<<20)
	entry, err := c.UploadFSLayerFile(ctx, "layer-stale-inode-obj", "/repo/obj.bin", bytes.NewReader(payload), int64(len(payload)), 0, 0o644, true)
	if err != nil {
		t.Fatalf("UploadFSLayerFile: %v", err)
	}
	if entry.StorageType != "s3" || entry.BaseInodeID == "" || entry.BaseRevision <= 0 {
		t.Fatalf("uploaded entry = %+v, want s3 copy-up snapshot", entry)
	}
	replaceMainFileAtPath(t, s.fallback, "/repo/obj.bin", []byte("replacement"))
	commit, err := c.CommitFSLayer(ctx, "layer-stale-inode-obj")
	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 != "/repo/obj.bin" {
		t.Fatalf("conflicts = %+v, want base inode changed", commit.Conflicts)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/obj.bin", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx: %v", err)
	}
	if !bytes.Equal(data, []byte("replacement")) {
		t.Fatalf("main data = %q, want replacement", data)
	}
}

func TestFSLayerCommitConflictWhenNewPathAppearsOnMain(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-new-exists",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-new-exists", fsLayerEntryRequest{
		Path:    "/repo/new.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("layer-new"),
		Mode:    0o644,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/new.txt", []byte("main-created"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main-created: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-new-exists")
	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 path exists" || commit.Conflicts[0].Path != "/repo/new.txt" {
		t.Fatalf("conflicts = %+v, want base path exists", commit.Conflicts)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/new.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx: %v", err)
	}
	if !bytes.Equal(data, []byte("main-created")) {
		t.Fatalf("main data = %q, want main-created", data)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/gone.txt", []byte("base-v1"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write base-v1: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-stale-whiteout",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-stale-whiteout", fsLayerEntryRequest{
		Path: "/repo/gone.txt",
		Op:   "whiteout",
		Kind: "file",
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry whiteout: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/gone.txt", []byte("main-v2"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main-v2: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-stale-whiteout")
	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 revision changed" || commit.Conflicts[0].Path != "/repo/gone.txt" {
		t.Fatalf("conflicts = %+v, want base revision changed", commit.Conflicts)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/gone.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx: %v", err)
	}
	if !bytes.Equal(data, []byte("main-v2")) {
		t.Fatalf("main data = %q, want main-v2", data)
	}
}

func TestFSLayerCommitConflictWhenWhiteoutPathAppearsOnMain(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-whiteout-exists",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-whiteout-exists", fsLayerEntryRequest{
		Path: "/repo/later.txt",
		Op:   "whiteout",
		Kind: "file",
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry whiteout: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/later.txt", []byte("main-created"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main-created: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-whiteout-exists")
	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 path exists" || commit.Conflicts[0].Path != "/repo/later.txt" {
		t.Fatalf("conflicts = %+v, want base path exists", commit.Conflicts)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/later.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx: %v", err)
	}
	if !bytes.Equal(data, []byte("main-created")) {
		t.Fatalf("main data = %q, want main-created", data)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/move.txt", []byte("base-v1"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write base-v1: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-upsert-rename-occ",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-upsert-rename-occ", fsLayerEntryRequest{
		Path:    "/repo/move.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("layer-edit"),
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry upsert: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/move.txt", []byte("main-v2"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main-v2: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-upsert-rename-occ", fsLayerEntryRequest{
		Path:        "/repo/move.txt",
		Op:          "rename",
		Kind:        "file",
		ContentText: "/repo/moved.txt",
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry rename: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-upsert-rename-occ")
	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) == 0 {
		t.Fatalf("conflicts empty, want base revision changed")
	}
	found := false
	for _, cfl := range commit.Conflicts {
		if cfl.Path == "/repo/move.txt" && cfl.Reason == "base revision changed" {
			found = true
			break
		}
	}
	if !found {
		t.Fatalf("conflicts = %+v, want /repo/move.txt base revision changed", commit.Conflicts)
	}
	data, err := s.fallback.ReadCtx(ctx, "/repo/move.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx: %v", err)
	}
	if !bytes.Equal(data, []byte("main-v2")) {
		t.Fatalf("main data = %q, want main-v2", data)
	}
}

func TestFSLayerCommitRollsBackAppliedEntriesOnFailure(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-rollback-commit",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-rollback-commit", fsLayerEntryRequest{
		Path:    "/repo/first.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("first"),
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry first: %v", err)
	}
	missingObjectEntry := datastore.FSLayerEntry{
		LayerID:    "layer-rollback-commit",
		Path:       "/repo/second.txt",
		Op:         datastore.FSLayerEntryOpUpsert,
		Kind:       datastore.FSLayerEntryKindFile,
		StorageRef: "layers/layer-rollback-commit/missing-object",
	}
	if err := s.fallback.Store().UpsertFSLayerEntry(ctx, &missingObjectEntry); err != nil {
		t.Fatalf("inject missing object entry: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-rollback-commit"); err == nil {
		t.Fatal("CommitFSLayer unexpectedly succeeded")
	}
	if _, err := s.fallback.ReadCtx(ctx, "/repo/first.txt", 0, -1); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("first entry after failed commit err=%v, want ErrNotFound", err)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("auto-created parent after failed commit err=%v, want ErrNotFound", err)
	}
	layer, err := c.GetFSLayer(ctx, "layer-rollback-commit")
	if err != nil {
		t.Fatalf("GetFSLayer: %v", err)
	}
	if layer.State != "conflicted" {
		t.Fatalf("layer state=%s, want conflicted", layer.State)
	}
}

func TestFSLayerCommitPreflightChecksBaseInodeID(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo", 0o755); err != nil {
		t.Fatalf("MkdirCtx: %v", err)
	}
	conflicts := preflightFSLayerCommit(ctx, s.fallback, []datastore.FSLayerEntry{{
		Path:        "/repo/",
		Op:          datastore.FSLayerEntryOpChmod,
		Kind:        datastore.FSLayerEntryKindDir,
		BaseInodeID: "stale-inode",
		Mode:        0o700,
	}}, false)
	if len(conflicts) != 1 || conflicts[0].Reason != "base inode changed" {
		t.Fatalf("conflicts=%+v, want base inode changed", conflicts)
	}
}

func TestValidateFSLayerCommitSnapshotsRejectsIncomplete(t *testing.T) {
	err := validateFSLayerCommitSnapshots([]fsLayerBaseSnapshot{{
		Path:       "/repo/a.txt",
		SnapshotOK: false,
	}})
	if err == nil {
		t.Fatal("validateFSLayerCommitSnapshots succeeded, want error")
	}
}

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

	putBase := func(body string) {
		req, _ := http.NewRequest(http.MethodPut, ts.URL+"/v1/fs/base.txt", strings.NewReader(body))
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			t.Fatal(err)
		}
		_ = resp.Body.Close()
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("write base: %d", resp.StatusCode)
		}
	}
	upsertEntry := func(baseRevision int64) int {
		payload := fmt.Sprintf(`{"path":"/base.txt","op":"upsert","kind":"file","base_revision":%d,"content":"bGF5ZXIgZGF0YQ=="}`, baseRevision)
		req, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/layers/layer-cas/entries", strings.NewReader(payload))
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			t.Fatal(err)
		}
		_ = resp.Body.Close()
		return resp.StatusCode
	}

	putBase("v1")
	req, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/layers", strings.NewReader(`{"layer_id":"layer-cas","base_root_path":"/"}`))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	_ = resp.Body.Close()
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
		t.Fatalf("layer create: %d", resp.StatusCode)
	}

	// Upsert against the current base revision is accepted.
	if code := upsertEntry(1); code != http.StatusOK {
		t.Fatalf("upsert with current base revision: %d, want 200", code)
	}

	// Base moves to revision 2; the stale base_revision=1 upsert must be
	// rejected synchronously (no silent splice into the layer).
	putBase("v2")
	if code := upsertEntry(1); code != http.StatusConflict {
		t.Fatalf("upsert with stale base revision: %d, want 409", code)
	}

	// The fresh base revision is accepted again.
	if code := upsertEntry(2); code != http.StatusOK {
		t.Fatalf("upsert with fresh base revision: %d, want 200", code)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/f.txt", []byte("v1"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write v1: %v", err)
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/old/f.txt")
	if err != nil || nf.File == nil {
		t.Fatalf("stat old/f: nf=%+v err=%v", nf, err)
	}
	rev := nf.File.Revision
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-dest-cas", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dest-cas", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dest-cas", fsLayerEntryRequest{
		Path: "/repo/new/f.txt", Op: "upsert", Kind: "file", Content: []byte("ok"), SizeBytes: 2, Mode: 0o644, BaseRevision: rev,
	}); err != nil {
		t.Fatalf("dest-space upsert with current remapped rev: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/f.txt", []byte("v2"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write v2: %v", err)
	}
	_, err = c.UpsertFSLayerEntry(ctx, "layer-dest-cas", fsLayerEntryRequest{
		Path: "/repo/new/f.txt", Op: "upsert", Kind: "file", Content: []byte("stale"), SizeBytes: 5, Mode: 0o644, BaseRevision: rev,
	})
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("stale dest-space CAS err=%v, want 409", err)
	}
}

func TestFSLayerForkChainDeleteAPI(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-fork-parent",
		BaseRootPath: "/repo",
		Name:         "parent",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-fork-parent", fsLayerEntryRequest{
		Path:    "/repo/a.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("hello"),
		Mode:    0o644,
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}
	child, err := c.ForkFSLayer(ctx, "layer-fork-parent", fsLayerForkRequest{
		LayerID: "layer-fork-child",
		Name:    "child",
	})
	if err != nil {
		t.Fatalf("ForkFSLayer: %v", err)
	}
	if child.ParentLayerID != "layer-fork-parent" || child.Origin != datastore.FSLayerOriginFork || child.OriginSeq != 1 {
		t.Fatalf("child = %+v", child)
	}
	chain, err := c.GetFSLayerChain(ctx, "layer-fork-child")
	if err != nil {
		t.Fatalf("GetFSLayerChain: %v", err)
	}
	if len(chain) != 2 {
		t.Fatalf("chain len=%d, want 2", len(chain))
	}
	diff, err := c.DiffFSLayer(ctx, "layer-fork-child")
	if err != nil {
		t.Fatalf("DiffFSLayer child: %v", err)
	}
	if len(diff) != 1 || diff[0].Path != "/repo/a.txt" {
		t.Fatalf("child diff = %+v, want inherited /repo/a.txt", diff)
	}
	if err := c.DeleteFSLayer(ctx, "layer-fork-parent", false); err == nil {
		t.Fatal("delete parent without cascade: want error")
	} else if !errors.Is(err, errConflict) {
		t.Fatalf("delete parent without cascade err=%v, want conflict", err)
	}
	if err := c.DeleteFSLayer(ctx, "layer-fork-parent", true); err != nil {
		t.Fatalf("cascade delete: %v", err)
	}
	got, err := c.GetFSLayer(ctx, "layer-fork-child")
	if err != nil {
		t.Fatalf("GetFSLayer child: %v", err)
	}
	if got.State != "abandoned" {
		t.Fatalf("child state=%s, want abandoned", got.State)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/base.txt", []byte("from-main"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-obj-main-parent",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-obj-main-parent", fsLayerForkRequest{
		LayerID: "layer-obj-main-child",
	}); err != nil {
		t.Fatalf("ForkFSLayer: %v", err)
	}
	readBefore := tenantFileBytesMetricValue(t, "read", "db9")
	got, err := c.ReadFSLayerFile(ctx, "layer-obj-main-child", "/repo/base.txt", nil)
	if err != nil {
		t.Fatalf("ReadFSLayerFile child main fallback: %v", err)
	}
	if !bytes.Equal(got, []byte("from-main")) {
		t.Fatalf("child read = %q, want from-main", got)
	}
	requireTenantFileBytesDelta(t, readBefore, int64(len("from-main")), "read", "db9")
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-obj-main-child", fsLayerEntryRequest{
		Path: "/repo/base.txt",
		Op:   "whiteout",
		Kind: "file",
	}); err != nil {
		t.Fatalf("whiteout: %v", err)
	}
	if _, err := c.ReadFSLayerFile(ctx, "layer-obj-main-child", "/repo/base.txt", nil); err == nil {
		t.Fatal("whiteout read err=nil, want not found")
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/only-main.txt", []byte("root-main"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-obj-root-fallback",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	readBefore := tenantFileBytesMetricValue(t, "read", "db9")
	got, err := c.ReadFSLayerFile(ctx, "layer-obj-root-fallback", "/repo/only-main.txt", nil)
	if err != nil {
		t.Fatalf("ReadFSLayerFile root main fallback: %v", err)
	}
	if !bytes.Equal(got, []byte("root-main")) {
		t.Fatalf("root read = %q, want root-main", got)
	}
	requireTenantFileBytesDelta(t, readBefore, int64(len("root-main")), "read", "db9")
}

func TestFSLayerChildObjectReadHonorsExplicitZeroMaxSeq(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-obj-parent",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-obj-parent", fsLayerEntryRequest{
		Path:    "/repo/a.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("v1"),
		Mode:    0o644,
	}); err != nil {
		t.Fatalf("parent upsert: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-obj-parent", fsLayerForkRequest{
		LayerID: "layer-obj-child",
	}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.CheckpointFSLayer(ctx, "layer-obj-child", fsLayerCheckpointRequest{
		CheckpointID: "cp-child-zero",
	}); err != nil {
		t.Fatalf("checkpoint: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-obj-child", fsLayerEntryRequest{
		Path:    "/repo/a.txt",
		Op:      "upsert",
		Kind:    "file",
		Content: []byte("v2"),
		Mode:    0o644,
	}); err != nil {
		t.Fatalf("child upsert: %v", err)
	}
	zero := int64(0)
	got, err := c.ReadFSLayerFile(ctx, "layer-obj-child", "/repo/a.txt", &zero)
	if err != nil {
		t.Fatalf("ReadFSLayerFile max_seq=0: %v", err)
	}
	if !bytes.Equal(got, []byte("v1")) {
		t.Fatalf("max_seq=0 content=%q, want v1 (parent pin, not later child write)", got)
	}
	tip, err := c.ReadFSLayerFile(ctx, "layer-obj-child", "/repo/a.txt", nil)
	if err != nil {
		t.Fatalf("ReadFSLayerFile tip: %v", err)
	}
	if !bytes.Equal(tip, []byte("v2")) {
		t.Fatalf("tip content=%q, want v2", tip)
	}
}

func TestFSLayerOverlayMatchesMainRejectsSizeOnlyEquality(t *testing.T) {
	e := &datastore.FSLayerEntry{
		Op:          datastore.FSLayerEntryOpUpsert,
		Kind:        datastore.FSLayerEntryKindFile,
		ContentBlob: []byte("aaaa"),
		SizeBytes:   4,
		Mode:        0o644,
	}
	nf := &datastore.NodeWithFile{
		File: &datastore.File{SizeBytes: 4, Mode: 0o644},
	}
	if fsLayerOverlayMatchesMain(e, nf) {
		t.Fatal("size-only inline blob must not match main")
	}
}

func TestPlanFSLayerCommitVsMainKeepsFileRecreateAfterRenameVacates(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("hello"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a.txt: %v", err)
	}
	overlay := map[string]datastore.FSLayerOverlayNode{
		"/repo/b.txt": {
			Path:       "/repo/b.txt",
			RenameFrom: "/repo/a.txt",
			Kind:       datastore.FSLayerEntryKindFile,
			Entry: datastore.FSLayerEntry{
				Path:        "/repo/a.txt",
				Op:          datastore.FSLayerEntryOpRename,
				Kind:        datastore.FSLayerEntryKindFile,
				ContentText: "/repo/b.txt",
			},
		},
		"/repo/a.txt": {
			Path:    "/repo/a.txt",
			HasBody: true,
			Kind:    datastore.FSLayerEntryKindFile,
			Mode:    0o644,
			Entry: datastore.FSLayerEntry{
				Path:           "/repo/a.txt",
				Op:             datastore.FSLayerEntryOpUpsert,
				Kind:           datastore.FSLayerEntryKindFile,
				ContentBlob:    []byte("hello"),
				ChecksumSHA256: "unused-equal-size-must-not-matter",
				SizeBytes:      5,
				Mode:           0o644,
			},
		},
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/a.txt")
	if err != nil {
		t.Fatalf("stat a.txt: %v", err)
	}
	if nf.File != nil {
		n := overlay["/repo/a.txt"]
		n.Entry.ChecksumSHA256 = nf.File.ChecksumSHA256
		overlay["/repo/a.txt"] = n
	}
	entries, err := planFSLayerCommitVsMain(ctx, s.fallback, overlay, nil)
	if err != nil {
		t.Fatalf("planFSLayerCommitVsMain: %v", err)
	}
	var sawRename, sawUpsert bool
	for _, e := range entries {
		if e.Op == datastore.FSLayerEntryOpRename && e.Path == "/repo/a.txt" {
			sawRename = true
		}
		if e.Op == datastore.FSLayerEntryOpUpsert && e.Path == "/repo/a.txt" {
			sawUpsert = true
		}
	}
	if !sawRename || !sawUpsert {
		t.Fatalf("entries=%+v, want rename plus identical-body recreate", entries)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("MkdirCtx /repo/: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID:      "layer-parent-dir",
		BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-parent-dir", fsLayerEntryRequest{
		Path: "/repo/old/",
		Op:   "mkdir",
		Kind: "dir",
		Mode: 0o755,
	}); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-parent-dir", fsLayerEntryRequest{
		Path:      "/repo/old/child.txt",
		Op:        "upsert",
		Kind:      "file",
		Content:   []byte("child"),
		SizeBytes: 5,
		Mode:      0o644,
	}); err != nil {
		t.Fatalf("upsert child: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-parent-dir", fsLayerForkRequest{
		LayerID: "layer-child-dir",
		Name:    "child",
	}); err != nil {
		t.Fatalf("ForkFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-child-dir", fsLayerEntryRequest{
		Path:        "/repo/old/",
		Op:          "rename",
		Kind:        "dir",
		ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename dir: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-child-dir")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit = %+v, want committed", commit)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/old/child.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("old child stat err=%v, want ErrNotFound", err)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/new/child.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx new child: %v", err)
	}
	if !bytes.Equal(got, []byte("child")) {
		t.Fatalf("new child content = %q, want child", got)
	}
}

func TestFSLayerDirectUploadAPICompletesBeforePublishingAndRedirectsRead(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)
	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	const (
		layerID = "layer-direct-api"
		path    = "/repo/large.bin"
	)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: layerID, BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create layer: %v", err)
	}
	payload := make([]byte, s3client.PartSize+257)
	for i := range payload {
		payload[i] = byte(i % 251)
	}
	plan, err := c.InitiateFSLayerUpload(ctx, layerID, path, int64(len(payload)), 0, 0o640)
	if err != nil {
		t.Fatalf("initiate direct layer upload: %v", err)
	}
	if plan.TotalParts != 2 {
		t.Fatalf("total_parts=%d, want 2", plan.TotalParts)
	}
	if _, err := c.GetFSLayerEntry(ctx, layerID, path); !apiIsNotFound(err) {
		t.Fatalf("entry before complete err=%v, want HTTP 404", err)
	}

	first, err := c.PresignFSLayerUploadPart(ctx, layerID, plan.UploadID, 1)
	if err != nil {
		t.Fatalf("presign first part: %v", err)
	}
	rest, err := c.PresignFSLayerUploadParts(ctx, layerID, plan.UploadID, []backend.PresignPartEntry{{PartNumber: 2}})
	if err != nil {
		t.Fatalf("presign remaining parts: %v", err)
	}
	urls := append([]s3client.UploadPartURL{*first}, rest...)
	completed := make([]backend.CompletePart, 0, len(urls))
	for _, part := range urls {
		start := int64(part.Number-1) * plan.PartSize
		end := start + part.Size
		req, err := http.NewRequestWithContext(ctx, http.MethodPut, ts.URL+part.URL, bytes.NewReader(payload[start:end]))
		if err != nil {
			t.Fatalf("new part %d request: %v", part.Number, err)
		}
		for name, value := range part.Headers {
			req.Header.Set(name, value)
		}
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			t.Fatalf("upload part %d: %v", part.Number, err)
		}
		_ = resp.Body.Close()
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("upload part %d status=%d", part.Number, resp.StatusCode)
		}
		completed = append(completed, backend.CompletePart{Number: part.Number, ETag: resp.Header.Get("ETag")})
	}
	digest := sha256.Sum256(payload)
	writeBefore := tenantFileBytesMetricValue(t, "write", "s3")
	writeOpsBefore := tenantFileOpsMetricValue(t, "write", "s3")
	entry, err := c.CompleteFSLayerUpload(ctx, layerID, plan.UploadID, completed, hex.EncodeToString(digest[:]))
	if err != nil {
		t.Fatalf("complete direct layer upload: %v", err)
	}
	if entry.StorageType != string(datastore.StorageS3) || entry.StorageRef != plan.Key || entry.EntrySeq == 0 {
		t.Fatalf("completed entry=%+v, want published s3 entry", entry)
	}
	requireTenantFileBytesDelta(t, writeBefore, int64(len(payload)), "write", "s3")
	requireTenantFileOpsDelta(t, writeOpsBefore, 1, "write", "s3")
	retryWriteBefore := tenantFileBytesMetricValue(t, "write", "s3")
	retryWriteOpsBefore := tenantFileOpsMetricValue(t, "write", "s3")
	retried, err := c.CompleteFSLayerUpload(ctx, layerID, plan.UploadID, completed, hex.EncodeToString(digest[:]))
	if err != nil {
		t.Fatalf("retry completed direct layer upload: %v", err)
	}
	if retried.EntrySeq != entry.EntrySeq || retried.StorageRef != entry.StorageRef {
		t.Fatalf("retried entry=%+v, want original seq=%d ref=%q", retried, entry.EntrySeq, entry.StorageRef)
	}
	requireTenantFileBytesDelta(t, retryWriteBefore, 0, "write", "s3")
	requireTenantFileOpsDelta(t, retryWriteOpsBefore, 0, "write", "s3")
	checkpoint, err := c.CheckpointFSLayer(ctx, layerID, fsLayerCheckpointRequest{CheckpointID: "checkpoint-direct-api"})
	if err != nil {
		t.Fatalf("checkpoint completed layer upload: %v", err)
	}
	if checkpoint.DurableSeq != entry.EntrySeq {
		t.Fatalf("checkpoint durable_seq=%d, want entry_seq=%d", checkpoint.DurableSeq, entry.EntrySeq)
	}

	noRedirect := &http.Client{CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
	readURL := ts.URL + "/v1/layers/" + url.PathEscape(layerID) + "/objects?path=" + url.QueryEscape(path)
	readBefore := tenantFileBytesMetricValue(t, "read", "s3")
	readOpsBefore := tenantFileOpsMetricValue(t, "read", "s3")
	resp, err := noRedirect.Get(readURL) //nolint:gosec // local test server
	if err != nil {
		t.Fatalf("read s3 layer object: %v", err)
	}
	_ = resp.Body.Close()
	if resp.StatusCode != http.StatusTemporaryRedirect {
		t.Fatalf("s3 layer read status=%d, want 307", resp.StatusCode)
	}
	location := resp.Header.Get("Location")
	if !strings.HasPrefix(location, "/s3/objects/layers/"+layerID+"/") {
		t.Fatalf("s3 layer redirect location=%q", location)
	}
	requireTenantFileBytesDelta(t, readBefore, int64(len(payload)), "read", "s3")
	requireTenantFileOpsDelta(t, readOpsBefore, 1, "read", "s3")
	gotResp, err := http.Get(ts.URL + location) //nolint:gosec // local test server
	if err != nil {
		t.Fatalf("follow s3 layer redirect: %v", err)
	}
	defer func() { _ = gotResp.Body.Close() }()
	got, err := io.ReadAll(gotResp.Body)
	if err != nil {
		t.Fatalf("read redirected layer payload: %v", err)
	}
	if gotResp.StatusCode != http.StatusOK || !bytes.Equal(got, payload) {
		t.Fatalf("redirected layer read status=%d bytes=%d, want 200/%d", gotResp.StatusCode, len(got), len(payload))
	}
}

// partialFailResponseWriter accepts the first accept bytes and then fails the
// Write, simulating a client disconnect after a partial transfer.
type partialFailResponseWriter struct {
	header http.Header
	accept int
}

func (w *partialFailResponseWriter) Header() http.Header { return w.header }
func (w *partialFailResponseWriter) WriteHeader(int)     {}
func (w *partialFailResponseWriter) Write(p []byte) (int, error) {
	if len(p) > w.accept {
		return w.accept, errors.New("client disconnected")
	}
	return len(p), nil
}

// TestFSLayerObjectReadCopyErrorCountsNoOp pins the billing contract that a
// failed db9 delivery (client disconnect mid-stream) records no read op while
// the partially transferred bytes are still recorded, and that a successful
// read of the same entry records exactly one op.
func TestFSLayerObjectReadCopyErrorCountsNoOp(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if err := s.fallback.Store().CreateFSLayer(ctx, &datastore.FSLayer{
		LayerID: "layer-copy-fail", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("create layer: %v", err)
	}
	const payload = "payload"
	entry := &datastore.FSLayerEntry{
		LayerID:     "layer-copy-fail",
		Path:        "/repo/a.txt",
		Op:          datastore.FSLayerEntryOpUpsert,
		Kind:        datastore.FSLayerEntryKindFile,
		ContentBlob: []byte(payload),
		SizeBytes:   int64(len(payload)),
	}
	if err := s.fallback.Store().UpsertFSLayerEntry(ctx, entry); err != nil {
		t.Fatalf("upsert entry: %v", err)
	}
	stored, err := s.fallback.Store().GetFSLayerEntry(ctx, "layer-copy-fail", "/repo/a.txt")
	if err != nil {
		t.Fatalf("get entry: %v", err)
	}

	req := httptest.NewRequest(http.MethodGet, "/v1/layers/layer-copy-fail/objects?path=%2Frepo%2Fa.txt", nil)
	req = req.WithContext(withRequestMetricState(withScope(req.Context(), &TenantScope{Backend: s.fallback}), &requestMetricState{}))
	setRequestMetricTenant(req.Context(), "local", "", "", "", tenantRequestClass{surface: "layer", action: "objects_get"})

	for _, tc := range []struct {
		name      string
		accept    int
		wantBytes int64
	}{
		{name: "fail before any byte", accept: 0, wantBytes: 0},
		{name: "fail after partial transfer", accept: 3, wantBytes: 3},
	} {
		t.Run(tc.name, func(t *testing.T) {
			readOpsBefore := tenantFileOpsMetricValue(t, "read", "db9")
			readBytesBefore := tenantFileBytesMetricValue(t, "read", "db9")
			writeFSLayerEntryObject(&partialFailResponseWriter{header: http.Header{}, accept: tc.accept}, req, s.fallback, stored)
			requireTenantFileOpsDelta(t, readOpsBefore, 0, "read", "db9")
			requireTenantFileBytesDelta(t, readBytesBefore, tc.wantBytes, "read", "db9")
		})
	}

	readOpsBefore := tenantFileOpsMetricValue(t, "read", "db9")
	readBytesBefore := tenantFileBytesMetricValue(t, "read", "db9")
	rr := httptest.NewRecorder()
	writeFSLayerEntryObject(rr, req, s.fallback, stored)
	if rr.Code != http.StatusOK || rr.Body.String() != payload {
		t.Fatalf("successful read status=%d body=%q", rr.Code, rr.Body.String())
	}
	requireTenantFileOpsDelta(t, readOpsBefore, 1, "read", "db9")
	requireTenantFileBytesDelta(t, readBytesBefore, int64(len(payload)), "read", "db9")
}

func TestFSLayerInlineObjectReadRemainsDirect(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)
	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	const payload = "inline layer content"
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-inline-read", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create layer: %v", err)
	}
	writeBefore := tenantFileBytesMetricValue(t, "write", "db9")
	writeOpsBefore := tenantFileOpsMetricValue(t, "write", "db9")
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-inline-read", fsLayerEntryRequest{Path: "/repo/inline.txt", Kind: "file", Content: []byte(payload)}); err != nil {
		t.Fatalf("upsert inline layer entry: %v", err)
	}
	requireTenantFileBytesDelta(t, writeBefore, int64(len(payload)), "write", "db9")
	requireTenantFileOpsDelta(t, writeOpsBefore, 1, "write", "db9")
	noRedirect := &http.Client{CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
	readBefore := tenantFileBytesMetricValue(t, "read", "db9")
	readOpsBefore := tenantFileOpsMetricValue(t, "read", "db9")
	resp, err := noRedirect.Get(ts.URL + "/v1/layers/layer-inline-read/objects?path=%2Frepo%2Finline.txt") //nolint:gosec // local test server
	if err != nil {
		t.Fatalf("read inline layer object: %v", err)
	}
	defer func() { _ = resp.Body.Close() }()
	got, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("read inline response: %v", err)
	}
	if resp.StatusCode != http.StatusOK || string(got) != payload || resp.Header.Get("Location") != "" {
		t.Fatalf("inline response status=%d location=%q body=%q", resp.StatusCode, resp.Header.Get("Location"), got)
	}
	requireTenantFileBytesDelta(t, readBefore, int64(len(payload)), "read", "db9")
	requireTenantFileOpsDelta(t, readOpsBefore, 1, "read", "db9")
}

func TestFSLayerS3ReadResolvesMaxSeqBeforePresigning(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)
	ctx := context.Background()
	const (
		layerID = "layer-max-seq-read"
		path    = "/repo/versioned.bin"
	)
	if err := s.fallback.Store().CreateFSLayer(ctx, &datastore.FSLayer{LayerID: layerID, BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create layer: %v", err)
	}
	emptyCheckpoint := &datastore.FSLayerCheckpoint{CheckpointID: "checkpoint-empty", LayerID: layerID}
	if err := s.fallback.Store().CreateFSLayerCheckpoint(ctx, emptyCheckpoint); err != nil {
		t.Fatalf("create empty checkpoint: %v", err)
	}
	if emptyCheckpoint.DurableSeq != 0 {
		t.Fatalf("empty checkpoint durable_seq=%d, want 0", emptyCheckpoint.DurableSeq)
	}
	publish := func(payload string) *datastore.FSLayerEntry {
		t.Helper()
		entry, err := s.fallback.PutFSLayerObject(ctx, layerID, path, strings.NewReader(payload), int64(len(payload)))
		if err != nil {
			t.Fatalf("put layer object: %v", err)
		}
		entry.LayerID = layerID
		entry.Path = path
		entry.Op = datastore.FSLayerEntryOpUpsert
		entry.Kind = datastore.FSLayerEntryKindFile
		entry.Mode = 0o644
		if err := s.fallback.Store().UpsertFSLayerEntry(ctx, entry); err != nil {
			t.Fatalf("publish layer object: %v", err)
		}
		return entry
	}
	oldEntry := publish("old layer payload")
	newEntry := publish("new layer payload")
	if oldEntry.EntrySeq == 0 || newEntry.EntrySeq <= oldEntry.EntrySeq {
		t.Fatalf("entry sequences old=%d new=%d", oldEntry.EntrySeq, newEntry.EntrySeq)
	}

	noRedirect := &http.Client{CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
	readURL := fmt.Sprintf("%s/v1/layers/%s/objects?path=%s&max_seq=%d", ts.URL, layerID, url.QueryEscape(path), oldEntry.EntrySeq)
	resp, err := noRedirect.Get(readURL) //nolint:gosec // local test server
	if err != nil {
		t.Fatalf("read historical layer object: %v", err)
	}
	_ = resp.Body.Close()
	location := resp.Header.Get("Location")
	if resp.StatusCode != http.StatusTemporaryRedirect || !strings.Contains(location, oldEntry.StorageRef) || strings.Contains(location, newEntry.StorageRef) {
		t.Fatalf("historical redirect status=%d location=%q, want old ref %q", resp.StatusCode, location, oldEntry.StorageRef)
	}
	gotResp, err := http.Get(ts.URL + location) //nolint:gosec // local test server
	if err != nil {
		t.Fatalf("follow historical layer redirect: %v", err)
	}
	defer func() { _ = gotResp.Body.Close() }()
	got, err := io.ReadAll(gotResp.Body)
	if err != nil {
		t.Fatalf("read historical layer payload: %v", err)
	}
	if string(got) != "old layer payload" {
		t.Fatalf("historical layer payload=%q", got)
	}

	emptyReadURL := fmt.Sprintf("%s/v1/layers/%s/objects?path=%s&max_seq=0", ts.URL, layerID, url.QueryEscape(path))
	emptyResp, err := noRedirect.Get(emptyReadURL) //nolint:gosec // local test server
	if err != nil {
		t.Fatalf("read empty-checkpoint layer object: %v", err)
	}
	defer func() { _ = emptyResp.Body.Close() }()
	if emptyResp.StatusCode != http.StatusNotFound || emptyResp.Header.Get("Location") != "" {
		t.Fatalf("empty-checkpoint read status=%d location=%q, want 404 without redirect", emptyResp.StatusCode, emptyResp.Header.Get("Location"))
	}
	invalidReadURL := fmt.Sprintf("%s/v1/layers/%s/objects?path=%s&max_seq=", ts.URL, layerID, url.QueryEscape(path))
	invalidResp, err := noRedirect.Get(invalidReadURL) //nolint:gosec // local test server
	if err != nil {
		t.Fatalf("read with empty max_seq: %v", err)
	}
	defer func() { _ = invalidResp.Body.Close() }()
	if invalidResp.StatusCode != http.StatusBadRequest {
		t.Fatalf("empty max_seq status=%d, want 400", invalidResp.StatusCode)
	}
}

func TestFSLayerDirectUploadAbortAndLayerBinding(t *testing.T) {
	s := newTestServer(t)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)
	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	for _, layerID := range []string{"layer-upload-a", "layer-upload-b"} {
		if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: layerID, BaseRootPath: "/repo"}); err != nil {
			t.Fatalf("create %s: %v", layerID, err)
		}
	}
	plan, err := c.InitiateFSLayerUpload(ctx, "layer-upload-a", "/repo/aborted.bin", s3client.PartSize, 0, 0o644)
	if err != nil {
		t.Fatalf("initiate layer upload: %v", err)
	}
	if _, err := c.PresignFSLayerUploadPart(ctx, "layer-upload-b", plan.UploadID, 1); !apiIsNotFound(err) {
		t.Fatalf("cross-layer presign err=%v, want HTTP 404", err)
	}
	if err := c.AbortFSLayerUpload(ctx, "layer-upload-a", plan.UploadID); err != nil {
		t.Fatalf("abort layer upload: %v", err)
	}
	if _, err := c.GetFSLayerEntry(ctx, "layer-upload-a", "/repo/aborted.bin"); !apiIsNotFound(err) {
		t.Fatalf("entry after abort err=%v, want HTTP 404", err)
	}
}

func TestFSLayerUploadContinuationReauthorizesStoredPath(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if err := s.fallback.Store().CreateFSLayer(ctx, &datastore.FSLayer{LayerID: "layer-upload-auth", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create layer: %v", err)
	}
	plan, err := s.fallback.InitiateFSLayerUploadV2(ctx, "layer-upload-auth", "/repo/secret.bin", s3client.PartSize, 0, 0o644)
	if err != nil {
		t.Fatalf("initiate layer upload: %v", err)
	}
	scope := &TenantScope{IsScoped: true, Backend: s.fallback, FSScopes: []FSScope{{
		Prefix: "/other",
		Ops:    map[FSOp]bool{FSOpWrite: true},
	}}}
	req := httptest.NewRequest(http.MethodPost, "/v1/layers/layer-upload-auth/uploads/"+plan.UploadID+"/presign", strings.NewReader(`{"part_number":1}`))
	req = req.WithContext(withScope(req.Context(), scope))
	rr := httptest.NewRecorder()
	s.handleFSLayers(rr, req)
	if rr.Code != http.StatusForbidden {
		t.Fatalf("presign after scope narrowed status=%d body=%q, want 403", rr.Code, rr.Body.String())
	}

	writeOnlyScope := &TenantScope{IsScoped: true, Backend: s.fallback, FSScopes: []FSScope{{
		Prefix: "/repo",
		Ops:    map[FSOp]bool{FSOpWrite: true},
	}}}
	req = httptest.NewRequest(http.MethodPost, "/v1/layers/layer-upload-auth/uploads/"+plan.UploadID+"/abort", strings.NewReader(`{}`))
	req = req.WithContext(withScope(req.Context(), writeOnlyScope))
	rr = httptest.NewRecorder()
	s.handleFSLayers(rr, req)
	if rr.Code != http.StatusForbidden {
		t.Fatalf("abort without delete scope status=%d body=%q, want 403", rr.Code, rr.Body.String())
	}

	if err := s.fallback.MkdirCtx(ctx, "/repo", 0o755); err != nil {
		t.Fatalf("create core upload parent: %v", err)
	}
	corePlan, err := s.fallback.InitiateUploadV2IfRevision(ctx, "/repo/core.bin", s3client.PartSize, -1, "")
	if err != nil {
		t.Fatalf("initiate core upload: %v", err)
	}
	ownerScope := &TenantScope{Backend: s.fallback}
	req = httptest.NewRequest(http.MethodPost, "/v1/layers/layer-upload-auth/uploads/"+corePlan.UploadID+"/presign", strings.NewReader(`{"part_number":1}`))
	req = req.WithContext(withScope(req.Context(), ownerScope))
	rr = httptest.NewRecorder()
	s.handleFSLayers(rr, req)
	if rr.Code != http.StatusNotFound {
		t.Fatalf("core upload through layer route status=%d body=%q, want 404", rr.Code, rr.Body.String())
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/base.txt", []byte("from-main"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-chmod-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-chmod-parent", fsLayerEntryRequest{
		Path: "/repo/base.txt", Op: "chmod", Kind: "file", Mode: 0o600,
	}); err != nil {
		t.Fatalf("chmod: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-chmod-parent", fsLayerForkRequest{LayerID: "layer-chmod-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	body, err := c.ReadFSLayerFile(ctx, "layer-chmod-child", "/repo/base.txt", nil)
	if err != nil {
		t.Fatalf("ReadFSLayerFile: %v", err)
	}
	if !bytes.Equal(body, []byte("from-main")) {
		t.Fatalf("got %q, want from-main", body)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("aaa"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-mv-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-mv-parent", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-mv-parent", fsLayerForkRequest{LayerID: "layer-mv-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	body, err := c.ReadFSLayerFile(ctx, "layer-mv-child", "/repo/b.txt", nil)
	if err != nil {
		t.Fatalf("GET dest: %v", err)
	}
	if !bytes.Equal(body, []byte("aaa")) {
		t.Fatalf("dest=%q, want aaa", body)
	}
	if _, err := c.ReadFSLayerFile(ctx, "layer-mv-child", "/repo/a.txt", nil); err == nil {
		t.Fatal("GET source err=nil, want not found")
	}
}

func TestPlanFSLayerCommitVsMainKeepsChmodUnderDirRenameDest(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/old/: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/a.txt", []byte("src"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	overlay := map[string]datastore.FSLayerOverlayNode{
		"/repo/new/": {
			Path: "/repo/new/", RenameFrom: "/repo/old/", Kind: datastore.FSLayerEntryKindDir,
			Entry: datastore.FSLayerEntry{Path: "/repo/old/", Op: datastore.FSLayerEntryOpRename, Kind: datastore.FSLayerEntryKindDir, ContentText: "/repo/new/"},
		},
		"/repo/old/": {Path: "/repo/old/", Whiteout: true, ConsumedByRename: true, Kind: datastore.FSLayerEntryKindDir},
		"/repo/new/a.txt": {
			Path: "/repo/new/a.txt", Kind: datastore.FSLayerEntryKindFile, Mode: 0o600, ModeSet: true,
			Entry: datastore.FSLayerEntry{Path: "/repo/new/a.txt", Op: datastore.FSLayerEntryOpChmod, Kind: datastore.FSLayerEntryKindFile, Mode: 0o600},
		},
	}
	entries, err := planFSLayerCommitVsMain(ctx, s.fallback, overlay, nil)
	if err != nil {
		t.Fatalf("plan: %v", err)
	}
	var sawRename, sawChmod bool
	for _, e := range entries {
		if e.Op == datastore.FSLayerEntryOpRename && e.Path == "/repo/old/" {
			sawRename = true
		}
		if e.Op == datastore.FSLayerEntryOpChmod && e.Path == "/repo/new/a.txt" {
			sawChmod = true
		}
	}
	if !sawRename || !sawChmod {
		t.Fatalf("entries=%+v, want dir rename plus dest chmod", entries)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/old/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/sub/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/old/sub/: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/sub/a.txt", []byte("nested"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-nested-mv", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-nested-mv", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/tmp/",
	}); err != nil {
		t.Fatalf("rename old→tmp: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-nested-mv", fsLayerEntryRequest{
		Path: "/repo/tmp/sub/", Op: "rename", Kind: "dir", ContentText: "/repo/new/sub/",
	}); err != nil {
		t.Fatalf("rename tmp/sub→new/sub: %v", err)
	}
	got, err := c.ReadFSLayerFile(ctx, "layer-nested-mv", "/repo/new/sub/a.txt", nil)
	if err != nil {
		t.Fatalf("GET nested dest: %v", err)
	}
	if !bytes.Equal(got, []byte("nested")) {
		t.Fatalf("got %q, want nested", got)
	}
	if _, err := c.ReadFSLayerFile(ctx, "layer-nested-mv", "/repo/old/sub/a.txt", nil); err == nil {
		t.Fatal("GET old path err=nil, want not found")
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/old/: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/a.txt", []byte("kept"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-fileout", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-fileout", fsLayerEntryRequest{
		Path: "/repo/old/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/x.txt",
	}); err != nil {
		t.Fatalf("rename file: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-fileout", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "whiteout", Kind: "dir",
	}); err != nil {
		t.Fatalf("whiteout dir: %v", err)
	}
	got, err := c.ReadFSLayerFile(ctx, "layer-fileout", "/repo/x.txt", nil)
	if err != nil {
		t.Fatalf("GET dest after dir whiteout: %v", err)
	}
	if !bytes.Equal(got, []byte("kept")) {
		t.Fatalf("got %q, want kept", got)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("gone"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-wo-chmod", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-wo-chmod", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "whiteout", Kind: "file",
	}); err != nil {
		t.Fatalf("whiteout: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-wo-chmod", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "chmod", Kind: "file", Mode: 0o600,
	}); err != nil {
		t.Fatalf("chmod: %v", err)
	}
	if _, err := c.ReadFSLayerFile(ctx, "layer-wo-chmod", "/repo/a.txt", nil); err == nil {
		t.Fatal("whiteout+chmod GET err=nil, want not found")
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/old/: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/a.txt", []byte("src-bytes"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-dir-chmod", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-chmod", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename dir: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-chmod", fsLayerEntryRequest{
		Path: "/repo/new/a.txt", Op: "chmod", Kind: "file", Mode: 0o600,
	}); err != nil {
		t.Fatalf("chmod dest: %v", err)
	}
	got, err := c.ReadFSLayerFile(ctx, "layer-dir-chmod", "/repo/new/a.txt", nil)
	if err != nil {
		t.Fatalf("GET remapped dest: %v", err)
	}
	if !bytes.Equal(got, []byte("src-bytes")) {
		t.Fatalf("got %q, want src-bytes", got)
	}
	if _, err := c.ReadFSLayerFile(ctx, "layer-dir-chmod", "/repo/old/a.txt", nil); err == nil {
		t.Fatal("GET old path err=nil, want not found")
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/dir/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/dir/: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/dir/a.txt", []byte("hidden"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-wo-dir", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-wo-dir", fsLayerEntryRequest{
		Path: "/repo/dir/", Op: "whiteout", Kind: "dir",
	}); err != nil {
		t.Fatalf("whiteout: %v", err)
	}
	if _, err := c.ReadFSLayerFile(ctx, "layer-wo-dir", "/repo/dir/a.txt", nil); err == nil {
		t.Fatal("dir whiteout descendant GET err=nil, want not found")
	}
	if _, err := c.ForkFSLayer(ctx, "layer-wo-dir", fsLayerForkRequest{LayerID: "layer-wo-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.ReadFSLayerFile(ctx, "layer-wo-child", "/repo/dir/a.txt", nil); err == nil {
		t.Fatal("child dir whiteout descendant GET err=nil, want not found")
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("aaa"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-settled-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-settled-parent", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-settled-parent", fsLayerForkRequest{LayerID: "layer-settled-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-settled-parent")
	if err != nil {
		t.Fatalf("CommitFSLayer parent: %v commit=%+v", err, commit)
	}
	got, err := c.ReadFSLayerFile(ctx, "layer-settled-child", "/repo/b.txt", nil)
	if err != nil {
		t.Fatalf("GET dest after parent commit: %v", err)
	}
	if !bytes.Equal(got, []byte("aaa")) {
		t.Fatalf("dest=%q, want aaa", got)
	}
	if _, err := c.ReadFSLayerFile(ctx, "layer-settled-child", "/repo/a.txt", nil); err == nil {
		t.Fatal("GET vacated source err=nil, want not found")
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/a.txt", []byte("nested"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-settled-dir-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-settled-dir-parent", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename dir: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-settled-dir-parent", fsLayerForkRequest{LayerID: "layer-settled-dir-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-settled-dir-parent"); err != nil {
		t.Fatalf("CommitFSLayer parent: %v", err)
	}
	got, err := c.ReadFSLayerFile(ctx, "layer-settled-dir-child", "/repo/new/a.txt", nil)
	if err != nil {
		t.Fatalf("GET dest descendant after parent commit: %v", err)
	}
	if !bytes.Equal(got, []byte("nested")) {
		t.Fatalf("got %q, want nested", got)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-settled-swap-parent", 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-settled-swap-parent", e); err != nil {
			t.Fatalf("rename %s→%s: %v", e.Path, e.ContentText, err)
		}
	}
	if _, err := c.ForkFSLayer(ctx, "layer-settled-swap-parent", fsLayerForkRequest{LayerID: "layer-settled-swap-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-settled-swap-parent"); err != nil {
		t.Fatalf("CommitFSLayer parent: %v", err)
	}
	gotA, err := c.ReadFSLayerFile(ctx, "layer-settled-swap-child", "/repo/a.txt", nil)
	if err != nil {
		t.Fatalf("GET a after parent swap commit: %v", err)
	}
	gotB, err := c.ReadFSLayerFile(ctx, "layer-settled-swap-child", "/repo/b.txt", nil)
	if err != nil {
		t.Fatalf("GET b after parent swap commit: %v", err)
	}
	if !bytes.Equal(gotA, []byte("B")) || !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("got a=%q b=%q, want swapped B/A", gotA, gotB)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-swap-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-swap-parent", fsLayerForkRequest{LayerID: "layer-swap-child"}); err != nil {
		t.Fatalf("fork: %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-child", e); err != nil {
			t.Fatalf("rename %s→%s: %v", e.Path, e.ContentText, err)
		}
	}
	commit, err := c.CommitFSLayer(ctx, "layer-swap-child")
	if err != nil {
		t.Fatalf("CommitFSLayer child swap: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	gotA, err := s.fallback.ReadCtx(ctx, "/repo/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx a: %v", err)
	}
	gotB, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(gotA, []byte("B")) || !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("main a=%q b=%q, want swapped B/A", gotA, gotB)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/tmp.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("tmp stat err=%v, want ErrNotFound", err)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("aaa"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-cont-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-cont-parent", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-cont-parent", fsLayerForkRequest{LayerID: "layer-cont-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-cont-child", fsLayerEntryRequest{
		Path: "/repo/b.txt", Op: "rename", Kind: "file", ContentText: "/repo/c.txt",
	}); err != nil {
		t.Fatalf("child rename: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-cont-parent"); err != nil {
		t.Fatalf("CommitFSLayer parent: %v", err)
	}
	got, err := c.ReadFSLayerFile(ctx, "layer-cont-child", "/repo/c.txt", nil)
	if err != nil {
		t.Fatalf("GET child dest after parent commit: %v", err)
	}
	if !bytes.Equal(got, []byte("aaa")) {
		t.Fatalf("got %q, want aaa", got)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-cont-child")
	if err != nil {
		t.Fatalf("CommitFSLayer child continuation: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	gotC, err := s.fallback.ReadCtx(ctx, "/repo/c.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx c: %v", err)
	}
	if !bytes.Equal(gotC, []byte("aaa")) {
		t.Fatalf("main c=%q, want aaa", gotC)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/a.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("a stat err=%v, want ErrNotFound", err)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/b.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("b stat err=%v, want ErrNotFound", err)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/sub/", 0o755); err != nil {
		t.Fatalf("mkdir old/sub: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/sub/a.txt", []byte("nested"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-dirhop-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dirhop-parent", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/tmp/",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-dirhop-parent", fsLayerForkRequest{LayerID: "layer-dirhop-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dirhop-child", fsLayerEntryRequest{
		Path: "/repo/tmp/sub/", Op: "rename", Kind: "dir", ContentText: "/repo/new/sub/",
	}); err != nil {
		t.Fatalf("child rename: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-dirhop-parent"); err != nil {
		t.Fatalf("CommitFSLayer parent: %v", err)
	}
	got, err := c.ReadFSLayerFile(ctx, "layer-dirhop-child", "/repo/new/sub/a.txt", nil)
	if err != nil {
		t.Fatalf("GET nested dest after parent commit: %v", err)
	}
	if !bytes.Equal(got, []byte("nested")) {
		t.Fatalf("got %q, want nested", got)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/sub/", 0o755); err != nil {
		t.Fatalf("mkdir old/sub: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/sub/a.txt", []byte("nested"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-dirhop-commit-p", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dirhop-commit-p", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/tmp/",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-dirhop-commit-p", fsLayerForkRequest{LayerID: "layer-dirhop-commit-c"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dirhop-commit-c", fsLayerEntryRequest{
		Path: "/repo/tmp/sub/", Op: "rename", Kind: "dir", ContentText: "/repo/new/sub/",
	}); err != nil {
		t.Fatalf("child rename: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-dirhop-commit-p"); err != nil {
		t.Fatalf("CommitFSLayer parent: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-dirhop-commit-c")
	if err != nil {
		t.Fatalf("CommitFSLayer child dest-space hop: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/new/sub/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx new/sub/a: %v", err)
	}
	if !bytes.Equal(got, []byte("nested")) {
		t.Fatalf("got %q, want nested", got)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/tmp/sub/"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("tmp/sub stat err=%v, want ErrNotFound", err)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/f.txt", []byte("body"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-destchmod-p", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-destchmod-p", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-destchmod-p", fsLayerForkRequest{LayerID: "layer-destchmod-c"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	entry, err := c.UpsertFSLayerEntry(ctx, "layer-destchmod-c", fsLayerEntryRequest{
		Path: "/repo/new/f.txt", Op: "chmod", Kind: "file", Mode: 0o600,
	})
	if err != nil {
		t.Fatalf("child dest-space chmod: %v", err)
	}
	if entry.BaseInodeID == "" || entry.BaseRevision <= 0 {
		t.Fatalf("dest-space chmod snapshot inode=%q rev=%d, want remapped copy-up", entry.BaseInodeID, entry.BaseRevision)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-destchmod-p"); err != nil {
		t.Fatalf("CommitFSLayer parent: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-destchmod-c")
	if err != nil {
		t.Fatalf("CommitFSLayer child dest-space chmod: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/new/f.txt")
	if err != nil {
		t.Fatalf("StatNodeCtx new/f: %v", err)
	}
	if nf.File == nil || nf.File.Mode&0o777 != 0o600 {
		t.Fatalf("new/f mode=%+v, want 0600", nf.File)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/f.txt", []byte("body"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-destchmod-occ-p", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-destchmod-occ-p", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-destchmod-occ-p", fsLayerForkRequest{LayerID: "layer-destchmod-occ-c"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-destchmod-occ-c", fsLayerEntryRequest{
		Path: "/repo/new/f.txt", Op: "chmod", Kind: "file", Mode: 0o600,
	}); err != nil {
		t.Fatalf("child dest-space chmod: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/f.txt", []byte("external"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("external write source: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-destchmod-occ-p"); err != nil {
		t.Fatalf("CommitFSLayer parent: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-destchmod-occ-c")
	if !errors.Is(err, errConflict) {
		t.Fatalf("child commit err=%v commit=%+v, want conflict", err, commit)
	}
	if commit == nil || commit.Status != "conflicted" {
		t.Fatalf("commit=%+v, want conflicted", commit)
	}
	if len(commit.Conflicts) != 1 || commit.Conflicts[0].Path != "/repo/new/f.txt" || commit.Conflicts[0].Reason != "base revision changed" {
		t.Fatalf("conflicts=%+v, want base revision changed at /repo/new/f.txt", commit.Conflicts)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/new/f.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx new/f: %v", err)
	}
	if !bytes.Equal(got, []byte("external")) {
		t.Fatalf("main new/f=%q, want external", got)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/f.txt", []byte("body"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-destchmod-first-p", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-destchmod-first-p", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-destchmod-first-p", fsLayerForkRequest{LayerID: "layer-destchmod-first-c"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-destchmod-first-c", fsLayerEntryRequest{
		Path: "/repo/new/f.txt", Op: "chmod", Kind: "file", Mode: 0o600,
	}); err != nil {
		t.Fatalf("child dest-space chmod: %v", err)
	}
	child, err := c.CommitFSLayer(ctx, "layer-destchmod-first-c")
	if err != nil {
		t.Fatalf("CommitFSLayer child-first dest-space chmod: %v commit=%+v", err, child)
	}
	if child.Status != "committed" {
		t.Fatalf("child=%+v, want committed", child)
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/new/f.txt")
	if err != nil {
		t.Fatalf("StatNodeCtx new/f: %v", err)
	}
	if nf.File == nil || nf.File.Mode&0o777 != 0o600 {
		t.Fatalf("new/f mode=%+v, want 0600", nf.File)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-fork-upsert-p", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-fork-upsert-p", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "upsert", Kind: "file", Content: []byte("a"), SizeBytes: 1, Mode: 0o644,
	}); err != nil {
		t.Fatalf("parent upsert: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-fork-upsert-p", fsLayerForkRequest{LayerID: "layer-fork-upsert-c"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	entry, err := c.UpsertFSLayerEntry(ctx, "layer-fork-upsert-c", fsLayerEntryRequest{
		Path: "/repo/b.txt", Op: "upsert", Kind: "file", Content: []byte("b"), SizeBytes: 1, Mode: 0o644,
	})
	if err != nil {
		t.Fatalf("child create: %v", err)
	}
	if entry.BaseRevision != 0 || entry.BaseInodeID != "" {
		t.Fatalf("child create snapshot inode=%q rev=%d, want claim-less", entry.BaseInodeID, entry.BaseRevision)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-fork-upsert-p"); err != nil {
		t.Fatalf("commit parent: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-fork-upsert-c")
	if err != nil {
		t.Fatalf("commit child: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(got, []byte("b")) {
		t.Fatalf("b=%q, want b", got)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/f.txt", []byte("body"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-destup2-p", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-destup2-p", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-destup2-p", fsLayerForkRequest{LayerID: "layer-destup2-c"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-destup2-c", fsLayerEntryRequest{
		Path: "/repo/new/f.txt", Op: "upsert", Kind: "file", Content: []byte("v1"), SizeBytes: 2, Mode: 0o644,
	}); err != nil {
		t.Fatalf("first dest-space upsert: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-destup2-c", fsLayerEntryRequest{
		Path: "/repo/new/f.txt", Op: "upsert", Kind: "file", Content: []byte("v2"), SizeBytes: 2, Mode: 0o644,
	}); err != nil {
		t.Fatalf("second dest-space upsert: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-destup2-p"); err != nil {
		t.Fatalf("CommitFSLayer parent: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-destup2-c")
	if err != nil {
		t.Fatalf("CommitFSLayer child dest-space upsert twice: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/new/f.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx new/f: %v", err)
	}
	if !bytes.Equal(got, []byte("v2")) {
		t.Fatalf("new/f=%q, want v2", got)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/sub/", 0o755); err != nil {
		t.Fatalf("mkdir old/sub: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/sub/f.txt", []byte("kept"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-under-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-under-parent", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-under-parent", fsLayerForkRequest{LayerID: "layer-under-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-under-parent"); err != nil {
		t.Fatalf("CommitFSLayer parent: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-under-child", fsLayerEntryRequest{
		Path: "/repo/new/sub/", Op: "rename", Kind: "dir", ContentText: "/repo/x/",
	}); err != nil {
		t.Fatalf("child rename under dest: %v", err)
	}
	got, err := c.ReadFSLayerFile(ctx, "layer-under-child", "/repo/x/f.txt", nil)
	if err != nil {
		t.Fatalf("GET under committed parent dest: %v", err)
	}
	if !bytes.Equal(got, []byte("kept")) {
		t.Fatalf("got %q, want kept", got)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("src"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write src: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("poison"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write poison: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-nofallback", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-nofallback", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if err := s.fallback.RemoveCtx(ctx, "/repo/a.txt"); err != nil {
		t.Fatalf("remove source: %v", err)
	}
	if _, err := c.ReadFSLayerFile(ctx, "layer-nofallback", "/repo/b.txt", nil); err == nil {
		t.Fatal("GET dest after source gone err=nil, want not found (not dest-space poison)")
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-swap-root", 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-root", e); err != nil {
			t.Fatalf("rename %s: %v", e.Path, err)
		}
	}
	if _, err := c.ForkFSLayer(ctx, "layer-swap-root", fsLayerForkRequest{LayerID: "layer-swap-first"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	child, err := c.CommitFSLayer(ctx, "layer-swap-first")
	if err != nil {
		t.Fatalf("CommitFSLayer child: %v commit=%+v", err, child)
	}
	if child.Status != "committed" {
		t.Fatalf("child=%+v, want committed", child)
	}
	parent, err := c.CommitFSLayer(ctx, "layer-swap-root")
	if err != nil {
		t.Fatalf("CommitFSLayer parent: %v commit=%+v", err, parent)
	}
	if parent.Status != "committed" {
		t.Fatalf("parent=%+v, want committed no-op", parent)
	}
	gotA, err := s.fallback.ReadCtx(ctx, "/repo/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx a: %v", err)
	}
	gotB, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(gotA, []byte("B")) || !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("after parent commit a=%q b=%q, want swapped B/A", gotA, gotB)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-swap-sib-root", 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-sib-root", e); err != nil {
			t.Fatalf("rename %s: %v", e.Path, err)
		}
	}
	if _, err := c.ForkFSLayer(ctx, "layer-swap-sib-root", fsLayerForkRequest{LayerID: "layer-swap-sib-a"}); err != nil {
		t.Fatalf("fork a: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-swap-sib-root", fsLayerForkRequest{LayerID: "layer-swap-sib-b"}); err != nil {
		t.Fatalf("fork b: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-swap-sib-a"); err != nil {
		t.Fatalf("commit a: %v", err)
	}
	sib, err := c.CommitFSLayer(ctx, "layer-swap-sib-b")
	if err != nil {
		t.Fatalf("commit sibling: %v commit=%+v", err, sib)
	}
	if sib.Status != "committed" {
		t.Fatalf("sibling=%+v, want committed no-op", sib)
	}
	gotA, err := s.fallback.ReadCtx(ctx, "/repo/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx a: %v", err)
	}
	gotB, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(gotA, []byte("B")) || !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("after sibling commit a=%q b=%q, want swapped B/A", gotA, gotB)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-preswap-root", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-preswap-root", fsLayerForkRequest{LayerID: "layer-preswap-child"}); err != nil {
		t.Fatalf("fork before swap: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-preswap-child", fsLayerEntryRequest{
		Path: "/repo/note.txt", Op: "upsert", Kind: "file", Content: []byte("note"), SizeBytes: 4,
	}); err != nil {
		t.Fatalf("child upsert: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-preswap-child"); err != nil {
		t.Fatalf("commit pre-swap child: %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-preswap-root", e); err != nil {
			t.Fatalf("rename %s: %v", e.Path, err)
		}
	}
	root, err := c.CommitFSLayer(ctx, "layer-preswap-root")
	if err != nil {
		t.Fatalf("CommitFSLayer root swap: %v commit=%+v", err, root)
	}
	if root.Status != "committed" || root.Applied == 0 {
		t.Fatalf("root=%+v, want committed applied>0", root)
	}
	gotA, err := s.fallback.ReadCtx(ctx, "/repo/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx a: %v", err)
	}
	gotB, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(gotA, []byte("B")) || !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("after root commit a=%q b=%q, want swapped B/A", gotA, gotB)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("aaa"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-parent-after", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-parent-after", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/b.txt",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-parent-after", fsLayerForkRequest{LayerID: "layer-child-first"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	child, err := c.CommitFSLayer(ctx, "layer-child-first")
	if err != nil {
		t.Fatalf("CommitFSLayer child: %v commit=%+v", err, child)
	}
	if child.Status != "committed" {
		t.Fatalf("child commit=%+v, want committed", child)
	}
	parent, err := c.CommitFSLayer(ctx, "layer-parent-after")
	if err != nil {
		t.Fatalf("CommitFSLayer parent after child: %v commit=%+v", err, parent)
	}
	if parent.Status != "committed" {
		t.Fatalf("parent commit=%+v, want committed no-op", parent)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(got, []byte("aaa")) {
		t.Fatalf("main b=%q, want aaa", got)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/sub/", 0o755); err != nil {
		t.Fatalf("mkdir old/sub: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/sub/a.txt", []byte("nested"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write src: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/x/", 0o755); err != nil {
		t.Fatalf("mkdir x: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/x/sub/", 0o755); err != nil {
		t.Fatalf("mkdir x/sub: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/x/sub/a.txt", []byte("poison"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write poison dest: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-dircont-get-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dircont-get-parent", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/tmp/",
	}); err != nil {
		t.Fatalf("parent rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-dircont-get-parent", fsLayerForkRequest{LayerID: "layer-dircont-get-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dircont-get-child", fsLayerEntryRequest{
		Path: "/repo/tmp/", Op: "rename", Kind: "dir", ContentText: "/repo/x/",
	}); err != nil {
		t.Fatalf("child rename: %v", err)
	}
	got, err := c.ReadFSLayerFile(ctx, "layer-dircont-get-child", "/repo/x/sub/a.txt", nil)
	if err != nil {
		t.Fatalf("GET dest descendant before parent commit: %v", err)
	}
	if !bytes.Equal(got, []byte("nested")) {
		t.Fatalf("got %q, want nested (not dest-space poison)", got)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/dir/", 0o755); err != nil {
		t.Fatalf("mkdir dir: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/dir/a.txt", []byte("aaa"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-dirmv-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dirmv-parent", fsLayerEntryRequest{
		Path: "/repo/dir/a.txt", Op: "rename", Kind: "file", ContentText: "/repo/dir/b.txt",
	}); err != nil {
		t.Fatalf("parent file rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-dirmv-parent", fsLayerForkRequest{LayerID: "layer-dirmv-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dirmv-child", fsLayerEntryRequest{
		Path: "/repo/dir/", Op: "rename", Kind: "dir", ContentText: "/repo/newdir/",
	}); err != nil {
		t.Fatalf("child dir rename: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-dirmv-parent"); err != nil {
		t.Fatalf("CommitFSLayer parent: %v", err)
	}
	got, err := c.ReadFSLayerFile(ctx, "layer-dirmv-child", "/repo/newdir/b.txt", nil)
	if err != nil {
		t.Fatalf("GET relocated dest: %v", err)
	}
	if !bytes.Equal(got, []byte("aaa")) {
		t.Fatalf("got %q, want aaa", got)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-dirmv-child")
	if err != nil {
		t.Fatalf("CommitFSLayer child dir move: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	gotB, err := s.fallback.ReadCtx(ctx, "/repo/newdir/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx newdir/b: %v", err)
	}
	if !bytes.Equal(gotB, []byte("aaa")) {
		t.Fatalf("main newdir/b=%q, want aaa", gotB)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/dir/a.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("old a stat err=%v, want ErrNotFound", err)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/dir/b.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("old b stat err=%v, want ErrNotFound", err)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/sub/", 0o755); err != nil {
		t.Fatalf("mkdir old/sub: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/sub/a.txt", []byte("nested"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-dircont-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dircont-parent", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/tmp/",
	}); err != nil {
		t.Fatalf("parent dir rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-dircont-parent", fsLayerForkRequest{LayerID: "layer-dircont-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-dircont-parent"); err != nil {
		t.Fatalf("CommitFSLayer parent: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dircont-child", fsLayerEntryRequest{
		Path: "/repo/tmp/", Op: "rename", Kind: "dir", ContentText: "/repo/x/",
	}); err != nil {
		t.Fatalf("child dir continuation: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-dircont-child")
	if err != nil {
		t.Fatalf("CommitFSLayer child dir continuation: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/x/sub/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx x/sub/a: %v", err)
	}
	if !bytes.Equal(got, []byte("nested")) {
		t.Fatalf("got %q, want nested", got)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/tmp/"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("tmp stat err=%v, want ErrNotFound", err)
	}
}

func TestPlanFSLayerCommitVsMainSwapBreaksCycle(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	overlay := map[string]datastore.FSLayerOverlayNode{
		"/repo/a.txt": {
			Path: "/repo/a.txt", RenameFrom: "/repo/b.txt", Kind: datastore.FSLayerEntryKindFile,
			Entry: datastore.FSLayerEntry{LayerID: "child", Path: "/repo/b.txt", Op: datastore.FSLayerEntryOpRename, Kind: datastore.FSLayerEntryKindFile, ContentText: "/repo/a.txt"},
		},
		"/repo/b.txt": {
			Path: "/repo/b.txt", RenameFrom: "/repo/a.txt", Kind: datastore.FSLayerEntryKindFile,
			Entry: datastore.FSLayerEntry{LayerID: "child", Path: "/repo/a.txt", Op: datastore.FSLayerEntryOpRename, Kind: datastore.FSLayerEntryKindFile, ContentText: "/repo/b.txt"},
		},
		"/repo/tmp.txt": {Path: "/repo/tmp.txt", Whiteout: true, Kind: datastore.FSLayerEntryKindFile},
	}
	entries, err := planFSLayerCommitVsMain(ctx, s.fallback, overlay, nil)
	if err != nil {
		t.Fatalf("plan: %v", err)
	}
	var renames []datastore.FSLayerEntry
	for _, e := range entries {
		if e.Op == datastore.FSLayerEntryOpRename {
			renames = append(renames, e)
		}
	}
	if len(renames) != 3 {
		t.Fatalf("renames=%+v, want 3 cycle-broken hops", renames)
	}
	if !strings.Contains(renames[0].ContentText, ".fs-layer-swap-") {
		t.Fatalf("first hop=%+v, want move onto temp", renames[0])
	}
}

func TestPlanFSLayerCommitVsMainSkipsCommittedSwap(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	overlay := map[string]datastore.FSLayerOverlayNode{
		"/repo/a.txt": {
			Path: "/repo/a.txt", RenameFrom: "/repo/b.txt", Kind: datastore.FSLayerEntryKindFile,
			Entry: datastore.FSLayerEntry{LayerID: "parent", Path: "/repo/b.txt", Op: datastore.FSLayerEntryOpRename, Kind: datastore.FSLayerEntryKindFile, ContentText: "/repo/a.txt"},
		},
		"/repo/b.txt": {
			Path: "/repo/b.txt", RenameFrom: "/repo/a.txt", Kind: datastore.FSLayerEntryKindFile,
			Entry: datastore.FSLayerEntry{LayerID: "parent", Path: "/repo/a.txt", Op: datastore.FSLayerEntryOpRename, Kind: datastore.FSLayerEntryKindFile, ContentText: "/repo/b.txt"},
		},
	}
	entries, err := planFSLayerCommitVsMain(ctx, s.fallback, overlay, func(hop datastore.FSLayerRenameHop) bool {
		return hop.LayerID == "parent"
	})
	if err != nil {
		t.Fatalf("plan: %v", err)
	}
	for _, e := range entries {
		if e.Op == datastore.FSLayerEntryOpRename {
			t.Fatalf("entries=%+v, committed swap must be skipped", entries)
		}
	}
}

func TestPlanFSLayerCommitVsMainContinuationKeepsSourceClaim(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("aaa"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	nf, err := s.fallback.StatNodeCtx(ctx, "/repo/a.txt")
	if err != nil || nf.File == nil {
		t.Fatalf("stat a: nf=%+v err=%v", nf, err)
	}
	wantID, wantRev := nf.File.FileID, nf.File.Revision
	if err := s.fallback.RenameCtx(ctx, "/repo/a.txt", "/repo/b.txt"); err != nil {
		t.Fatalf("rename a→b: %v", err)
	}
	overlay := map[string]datastore.FSLayerOverlayNode{
		"/repo/c.txt": {
			Path:       "/repo/c.txt",
			RenameFrom: "/repo/a.txt",
			RenameVia:  "/repo/b.txt",
			Kind:       datastore.FSLayerEntryKindFile,
			RenameHops: []datastore.FSLayerRenameHop{
				{From: "/repo/a.txt", To: "/repo/b.txt", LayerID: "parent", EntrySeq: 1},
				{From: "/repo/b.txt", To: "/repo/c.txt", LayerID: "child", EntrySeq: 1},
			},
			Entry: datastore.FSLayerEntry{
				LayerID:      "child",
				Path:         "/repo/a.txt",
				Op:           datastore.FSLayerEntryOpRename,
				Kind:         datastore.FSLayerEntryKindFile,
				ContentText:  "/repo/c.txt",
				BaseInodeID:  wantID,
				BaseRevision: wantRev,
			},
		},
		"/repo/a.txt": {
			Path:             "/repo/a.txt",
			Whiteout:         true,
			ConsumedByRename: true,
			Kind:             datastore.FSLayerEntryKindFile,
			Entry: datastore.FSLayerEntry{
				Path:         "/repo/a.txt",
				Op:           datastore.FSLayerEntryOpWhiteout,
				Kind:         datastore.FSLayerEntryKindFile,
				BaseInodeID:  wantID,
				BaseRevision: wantRev,
			},
		},
		"/repo/b.txt": {
			Path:     "/repo/b.txt",
			Whiteout: true,
			Kind:     datastore.FSLayerEntryKindFile,
			Entry: datastore.FSLayerEntry{
				Path: "/repo/b.txt",
				Op:   datastore.FSLayerEntryOpWhiteout,
				Kind: datastore.FSLayerEntryKindFile,
			},
		},
	}
	entries, err := planFSLayerCommitVsMain(ctx, s.fallback, overlay, func(hop datastore.FSLayerRenameHop) bool {
		return hop.LayerID == "parent"
	})
	if err != nil {
		t.Fatalf("plan: %v", err)
	}
	var rename *datastore.FSLayerEntry
	for i := range entries {
		if entries[i].Op == datastore.FSLayerEntryOpRename {
			rename = &entries[i]
			break
		}
	}
	if rename == nil {
		t.Fatalf("entries=%+v, want continuation rename", entries)
	}
	if rename.Path != "/repo/b.txt" || rename.ContentText != "/repo/c.txt" {
		t.Fatalf("rename=%+v, want b→c", rename)
	}
	if rename.BaseInodeID != wantID || rename.BaseRevision != wantRev {
		t.Fatalf("claim inode=%s rev=%d, want first-source %s@%d", rename.BaseInodeID, rename.BaseRevision, wantID, wantRev)
	}
}

func TestFSLayerHopCoveredByLineage(t *testing.T) {
	root := &datastore.FSLayer{LayerID: "R", RootLayerID: "R", State: datastore.FSLayerStateActive}
	sib := &datastore.FSLayer{LayerID: "A", ParentLayerID: "R", OriginSeq: 5, RootLayerID: "R", State: datastore.FSLayerStateCommitted}
	child := &datastore.FSLayer{LayerID: "B", ParentLayerID: "R", OriginSeq: 5, RootLayerID: "R", State: datastore.FSLayerStateActive}
	layers := map[string]*datastore.FSLayer{"R": root, "A": sib, "B": child}

	if fsLayerHopCoveredByLineage(datastore.FSLayerRenameHop{LayerID: "B", EntrySeq: 2}, layers) {
		t.Fatal("sibling origin_seq must not cover a hop recorded on the other child")
	}
	if !fsLayerHopCoveredByLineage(datastore.FSLayerRenameHop{LayerID: "R", EntrySeq: 3}, layers) {
		t.Fatal("committed child pin into R must cover R hop seq 3")
	}
	if fsLayerHopCoveredByLineage(datastore.FSLayerRenameHop{LayerID: "R", EntrySeq: 6}, layers) {
		t.Fatal("child pin origin_seq=5 must not cover later R hop seq 6")
	}

	parent := &datastore.FSLayer{LayerID: "P", ParentLayerID: "R", OriginSeq: 3, RootLayerID: "R", State: datastore.FSLayerStateActive}
	gc := &datastore.FSLayer{LayerID: "G", ParentLayerID: "P", OriginSeq: 0, RootLayerID: "R", State: datastore.FSLayerStateCommitted}
	gcLayers := map[string]*datastore.FSLayer{"R": root, "P": parent, "G": gc}
	if !fsLayerHopCoveredByLineage(datastore.FSLayerRenameHop{LayerID: "R", EntrySeq: 3}, gcLayers) {
		t.Fatal("grandchild flatten must cover root hop via parent pin")
	}

	rec := &datastore.FSLayer{LayerID: "C", RootLayerID: "C", State: datastore.FSLayerStateCommitted}
	if !fsLayerHopCoveredByLineage(datastore.FSLayerRenameHop{LayerID: "C", EntrySeq: 1}, map[string]*datastore.FSLayer{"C": rec}) {
		t.Fatal("committed recording layer must cover its own hop")
	}

	abandoned := &datastore.FSLayer{LayerID: "A", ParentLayerID: "R", OriginSeq: 5, RootLayerID: "R", State: datastore.FSLayerStateAbandoned}
	afterDelete := map[string]*datastore.FSLayer{"R": root, "A": abandoned, "B": child}
	if fsLayerHopCoveredByLineage(datastore.FSLayerRenameHop{LayerID: "R", EntrySeq: 3}, afterDelete) {
		t.Fatal("abandoned flattener without apply mark must not cover")
	}
	abandoned.CommitApplied = true
	afterDelete["A"] = abandoned
	if !fsLayerHopCoveredByLineage(datastore.FSLayerRenameHop{LayerID: "R", EntrySeq: 3}, afterDelete) {
		t.Fatal("abandoned flattener with commit_applied must still cover")
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-sibseq-root", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	for _, e := range []fsLayerEntryRequest{
		{Path: "/repo/note1.txt", Op: "upsert", Kind: "file", Content: []byte("n1"), SizeBytes: 2},
		{Path: "/repo/note2.txt", Op: "upsert", Kind: "file", Content: []byte("n2"), SizeBytes: 2},
	} {
		if _, err := c.UpsertFSLayerEntry(ctx, "layer-sibseq-root", e); err != nil {
			t.Fatalf("root upsert %s: %v", e.Path, err)
		}
	}
	if _, err := c.ForkFSLayer(ctx, "layer-sibseq-root", fsLayerForkRequest{LayerID: "layer-sibseq-a"}); err != nil {
		t.Fatalf("fork a: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-sibseq-root", fsLayerForkRequest{LayerID: "layer-sibseq-b"}); err != nil {
		t.Fatalf("fork b: %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-sibseq-a", e); err != nil {
			t.Fatalf("swap %s: %v", e.Path, err)
		}
	}
	if _, err := c.CommitFSLayer(ctx, "layer-sibseq-b"); err != nil {
		t.Fatalf("commit sibling: %v", err)
	}
	got, err := c.CommitFSLayer(ctx, "layer-sibseq-a")
	if err != nil {
		t.Fatalf("commit child swap: %v commit=%+v", err, got)
	}
	if got.Status != "committed" || got.Applied == 0 {
		t.Fatalf("child swap=%+v, want committed applied>0", got)
	}
	gotA, err := s.fallback.ReadCtx(ctx, "/repo/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx a: %v", err)
	}
	gotB, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(gotA, []byte("B")) || !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("after child swap a=%q b=%q, want B/A", gotA, gotB)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-get-sib-root", 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-get-sib-root", e); err != nil {
			t.Fatalf("rename %s: %v", e.Path, err)
		}
	}
	if _, err := c.ForkFSLayer(ctx, "layer-get-sib-root", fsLayerForkRequest{LayerID: "layer-get-sib-a"}); err != nil {
		t.Fatalf("fork a: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-get-sib-root", fsLayerForkRequest{LayerID: "layer-get-sib-b"}); err != nil {
		t.Fatalf("fork b: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-get-sib-a"); err != nil {
		t.Fatalf("commit a: %v", err)
	}
	gotA, err := c.ReadFSLayerFile(ctx, "layer-get-sib-b", "/repo/a.txt", nil)
	if err != nil {
		t.Fatalf("GET a on sibling B: %v", err)
	}
	gotB, err := c.ReadFSLayerFile(ctx, "layer-get-sib-b", "/repo/b.txt", nil)
	if err != nil {
		t.Fatalf("GET b on sibling B: %v", err)
	}
	if !bytes.Equal(gotA, []byte("B")) || !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("sibling B GET a=%q b=%q, want swapped B/A", gotA, gotB)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-gc-root", 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-gc-root", e); err != nil {
			t.Fatalf("rename %s: %v", e.Path, err)
		}
	}
	if _, err := c.ForkFSLayer(ctx, "layer-gc-root", fsLayerForkRequest{LayerID: "layer-gc-parent"}); err != nil {
		t.Fatalf("fork parent: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-gc-parent", fsLayerForkRequest{LayerID: "layer-gc-child"}); err != nil {
		t.Fatalf("fork grandchild: %v", err)
	}
	child, err := c.CommitFSLayer(ctx, "layer-gc-child")
	if err != nil {
		t.Fatalf("commit grandchild: %v commit=%+v", err, child)
	}
	if child.Status != "committed" {
		t.Fatalf("grandchild=%+v, want committed", child)
	}
	root, err := c.CommitFSLayer(ctx, "layer-gc-root")
	if err != nil {
		t.Fatalf("commit root: %v commit=%+v", err, root)
	}
	if root.Status != "committed" {
		t.Fatalf("root=%+v, want committed no-op", root)
	}
	gotA, err := s.fallback.ReadCtx(ctx, "/repo/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx a: %v", err)
	}
	gotB, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(gotA, []byte("B")) || !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("after root commit a=%q b=%q, want swapped B/A", gotA, gotB)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-del-root", 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-del-root", e); err != nil {
			t.Fatalf("rename %s: %v", e.Path, err)
		}
	}
	if _, err := c.ForkFSLayer(ctx, "layer-del-root", fsLayerForkRequest{LayerID: "layer-del-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-del-child"); err != nil {
		t.Fatalf("commit child: %v", err)
	}
	if err := c.DeleteFSLayer(ctx, "layer-del-child", false); err != nil {
		t.Fatalf("delete committed child: %v", err)
	}
	root, err := c.CommitFSLayer(ctx, "layer-del-root")
	if err != nil {
		t.Fatalf("commit root after child delete: %v commit=%+v", err, root)
	}
	if root.Status != "committed" {
		t.Fatalf("root=%+v, want committed no-op", root)
	}
	gotA, err := s.fallback.ReadCtx(ctx, "/repo/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx a: %v", err)
	}
	gotB, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx b: %v", err)
	}
	if !bytes.Equal(gotA, []byte("B")) || !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("after parent commit a=%q b=%q, want swapped B/A", gotA, gotB)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("A"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write a: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/b.txt", []byte("B"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write b: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-delget-root", 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-delget-root", e); err != nil {
			t.Fatalf("rename %s: %v", e.Path, err)
		}
	}
	if _, err := c.ForkFSLayer(ctx, "layer-delget-root", fsLayerForkRequest{LayerID: "layer-delget-a"}); err != nil {
		t.Fatalf("fork a: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-delget-root", fsLayerForkRequest{LayerID: "layer-delget-b"}); err != nil {
		t.Fatalf("fork b: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-delget-a"); err != nil {
		t.Fatalf("commit a: %v", err)
	}
	if err := c.DeleteFSLayer(ctx, "layer-delget-a", false); err != nil {
		t.Fatalf("delete committed sibling: %v", err)
	}
	gotA, err := c.ReadFSLayerFile(ctx, "layer-delget-b", "/repo/a.txt", nil)
	if err != nil {
		t.Fatalf("GET a on sibling B: %v", err)
	}
	gotB, err := c.ReadFSLayerFile(ctx, "layer-delget-b", "/repo/b.txt", nil)
	if err != nil {
		t.Fatalf("GET b on sibling B: %v", err)
	}
	if !bytes.Equal(gotA, []byte("B")) || !bytes.Equal(gotB, []byte("A")) {
		t.Fatalf("sibling B GET a=%q b=%q, want swapped B/A", gotA, gotB)
	}
}

func TestFSLayerEntryGetRemapRequiresSourceScope(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if err := s.fallback.Store().CreateFSLayer(ctx, &datastore.FSLayer{
		LayerID: "layer-entry-remap-parent", BaseRootPath: "/",
	}); err != nil {
		t.Fatalf("create parent: %v", err)
	}
	if err := s.fallback.Store().UpsertFSLayerEntry(ctx, &datastore.FSLayerEntry{
		LayerID: "layer-entry-remap-parent", Path: "/secret/b.txt", Op: datastore.FSLayerEntryOpRename,
		Kind: datastore.FSLayerEntryKindFile, ContentText: "/repo/a.txt",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	child, err := s.fallback.Store().ForkFSLayer(ctx, datastore.FSLayerForkOptions{
		ChildLayerID: "layer-entry-remap-child", ParentLayerID: "layer-entry-remap-parent",
	})
	if err != nil {
		t.Fatalf("fork: %v", err)
	}
	req := httptest.NewRequest(http.MethodGet, "/v1/layers/layer-entry-remap-child/entries?path="+url.QueryEscape("/repo/a.txt"), nil)
	req = req.WithContext(withScope(req.Context(), &TenantScope{
		IsScoped: true,
		Backend:  s.fallback,
		FSScopes: []FSScope{{
			Prefix: "/repo",
			Ops:    map[FSOp]bool{FSOpRead: true},
		}},
	}))
	rr := httptest.NewRecorder()
	s.handleFSLayerEntryGet(rr, req, s.fallback.Store(), child)
	if rr.Code != http.StatusForbidden {
		t.Fatalf("status=%d body=%s, want 403", rr.Code, rr.Body.String())
	}

	okReq := httptest.NewRequest(http.MethodGet, "/v1/layers/layer-entry-remap-child/entries?path="+url.QueryEscape("/repo/a.txt"), nil)
	okReq = okReq.WithContext(withScope(okReq.Context(), &TenantScope{
		IsScoped: true,
		Backend:  s.fallback,
		FSScopes: []FSScope{
			{Prefix: "/repo", Ops: map[FSOp]bool{FSOpRead: true}},
			{Prefix: "/secret", Ops: map[FSOp]bool{FSOpRead: true}},
		},
	}))
	okRR := httptest.NewRecorder()
	s.handleFSLayerEntryGet(okRR, okReq, s.fallback.Store(), child)
	if okRR.Code != http.StatusOK {
		t.Fatalf("both-scopes status=%d body=%s, want 200", okRR.Code, okRR.Body.String())
	}
	if !strings.Contains(okRR.Body.String(), "/secret/b.txt") {
		t.Fatalf("both-scopes body=%s, want rename source", okRR.Body.String())
	}

	srcReq := httptest.NewRequest(http.MethodGet, "/v1/layers/layer-entry-remap-child/entries?path="+url.QueryEscape("/secret/b.txt"), nil)
	srcReq = srcReq.WithContext(withScope(srcReq.Context(), &TenantScope{
		IsScoped: true,
		Backend:  s.fallback,
		FSScopes: []FSScope{{
			Prefix: "/secret",
			Ops:    map[FSOp]bool{FSOpRead: true},
		}},
	}))
	srcRR := httptest.NewRecorder()
	s.handleFSLayerEntryGet(srcRR, srcReq, s.fallback.Store(), child)
	if srcRR.Code != http.StatusOK {
		t.Fatalf("source-scope status=%d body=%s, want 200", srcRR.Code, srcRR.Body.String())
	}
	if strings.Contains(srcRR.Body.String(), "/repo/a.txt") {
		t.Fatalf("source-scope body=%s, dest should be redacted", srcRR.Body.String())
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo", 0o755); err != nil {
		t.Fatalf("mkdir /repo: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/secret", 0o755); err != nil {
		t.Fatalf("mkdir /secret: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/secret/b.txt", []byte("secret"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write secret: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-entry-landed-parent", BaseRootPath: "/",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-entry-landed-parent", fsLayerEntryRequest{
		Path: "/secret/b.txt", Op: "rename", Kind: "file", ContentText: "/repo/a.txt",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-entry-landed-parent", fsLayerForkRequest{LayerID: "layer-entry-landed-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-entry-landed-parent"); err != nil {
		t.Fatalf("commit parent: %v", err)
	}
	child, err := s.fallback.Store().GetFSLayer(ctx, "layer-entry-landed-child")
	if err != nil {
		t.Fatalf("get child: %v", err)
	}
	req := httptest.NewRequest(http.MethodGet, "/v1/layers/layer-entry-landed-child/entries?path="+url.QueryEscape("/repo/a.txt"), nil)
	req = req.WithContext(withScope(req.Context(), &TenantScope{
		IsScoped: true,
		Backend:  s.fallback,
		FSScopes: []FSScope{{
			Prefix: "/repo",
			Ops:    map[FSOp]bool{FSOpRead: true},
		}},
	}))
	rr := httptest.NewRecorder()
	s.handleFSLayerEntryGet(rr, req, s.fallback.Store(), child)
	if rr.Code != http.StatusOK {
		t.Fatalf("landed dest status=%d body=%s, want 200", rr.Code, rr.Body.String())
	}
}

func TestFSLayerObjectReadRemapRequiresSourceScope(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo", 0o755); err != nil {
		t.Fatalf("mkdir /repo: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/secret", 0o755); err != nil {
		t.Fatalf("mkdir /secret: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/secret/b.txt", []byte("secret"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write secret: %v", err)
	}
	if err := s.fallback.Store().CreateFSLayer(ctx, &datastore.FSLayer{
		LayerID: "layer-remap-auth", BaseRootPath: "/",
	}); err != nil {
		t.Fatalf("create layer: %v", err)
	}
	layer, err := s.fallback.Store().GetFSLayer(ctx, "layer-remap-auth")
	if err != nil {
		t.Fatalf("get layer: %v", err)
	}
	if err := s.fallback.Store().UpsertFSLayerEntry(ctx, &datastore.FSLayerEntry{
		LayerID: "layer-remap-auth", Path: "/secret/b.txt", Op: datastore.FSLayerEntryOpRename,
		Kind: datastore.FSLayerEntryKindFile, ContentText: "/repo/a.txt",
	}); err != nil {
		t.Fatalf("rename draft: %v", err)
	}
	req := httptest.NewRequest(http.MethodGet, "/v1/layers/layer-remap-auth/objects?path="+url.QueryEscape("/repo/a.txt"), nil)
	req = req.WithContext(withScope(req.Context(), &TenantScope{
		IsScoped: true,
		Backend:  s.fallback,
		FSScopes: []FSScope{{
			Prefix: "/repo",
			Ops:    map[FSOp]bool{FSOpRead: true},
		}},
	}))
	rr := httptest.NewRecorder()
	s.handleFSLayerObjectRead(rr, req, s.fallback, s.fallback.Store(), layer)
	if rr.Code != http.StatusForbidden {
		t.Fatalf("status=%d body=%s, want 403", rr.Code, rr.Body.String())
	}

	okReq := httptest.NewRequest(http.MethodGet, "/v1/layers/layer-remap-auth/objects?path="+url.QueryEscape("/repo/a.txt"), nil)
	okReq = okReq.WithContext(withScope(okReq.Context(), &TenantScope{
		IsScoped: true,
		Backend:  s.fallback,
		FSScopes: []FSScope{
			{Prefix: "/repo", Ops: map[FSOp]bool{FSOpRead: true}},
			{Prefix: "/secret", Ops: map[FSOp]bool{FSOpRead: true}},
		},
	}))
	okRR := httptest.NewRecorder()
	s.handleFSLayerObjectRead(okRR, okReq, s.fallback, s.fallback.Store(), layer)
	if okRR.Code != http.StatusOK {
		t.Fatalf("both-scopes status=%d body=%s, want 200", okRR.Code, okRR.Body.String())
	}
	if !bytes.Equal(okRR.Body.Bytes(), []byte("secret")) {
		t.Fatalf("both-scopes body=%q, want secret", okRR.Body.Bytes())
	}
}

func TestFSLayerObjectReadDirectoryMainFallback(t *testing.T) {
	s := newTestServer(t)
	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo", 0o755); err != nil {
		t.Fatalf("mkdir /repo: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/dir", 0o755); err != nil {
		t.Fatalf("mkdir /repo/dir: %v", err)
	}
	if err := s.fallback.Store().CreateFSLayer(ctx, &datastore.FSLayer{
		LayerID: "layer-dir-get", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("create layer: %v", err)
	}
	layer, err := s.fallback.Store().GetFSLayer(ctx, "layer-dir-get")
	if err != nil {
		t.Fatalf("get layer: %v", err)
	}
	req := httptest.NewRequest(http.MethodGet, "/v1/layers/layer-dir-get/objects?path="+url.QueryEscape("/repo/dir"), nil)
	req = req.WithContext(withScope(req.Context(), &TenantScope{Backend: s.fallback}))
	rr := httptest.NewRecorder()
	s.handleFSLayerObjectRead(rr, req, s.fallback, s.fallback.Store(), layer)
	if rr.Code != http.StatusBadRequest {
		t.Fatalf("status=%d body=%s, want 400", rr.Code, rr.Body.String())
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/old/: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/child.txt", []byte("keep"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write child: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-mkdir-exists", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	_, err := c.UpsertFSLayerEntry(ctx, "layer-mkdir-exists", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "mkdir", Kind: "dir", Mode: 0o755,
	})
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("mkdir existing err=%v, want 409", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-mkdir-exists", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-mkdir-exists")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v", err)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/new/child.txt"); err != nil {
		t.Fatalf("moved child missing: %v", err)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/old/child.txt"); !errors.Is(err, datastore.ErrNotFound) {
		t.Fatalf("old child still present: %v", err)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/old/: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/a.txt", []byte("old"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-dir-rename-upsert", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-rename-upsert", 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-upsert", fsLayerEntryRequest{
		Path: "/repo/new/a.txt", Op: "upsert", Kind: "file", Content: []byte("new"), SizeBytes: 3, Mode: 0o644,
	}); err != nil {
		t.Fatalf("upsert dest: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-dir-rename-upsert", fsLayerEntryRequest{
		Path: "/repo/new/extra.txt", Op: "upsert", Kind: "file", Content: []byte("x"), SizeBytes: 1, Mode: 0o644,
	}); err != nil {
		t.Fatalf("upsert dest-space create: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-dir-rename-upsert")
	if err != nil {
		t.Fatalf("CommitFSLayer: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	got, err := s.fallback.ReadCtx(ctx, "/repo/new/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx: %v", err)
	}
	if !bytes.Equal(got, []byte("new")) {
		t.Fatalf("content=%q, want new", got)
	}
	gotX, err := s.fallback.ReadCtx(ctx, "/repo/new/extra.txt", 0, -1)
	if err != nil {
		t.Fatalf("ReadCtx extra: %v", err)
	}
	if !bytes.Equal(gotX, []byte("x")) {
		t.Fatalf("extra=%q, want x", gotX)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/old/: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-mkdir-dest", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-mkdir-dest", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	_, err := c.UpsertFSLayerEntry(ctx, "layer-mkdir-dest", fsLayerEntryRequest{
		Path: "/repo/new/", Op: "mkdir", Kind: "dir", Mode: 0o755,
	})
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("mkdir dest err=%v, want 409", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/sub/", 0o755); err != nil {
		t.Fatalf("mkdir main sub: %v", err)
	}
	_, err = c.UpsertFSLayerEntry(ctx, "layer-mkdir-dest", fsLayerEntryRequest{
		Path: "/repo/new/sub/", Op: "mkdir", Kind: "dir", Mode: 0o755,
	})
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("mkdir remapped sub err=%v, want 409", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/a.txt", []byte("f"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write remapped file: %v", err)
	}
	_, err = c.UpsertFSLayerEntry(ctx, "layer-mkdir-dest", fsLayerEntryRequest{
		Path: "/repo/new/a.txt/", Op: "mkdir", Kind: "dir", Mode: 0o755,
	})
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("mkdir remapped file err=%v, want 409", err)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/old/: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-upsert-dir", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	_, err := c.UpsertFSLayerEntry(ctx, "layer-upsert-dir", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "upsert", Kind: "dir", Mode: 0o755,
	})
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("upsert dir err=%v, want 409", err)
	}
}

func TestFSLayerMkdirOverlayDirRejected(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-mkdir-twice", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-mkdir-twice", fsLayerEntryRequest{
		Path: "/repo/new/", Op: "mkdir", Kind: "dir", Mode: 0o755,
	}); err != nil {
		t.Fatalf("first mkdir: %v", err)
	}
	_, err := c.UpsertFSLayerEntry(ctx, "layer-mkdir-twice", fsLayerEntryRequest{
		Path: "/repo/new/", Op: "mkdir", Kind: "dir", Mode: 0o755,
	})
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("second mkdir err=%v, want 409", err)
	}
}

func TestFSLayerMkdirOverlayFileRejected(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-mkdir-over-file", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-mkdir-over-file", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "upsert", Kind: "file", Content: []byte("x"), SizeBytes: 1, Mode: 0o644,
	}); err != nil {
		t.Fatalf("upsert file: %v", err)
	}
	_, err := c.UpsertFSLayerEntry(ctx, "layer-mkdir-over-file", fsLayerEntryRequest{
		Path: "/repo/a.txt/", Op: "mkdir", Kind: "dir", Mode: 0o755,
	})
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("mkdir over overlay file err=%v, want 409", err)
	}
}

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

	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/a.txt", []byte("x"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write main: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-mkdir-main-file", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	_, err := c.UpsertFSLayerEntry(ctx, "layer-mkdir-main-file", fsLayerEntryRequest{
		Path: "/repo/a.txt/", Op: "mkdir", Kind: "dir", Mode: 0o755,
	})
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("mkdir over main file err=%v, want 409", err)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/sub/", 0o755); err != nil {
		t.Fatalf("mkdir sub: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{
		LayerID: "layer-landed-mkdir-parent", BaseRootPath: "/repo",
	}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-landed-mkdir-parent", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-landed-mkdir-parent", fsLayerForkRequest{LayerID: "layer-landed-mkdir-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-landed-mkdir-parent"); err != nil {
		t.Fatalf("commit parent: %v", err)
	}
	_, err := c.UpsertFSLayerEntry(ctx, "layer-landed-mkdir-child", fsLayerEntryRequest{
		Path: "/repo/new/sub/", Op: "mkdir", Kind: "dir", Mode: 0o755,
	})
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("mkdir landed dest sub err=%v, want 409", err)
	}
}

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

	ctx := context.Background()
	if err := s.fallback.MkdirCtx(ctx, "/repo/", 0o755); err != nil {
		t.Fatalf("mkdir /repo/: %v", err)
	}
	if err := s.fallback.MkdirCtx(ctx, "/repo/old/", 0o755); err != nil {
		t.Fatalf("mkdir old: %v", err)
	}
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(ctx, "/repo/old/a.txt", []byte("nested"), 0, filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate, -1, nil, ""); err != nil {
		t.Fatalf("write: %v", err)
	}
	c := newAPITestClient(ts.URL)
	if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: "layer-entry-remap-dir", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-entry-remap-dir", fsLayerEntryRequest{
		Path: "/repo/old/", Op: "rename", Kind: "dir", ContentText: "/repo/new/",
	}); err != nil {
		t.Fatalf("rename: %v", err)
	}
	if _, err := c.ForkFSLayer(ctx, "layer-entry-remap-dir", fsLayerForkRequest{LayerID: "layer-entry-remap-dir-child"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	got, err := c.GetFSLayerEntry(ctx, "layer-entry-remap-dir-child", "/repo/new/a.txt")
	if err != nil {
		t.Fatalf("entry GET dest: %v", err)
	}
	if got.Op != "rename" || got.Path != "/repo/old/a.txt" {
		t.Fatalf("entry=%+v, want rename from /repo/old/a.txt", got)
	}
}

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

	ctx := context.Background()
	c := newAPITestClient(ts.URL)
	for _, id := range []string{"layer-sib-root-a", "layer-sib-root-b"} {
		if _, err := c.CreateFSLayer(ctx, fsLayerCreateRequest{LayerID: id, BaseRootPath: "/repo"}); err != nil {
			t.Fatalf("CreateFSLayer %s: %v", id, err)
		}
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-sib-root-a", fsLayerEntryRequest{
		Path: "/repo/sib/f.txt", Op: "upsert", Kind: "file", Content: []byte("keep"), SizeBytes: 4, Mode: 0o644,
	}); err != nil {
		t.Fatalf("upsert A: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-sib-root-b", fsLayerEntryRequest{
		Path: "/repo/sib/", Op: "whiteout", Kind: "dir",
	}); err != nil {
		t.Fatalf("whiteout B: %v", err)
	}
	if _, err := c.CommitFSLayer(ctx, "layer-sib-root-a"); err != nil {
		t.Fatalf("commit A: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-sib-root-b")
	if err == nil {
		t.Fatalf("commit B err=nil commit=%+v, want conflict", commit)
	}
	var statusErr *apiStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusConflict {
		t.Fatalf("commit B err=%v, want 409", err)
	}
	if commit == nil || len(commit.Conflicts) == 0 || commit.Conflicts[0].Reason != "directory whiteout requires empty directory" {
		t.Fatalf("commit B conflicts=%+v", commit)
	}
	if _, err := s.fallback.StatNodeCtx(ctx, "/repo/sib/f.txt"); err != nil {
		t.Fatalf("A's child should remain: %v", err)
	}
}

func TestFSLayerCheckpointForkWriteThenCommit(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-cp-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-cp-parent", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "upsert", Kind: "file", Content: []byte("v1"), SizeBytes: 2, Mode: 0o644,
	}); err != nil {
		t.Fatalf("upsert v1: %v", err)
	}
	cp, err := c.CheckpointFSLayer(ctx, "layer-cp-parent", fsLayerCheckpointRequest{CheckpointID: "cp-writable", Label: "v1"})
	if err != nil {
		t.Fatalf("checkpoint: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-cp-parent", fsLayerEntryRequest{
		Path: "/repo/a.txt", Op: "upsert", Kind: "file", Content: []byte("v2"), SizeBytes: 2, Mode: 0o644,
	}); err != nil {
		t.Fatalf("upsert v2: %v", err)
	}
	child, err := c.ForkFSLayer(ctx, "layer-cp-parent", fsLayerForkRequest{
		LayerID: "layer-cp-child", CheckpointID: cp.CheckpointID,
	})
	if err != nil {
		t.Fatalf("fork: %v", err)
	}
	if child.State != "active" {
		t.Fatalf("child state=%s, want active", child.State)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-cp-child", fsLayerEntryRequest{
		Path: "/repo/b.txt", Op: "upsert", Kind: "file", Content: []byte("new"), SizeBytes: 3, Mode: 0o644,
	}); err != nil {
		t.Fatalf("child write: %v", err)
	}
	commit, err := c.CommitFSLayer(ctx, "layer-cp-child")
	if err != nil {
		t.Fatalf("commit child: %v commit=%+v", err, commit)
	}
	if commit.Status != "committed" {
		t.Fatalf("commit=%+v, want committed", commit)
	}
	gotA, err := s.fallback.ReadCtx(ctx, "/repo/a.txt", 0, -1)
	if err != nil {
		t.Fatalf("read a: %v", err)
	}
	if !bytes.Equal(gotA, []byte("v1")) {
		t.Fatalf("a=%q, want v1 (checkpoint pin, parent v2 not applied)", gotA)
	}
	gotB, err := s.fallback.ReadCtx(ctx, "/repo/b.txt", 0, -1)
	if err != nil {
		t.Fatalf("read b: %v", err)
	}
	if !bytes.Equal(gotB, []byte("new")) {
		t.Fatalf("b=%q, want new", gotB)
	}
}

func TestFSLayerRootObjectReadPointLookupUpsert(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-point", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if _, err := c.UpsertFSLayerEntry(ctx, "layer-point", fsLayerEntryRequest{
		Path: "/repo/only.txt", Op: "upsert", Kind: "file", Content: []byte("point"), SizeBytes: 5,
	}); err != nil {
		t.Fatalf("upsert: %v", err)
	}
	got, err := c.ReadFSLayerFile(ctx, "layer-point", "/repo/only.txt", nil)
	if err != nil {
		t.Fatalf("GET upsert: %v", err)
	}
	if !bytes.Equal(got, []byte("point")) {
		t.Fatalf("got %q, want point", got)
	}
}
