package vault

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/stretchr/testify/require"
)

// makeOriginRepo seeds a normal (non-bare) source repo at originDir with one
// commit on main, then returns its file:// URL. We force HEAD to
// refs/heads/main before committing so subsequent test commits land on main
// (go-git's PlainInit default is master, which would silently break
// origin/main tracking in fetch tests).
func makeOriginRepo(t *testing.T, originDir string) string {
	t.Helper()
	repo, err := git.PlainInit(originDir, false)
	require.NoError(t, err)
	require.NoError(t, repo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName("main"))))
	wt, err := repo.Worktree()
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(originDir, "MEMORY.md"), []byte("# Origin memory\n\nInitial content.\n"), 0o644))
	_, err = wt.Add("MEMORY.md")
	require.NoError(t, err)
	_, err = wt.Commit("initial commit", &git.CommitOptions{})
	require.NoError(t, err)
	return "file://" + originDir
}

// headShort returns the short branch name currently checked out in the repo
// at dir. Used to verify EnsureClone actually moved HEAD to the requested branch.
func headShort(t *testing.T, dir string) string {
	t.Helper()
	repo, err := git.PlainOpen(dir)
	require.NoError(t, err)
	ref, err := repo.Head()
	require.NoError(t, err)
	return ref.Name().Short()
}

func TestEnsureCloneCreatesFreshCloneOnExplicitBranch(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	c, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.NotNil(t, c)
	require.Equal(t, "mem_alpha", c.MemoryID())
	require.Equal(t, "agt_alice", c.AgentID())
	require.Equal(t, "main", c.Branch())
	require.Equal(t, clonePath, c.Path())
	require.FileExists(t, filepath.Join(clonePath, "MEMORY.md"))
	require.Equal(t, "main", headShort(t, clonePath))
	state, err := ReadRetentionState(clonePath)
	require.NoError(t, err)
	require.Equal(t, "mem_alpha", state.MemoryID)
	require.Equal(t, "agt_alice", state.AgentID)
	require.Equal(t, "main", state.Branch)
	require.Empty(t, state.RevokedAt)
}

func TestEnsureCloneIsIdempotentAndFetches(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	first, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)

	// Add a new commit on origin's main branch.
	repo, err := git.PlainOpen(filepath.Join(tmp, "origin"))
	require.NoError(t, err)
	wt, err := repo.Worktree()
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(tmp, "origin", "NOTES.md"), []byte("extra\n"), 0o644))
	_, err = wt.Add("NOTES.md")
	require.NoError(t, err)
	_, err = wt.Commit("second commit", &git.CommitOptions{})
	require.NoError(t, err)

	second, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.Equal(t, first.Path(), second.Path())
	require.Equal(t, "main", headShort(t, clonePath))

	// Fetch should have updated origin/main to the new commit.
	ref, err := second.Repository().Reference(plumbing.ReferenceName("refs/remotes/origin/main"), true)
	require.NoError(t, err)
	require.False(t, ref.Hash().IsZero())
	commit, err := second.Repository().CommitObject(ref.Hash())
	require.NoError(t, err)
	require.Equal(t, "second commit", commit.Message)
}

func TestEnsureCloneMultipleAgentsGetIndependentClones(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	alicePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	bobPath := filepath.Join(tmp, "agents", "agt_bob", "memory")

	alice, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: alicePath,
	})
	require.NoError(t, err)
	bob, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_bob",
		Branch:     "bob-notes",
		RepoURL:    origin,
		TargetPath: bobPath,
	})
	require.NoError(t, err)

	require.NotEqual(t, alice.Path(), bob.Path())
	require.Equal(t, "main", alice.Branch())
	require.Equal(t, "bob-notes", bob.Branch())

	// Each clone has its own HEAD on its own branch.
	require.Equal(t, "main", headShort(t, alicePath))
	require.Equal(t, "bob-notes", headShort(t, bobPath))

	// A local commit on Alice's clone must not bleed into Bob's.
	require.NoError(t, os.WriteFile(filepath.Join(alicePath, "alice-notes.md"), []byte("alice only\n"), 0o644))
	_, err = alice.CommitPending(ctx, "alice local change", CommitPendingOpts{})
	require.NoError(t, err)
	require.NoFileExists(t, filepath.Join(bobPath, "alice-notes.md"))
}

func TestEnsureCloneBacksUpNonGitDirAtPath(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	// Pre-create the target path with non-git content. EnsureClone should
	// back it up and re-clone fresh.
	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	require.NoError(t, os.MkdirAll(clonePath, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(clonePath, "stray.txt"), []byte("old\n"), 0o644))

	clone, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.NotNil(t, clone)

	// Original stray file should be gone (backed up, not merged).
	_, err = os.Stat(filepath.Join(clonePath, "stray.txt"))
	require.True(t, os.IsNotExist(err), "stray file should not exist in fresh clone")

	// Backup should exist in the parent directory.
	entries, err := os.ReadDir(filepath.Join(tmp, "agents", "agt_alice"))
	require.NoError(t, err)
	var foundBackup bool
	for _, e := range entries {
		if strings.HasPrefix(e.Name(), "memory.backup-") {
			foundBackup = true
		}
	}
	require.True(t, foundBackup, "expected a backup directory to exist")
}

func TestEnsureCloneSwitchesBranchForSameRepo(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	// First clone on the original binding branch.
	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	_, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)

	// Switching the same repo to a different explicit branch must reuse the
	// clone path instead of backing up and re-cloning.
	switched, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "experiment",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.Equal(t, clonePath, switched.Path())
	require.Equal(t, "experiment", switched.Branch())
	require.Equal(t, "experiment", headShort(t, clonePath))

	entries, err := os.ReadDir(filepath.Dir(clonePath))
	require.NoError(t, err)
	for _, entry := range entries {
		require.False(t, strings.HasPrefix(entry.Name(), "memory.backup-"), "branch switch should not backup/reclone")
	}
}

func TestEnsureCloneAcceptsEmptyTargetDir(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	// Pre-create an empty target dir. PlainClone should still succeed.
	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	require.NoError(t, os.MkdirAll(clonePath, 0o755))

	_, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.Equal(t, "main", headShort(t, clonePath))
}

func TestEnsureCloneUsesMatchingLocalCloneWhenOriginIsUnavailable(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	originPath := filepath.Join(tmp, "origin")
	origin := makeOriginRepo(t, originPath)
	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")

	_, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.NoError(t, os.Rename(originPath, originPath+"-offline"))

	offline, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.Equal(t, clonePath, offline.Path())
	require.Equal(t, "main", offline.Branch())
	require.NoError(t, os.WriteFile(filepath.Join(clonePath, "offline.md"), []byte("kept locally\n"), 0o644))
	_, err = offline.CommitPending(ctx, "record offline memory", CommitPendingOpts{})
	require.NoError(t, err)
	require.FileExists(t, filepath.Join(clonePath, "offline.md"))
}

func TestEnsureLocalJournalPreservesWorkingSetAndRemovesLegacyRemote(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	cloneURL := makeOriginRepo(t, filepath.Join(tmp, "origin"))
	target := filepath.Join(tmp, "memory")
	legacy, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID: "mem_1", AgentID: "agt_1", RepoURL: cloneURL, TargetPath: target, Branch: "main",
	})
	require.NoError(t, err)
	require.True(t, legacy.UsesRemote())
	require.NoError(t, os.WriteFile(filepath.Join(target, "offline.md"), []byte("offline\n"), 0o644))

	local, err := EnsureLocalJournal(ctx, "mem_1", "agt_1", target)
	require.NoError(t, err)
	require.False(t, local.UsesRemote())
	require.Equal(t, "offline\n", readRepoFile(t, target, "offline.md"))
	remotes, err := local.Repository().Remotes()
	require.NoError(t, err)
	require.Empty(t, remotes)
	hash, err := local.CommitPending(ctx, "native local checkpoint", CommitPendingOpts{})
	require.NoError(t, err)
	require.NotEmpty(t, hash)
}

func TestEnsureLocalJournalBacksUpDifferentMemoryBinding(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	target := filepath.Join(tmp, "memory")
	first, err := EnsureLocalJournal(ctx, "mem_1", "agt_1", target)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(target, "private.md"), []byte("memory one\n"), 0o600))
	_, err = first.CommitPending(ctx, "memory one", CommitPendingOpts{})
	require.NoError(t, err)

	second, err := EnsureLocalJournal(ctx, "mem_2", "agt_1", target)
	require.NoError(t, err)
	require.False(t, second.UsesRemote())
	require.NoFileExists(t, filepath.Join(target, "private.md"), "content from another binding must never enter the new journal")
	backups, err := filepath.Glob(target + ".backup-*")
	require.NoError(t, err)
	require.Len(t, backups, 1)
	require.Equal(t, "memory one\n", readRepoFile(t, backups[0], "private.md"))
	state, err := ReadRetentionState(target)
	require.NoError(t, err)
	require.Equal(t, "mem_2", state.MemoryID)
	require.Equal(t, "agt_1", state.AgentID)
}

func TestEnsureCloneRejectsUncachedBranchWhenOriginIsUnavailable(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	originPath := filepath.Join(tmp, "origin")
	origin := makeOriginRepo(t, originPath)
	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")

	_, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.NoError(t, os.Rename(originPath, originPath+"-offline"))

	_, err = EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "uncached",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "local branch unavailable")
	require.Equal(t, "main", headShort(t, clonePath))
}

func TestEnsureCloneKeepsDivergedLocalCloneWritable(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	originPath := filepath.Join(tmp, "origin")
	origin := makeOriginRepo(t, originPath)
	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")

	local, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(clonePath, "local.md"), []byte("local\n"), 0o644))
	_, err = local.CommitPending(ctx, "local memory", CommitPendingOpts{})
	require.NoError(t, err)

	originRepo, err := git.PlainOpen(originPath)
	require.NoError(t, err)
	originWorktree, err := originRepo.Worktree()
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(originPath, "remote.md"), []byte("remote\n"), 0o644))
	_, err = originWorktree.Add("remote.md")
	require.NoError(t, err)
	_, err = originWorktree.Commit("remote memory", &git.CommitOptions{})
	require.NoError(t, err)

	diverged, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.Equal(t, clonePath, diverged.Path())
	require.FileExists(t, filepath.Join(clonePath, "local.md"))
	require.NoFileExists(t, filepath.Join(clonePath, "remote.md"))
	require.NoError(t, os.WriteFile(filepath.Join(clonePath, "after-diverge.md"), []byte("still local\n"), 0o644))
	_, err = diverged.CommitPending(ctx, "continue local memory", CommitPendingOpts{})
	require.NoError(t, err)
}

func TestEnsureCloneFailureDoesNotPublishPartialTarget(t *testing.T) {
	tmp := t.TempDir()
	target := filepath.Join(tmp, "agents", "agt_alice", "memory")
	_, err := EnsureClone(context.Background(), EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    "file://" + filepath.Join(tmp, "missing-origin"),
		TargetPath: target,
	})
	require.Error(t, err)
	require.NoDirExists(t, target)
}

func TestEnsureCloneRequiresInputs(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()

	_, err := EnsureClone(ctx, EnsureCloneOpts{
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    "file:///tmp/whatever",
		TargetPath: filepath.Join(tmp, "memory"),
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "memory id")

	_, err = EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		RepoURL:    "file:///tmp/whatever",
		TargetPath: filepath.Join(tmp, "memory"),
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "agent id")

	_, err = EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		TargetPath: filepath.Join(tmp, "memory"),
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "repo url")

	_, err = EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		RepoURL:    "file:///tmp/whatever",
		TargetPath: filepath.Join(tmp, "memory"),
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "branch")

	_, err = EnsureClone(ctx, EnsureCloneOpts{
		MemoryID: "mem_alpha",
		AgentID:  "agt_alice",
		Branch:   "main",
		RepoURL:  "file:///tmp/whatever",
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "target path")
}

func TestCloneDetachRemovesGitAndPreservesContent(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	c, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)

	// Seed local-only content.
	require.NoError(t, os.WriteFile(filepath.Join(clonePath, "notes.md"), []byte("# notes\n"), 0o644))

	// Sanity: .git exists before Detach.
	require.DirExists(t, filepath.Join(clonePath, ".git"))

	require.NoError(t, c.Detach(ctx))

	// .git directory removed; content preserved.
	require.NoDirExists(t, filepath.Join(clonePath, ".git"))
	require.FileExists(t, filepath.Join(clonePath, "notes.md"))
	require.FileExists(t, filepath.Join(clonePath, "MEMORY.md"))
}

func TestCloneRetainOfflineWritesRevokedStateAndPreservesContent(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	c, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(clonePath, "offline.md"), []byte("# offline\n"), 0o644))

	revokedAt := time.Date(2026, 6, 16, 12, 0, 0, 0, time.UTC)
	require.NoError(t, c.RetainOffline(ctx, revokedAt))

	require.NoDirExists(t, filepath.Join(clonePath, ".git"))
	require.FileExists(t, filepath.Join(clonePath, "MEMORY.md"))
	require.FileExists(t, filepath.Join(clonePath, "offline.md"))
	state, err := ReadRetentionState(clonePath)
	require.NoError(t, err)
	require.Equal(t, "mem_alpha", state.MemoryID)
	require.Equal(t, "agt_alice", state.AgentID)
	require.Equal(t, "main", state.Branch)
	require.Equal(t, revokedAt.Format(time.RFC3339Nano), state.RevokedAt)
}

func TestEnsureCloneReattachesRetainedOfflineCopy(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	c, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.NoError(t, c.RetainOffline(ctx, time.Date(2026, 6, 16, 12, 0, 0, 0, time.UTC)))
	require.NoError(t, os.WriteFile(filepath.Join(clonePath, "MEMORY.md"), []byte("# Offline memory\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(clonePath, "offline-only.md"), []byte("kept\n"), 0o644))

	reattached, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)
	require.Equal(t, clonePath, reattached.Path())
	require.DirExists(t, filepath.Join(clonePath, ".git"))
	require.Equal(t, "main", headShort(t, clonePath))
	require.FileExists(t, filepath.Join(clonePath, "offline-only.md"))
	data, err := os.ReadFile(filepath.Join(clonePath, "MEMORY.md"))
	require.NoError(t, err)
	require.Equal(t, "# Offline memory\n", string(data))

	entries, err := os.ReadDir(filepath.Dir(clonePath))
	require.NoError(t, err)
	for _, entry := range entries {
		require.False(t, strings.HasPrefix(entry.Name(), "memory.backup-"), "retained regrant should reattach in place instead of backing up")
	}
	state, err := ReadRetentionState(clonePath)
	require.NoError(t, err)
	require.Equal(t, "mem_alpha", state.MemoryID)
	require.Equal(t, "agt_alice", state.AgentID)
	require.Equal(t, "main", state.Branch)
	require.Empty(t, state.RevokedAt)
}

func TestCloneDetachIsIdempotent(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	c, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)

	require.NoError(t, c.Detach(ctx))
	// Second call: .git already gone.
	require.NoError(t, c.Detach(ctx))
	require.NoDirExists(t, filepath.Join(clonePath, ".git"))
}

func TestCloneDetachOnPlainDirectoryIsNoOp(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()

	// A plain directory with content but no .git (e.g., post-detach).
	plain := filepath.Join(tmp, "agents", "agt_alice", "memory")
	require.NoError(t, os.MkdirAll(plain, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(plain, "leftover.md"), []byte("# kept\n"), 0o644))

	c := &Clone{
		memoryID: "mem_alpha",
		agentID:  "agt_alice",
		branch:   "main",
		path:     plain,
	}
	require.NoError(t, c.Detach(ctx))
	require.FileExists(t, filepath.Join(plain, "leftover.md"))
}

func TestCommitPendingStagesNewFilesAndCommits(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	c, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)

	// New untracked file in a fresh subdirectory. Must be picked up by
	// AddWithOptions(All: true).
	require.NoError(t, os.MkdirAll(filepath.Join(clonePath, "kb"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(clonePath, "kb", "note.md"), []byte("# note\n"), 0o644))

	hash, err := c.CommitPending(ctx, "Add note", CommitPendingOpts{
		Author: &Signature{Name: "alice", Email: "alice@agentteam.local"},
	})
	require.NoError(t, err)
	require.NotEmpty(t, hash)

	repo, err := git.PlainOpen(clonePath)
	require.NoError(t, err)
	head, err := repo.Head()
	require.NoError(t, err)
	commit, err := repo.CommitObject(head.Hash())
	require.NoError(t, err)
	require.Equal(t, "Add note", commit.Message)
	require.Equal(t, "alice", commit.Author.Name)
}

func TestCommitPendingReturnsNothingToCommitOnCleanTree(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	c, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)

	_, err = c.CommitPending(ctx, "nothing here", CommitPendingOpts{})
	require.ErrorIs(t, err, ErrNothingToCommit)
}

func TestCommitPendingAllowsExplicitEmptyJournalVersion(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))
	c, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID: "mem_alpha", AgentID: "agt_alice", Branch: "main",
		RepoURL: origin, TargetPath: filepath.Join(tmp, "agents", "agt_alice", "memory"),
	})
	require.NoError(t, err)
	oldHead, err := c.HeadRevision(ctx)
	require.NoError(t, err)

	newHead, err := c.CommitPending(ctx, "Adopt distinct cloud version", CommitPendingOpts{AllowEmpty: true})
	require.NoError(t, err)
	require.NotEqual(t, oldHead, newHead)
	commit, err := c.Repository().CommitObject(plumbing.NewHash(newHead))
	require.NoError(t, err)
	require.Equal(t, []plumbing.Hash{plumbing.NewHash(oldHead)}, commit.ParentHashes)
	parent, err := c.Repository().CommitObject(plumbing.NewHash(oldHead))
	require.NoError(t, err)
	require.Equal(t, parent.TreeHash, commit.TreeHash)
}

func TestCommitPendingAmendRewritesHead(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	c, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)

	require.NoError(t, os.WriteFile(filepath.Join(clonePath, "a.md"), []byte("a\n"), 0o644))
	firstHash, err := c.CommitPending(ctx, "first", CommitPendingOpts{})
	require.NoError(t, err)

	require.NoError(t, os.WriteFile(filepath.Join(clonePath, "b.md"), []byte("b\n"), 0o644))
	amendedHash, err := c.CommitPending(ctx, "first (amended)", CommitPendingOpts{Amend: true})
	require.NoError(t, err)

	require.NotEqual(t, firstHash, amendedHash)

	repo, err := git.PlainOpen(clonePath)
	require.NoError(t, err)
	head, err := repo.Head()
	require.NoError(t, err)
	commit, err := repo.CommitObject(head.Hash())
	require.NoError(t, err)
	require.Equal(t, "first (amended)", commit.Message)
	// Both files must be reachable from the amended commit (single parent).
	tree, err := commit.Tree()
	require.NoError(t, err)
	_, err = tree.FindEntry("a.md")
	require.NoError(t, err)
	_, err = tree.FindEntry("b.md")
	require.NoError(t, err)
}

func TestCommitPendingRequiresMessage(t *testing.T) {
	ctx := context.Background()
	tmp := t.TempDir()
	origin := makeOriginRepo(t, filepath.Join(tmp, "origin"))

	clonePath := filepath.Join(tmp, "agents", "agt_alice", "memory")
	c, err := EnsureClone(ctx, EnsureCloneOpts{
		MemoryID:   "mem_alpha",
		AgentID:    "agt_alice",
		Branch:     "main",
		RepoURL:    origin,
		TargetPath: clonePath,
	})
	require.NoError(t, err)

	_, err = c.CommitPending(ctx, "   ", CommitPendingOpts{})
	require.Error(t, err)
	require.Contains(t, err.Error(), "commit message")
}

func TestSentinelErrorsAreErrorsIsCompatible(t *testing.T) {
	// Smoke check: package-level sentinels satisfy errors.Is identity.
	require.True(t, errors.Is(ErrPathConflict, ErrPathConflict))
	require.True(t, errors.Is(ErrRepoCorrupt, ErrRepoCorrupt))
	require.True(t, errors.Is(ErrNothingToCommit, ErrNothingToCommit))
}

func TestResolveSignatureDefaultsWhenNil(t *testing.T) {
	sig := resolveSignature(nil)
	require.Equal(t, DefaultAuthorName, sig.Name)
	require.Equal(t, DefaultAuthorEmail, sig.Email)
	require.False(t, sig.When.IsZero())
}

func TestResolveSignatureTrimsAndOverrides(t *testing.T) {
	sig := resolveSignature(&Signature{Name: "  alice  ", Email: " alice@agentteam.local "})
	require.Equal(t, "alice", sig.Name)
	require.Equal(t, "alice@agentteam.local", sig.Email)
}

// Ensure no stray "strings" unused warning — strings is used in resolveSignature
// and CommitPending's TrimSpace path.
var _ = strings.TrimSpace
