package datastore

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"
)

func TestForkFSLayerTipPinSeesUncheckpointedEntries(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "parent-tip", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if err := s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
		LayerID:     "parent-tip",
		Path:        "/repo/a.txt",
		Op:          FSLayerEntryOpUpsert,
		Kind:        FSLayerEntryKindFile,
		ContentBlob: []byte("hello"),
	}); err != nil {
		t.Fatalf("UpsertFSLayerEntry: %v", err)
	}
	// No checkpoint: durable_seq stays 0; tip pin must still see entry_seq=1.
	child, err := s.ForkFSLayer(ctx, FSLayerForkOptions{
		ChildLayerID:  "child-tip",
		ParentLayerID: "parent-tip",
		Name:          "child",
	})
	if err != nil {
		t.Fatalf("ForkFSLayer: %v", err)
	}
	if child.ParentLayerID != "parent-tip" {
		t.Fatalf("ParentLayerID=%q", child.ParentLayerID)
	}
	if child.OriginSeq != 1 {
		t.Fatalf("OriginSeq=%d, want 1 (MAX entry_seq)", child.OriginSeq)
	}
	if child.Depth != 1 {
		t.Fatalf("Depth=%d, want 1", child.Depth)
	}
	if child.RootLayerID != "parent-tip" {
		t.Fatalf("RootLayerID=%q, want parent-tip", child.RootLayerID)
	}
	entry, hit, err := s.ResolveFSLayerPath(ctx, "child-tip", "/repo/a.txt")
	if err != nil {
		t.Fatalf("ResolveFSLayerPath: %v", err)
	}
	if !hit || entry == nil || string(entry.ContentBlob) != "hello" {
		t.Fatalf("child should see parent tip entry, hit=%v entry=%+v", hit, entry)
	}
}

func TestForkFSLayerCheckpointPin(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "parent-cp", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("CreateFSLayer: %v", err)
	}
	if err := s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
		LayerID: "parent-cp", Path: "/repo/a.txt", Op: FSLayerEntryOpUpsert, Kind: FSLayerEntryKindFile, ContentBlob: []byte("v1"),
	}); err != nil {
		t.Fatalf("upsert1: %v", err)
	}
	cp := FSLayerCheckpoint{CheckpointID: "cp-1", LayerID: "parent-cp", Label: "v1"}
	if err := s.CreateFSLayerCheckpoint(ctx, &cp); err != nil {
		t.Fatalf("checkpoint: %v", err)
	}
	if err := s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
		LayerID: "parent-cp", Path: "/repo/a.txt", Op: FSLayerEntryOpUpsert, Kind: FSLayerEntryKindFile, ContentBlob: []byte("v2"),
	}); err != nil {
		t.Fatalf("upsert2: %v", err)
	}
	child, err := s.ForkFSLayer(ctx, FSLayerForkOptions{
		ChildLayerID:  "child-cp",
		ParentLayerID: "parent-cp",
		CheckpointID:  "cp-1",
	})
	if err != nil {
		t.Fatalf("ForkFSLayer: %v", err)
	}
	if child.OriginSeq != 1 {
		t.Fatalf("OriginSeq=%d, want 1 (checkpoint durable_seq)", child.OriginSeq)
	}
	entry, hit, err := s.ResolveFSLayerPath(ctx, "child-cp", "/repo/a.txt")
	if err != nil || !hit {
		t.Fatalf("resolve: hit=%v err=%v", hit, err)
	}
	if string(entry.ContentBlob) != "v1" {
		t.Fatalf("content=%q, want v1 (checkpoint pin)", entry.ContentBlob)
	}
}

func TestForkAndParentWritePinInvariant(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "p-race", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	errCh := make(chan error, 2)
	go func() {
		errCh <- s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
			LayerID: "p-race", Path: "/repo/a.txt", Op: FSLayerEntryOpUpsert,
			Kind: FSLayerEntryKindFile, ContentBlob: []byte("x"),
		})
	}()
	go func() {
		_, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "c-race", ParentLayerID: "p-race"})
		errCh <- err
	}()
	for i := 0; i < 2; i++ {
		if err := <-errCh; err != nil {
			t.Fatalf("concurrent op: %v", err)
		}
	}
	child, err := s.GetFSLayer(ctx, "c-race")
	if err != nil {
		t.Fatalf("get child: %v", err)
	}
	entry, err := s.GetFSLayerEntry(ctx, "p-race", "/repo/a.txt")
	if err != nil {
		t.Fatalf("get entry: %v", err)
	}
	_, hit, err := s.ResolveFSLayerPath(ctx, "c-race", "/repo/a.txt")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if entry.EntrySeq <= child.OriginSeq && !hit {
		t.Fatalf("origin_seq=%d entry_seq=%d: child must see pinned parent write", child.OriginSeq, entry.EntrySeq)
	}
	if entry.EntrySeq > child.OriginSeq && hit {
		t.Fatalf("origin_seq=%d entry_seq=%d: child must not see post-pin parent write", child.OriginSeq, entry.EntrySeq)
	}
}

func TestForkHidesParentWritesAfterPin(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "p-hide", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
		LayerID: "p-hide", Path: "/repo/a.txt", Op: FSLayerEntryOpUpsert, Kind: FSLayerEntryKindFile, ContentBlob: []byte("before"),
	}); err != nil {
		t.Fatalf("upsert before: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "c-hide", ParentLayerID: "p-hide"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if err := s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
		LayerID: "p-hide", Path: "/repo/b.txt", Op: FSLayerEntryOpUpsert, Kind: FSLayerEntryKindFile, ContentBlob: []byte("after"),
	}); err != nil {
		t.Fatalf("upsert after: %v", err)
	}
	_, hit, err := s.ResolveFSLayerPath(ctx, "c-hide", "/repo/b.txt")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if hit {
		t.Fatal("child must not see parent writes after fork pin")
	}
}

func TestDeleteFSLayerHasChildrenGate(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "p-del", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "c-del", ParentLayerID: "p-del"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	err := s.DeleteFSLayer(ctx, "p-del", DeleteFSLayerOptions{})
	if !errors.Is(err, ErrFSLayerHasChildren) {
		t.Fatalf("delete err=%v, want ErrFSLayerHasChildren", err)
	}
	if err := s.DeleteFSLayer(ctx, "p-del", DeleteFSLayerOptions{Cascade: true}); err != nil {
		t.Fatalf("cascade delete: %v", err)
	}
	parent, err := s.GetFSLayer(ctx, "p-del")
	if err != nil {
		t.Fatalf("get parent: %v", err)
	}
	if parent.State != FSLayerStateAbandoned {
		t.Fatalf("parent state=%s, want abandoned", parent.State)
	}
	child, err := s.GetFSLayer(ctx, "c-del")
	if err != nil {
		t.Fatalf("get child: %v", err)
	}
	if child.State != FSLayerStateAbandoned {
		t.Fatalf("child state=%s, want abandoned", child.State)
	}
}

func TestStillPinsAbandonedMiddleWithActiveGrandchild(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "root-pin", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create root: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "mid-pin", ParentLayerID: "root-pin"}); err != nil {
		t.Fatalf("fork mid: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "leaf-pin", ParentLayerID: "mid-pin"}); err != nil {
		t.Fatalf("fork leaf: %v", err)
	}
	if err := s.RollbackFSLayer(ctx, "mid-pin"); err != nil {
		t.Fatalf("abandon mid: %v", err)
	}
	pins, err := s.FSLayerStillPins(ctx, "root-pin")
	if err != nil {
		t.Fatalf("still pins: %v", err)
	}
	if !pins {
		t.Fatal("root should still pin via active grandchild")
	}
	err = s.DeleteFSLayer(ctx, "root-pin", DeleteFSLayerOptions{})
	if !errors.Is(err, ErrFSLayerHasChildren) {
		t.Fatalf("delete root err=%v, want ErrFSLayerHasChildren", err)
	}
}

func TestListFSLayerChainAndMerged(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "root-m", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
		LayerID: "root-m", Path: "/repo/a.txt", Op: FSLayerEntryOpUpsert, Kind: FSLayerEntryKindFile, ContentBlob: []byte("root"),
	}); err != nil {
		t.Fatalf("upsert root: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "child-m", ParentLayerID: "root-m"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if err := s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
		LayerID: "child-m", Path: "/repo/b.txt", Op: FSLayerEntryOpUpsert, Kind: FSLayerEntryKindFile, ContentBlob: []byte("child"),
	}); err != nil {
		t.Fatalf("upsert child: %v", err)
	}
	chain, err := s.ListFSLayerChain(ctx, "child-m")
	if err != nil {
		t.Fatalf("chain: %v", err)
	}
	if len(chain) != 2 {
		t.Fatalf("chain len=%d, want 2", len(chain))
	}
	if chain[0].Layer.LayerID != "root-m" || chain[1].Layer.LayerID != "child-m" {
		t.Fatalf("chain order wrong: %+v", chain)
	}
	merged, err := s.ListFSLayerChainMergedEntries(ctx, "child-m")
	if err != nil {
		t.Fatalf("merged: %v", err)
	}
	if len(merged) != 2 {
		t.Fatalf("merged len=%d, want 2", len(merged))
	}
}

func TestForkRejectsCheckpointFromOtherLayer(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "p-cp-a", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create a: %v", err)
	}
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "p-cp-b", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create b: %v", err)
	}
	if err := s.CreateFSLayerCheckpoint(ctx, &FSLayerCheckpoint{CheckpointID: "cp-b", LayerID: "p-cp-b"}); err != nil {
		t.Fatalf("checkpoint: %v", err)
	}
	_, err := s.ForkFSLayer(ctx, FSLayerForkOptions{
		ChildLayerID:  "c-cp-mismatch",
		ParentLayerID: "p-cp-a",
		CheckpointID:  "cp-b",
	})
	if !errors.Is(err, ErrFSLayerStateConflict) {
		t.Fatalf("err=%v, want ErrFSLayerStateConflict", err)
	}
}

func TestDeleteFSLayerRejectsCommitting(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "p-commit-del", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := s.BeginFSLayerCommit(ctx, "p-commit-del"); err != nil {
		t.Fatalf("begin commit: %v", err)
	}
	err := s.DeleteFSLayer(ctx, "p-commit-del", DeleteFSLayerOptions{})
	if !errors.Is(err, ErrFSLayerStateConflict) {
		t.Fatalf("delete err=%v, want ErrFSLayerStateConflict", err)
	}
}

func TestDeleteFSLayerCascadeThroughAbandonedMiddle(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "root-cas", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create root: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "mid-cas", ParentLayerID: "root-cas"}); err != nil {
		t.Fatalf("fork mid: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "leaf-cas", ParentLayerID: "mid-cas"}); err != nil {
		t.Fatalf("fork leaf: %v", err)
	}
	if err := s.RollbackFSLayer(ctx, "mid-cas"); err != nil {
		t.Fatalf("abandon mid: %v", err)
	}
	if err := s.DeleteFSLayer(ctx, "root-cas", DeleteFSLayerOptions{Cascade: true}); err != nil {
		t.Fatalf("cascade delete: %v", err)
	}
	for _, id := range []string{"root-cas", "mid-cas", "leaf-cas"} {
		got, err := s.GetFSLayer(ctx, id)
		if err != nil {
			t.Fatalf("get %s: %v", id, err)
		}
		if got.State != FSLayerStateAbandoned {
			t.Fatalf("%s state=%s, want abandoned", id, got.State)
		}
	}
}

func TestResolveFSLayerPathAncestorDirWhiteout(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "wo-root", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
		LayerID: "wo-root", Path: "/repo/dir/", Op: FSLayerEntryOpWhiteout, Kind: FSLayerEntryKindDir,
	}); err != nil {
		t.Fatalf("whiteout: %v", err)
	}
	entry, hit, err := s.ResolveFSLayerPath(ctx, "wo-root", "/repo/dir/a.txt")
	if err != nil {
		t.Fatalf("resolve: %v", err)
	}
	if !hit || entry == nil || entry.Op != FSLayerEntryOpWhiteout {
		t.Fatalf("hit=%v entry=%+v, want whiteout", hit, entry)
	}
}

func TestDeleteFSLayerCommittedKeepsApplyMark(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "del-applied", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := s.BeginFSLayerCommit(ctx, "del-applied"); err != nil {
		t.Fatalf("begin commit: %v", err)
	}
	if err := s.FinalizeFSLayerCommit(ctx, "del-applied"); err != nil {
		t.Fatalf("finalize: %v", err)
	}
	if err := s.DeleteFSLayer(ctx, "del-applied", DeleteFSLayerOptions{}); err != nil {
		t.Fatalf("delete committed: %v", err)
	}
	got, err := s.GetFSLayer(ctx, "del-applied")
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	if got.State != FSLayerStateAbandoned {
		t.Fatalf("state=%s, want abandoned", got.State)
	}
	if !got.CommitApplied {
		t.Fatal("commit_applied missing after DELETE of committed layer")
	}
}

func TestDeleteFSLayerSeededCommittedSetsApplyMark(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "del-seeded", BaseRootPath: "/repo", State: FSLayerStateCommitted}); err != nil {
		t.Fatalf("create: %v", err)
	}
	before, err := s.GetFSLayer(ctx, "del-seeded")
	if err != nil {
		t.Fatalf("get before: %v", err)
	}
	if before.CommitApplied {
		t.Fatal("seeded committed row should start without commit_applied")
	}
	if err := s.DeleteFSLayer(ctx, "del-seeded", DeleteFSLayerOptions{}); err != nil {
		t.Fatalf("delete: %v", err)
	}
	got, err := s.GetFSLayer(ctx, "del-seeded")
	if err != nil {
		t.Fatalf("get after: %v", err)
	}
	if got.State != FSLayerStateAbandoned || !got.CommitApplied {
		t.Fatalf("after delete state=%s applied=%v, want abandoned+applied", got.State, got.CommitApplied)
	}
}

func TestForkRejectsDuplicateChildID(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "dup-parent", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create parent: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "dup-child", ParentLayerID: "dup-parent"}); err != nil {
		t.Fatalf("first fork: %v", err)
	}
	_, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "dup-child", ParentLayerID: "dup-parent"})
	if !errors.Is(err, ErrFSLayerStateConflict) {
		t.Fatalf("err=%v, want ErrFSLayerStateConflict", err)
	}
}

func TestForkRejectsDepthCap(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "depth-0", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create root: %v", err)
	}
	parent := "depth-0"
	for i := 1; i <= FSLayerDefaultMaxDepth; i++ {
		child := fmt.Sprintf("depth-%d", i)
		if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: child, ParentLayerID: parent}); err != nil {
			t.Fatalf("fork depth %d: %v", i, err)
		}
		parent = child
	}
	_, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "depth-overflow", ParentLayerID: parent})
	if !errors.Is(err, ErrFSLayerDepthExceeded) {
		t.Fatalf("err=%v, want ErrFSLayerDepthExceeded", err)
	}
}

func TestListFSLayersByRootOmitsOtherTrees(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "root-a", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create a: %v", err)
	}
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "root-b", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create b: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "child-a", ParentLayerID: "root-a"}); err != nil {
		t.Fatalf("fork a: %v", err)
	}
	got, err := s.ListFSLayersByRoot(ctx, "root-a")
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	ids := make(map[string]struct{}, len(got))
	for i := range got {
		ids[got[i].LayerID] = struct{}{}
	}
	if _, ok := ids["root-a"]; !ok {
		t.Fatalf("ids=%v, want root-a", ids)
	}
	if _, ok := ids["child-a"]; !ok {
		t.Fatalf("ids=%v, want child-a", ids)
	}
	if _, ok := ids["root-b"]; ok {
		t.Fatalf("ids=%v, must not include root-b", ids)
	}
	meta, err := s.ListFSLayersByRootMeta(ctx, "root-a")
	if err != nil {
		t.Fatalf("list meta: %v", err)
	}
	if len(meta) != len(got) {
		t.Fatalf("meta=%d tagged=%d", len(meta), len(got))
	}
	for i := range meta {
		if meta[i].Tags != nil {
			t.Fatalf("meta[%s] tags=%v, want nil", meta[i].LayerID, meta[i].Tags)
		}
	}
}

func TestForkRejectsAbandonedParent(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "p-abandoned", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := s.RollbackFSLayer(ctx, "p-abandoned"); err != nil {
		t.Fatalf("rollback: %v", err)
	}
	_, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "c-abandoned", ParentLayerID: "p-abandoned"})
	if !errors.Is(err, ErrFSLayerStateConflict) {
		t.Fatalf("err=%v, want ErrFSLayerStateConflict", err)
	}
}

func TestChildReadsPinnedPrefixAfterParentRollback(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "p-rb-pin", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
		LayerID: "p-rb-pin", Path: "/repo/a.txt", Op: FSLayerEntryOpUpsert,
		Kind: FSLayerEntryKindFile, ContentBlob: []byte("pinned"),
	}); err != nil {
		t.Fatalf("upsert: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "c-rb-pin", ParentLayerID: "p-rb-pin"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "g-rb-pin", ParentLayerID: "c-rb-pin"}); err != nil {
		t.Fatalf("fork grandchild: %v", err)
	}
	if err := s.RollbackFSLayer(ctx, "p-rb-pin"); err != nil {
		t.Fatalf("rollback parent: %v", err)
	}
	for _, id := range []string{"c-rb-pin", "g-rb-pin"} {
		entry, hit, err := s.ResolveFSLayerPath(ctx, id, "/repo/a.txt")
		if err != nil {
			t.Fatalf("resolve %s: %v", id, err)
		}
		if !hit || entry == nil || string(entry.ContentBlob) != "pinned" {
			t.Fatalf("%s after parent rollback hit=%v entry=%+v, want pinned body", id, hit, entry)
		}
	}
}

func TestCheckpointForkIsWritable(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "p-cp-w", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if err := s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
		LayerID: "p-cp-w", Path: "/repo/a.txt", Op: FSLayerEntryOpUpsert,
		Kind: FSLayerEntryKindFile, ContentBlob: []byte("v1"),
	}); err != nil {
		t.Fatalf("upsert parent: %v", err)
	}
	if err := s.CreateFSLayerCheckpoint(ctx, &FSLayerCheckpoint{CheckpointID: "cp-w", LayerID: "p-cp-w"}); err != nil {
		t.Fatalf("checkpoint: %v", err)
	}
	child, err := s.ForkFSLayer(ctx, FSLayerForkOptions{
		ChildLayerID: "c-cp-w", ParentLayerID: "p-cp-w", CheckpointID: "cp-w",
	})
	if err != nil {
		t.Fatalf("fork: %v", err)
	}
	if child.State != FSLayerStateActive {
		t.Fatalf("child state=%s, want active", child.State)
	}
	if err := s.UpsertFSLayerEntry(ctx, &FSLayerEntry{
		LayerID: "c-cp-w", Path: "/repo/b.txt", Op: FSLayerEntryOpUpsert,
		Kind: FSLayerEntryKindFile, ContentBlob: []byte("child"),
	}); err != nil {
		t.Fatalf("child write: %v", err)
	}
	entry, hit, err := s.ResolveFSLayerPath(ctx, "c-cp-w", "/repo/b.txt")
	if err != nil || !hit || entry == nil || string(entry.ContentBlob) != "child" {
		t.Fatalf("child write resolve hit=%v entry=%+v err=%v", hit, entry, err)
	}
}

func TestDeleteFSLayerEmptyIDIsInvalidArgument(t *testing.T) {
	s := newTestStore(t)
	err := s.DeleteFSLayer(context.Background(), "  ", DeleteFSLayerOptions{})
	if !errors.Is(err, ErrFSLayerInvalidArgument) {
		t.Fatalf("err=%v, want ErrFSLayerInvalidArgument", err)
	}
}

func TestDeleteFSLayerHasChildrenListsDescendants(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "p-list", BaseRootPath: "/repo"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if _, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "c-list", ParentLayerID: "p-list"}); err != nil {
		t.Fatalf("fork: %v", err)
	}
	err := s.DeleteFSLayer(ctx, "p-list", DeleteFSLayerOptions{})
	if !errors.Is(err, ErrFSLayerHasChildren) {
		t.Fatalf("err=%v, want ErrFSLayerHasChildren", err)
	}
	if !strings.Contains(err.Error(), "c-list") {
		t.Fatalf("err=%v, want descendant id listed", err)
	}
}

func TestForkRejectsCommittedParent(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	if err := s.CreateFSLayer(ctx, &FSLayer{LayerID: "p-committed", BaseRootPath: "/repo", State: FSLayerStateCommitted}); err != nil {
		t.Fatalf("create: %v", err)
	}
	_, err := s.ForkFSLayer(ctx, FSLayerForkOptions{ChildLayerID: "c-committed", ParentLayerID: "p-committed"})
	if !errors.Is(err, ErrFSLayerStateConflict) {
		t.Fatalf("err=%v, want ErrFSLayerStateConflict", err)
	}
}
