package vault_test

import (
	"context"
	"os"
	"path/filepath"
	"testing"

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

	"github.com/tidbcloud/AgentTeam/desktop/internal/vault"
)

// syncFixture builds a bare origin with one initial commit on main and a
// bound-branch clone. Tests push new commits to origin/<branch> via
// pushToAgentBranch() and write agent-side commits via commitOnAgent().
type syncFixture struct {
	t       *testing.T
	origin  string
	stage   string
	stageR  *git.Repository
	clone   *vault.Clone
	agentID string
}

func newSyncFixture(t *testing.T, agentID string) *syncFixture {
	t.Helper()
	ctx := context.Background()

	originDir := filepath.Join(t.TempDir(), "origin")
	bareRepo, err := git.PlainInit(originDir, true)
	require.NoError(t, err)
	require.NoError(t, bareRepo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName("main"))))

	stageDir := t.TempDir()
	stageRepo, err := git.PlainInit(stageDir, false)
	require.NoError(t, err)
	require.NoError(t, stageRepo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName("main"))))
	wt, err := stageRepo.Worktree()
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(stageDir, "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)
	_, err = stageRepo.CreateRemote(&config.RemoteConfig{
		Name: "origin",
		URLs: []string{"file://" + originDir},
	})
	require.NoError(t, err)
	require.NoError(t, stageRepo.PushContext(ctx, &git.PushOptions{
		RemoteName: "origin",
		RefSpecs:   []config.RefSpec{"+refs/heads/main:refs/heads/main"},
	}))

	tmp := t.TempDir()
	clone, err := vault.EnsureClone(ctx, vault.EnsureCloneOpts{
		MemoryID:   "mem_test",
		AgentID:    agentID,
		Branch:     "memory-" + agentID,
		RepoURL:    "file://" + originDir,
		TargetPath: filepath.Join(tmp, "agents", agentID, "memory"),
	})
	require.NoError(t, err)

	return &syncFixture{
		t:       t,
		origin:  originDir,
		stage:   stageDir,
		stageR:  stageRepo,
		clone:   clone,
		agentID: agentID,
	}
}

func (f *syncFixture) commitOnAgent(path, content, msg string) {
	f.t.Helper()
	full := filepath.Join(f.clone.Path(), path)
	require.NoError(f.t, os.MkdirAll(filepath.Dir(full), 0o755))
	require.NoError(f.t, os.WriteFile(full, []byte(content), 0o644))
	_, err := f.clone.CommitPending(context.Background(), msg, vault.CommitPendingOpts{})
	require.NoError(f.t, err)
}

// pushToAgentBranch pushes the clone's bound branch to origin from the staging
// clone so origin/<branch> exists. Then fetches in the agent clone.
func (f *syncFixture) pushToAgentBranch() {
	f.t.Helper()
	// Push from staging: create the bound branch from main on origin.
	require.NoError(f.t, f.stageR.PushContext(context.Background(), &git.PushOptions{
		RemoteName: "origin",
		RefSpecs:   []config.RefSpec{config.RefSpec("+refs/heads/main:refs/heads/" + f.clone.Branch())},
	}))
	// Fetch so the agent clone sees origin/<branch>.
	require.NoError(f.t, f.clone.Repository().FetchContext(context.Background(), &git.FetchOptions{RemoteName: "origin"}))
}

// pushToAgentBranchFromStage pushes a commit from the staging repo to the
// bound branch on origin, then fetches in the agent clone. Used to simulate
// another machine pushing to the same memory branch.
func (f *syncFixture) pushToAgentBranchFromStage(path, content, msg string) {
	f.t.Helper()
	wt, err := f.stageR.Worktree()
	require.NoError(f.t, err)
	branchRef := plumbing.NewBranchReferenceName(f.clone.Branch())

	// Checkout the bound branch (create if missing).
	_, err = f.stageR.Reference(branchRef, true)
	create := err != nil
	require.NoError(f.t, wt.Checkout(&git.CheckoutOptions{Branch: branchRef, Create: create, Force: true}))

	full := filepath.Join(f.stage, path)
	require.NoError(f.t, os.MkdirAll(filepath.Dir(full), 0o755))
	require.NoError(f.t, os.WriteFile(full, []byte(content), 0o644))
	_, err = wt.Add(path)
	require.NoError(f.t, err)
	_, err = wt.Commit(msg, &git.CommitOptions{})
	require.NoError(f.t, err)
	require.NoError(f.t, f.stageR.PushContext(context.Background(), &git.PushOptions{
		RemoteName: "origin",
		RefSpecs:   []config.RefSpec{config.RefSpec("+refs/heads/" + f.clone.Branch() + ":refs/heads/" + f.clone.Branch())},
	}))

	require.NoError(f.t, f.clone.Repository().FetchContext(context.Background(), &git.FetchOptions{RemoteName: "origin"}))
}

func TestSyncUpToDate(t *testing.T) {
	f := newSyncFixture(t, "agt_alice")
	f.pushToAgentBranch()

	res, err := f.clone.Sync(context.Background(), vault.SyncOpts{})
	require.NoError(t, err)
	require.Equal(t, vault.SyncUpToDate, res.State)
}

func TestSyncLocalAhead(t *testing.T) {
	f := newSyncFixture(t, "agt_alice")
	f.pushToAgentBranch()
	f.commitOnAgent("kb/notes.md", "# alice's notes\n", "agent local edit")

	res, err := f.clone.Sync(context.Background(), vault.SyncOpts{})
	require.NoError(t, err)
	require.Equal(t, vault.SyncLocalAhead, res.State)
}

func TestSyncRemoteAhead(t *testing.T) {
	f := newSyncFixture(t, "agt_alice")
	f.pushToAgentBranch()
	// Another machine pushes to the agent branch.
	f.pushToAgentBranchFromStage("docs/remote.md", "remote content\n", "remote push")

	res, err := f.clone.Sync(context.Background(), vault.SyncOpts{})
	require.NoError(t, err)
	require.Equal(t, vault.SyncRemoteAhead, res.State)
}

func TestSyncDiverged(t *testing.T) {
	f := newSyncFixture(t, "agt_alice")
	f.pushToAgentBranch()
	// Local commit.
	f.commitOnAgent("kb/local.md", "local\n", "local edit")
	// Remote commit (simulating another machine).
	f.pushToAgentBranchFromStage("docs/remote.md", "remote\n", "remote push")

	res, err := f.clone.Sync(context.Background(), vault.SyncOpts{})
	require.NoError(t, err)
	require.Equal(t, vault.SyncDiverged, res.State)
}

func TestSyncNoUpstreamBranch(t *testing.T) {
	f := newSyncFixture(t, "agt_alice")
	// Don't push — origin doesn't have the agent branch yet.
	_, err := f.clone.Sync(context.Background(), vault.SyncOpts{})
	require.ErrorIs(t, err, vault.ErrNoUpstreamBranch)
}
