package server

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

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

	"github.com/mem9-ai/drive9/pkg/datastore"
	"github.com/mem9-ai/drive9/pkg/gitwsindex"
	"github.com/mem9-ai/drive9/pkg/tenant/schema"
)

func ensureGitWorkspaceSchema(t *testing.T, s *Server) {
	t.Helper()
	for _, stmt := range schema.GitWorkspaceTiDBSchemaStatements() {
		if _, err := s.fallback.Store().DB().Exec(stmt); err != nil {
			msg := strings.ToLower(err.Error())
			if strings.Contains(msg, "duplicate key name") || strings.Contains(msg, "already exists") || strings.Contains(msg, "duplicate column") {
				continue
			}
			t.Fatal(err)
		}
	}
}

func writeGitWorkspaceIndexForTest(t *testing.T, s *Server, body []byte) {
	t.Helper()
	ctx := context.Background()
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(
		ctx,
		gitWorkspaceIndexPath,
		body,
		0,
		filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate,
		-1,
		nil,
		"",
	); err != nil {
		t.Fatalf("seed git workspace index: %v", err)
	}
}

func TestGitWorkspaceDeleteRemovesIndexEntry(t *testing.T) {
	s := newTestServer(t)
	ensureGitWorkspaceSchema(t, s)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)

	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	ws, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo/",
		RepoURL:    "https://example.test/repo.git",
		RemoteName: "origin",
		HeadCommit: "1111111111111111111111111111111111111111",
	})
	if err != nil {
		t.Fatalf("UpsertGitWorkspace: %v", err)
	}

	idxBody, err := json.MarshalIndent(gitwsindex.Index{
		Version: 1,
		Workspaces: []gitwsindex.Entry{
			{WorkspaceID: ws.WorkspaceID, RootPath: "/repo/"},
			{WorkspaceID: "ws-other", RootPath: "/other/"},
		},
	}, "", "  ")
	if err != nil {
		t.Fatal(err)
	}
	writeGitWorkspaceIndexForTest(t, s, idxBody)

	if err := c.DeleteGitWorkspace(ctx, ws.WorkspaceID); err != nil {
		t.Fatalf("DeleteGitWorkspace: %v", err)
	}

	data, err := s.fallback.ReadCtx(ctx, gitWorkspaceIndexPath, 0, -1)
	if err != nil {
		t.Fatalf("read index after delete: %v", err)
	}
	idx, err := gitwsindex.Parse(data)
	if err != nil {
		t.Fatalf("parse index: %v", err)
	}
	if len(idx.Workspaces) != 1 || idx.Workspaces[0].WorkspaceID != "ws-other" {
		t.Fatalf("index after delete = %+v, want only ws-other", idx.Workspaces)
	}
}

func TestGitWorkspaceDeleteIdempotentCleansStaleIndex(t *testing.T) {
	// Convergent repair: workspace row already non-live, index still lists it.
	// A second DELETE must succeed and remove the stale index entry.
	// Also covers "new server, old client partial failure" recovery.
	s := newTestServer(t)
	ensureGitWorkspaceSchema(t, s)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)

	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	ws, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo2/",
		RepoURL:    "https://example.test/repo2.git",
		RemoteName: "origin",
		HeadCommit: "2222222222222222222222222222222222222222",
	})
	if err != nil {
		t.Fatalf("UpsertGitWorkspace: %v", err)
	}
	// Soft-delete row only (bypass handler) to simulate partial prior failure.
	if err := s.fallback.Store().DeleteGitWorkspace(ctx, ws.WorkspaceID); err != nil {
		t.Fatalf("store.DeleteGitWorkspace: %v", err)
	}
	idxBody, err := json.MarshalIndent(gitwsindex.Index{
		Version: 1,
		Workspaces: []gitwsindex.Entry{
			{WorkspaceID: ws.WorkspaceID, RootPath: "/repo2/"},
		},
	}, "", "  ")
	if err != nil {
		t.Fatal(err)
	}
	writeGitWorkspaceIndexForTest(t, s, idxBody)

	if err := c.DeleteGitWorkspace(ctx, ws.WorkspaceID); err != nil {
		t.Fatalf("DeleteGitWorkspace repair: %v", err)
	}
	data, err := s.fallback.ReadCtx(ctx, gitWorkspaceIndexPath, 0, -1)
	if err != nil {
		t.Fatalf("read index after repair delete: %v", err)
	}
	idx, err := gitwsindex.Parse(data)
	if err != nil {
		t.Fatalf("parse index: %v", err)
	}
	if len(idx.Workspaces) != 0 {
		t.Fatalf("index after repair = %+v, want empty workspaces", idx.Workspaces)
	}

	// Third call still OK (fully clean) — new clients/old tooling can retry freely.
	if err := c.DeleteGitWorkspace(ctx, ws.WorkspaceID); err != nil {
		t.Fatalf("DeleteGitWorkspace already clean: %v", err)
	}
}

func TestGitWorkspaceDeleteWithoutIndexIsOK(t *testing.T) {
	// Old deployments / never --fast: no index file. DELETE must still succeed.
	s := newTestServer(t)
	ensureGitWorkspaceSchema(t, s)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)

	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	ws, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo3/",
		RepoURL:    "https://example.test/repo3.git",
		RemoteName: "origin",
		HeadCommit: "3333333333333333333333333333333333333333",
	})
	if err != nil {
		t.Fatalf("UpsertGitWorkspace: %v", err)
	}
	if err := c.DeleteGitWorkspace(ctx, ws.WorkspaceID); err != nil {
		t.Fatalf("DeleteGitWorkspace without index: %v", err)
	}
}

func TestGitWorkspaceDeleteIndexFailureStillReturns200(t *testing.T) {
	// Soft-delete is the durable state; index is an arming hint. A permanent
	// index parse failure must not turn DELETE into 500/EIO for the caller.
	s := newTestServer(t)
	ensureGitWorkspaceSchema(t, s)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)

	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	ws, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-bad-idx/",
		RepoURL:    "https://example.test/repo-bad-idx.git",
		RemoteName: "origin",
		HeadCommit: "4444444444444444444444444444444444444444",
	})
	if err != nil {
		t.Fatalf("UpsertGitWorkspace: %v", err)
	}
	writeGitWorkspaceIndexForTest(t, s, []byte("{"))

	if err := c.DeleteGitWorkspace(ctx, ws.WorkspaceID); err != nil {
		t.Fatalf("DeleteGitWorkspace with bad index: %v (want 200)", err)
	}
	rebuilt, err := s.fallback.ReadCtx(ctx, gitWorkspaceIndexPath, 0, maxGitWorkspaceIndexBytes+1)
	if err != nil {
		t.Fatalf("read rebuilt index: %v", err)
	}
	idx, err := gitwsindex.Parse(rebuilt)
	if err != nil {
		t.Fatalf("parse rebuilt index: %v", err)
	}
	for _, e := range idx.Workspaces {
		if e.WorkspaceID == ws.WorkspaceID {
			t.Fatalf("rebuilt index still lists deleted %s", ws.WorkspaceID)
		}
	}
	got, err := s.fallback.Store().GetGitWorkspace(ctx, ws.WorkspaceID)
	if err != nil {
		t.Fatalf("GetGitWorkspace after DELETE: %v", err)
	}
	if got.Status != datastore.GitWorkspaceStatusDeleted {
		t.Fatalf("workspace status = %q, want %q", got.Status, datastore.GitWorkspaceStatusDeleted)
	}
	if err := c.DeleteGitWorkspace(ctx, ws.WorkspaceID); err != nil {
		t.Fatalf("DeleteGitWorkspace retry: %v", err)
	}
}

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

	c := newAPITestClient(ts.URL)
	mainWS, err := c.UpsertGitWorkspace(context.Background(), gitWorkspaceRequest{
		RootPath:   "/repo/",
		RepoURL:    "https://example.test/repo.git",
		RemoteName: "origin",
		HeadCommit: "1111111111111111111111111111111111111111",
	})
	if err != nil {
		t.Fatalf("UpsertGitWorkspace main: %v", err)
	}

	body, err := json.Marshal(gitWorkspaceRequest{
		RootPath:          "/repo/",
		RepoURL:           "https://example.test/repo.git",
		RemoteName:        "origin",
		HeadCommit:        "1111111111111111111111111111111111111111",
		WorkspaceKind:     "linked",
		CommonWorkspaceID: mainWS.WorkspaceID,
		WorktreeName:      "repo-wt",
		GitDirRel:         "worktrees/repo-wt",
	})
	if err != nil {
		t.Fatal(err)
	}
	resp, err := http.Post(ts.URL+"/v1/git-workspaces", "application/json", bytes.NewReader(body))
	if err != nil {
		t.Fatal(err)
	}
	defer func() { _ = resp.Body.Close() }()
	gotBody, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("read response body: %v", err)
	}
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400, body=%s", resp.StatusCode, gotBody)
	}
	if !strings.Contains(string(gotBody), "cannot reference itself") {
		t.Fatalf("body = %s, want self-link error", gotBody)
	}
}

func TestGitWorkspaceSubresourcesRejectedAfterDelete(t *testing.T) {
	s := newTestServer(t)
	ensureGitWorkspaceSchema(t, s)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)

	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	ws, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-sub/",
		RepoURL:    "https://example.test/repo-sub.git",
		RemoteName: "origin",
		HeadCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
	})
	if err != nil {
		t.Fatalf("UpsertGitWorkspace: %v", err)
	}
	if _, err := c.PutGitOverlayEntry(ctx, ws.WorkspaceID, gitOverlayEntryRequest{
		Path: "dirty.txt", Op: "upsert", Kind: "file", Content: []byte("old"),
	}); err != nil {
		t.Fatalf("PutGitOverlayEntry: %v", err)
	}
	if err := c.ReplaceGitTree(ctx, ws.WorkspaceID, gitTreeReplaceRequest{
		CommitSHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
		Nodes: []gitTreeNodeResponse{{
			Path: "README.md", Name: "README.md", Kind: "file", Mode: "100644",
			ObjectSHA: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
		}},
	}); err != nil {
		t.Fatalf("ReplaceGitTree: %v", err)
	}
	if _, err := c.UpsertGitState(ctx, ws.WorkspaceID, gitStateRequest{
		CheckpointCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
	}); err != nil {
		t.Fatalf("UpsertGitState: %v", err)
	}
	if _, err := c.PutGitObjectPack(ctx, ws.WorkspaceID, gitObjectPackRequest{
		Content: []byte("PACK-stale"),
	}); err != nil {
		t.Fatalf("PutGitObjectPack: %v", err)
	}

	writeGitWorkspaceIndexForTest(t, s, []byte("{"))
	if err := c.DeleteGitWorkspace(ctx, ws.WorkspaceID); err != nil {
		t.Fatalf("DeleteGitWorkspace: %v", err)
	}

	if _, err := c.PutGitOverlayEntry(ctx, ws.WorkspaceID, gitOverlayEntryRequest{
		Path: "after.txt", Op: "upsert", Kind: "file", Content: []byte("new"),
	}); !apiIsNotFound(err) {
		t.Fatalf("overlay write after delete: %v, want NotFound", err)
	}
	if _, err := c.GetGitOverlayEntry(ctx, ws.WorkspaceID, "dirty.txt"); !apiIsNotFound(err) {
		t.Fatalf("overlay read after delete: %v, want NotFound", err)
	}
	if _, err := c.ListGitOverlayEntries(ctx, ws.WorkspaceID); !apiIsNotFound(err) {
		t.Fatalf("overlay list after delete: %v, want NotFound", err)
	}
	if err := c.ReplaceGitTree(ctx, ws.WorkspaceID, gitTreeReplaceRequest{
		CommitSHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
		Nodes: []gitTreeNodeResponse{{
			Path: "README.md", Name: "README.md", Kind: "file", Mode: "100644",
			ObjectSHA: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
		}},
	}); !apiIsNotFound(err) {
		t.Fatalf("tree write after delete: %v, want NotFound", err)
	}
	if _, err := c.ListGitTree(ctx, ws.WorkspaceID, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); !apiIsNotFound(err) {
		t.Fatalf("tree list after delete: %v, want NotFound", err)
	}
	if _, err := c.UpsertGitState(ctx, ws.WorkspaceID, gitStateRequest{
		CheckpointCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
	}); !apiIsNotFound(err) {
		t.Fatalf("git-state write after delete: %v, want NotFound", err)
	}
	if _, err := c.GetGitState(ctx, ws.WorkspaceID); !apiIsNotFound(err) {
		t.Fatalf("git-state read after delete: %v, want NotFound", err)
	}
	if _, err := c.PutGitObjectPack(ctx, ws.WorkspaceID, gitObjectPackRequest{Content: []byte("PACK-new")}); !apiIsNotFound(err) {
		t.Fatalf("object-pack write after delete: %v, want NotFound", err)
	}

	recreated, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-sub/",
		RepoURL:    "https://example.test/repo-sub.git",
		RemoteName: "origin",
		HeadCommit: "cccccccccccccccccccccccccccccccccccccccc",
	})
	if err != nil {
		t.Fatalf("recreate UpsertGitWorkspace: %v", err)
	}
	if recreated.WorkspaceID == ws.WorkspaceID {
		t.Fatalf("recreate reused deleted workspace id %s", ws.WorkspaceID)
	}
	if _, err := c.GetGitWorkspace(ctx, ws.WorkspaceID); !apiIsNotFound(err) {
		t.Fatalf("GET old workspace id after recreate: %v, want NotFound", err)
	}
	if _, err := c.PutGitOverlayEntry(ctx, ws.WorkspaceID, gitOverlayEntryRequest{
		Path: "stale-runtime.txt", Op: "upsert", Kind: "file", Content: []byte("from-old-mount"),
	}); !apiIsNotFound(err) {
		t.Fatalf("old-generation overlay write after recreate: %v, want NotFound", err)
	}
	entries, err := c.ListGitOverlayEntries(ctx, recreated.WorkspaceID)
	if err != nil {
		t.Fatalf("list overlay after recreate: %v", err)
	}
	if len(entries) != 0 {
		t.Fatalf("recreate inherited overlay = %+v", entries)
	}
	if _, err := c.PutGitOverlayEntry(ctx, recreated.WorkspaceID, gitOverlayEntryRequest{
		Path: "fresh.txt", Op: "upsert", Kind: "file", Content: []byte("new-gen"),
	}); err != nil {
		t.Fatalf("new-generation overlay write: %v", err)
	}
}

func TestGitWorkspaceLiveUpsertKeepsID(t *testing.T) {
	s := newTestServer(t)
	ensureGitWorkspaceSchema(t, s)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)

	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	first, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-live/",
		RepoURL:    "https://example.test/repo-live.git",
		RemoteName: "origin",
		HeadCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
	})
	if err != nil {
		t.Fatalf("first upsert: %v", err)
	}
	second, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-live/",
		RepoURL:    "https://example.test/repo-live.git",
		RemoteName: "origin",
		HeadCommit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
	})
	if err != nil {
		t.Fatalf("live upsert: %v", err)
	}
	if second.WorkspaceID != first.WorkspaceID {
		t.Fatalf("live upsert id = %s, want %s", second.WorkspaceID, first.WorkspaceID)
	}
}

func TestGitWorkspaceUpsertMintsNewIDWhenDeleteInterleavesAfterLiveRead(t *testing.T) {
	s := newTestServer(t)
	ensureGitWorkspaceSchema(t, s)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)

	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	old, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-race/",
		RepoURL:    "https://example.test/repo-race.git",
		RemoteName: "origin",
		HeadCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
	})
	if err != nil {
		t.Fatalf("seed upsert: %v", err)
	}
	if _, err := c.PutGitOverlayEntry(ctx, old.WorkspaceID, gitOverlayEntryRequest{
		Path: "stale.txt", Op: "upsert", Kind: "file", Content: []byte("old"),
	}); err != nil {
		t.Fatalf("seed overlay: %v", err)
	}

	lookupSeen := make(chan struct{})
	resume := make(chan struct{})
	var once sync.Once
	var closeResume sync.Once
	testHookAfterGitWorkspaceLookup = func() {
		once.Do(func() { close(lookupSeen) })
		<-resume
	}
	t.Cleanup(func() {
		closeResume.Do(func() { close(resume) })
		testHookAfterGitWorkspaceLookup = nil
	})

	type upsertResult struct {
		ws  *gitWorkspaceResponse
		err error
	}
	ch := make(chan upsertResult, 1)
	go func() {
		ws, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
			RootPath:   "/repo-race/",
			RepoURL:    "https://example.test/repo-race.git",
			RemoteName: "origin",
			HeadCommit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
		})
		ch <- upsertResult{ws: ws, err: err}
	}()
	select {
	case <-lookupSeen:
	case <-time.After(5 * time.Second):
		t.Fatal("upsert never reached lookup hook")
	}
	if err := c.DeleteGitWorkspace(ctx, old.WorkspaceID); err != nil {
		t.Fatalf("interleaved DELETE: %v", err)
	}
	closeResume.Do(func() { close(resume) })
	got := <-ch
	if got.err != nil {
		t.Fatalf("upsert after interleaved DELETE: %v", got.err)
	}
	if got.ws == nil || got.ws.WorkspaceID == old.WorkspaceID {
		t.Fatalf("upsert reused old id %s after DELETE", old.WorkspaceID)
	}
	if _, err := c.GetGitWorkspace(ctx, old.WorkspaceID); !apiIsNotFound(err) {
		t.Fatalf("GET old id after interleaved recreate: %v, want NotFound", err)
	}
	if _, err := c.PutGitOverlayEntry(ctx, old.WorkspaceID, gitOverlayEntryRequest{
		Path: "from-stale-runtime.txt", Op: "upsert", Kind: "file", Content: []byte("nope"),
	}); !apiIsNotFound(err) {
		t.Fatalf("old-id overlay write after interleaved recreate: %v, want NotFound", err)
	}
}

func TestCommitGitWorkspaceDoesNotReturnSuccessorGeneration(t *testing.T) {
	s := newTestServer(t)
	ensureGitWorkspaceSchema(t, s)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)

	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	old, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-gen/",
		RepoURL:    "https://example.test/repo-gen.git",
		RemoteName: "origin",
		HeadCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
	})
	if err != nil {
		t.Fatalf("seed upsert: %v", err)
	}

	committed := make(chan struct{})
	resume := make(chan struct{})
	var parkFirst atomic.Bool
	parkFirst.Store(true)
	var closeResume sync.Once
	datastore.TestHookAfterCommitGitWorkspaceTx = func() {
		if !parkFirst.CompareAndSwap(true, false) {
			return
		}
		close(committed)
		<-resume
	}
	t.Cleanup(func() {
		closeResume.Do(func() { close(resume) })
		datastore.TestHookAfterCommitGitWorkspaceTx = nil
	})

	type upsertResult struct {
		ws  *gitWorkspaceResponse
		err error
	}
	ch := make(chan upsertResult, 1)
	go func() {
		ws, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
			RootPath:   "/repo-gen/",
			RepoURL:    "https://example.test/repo-gen.git",
			RemoteName: "origin",
			HeadCommit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
		})
		ch <- upsertResult{ws: ws, err: err}
	}()
	select {
	case <-committed:
	case <-time.After(5 * time.Second):
		t.Fatal("commit hook never ran")
	}
	if err := c.DeleteGitWorkspace(ctx, old.WorkspaceID); err != nil {
		t.Fatalf("delete after commit: %v", err)
	}
	newer, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-gen/",
		RepoURL:    "https://example.test/repo-gen.git",
		RemoteName: "origin",
		HeadCommit: "cccccccccccccccccccccccccccccccccccccccc",
	})
	if err != nil {
		t.Fatalf("recreate after commit: %v", err)
	}
	closeResume.Do(func() { close(resume) })
	got := <-ch
	if got.err != nil {
		t.Fatalf("older upsert: %v", got.err)
	}
	if got.ws == nil || got.ws.WorkspaceID != old.WorkspaceID {
		t.Fatalf("older upsert id = %v, want original %s", got.ws, old.WorkspaceID)
	}
	if newer.WorkspaceID == old.WorkspaceID {
		t.Fatal("recreate reused old id")
	}
	if _, err := c.PutGitOverlayEntry(ctx, got.ws.WorkspaceID, gitOverlayEntryRequest{
		Path: "from-older-request.txt", Op: "upsert", Kind: "file", Content: []byte("nope"),
	}); !apiIsNotFound(err) {
		t.Fatalf("older request wrote into successor generation: %v", err)
	}
}

func TestGitWorkspaceDeleteSucceedsWithOversizedIndex(t *testing.T) {
	s := newTestServer(t)
	ensureGitWorkspaceSchema(t, s)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)

	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	ws, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-huge-idx/",
		RepoURL:    "https://example.test/repo-huge-idx.git",
		RemoteName: "origin",
		HeadCommit: "dddddddddddddddddddddddddddddddddddddddd",
	})
	if err != nil {
		t.Fatalf("UpsertGitWorkspace: %v", err)
	}
	huge := bytes.Repeat([]byte("x"), maxGitWorkspaceIndexBytes+1)
	writeGitWorkspaceIndexForTest(t, s, huge)
	if err := c.DeleteGitWorkspace(ctx, ws.WorkspaceID); err != nil {
		t.Fatalf("DeleteGitWorkspace oversized index: %v", err)
	}
	got, err := s.fallback.Store().GetGitWorkspace(ctx, ws.WorkspaceID)
	if err != nil {
		t.Fatalf("GetGitWorkspace: %v", err)
	}
	if got.Status != datastore.GitWorkspaceStatusDeleted {
		t.Fatalf("status = %q, want deleted", got.Status)
	}
	rebuilt, err := s.fallback.ReadCtx(ctx, gitWorkspaceIndexPath, 0, maxGitWorkspaceIndexBytes+1)
	if err != nil {
		t.Fatalf("read rebuilt oversized index: %v", err)
	}
	idx, err := gitwsindex.Parse(rebuilt)
	if err != nil {
		t.Fatalf("parse rebuilt oversized index: %v", err)
	}
	for _, e := range idx.Workspaces {
		if e.WorkspaceID == ws.WorkspaceID {
			t.Fatalf("rebuilt index still lists deleted %s", ws.WorkspaceID)
		}
	}
}

func TestGitWorkspaceDeleteRebuildPreservesUnknownFields(t *testing.T) {
	// Oversized-but-still-JSON (compact object + padding) must salvage
	// unknown top-level and per-entry keys when rebuilding from live rows.
	s := newTestServer(t)
	ensureGitWorkspaceSchema(t, s)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)

	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	keep, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-keep/",
		RepoURL:    "https://example.test/repo-keep.git",
		RemoteName: "origin",
		HeadCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
	})
	if err != nil {
		t.Fatalf("upsert keep: %v", err)
	}
	drop, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-drop/",
		RepoURL:    "https://example.test/repo-drop.git",
		RemoteName: "origin",
		HeadCommit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
	})
	if err != nil {
		t.Fatalf("upsert drop: %v", err)
	}
	core := fmt.Sprintf(
		`{"version":1,"schema_note":"keep-top","workspaces":[{"workspace_id":%q,"root_path":"/repo-keep/","workspace_kind":"main","alias":"keep"},{"workspace_id":%q,"root_path":"/repo-drop/","workspace_kind":"main","alias":"drop"}]}`,
		keep.WorkspaceID, drop.WorkspaceID,
	)
	pad := bytes.Repeat([]byte(" "), int(maxGitWorkspaceIndexBytes)+1-len(core))
	writeGitWorkspaceIndexForTest(t, s, append([]byte(core), pad...))

	if err := c.DeleteGitWorkspace(ctx, drop.WorkspaceID); err != nil {
		t.Fatalf("DeleteGitWorkspace: %v", err)
	}
	rebuilt, err := s.fallback.ReadCtx(ctx, gitWorkspaceIndexPath, 0, maxGitWorkspaceIndexBytes+1)
	if err != nil {
		t.Fatalf("read rebuilt: %v", err)
	}
	raw := string(rebuilt)
	if !strings.Contains(raw, `"schema_note"`) || !strings.Contains(raw, "keep-top") {
		t.Fatalf("missing salvaged top-level extra: %s", raw)
	}
	if !strings.Contains(raw, `"alias"`) || !strings.Contains(raw, "keep") {
		t.Fatalf("missing salvaged per-entry extra: %s", raw)
	}
	idx, err := gitwsindex.Parse(rebuilt)
	if err != nil {
		t.Fatalf("parse rebuilt: %v", err)
	}
	var sawKeep, sawDrop bool
	for _, e := range idx.Workspaces {
		if e.WorkspaceID == keep.WorkspaceID {
			sawKeep = true
		}
		if e.WorkspaceID == drop.WorkspaceID {
			sawDrop = true
		}
	}
	if !sawKeep || sawDrop {
		t.Fatalf("rebuilt workspaces = %+v, want keep present and drop gone", idx.Workspaces)
	}
}

func TestGitWorkspaceDeleteNonFileIndexStillReturns200(t *testing.T) {
	s := newTestServer(t)
	ensureGitWorkspaceSchema(t, s)
	ts := httptest.NewServer(s)
	t.Cleanup(ts.Close)

	c := newAPITestClient(ts.URL)
	ctx := context.Background()
	ws, err := c.UpsertGitWorkspace(ctx, gitWorkspaceRequest{
		RootPath:   "/repo-dir-idx/",
		RepoURL:    "https://example.test/repo-dir-idx.git",
		RemoteName: "origin",
		HeadCommit: "cccccccccccccccccccccccccccccccccccccccc",
	})
	if err != nil {
		t.Fatalf("UpsertGitWorkspace: %v", err)
	}
	// Make the index path a directory (inode may still carry File).
	if _, _, err := s.fallback.WriteCtxIfRevisionWithTagsResult(
		ctx,
		gitWorkspaceIndexPath+"/nested.txt",
		[]byte("x"),
		0,
		filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate,
		-1,
		nil,
		"",
	); err != nil {
		t.Fatalf("seed nested file under index path: %v", err)
	}
	if err := c.DeleteGitWorkspace(ctx, ws.WorkspaceID); err != nil {
		t.Fatalf("DeleteGitWorkspace with directory index: %v (want 200)", err)
	}
	got, err := s.fallback.Store().GetGitWorkspace(ctx, ws.WorkspaceID)
	if err != nil {
		t.Fatalf("GetGitWorkspace: %v", err)
	}
	if got.Status != datastore.GitWorkspaceStatusDeleted {
		t.Fatalf("status = %q, want deleted", got.Status)
	}
	if err := removeGitWorkspaceIndexEntry(ctx, s.fallback, ws.WorkspaceID); err == nil {
		t.Fatal("removeGitWorkspaceIndexEntry on directory index succeeded, want error")
	}
}

func TestGitWorkspaceDeleteStoreOK(t *testing.T) {
	if !gitWorkspaceDeleteStoreOK(nil) {
		t.Fatal("nil should be OK")
	}
	if !gitWorkspaceDeleteStoreOK(datastore.ErrNotFound) {
		t.Fatal("ErrNotFound should be OK")
	}
	if !gitWorkspaceDeleteStoreOK(fmt.Errorf("wrap: %w", datastore.ErrCommitOutcomeUnknown)) {
		t.Fatal("ErrCommitOutcomeUnknown should be OK")
	}
	if gitWorkspaceDeleteStoreOK(fmt.Errorf("disk full")) {
		t.Fatal("unrelated error should not be OK")
	}
}
