package mcp

import (
	"context"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	"github.com/yan1h/agent-coding-workflow/internal/infra/git"
	"github.com/yan1h/agent-coding-workflow/internal/workspace"
)

// ===== fakes =====

// fakeWorktreeSvc implements workspace.WorktreeService.
type fakeWorktreeSvc struct {
	items []*workspace.Worktree
}

func (f *fakeWorktreeSvc) Create(_ context.Context, _ workspace.Caller, wsID uuid.UUID, in workspace.CreateWorktreeInput) (*workspace.Worktree, error) {
	wt := &workspace.Worktree{
		ID: uuid.New(), WorkspaceID: wsID, Branch: in.Branch,
		Status: workspace.WorktreeStatusIdle, CreatedAt: time.Now(),
	}
	f.items = append(f.items, wt)
	return wt, nil
}
func (f *fakeWorktreeSvc) List(_ context.Context, _ workspace.Caller, wsID uuid.UUID) ([]*workspace.Worktree, error) {
	out := make([]*workspace.Worktree, 0, len(f.items))
	for _, wt := range f.items {
		if wt.WorkspaceID == wsID {
			out = append(out, wt)
		}
	}
	return out, nil
}
func (f *fakeWorktreeSvc) Delete(_ context.Context, _ workspace.Caller, wtID uuid.UUID) error {
	kept := f.items[:0]
	for _, wt := range f.items {
		if wt.ID != wtID {
			kept = append(kept, wt)
		}
	}
	f.items = kept
	return nil
}
func (f *fakeWorktreeSvc) Acquire(_ context.Context, c workspace.Caller, wtID uuid.UUID) (*workspace.Worktree, error) {
	for _, wt := range f.items {
		if wt.ID == wtID {
			wt.Status = workspace.WorktreeStatusActive
			wt.ActiveHolder = c.Holder()
			return wt, nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "worktree")
}
func (f *fakeWorktreeSvc) Release(_ context.Context, _ workspace.Caller, wtID uuid.UUID) (*workspace.Worktree, error) {
	for _, wt := range f.items {
		if wt.ID == wtID {
			wt.Status = workspace.WorktreeStatusIdle
			wt.ActiveHolder = ""
			return wt, nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "worktree")
}
func (f *fakeWorktreeSvc) ReleaseByHolder(_ context.Context, _ string) ([]*workspace.Worktree, error) {
	return nil, nil
}

// fakeGitOpsSvc implements workspace.GitOpsService 并记录最近一次调用目标。
type fakeGitOpsSvc struct {
	statusFiles []git.FileStatus
	lastTarget  string // "main:<wsID>" / "worktree:<wtID>"
	lastCommit  workspace.CommitInput
	diffText    string
}

func (f *fakeGitOpsSvc) StatusOnMain(_ context.Context, _ workspace.Caller, wsID uuid.UUID) ([]git.FileStatus, error) {
	f.lastTarget = "main:" + wsID.String()
	return f.statusFiles, nil
}
func (f *fakeGitOpsSvc) CommitOnMain(_ context.Context, _ workspace.Caller, wsID uuid.UUID, in workspace.CommitInput) (string, error) {
	f.lastTarget = "main:" + wsID.String()
	f.lastCommit = in
	return "hash-main", nil
}
func (f *fakeGitOpsSvc) PushOnMain(_ context.Context, _ workspace.Caller, wsID uuid.UUID) error {
	f.lastTarget = "main:" + wsID.String()
	return nil
}
func (f *fakeGitOpsSvc) StatusOnWorktree(_ context.Context, _ workspace.Caller, wtID uuid.UUID) ([]git.FileStatus, error) {
	f.lastTarget = "worktree:" + wtID.String()
	return f.statusFiles, nil
}
func (f *fakeGitOpsSvc) CommitOnWorktree(_ context.Context, _ workspace.Caller, wtID uuid.UUID, in workspace.CommitInput) (string, error) {
	f.lastTarget = "worktree:" + wtID.String()
	f.lastCommit = in
	return "hash-wt", nil
}
func (f *fakeGitOpsSvc) PushOnWorktree(_ context.Context, _ workspace.Caller, wtID uuid.UUID) error {
	f.lastTarget = "worktree:" + wtID.String()
	return nil
}
func (f *fakeGitOpsSvc) LogOnMain(_ context.Context, _ workspace.Caller, wsID uuid.UUID, _ int) ([]git.Commit, error) {
	f.lastTarget = "main:" + wsID.String()
	return nil, nil
}
func (f *fakeGitOpsSvc) LogOnWorktree(_ context.Context, _ workspace.Caller, wtID uuid.UUID, _ int) ([]git.Commit, error) {
	f.lastTarget = "worktree:" + wtID.String()
	return nil, nil
}
func (f *fakeGitOpsSvc) DiffOnMain(_ context.Context, _ workspace.Caller, wsID uuid.UUID, _ git.DiffOptions) (git.DiffResult, error) {
	f.lastTarget = "main:" + wsID.String()
	return git.DiffResult{Diff: f.diffText}, nil
}
func (f *fakeGitOpsSvc) DiffOnWorktree(_ context.Context, _ workspace.Caller, wtID uuid.UUID, _ git.DiffOptions) (git.DiffResult, error) {
	f.lastTarget = "worktree:" + wtID.String()
	return git.DiffResult{Diff: f.diffText}, nil
}

// wsTestDeps 在 testDeps 基础上挂一个已存在的 workspace + worktree 服务。
func wsTestDeps() (ServerDeps, *fakeWorkspaceSvc, *fakeWorktreeSvc, *fakeGitOpsSvc) {
	deps := testDeps()
	wsSvc := &fakeWorkspaceSvc{
		items: []*workspace.Workspace{{
			ID: testWSID, ProjectID: testPID, Slug: "ws-1", Name: "Workspace 1",
			DefaultBranch: "main", SyncStatus: workspace.SyncStatusIdle, CreatedAt: time.Now(),
		}},
		branches: []string{"main", "dev"},
	}
	wtSvc := &fakeWorktreeSvc{}
	gitSvc := &fakeGitOpsSvc{}
	deps.Workspaces = wsSvc
	deps.Worktrees = wtSvc
	deps.GitOps = gitSvc
	return deps, wsSvc, wtSvc, gitSvc
}

// ===== tests =====

func TestGetWorkspace(t *testing.T) {
	deps, _, _, _ := wsTestDeps()
	result, err := callTool(ctxWithAuth(Scope{}), deps, "get_workspace", map[string]any{
		"workspace_id": testWSID.String(),
	})
	require.NoError(t, err)
	require.False(t, result.IsError)

	var out workspaceDetail
	unmarshalStructured(t, result, &out)
	assert.Equal(t, "ws-1", out.Slug)
	assert.Equal(t, testPID.String(), out.ProjectID)
}

func TestGetWorkspace_ScopeDenied(t *testing.T) {
	deps, _, _, _ := wsTestDeps()
	// token 限定到其他 project → 拒绝
	result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{uuid.New()}}), deps,
		"get_workspace", map[string]any{"workspace_id": testWSID.String()})
	require.NoError(t, err)
	assert.True(t, result.IsError)
}

func TestCreateUpdateDeleteWorkspace(t *testing.T) {
	deps, wsSvc, _, _ := wsTestDeps()

	result, err := callTool(ctxWithAuth(Scope{}), deps, "create_workspace", map[string]any{
		"project_slug": "test-project", "slug": "ws-2", "name": "Workspace 2",
		"git_remote_url": "https://example.com/repo.git",
	})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var createOut workspaceCreateOut
	unmarshalStructured(t, result, &createOut)
	assert.True(t, createOut.OK)
	assert.Equal(t, "ws-2", createOut.Workspace.Slug)
	require.Len(t, wsSvc.items, 2)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "update_workspace", map[string]any{
		"workspace_id": createOut.Workspace.ID, "name": "Renamed",
	})
	require.NoError(t, err)
	require.False(t, result.IsError)
	unmarshalStructured(t, result, &createOut)
	assert.Equal(t, "Renamed", createOut.Workspace.Name)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "delete_workspace", map[string]any{
		"workspace_id": createOut.Workspace.ID,
	})
	require.NoError(t, err)
	require.False(t, result.IsError)
	assert.Len(t, wsSvc.items, 1)
}

func TestListBranchesAndSync(t *testing.T) {
	deps, _, _, _ := wsTestDeps()

	result, err := callTool(ctxWithAuth(Scope{}), deps, "list_branches", map[string]any{
		"workspace_id": testWSID.String(),
	})
	require.NoError(t, err)
	var out listBranchesOut
	unmarshalStructured(t, result, &out)
	assert.Equal(t, []string{"main", "dev"}, out.Branches)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "sync_workspace", map[string]any{
		"workspace_id": testWSID.String(),
	})
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestWorktreeLifecycle(t *testing.T) {
	deps, _, wtSvc, _ := wsTestDeps()

	result, err := callTool(ctxWithAuth(Scope{}), deps, "create_worktree", map[string]any{
		"workspace_id": testWSID.String(), "branch": "feat/x",
	})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var createOut worktreeCreateOut
	unmarshalStructured(t, result, &createOut)
	assert.Equal(t, "feat/x", createOut.Worktree.Branch)
	wtID := createOut.Worktree.ID

	result, err = callTool(ctxWithAuth(Scope{}), deps, "list_worktrees", map[string]any{
		"workspace_id": testWSID.String(),
	})
	require.NoError(t, err)
	var listOut listWorktreesOut
	unmarshalStructured(t, result, &listOut)
	require.Len(t, listOut.Worktrees, 1)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "acquire_worktree", map[string]any{
		"workspace_id": testWSID.String(), "worktree_id": wtID,
	})
	require.NoError(t, err)
	unmarshalStructured(t, result, &createOut)
	assert.Equal(t, string(workspace.WorktreeStatusActive), createOut.Worktree.Status)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "release_worktree", map[string]any{
		"workspace_id": testWSID.String(), "worktree_id": wtID,
	})
	require.NoError(t, err)
	unmarshalStructured(t, result, &createOut)
	assert.Equal(t, string(workspace.WorktreeStatusIdle), createOut.Worktree.Status)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "delete_worktree", map[string]any{
		"workspace_id": testWSID.String(), "worktree_id": wtID,
	})
	require.NoError(t, err)
	assert.False(t, result.IsError)
	assert.Empty(t, wtSvc.items)
}

func TestWorktreeMembershipEnforced(t *testing.T) {
	deps, _, wtSvc, _ := wsTestDeps()
	// worktree 属于另一个 workspace → 不可经本 workspace 操作
	alien := &workspace.Worktree{
		ID: uuid.New(), WorkspaceID: uuid.New(), Branch: "other",
		Status: workspace.WorktreeStatusIdle, CreatedAt: time.Now(),
	}
	wtSvc.items = append(wtSvc.items, alien)

	result, err := callTool(ctxWithAuth(Scope{}), deps, "delete_worktree", map[string]any{
		"workspace_id": testWSID.String(), "worktree_id": alien.ID.String(),
	})
	require.NoError(t, err)
	assert.True(t, result.IsError)
	assert.Len(t, wtSvc.items, 1)
}

func TestGitStatusCommitPush_Main(t *testing.T) {
	deps, _, _, gitSvc := wsTestDeps()
	gitSvc.statusFiles = []git.FileStatus{{Path: "a.go", IndexX: 'M', WorktreeY: ' '}}

	result, err := callTool(ctxWithAuth(Scope{}), deps, "git_status", map[string]any{
		"workspace_id": testWSID.String(),
	})
	require.NoError(t, err)
	var statusOut gitStatusOut
	unmarshalStructured(t, result, &statusOut)
	require.Len(t, statusOut.Files, 1)
	assert.Equal(t, "M", statusOut.Files[0].Staged)
	assert.Equal(t, "main:"+testWSID.String(), gitSvc.lastTarget)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "git_commit", map[string]any{
		"workspace_id": testWSID.String(), "message": "feat: x",
	})
	require.NoError(t, err)
	var commitOut gitCommitOut
	unmarshalStructured(t, result, &commitOut)
	assert.Equal(t, "hash-main", commitOut.CommitHash)
	assert.True(t, gitSvc.lastCommit.AddAll, "add_all defaults to true")
	assert.False(t, gitSvc.lastCommit.Push)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "git_push", map[string]any{
		"workspace_id": testWSID.String(),
	})
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGitCommit_Worktree(t *testing.T) {
	deps, _, wtSvc, gitSvc := wsTestDeps()
	wt := &workspace.Worktree{
		ID: uuid.New(), WorkspaceID: testWSID, Branch: "feat/y",
		Status: workspace.WorktreeStatusIdle, CreatedAt: time.Now(),
	}
	wtSvc.items = append(wtSvc.items, wt)

	addAll := false
	result, err := callTool(ctxWithAuth(Scope{}), deps, "git_commit", map[string]any{
		"workspace_id": testWSID.String(), "worktree_id": wt.ID.String(),
		"message": "fix: y", "add_all": addAll, "push": true,
	})
	require.NoError(t, err)
	var commitOut gitCommitOut
	unmarshalStructured(t, result, &commitOut)
	assert.Equal(t, "hash-wt", commitOut.CommitHash)
	assert.Equal(t, "worktree:"+wt.ID.String(), gitSvc.lastTarget)
	assert.False(t, gitSvc.lastCommit.AddAll)
	assert.True(t, gitSvc.lastCommit.Push)
}

func TestGitDiff_MainAndScope(t *testing.T) {
	deps, _, _, gitSvc := wsTestDeps()
	gitSvc.diffText = "diff --git a/x b/x\n+added\n"

	result, err := callTool(ctxWithAuth(Scope{}), deps, "git_diff", map[string]any{
		"workspace_id": testWSID.String(),
	})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var out gitDiffOut
	unmarshalStructured(t, result, &out)
	assert.Contains(t, out.Diff, "+added")
	assert.Equal(t, "main:"+testWSID.String(), gitSvc.lastTarget)

	// scope to a different project => denied
	result, err = callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{uuid.New()}}), deps, "git_diff", map[string]any{
		"workspace_id": testWSID.String(),
	})
	require.NoError(t, err)
	assert.True(t, result.IsError)
}
