package datastore

import (
	"bytes"
	"context"
	"errors"
	"strings"
	"sync"
	"testing"

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

func TestGitWorkspaceLinkedMetadataRoundTrip(t *testing.T) {
	s := newTestStore(t)
	initGitWorkspaceTestSchema(t, s)
	ctx := context.Background()
	main := GitWorkspace{
		WorkspaceID: "base",
		RootPath:    "/repo/",
		RepoURL:     "https://github.com/example/repo.git",
		RemoteName:  "origin",
		BranchName:  "main",
		BaseCommit:  strings.Repeat("1", 40),
		HeadCommit:  strings.Repeat("1", 40),
		Mode:        GitWorkspaceModeFast,
		Kind:        GitWorkspaceKindMain,
	}
	if err := s.UpsertGitWorkspace(ctx, main); err != nil {
		t.Fatalf("UpsertGitWorkspace main: %v", err)
	}
	linked := GitWorkspace{
		WorkspaceID:  "wt",
		RootPath:     "/repo-wt/",
		RepoURL:      main.RepoURL,
		RemoteName:   "origin",
		BranchName:   "feature",
		BaseCommit:   strings.Repeat("2", 40),
		HeadCommit:   strings.Repeat("2", 40),
		Mode:         GitWorkspaceModeFastBlobless,
		Kind:         GitWorkspaceKindLinked,
		CommonID:     main.WorkspaceID,
		WorktreeName: "wt",
		GitDirRel:    "worktrees/wt",
	}
	if err := s.UpsertGitWorkspace(ctx, linked); err != nil {
		t.Fatalf("UpsertGitWorkspace linked: %v", err)
	}
	got, err := s.GetGitWorkspaceByRoot(ctx, "/repo-wt")
	if err != nil {
		t.Fatalf("GetGitWorkspaceByRoot linked: %v", err)
	}
	if got.Kind != GitWorkspaceKindLinked || got.CommonID != "base" || got.WorktreeName != "wt" || got.GitDirRel != "worktrees/wt" {
		t.Fatalf("linked metadata = %+v", got)
	}
}

func TestGitObjectPackRoundTrip(t *testing.T) {
	s := newTestStore(t)
	initGitWorkspaceTestSchema(t, s)
	ctx := context.Background()
	if err := s.UpsertGitWorkspace(ctx, GitWorkspace{
		WorkspaceID: "ws1",
		RootPath:    "/repo-pack/",
		RepoURL:     "https://example.test/repo.git",
		RemoteName:  "origin",
		HeadCommit:  strings.Repeat("1", 40),
		Status:      GitWorkspaceStatusLive,
	}); err != nil {
		t.Fatalf("UpsertGitWorkspace: %v", err)
	}
	content := []byte("PACK test")
	pack := GitObjectPack{
		WorkspaceID:    "ws1",
		PackID:         "pack1",
		ChecksumSHA256: "sum1",
		SizeBytes:      int64(len(content)),
		ContentBlob:    content,
	}
	if err := s.UpsertGitObjectPack(ctx, pack); err != nil {
		t.Fatalf("UpsertGitObjectPack: %v", err)
	}
	got, err := s.GetGitObjectPack(ctx, "ws1", "pack1")
	if err != nil {
		t.Fatalf("GetGitObjectPack: %v", err)
	}
	if got.WorkspaceID != "ws1" || got.PackID != "pack1" || got.ChecksumSHA256 != "sum1" || got.SizeBytes != int64(len(content)) {
		t.Fatalf("unexpected pack metadata: %+v", got)
	}
	if !bytes.Equal(got.ContentBlob, content) {
		t.Fatalf("pack content = %q, want %q", got.ContentBlob, content)
	}
	packs, err := s.ListGitObjectPacks(ctx, "ws1")
	if err != nil {
		t.Fatalf("ListGitObjectPacks: %v", err)
	}
	if len(packs) != 1 || packs[0].PackID != "pack1" || len(packs[0].ContentBlob) != 0 {
		t.Fatalf("listed packs = %+v, want metadata only", packs)
	}
}

func TestDeleteGitWorkspaceWipesSubresourcesAndFencesWrites(t *testing.T) {
	s := newTestStore(t)
	initGitWorkspaceTestSchema(t, s)
	runDeleteGitWorkspaceWipeAndFence(t, s, "ws-del", "/repo-del/")
}

func TestDeleteGitWorkspaceConcurrentWithOverlayWrite(t *testing.T) {
	s := newTestStore(t)
	initGitWorkspaceTestSchema(t, s)
	runDeleteGitWorkspaceConcurrentOverlay(t, s, "ws-race", "/repo-race/")
}

func TestCommitGitWorkspaceKeepsLiveID(t *testing.T) {
	s := newTestStore(t)
	initGitWorkspaceTestSchema(t, s)
	ctx := context.Background()
	first, err := s.CommitGitWorkspace(ctx, GitWorkspace{
		WorkspaceID: "ws-live-a",
		RootPath:    "/repo-live/",
		RepoURL:     "https://example.test/repo-live.git",
		RemoteName:  "origin",
		HeadCommit:  strings.Repeat("a", 40),
	})
	if err != nil {
		t.Fatalf("first CommitGitWorkspace: %v", err)
	}
	second, err := s.CommitGitWorkspace(ctx, GitWorkspace{
		WorkspaceID: "ws-live-b",
		RootPath:    "/repo-live/",
		RepoURL:     "https://example.test/repo-live.git",
		RemoteName:  "origin",
		HeadCommit:  strings.Repeat("b", 40),
	})
	if err != nil {
		t.Fatalf("second CommitGitWorkspace: %v", err)
	}
	if second.WorkspaceID != first.WorkspaceID {
		t.Fatalf("live commit id = %s, want %s", second.WorkspaceID, first.WorkspaceID)
	}
	if second.HeadCommit != strings.Repeat("b", 40) {
		t.Fatalf("live commit head = %q", second.HeadCommit)
	}
	// Same payload must stay live (RowsAffected can be 0 on MySQL).
	again, err := s.CommitGitWorkspace(ctx, GitWorkspace{
		WorkspaceID: "ws-live-c",
		RootPath:    "/repo-live/",
		RepoURL:     "https://example.test/repo-live.git",
		RemoteName:  "origin",
		HeadCommit:  strings.Repeat("b", 40),
	})
	if err != nil {
		t.Fatalf("same-payload CommitGitWorkspace: %v", err)
	}
	if again.WorkspaceID != first.WorkspaceID {
		t.Fatalf("same-payload id = %s, want %s", again.WorkspaceID, first.WorkspaceID)
	}
}

func TestGitReplaceDeletedRepointsLinkedAndWipesLegacyChildren(t *testing.T) {
	s := newTestStore(t)
	initGitWorkspaceTestSchema(t, s)
	runReplaceDeletedRepointsLinkedAndWipesLegacyChildren(t, s, "ws-main", "ws-link", "/repo-main/", "/repo-link/")
}

func runReplaceDeletedRepointsLinkedAndWipesLegacyChildren(t *testing.T, s *Store, mainID, linkID, mainRoot, linkRoot string) {
	t.Helper()
	ctx := context.Background()
	if err := s.UpsertGitWorkspace(ctx, GitWorkspace{
		WorkspaceID: mainID,
		RootPath:    mainRoot,
		RepoURL:     "https://example.test/repo-main.git",
		RemoteName:  "origin",
		HeadCommit:  strings.Repeat("a", 40),
		Kind:        GitWorkspaceKindMain,
	}); err != nil {
		t.Fatalf("upsert main: %v", err)
	}
	if err := s.UpsertGitWorkspace(ctx, GitWorkspace{
		WorkspaceID:  linkID,
		RootPath:     linkRoot,
		RepoURL:      "https://example.test/repo-main.git",
		RemoteName:   "origin",
		HeadCommit:   strings.Repeat("b", 40),
		Kind:         GitWorkspaceKindLinked,
		CommonID:     mainID,
		WorktreeName: "wt",
		GitDirRel:    "worktrees/wt",
	}); err != nil {
		t.Fatalf("upsert linked: %v", err)
	}
	if err := s.DeleteGitWorkspace(ctx, mainID); err != nil {
		t.Fatalf("delete main: %v", err)
	}
	// Simulate a pre-upgrade leftover child row the live fence would reject.
	if _, err := s.DB().ExecContext(ctx, `
INSERT INTO git_workspace_overlay (
	`+s.scope.InsCols(`workspace_id, path, path_hash, op, kind, mode, storage_type, storage_ref, storage_ref_hash,
	checksum_sha256, size_bytes, base_object_sha, content_blob, created_at, updated_at`)+`
) VALUES (`+s.scope.InsVals(`?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(3), UTC_TIMESTAMP(3)`)+`)`,
		s.scope.Args(mainID, "legacy.txt", gitPathHash("legacy.txt"), string(GitOverlayOpUpsert), string(GitOverlayKindFile),
			"", "", "", "", "", int64(0), "", []byte(nil))...); err != nil {
		t.Fatalf("seed leftover overlay: %v", err)
	}

	newID := mainID + "-new"
	if err := s.replaceDeletedGitWorkspace(ctx, mainID, GitWorkspace{
		WorkspaceID: newID,
		RootPath:    mainRoot,
		RepoURL:     "https://example.test/repo-main.git",
		RemoteName:  "origin",
		HeadCommit:  strings.Repeat("c", 40),
		Kind:        GitWorkspaceKindMain,
	}); err != nil {
		t.Fatalf("replaceDeletedGitWorkspace: %v", err)
	}
	linked, err := s.GetGitWorkspace(ctx, linkID)
	if err != nil {
		t.Fatalf("GetGitWorkspace linked: %v", err)
	}
	if linked.CommonID != newID {
		t.Fatalf("linked common = %q, want %s", linked.CommonID, newID)
	}
	var leftover int
	if err := s.DB().QueryRowContext(ctx, `
SELECT COUNT(*) FROM git_workspace_overlay WHERE `+s.scope.And(`workspace_id = ?`),
		s.scope.Args(mainID)...).Scan(&leftover); err != nil {
		t.Fatalf("count leftover overlay: %v", err)
	}
	if leftover != 0 {
		t.Fatalf("legacy overlay rows left on old id: %d", leftover)
	}
}

func runDeleteGitWorkspaceWipeAndFence(t *testing.T, s *Store, wsID, root string) {
	t.Helper()
	ctx := context.Background()
	if err := s.UpsertGitWorkspace(ctx, GitWorkspace{
		WorkspaceID: wsID,
		RootPath:    root,
		RepoURL:     "https://example.test/repo-del.git",
		RemoteName:  "origin",
		HeadCommit:  strings.Repeat("a", 40),
	}); err != nil {
		t.Fatalf("UpsertGitWorkspace: %v", err)
	}
	if err := s.UpsertGitOverlayEntry(ctx, GitOverlayEntry{
		WorkspaceID: wsID,
		Path:        "dirty.txt",
		Op:          GitOverlayOpUpsert,
		Kind:        GitOverlayKindFile,
		ContentBlob: []byte("stale"),
	}); err != nil {
		t.Fatalf("UpsertGitOverlayEntry: %v", err)
	}
	if err := s.UpsertGitState(ctx, GitState{WorkspaceID: wsID, CheckpointCommit: strings.Repeat("a", 40)}); err != nil {
		t.Fatalf("UpsertGitState: %v", err)
	}
	if err := s.ReplaceGitTreeNodes(ctx, wsID, strings.Repeat("a", 40), []GitTreeNode{{
		Path: "README.md", Kind: GitTreeNodeKindFile, Mode: "100644", ObjectSHA: strings.Repeat("b", 40),
	}}); err != nil {
		t.Fatalf("ReplaceGitTreeNodes: %v", err)
	}
	if err := s.UpsertGitObjectPack(ctx, GitObjectPack{
		WorkspaceID: wsID, PackID: strings.Repeat("c", 64), ChecksumSHA256: strings.Repeat("c", 64), ContentBlob: []byte("P"),
	}); err != nil {
		t.Fatalf("UpsertGitObjectPack: %v", err)
	}

	if err := s.DeleteGitWorkspace(ctx, wsID); err != nil {
		t.Fatalf("DeleteGitWorkspace: %v", err)
	}
	if _, err := s.GetGitOverlayEntry(ctx, wsID, "dirty.txt"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("overlay after delete: %v, want NotFound", err)
	}
	if _, err := s.GetGitState(ctx, wsID); !errors.Is(err, ErrNotFound) {
		t.Fatalf("git-state after delete: %v, want NotFound", err)
	}
	if _, err := s.ListGitTreeNodes(ctx, wsID, strings.Repeat("a", 40)); !errors.Is(err, ErrNotFound) {
		t.Fatalf("tree after delete: %v, want NotFound", err)
	}
	if _, err := s.GetGitObjectPack(ctx, wsID, strings.Repeat("c", 64)); !errors.Is(err, ErrNotFound) {
		t.Fatalf("pack after delete: %v, want NotFound", err)
	}
	if err := s.UpsertGitOverlayEntry(ctx, GitOverlayEntry{
		WorkspaceID: wsID, Path: "after-delete.txt", Op: GitOverlayOpUpsert, Kind: GitOverlayKindFile,
	}); !errors.Is(err, ErrNotFound) {
		t.Fatalf("overlay write after delete: %v, want NotFound", err)
	}

	newID := wsID + "-new"
	if err := s.replaceDeletedGitWorkspace(ctx, wsID, GitWorkspace{
		WorkspaceID: newID,
		RootPath:    root,
		RepoURL:     "https://example.test/repo-del.git",
		RemoteName:  "origin",
		HeadCommit:  strings.Repeat("d", 40),
	}); err != nil {
		t.Fatalf("replaceDeletedGitWorkspace: %v", err)
	}
	if _, err := s.GetGitWorkspace(ctx, wsID); !errors.Is(err, ErrNotFound) {
		t.Fatalf("old id after replace: %v, want NotFound", err)
	}
	got, err := s.GetGitWorkspaceByRoot(ctx, root)
	if err != nil {
		t.Fatalf("GetGitWorkspaceByRoot after replace: %v", err)
	}
	if got.WorkspaceID != newID || got.Status != GitWorkspaceStatusLive {
		t.Fatalf("revived workspace = %+v, want id=%s live", got, newID)
	}
	if _, err := s.GetGitOverlayEntry(ctx, newID, "dirty.txt"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("recreate inherited overlay: %v, want NotFound", err)
	}
	if err := s.UpsertGitOverlayEntry(ctx, GitOverlayEntry{
		WorkspaceID: wsID, Path: "stale-runtime.txt", Op: GitOverlayOpUpsert, Kind: GitOverlayKindFile,
	}); !errors.Is(err, ErrNotFound) {
		t.Fatalf("old-generation overlay write: %v, want NotFound", err)
	}
}

func runDeleteGitWorkspaceConcurrentOverlay(t *testing.T, s *Store, wsID, root string) {
	t.Helper()
	ctx := context.Background()
	if err := s.UpsertGitWorkspace(ctx, GitWorkspace{
		WorkspaceID: wsID,
		RootPath:    root,
		RepoURL:     "https://example.test/repo-race.git",
		RemoteName:  "origin",
		HeadCommit:  strings.Repeat("e", 40),
	}); err != nil {
		t.Fatalf("UpsertGitWorkspace: %v", err)
	}

	var wg sync.WaitGroup
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			_ = s.UpsertGitOverlayEntry(ctx, GitOverlayEntry{
				WorkspaceID: wsID,
				Path:        "race.txt",
				Op:          GitOverlayOpUpsert,
				Kind:        GitOverlayKindFile,
				ContentBlob: []byte{byte(i)},
			})
		}(i)
	}
	wg.Add(1)
	go func() {
		defer wg.Done()
		if err := s.DeleteGitWorkspace(ctx, wsID); err != nil {
			t.Errorf("DeleteGitWorkspace: %v", err)
		}
	}()
	wg.Wait()

	got, err := s.GetGitWorkspace(ctx, wsID)
	if err != nil {
		t.Fatalf("GetGitWorkspace: %v", err)
	}
	if got.Status != GitWorkspaceStatusDeleted {
		t.Fatalf("status = %q, want deleted", got.Status)
	}
	if _, err := s.GetGitOverlayEntry(ctx, wsID, "race.txt"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("overlay after concurrent delete: %v, want NotFound", err)
	}

	recreated, err := s.CommitGitWorkspace(ctx, GitWorkspace{
		WorkspaceID: wsID + "-new",
		RootPath:    root,
		RepoURL:     "https://example.test/repo-race.git",
		RemoteName:  "origin",
		HeadCommit:  strings.Repeat("f", 40),
	})
	if err != nil {
		t.Fatalf("recreate: %v", err)
	}
	if recreated.WorkspaceID == wsID {
		t.Fatalf("recreate reused deleted id %s", wsID)
	}
	if _, err := s.GetGitOverlayEntry(ctx, recreated.WorkspaceID, "race.txt"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("recreate inherited concurrent overlay: %v, want NotFound", err)
	}
}

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