package mcp

import (
	"context"
	"encoding/json"
	"testing"
	"time"

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

	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"

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

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

// fakeUserSvc implements user.Service for tests.
type fakeUserSvc struct{ users map[uuid.UUID]*user.User }

func (s *fakeUserSvc) Login(_ context.Context, _, _, _ string) (string, *user.User, error) {
	return "", nil, nil
}
func (s *fakeUserSvc) Logout(_ context.Context, _ string) error { return nil }
func (s *fakeUserSvc) ResolveSession(_ context.Context, _ string) (uuid.UUID, bool, error) {
	return uuid.Nil, false, nil
}
func (s *fakeUserSvc) Get(_ context.Context, id uuid.UUID) (*user.User, error) {
	if u, ok := s.users[id]; ok {
		return u, nil
	}
	return nil, errs.New(errs.CodeNotFound, "user")
}
func (s *fakeUserSvc) Bootstrap(_ context.Context, _, _ string) (*user.User, error) {
	return nil, nil
}
func (s *fakeUserSvc) ListAdmins(_ context.Context) ([]*user.User, error) { return nil, nil }

func (s *fakeUserSvc) ListUsers(_ context.Context) ([]*user.User, error) { return nil, nil }
func (s *fakeUserSvc) CreateUser(_ context.Context, _ user.CreateUserInput) (*user.User, error) {
	return nil, nil
}
func (s *fakeUserSvc) UpdateProfile(_ context.Context, _ uuid.UUID, _ string) (*user.User, error) {
	return nil, nil
}
func (s *fakeUserSvc) SetAdmin(_ context.Context, _, _ uuid.UUID, _ bool) (*user.User, error) {
	return nil, nil
}
func (s *fakeUserSvc) SetEnabled(_ context.Context, _, _ uuid.UUID, _ bool) (*user.User, error) {
	return nil, nil
}
func (s *fakeUserSvc) AdminResetPassword(_ context.Context, _ uuid.UUID) (string, error) {
	return "", nil
}
func (s *fakeUserSvc) ChangePassword(_ context.Context, _ uuid.UUID, _, _ string) error { return nil }
func (s *fakeUserSvc) DeleteUser(_ context.Context, _, _ uuid.UUID) error               { return nil }

// fakeProjectSvc implements project.ProjectService.
type fakeProjectSvc struct {
	projects []*project.Project
	slugMap  map[string]*project.Project
	idMap    map[uuid.UUID]*project.Project
}

func newFakeProjectSvc(ps ...*project.Project) *fakeProjectSvc {
	f := &fakeProjectSvc{slugMap: map[string]*project.Project{}, idMap: map[uuid.UUID]*project.Project{}}
	for _, p := range ps {
		f.projects = append(f.projects, p)
		f.slugMap[p.Slug] = p
		f.idMap[p.ID] = p
	}
	return f
}
func (f *fakeProjectSvc) Create(_ context.Context, c project.Caller, in project.CreateProjectInput) (*project.Project, error) {
	v := in.Visibility
	if v == "" {
		v = project.VisibilityPrivate
	}
	p := &project.Project{
		ID: uuid.New(), Slug: in.Slug, Name: in.Name, Description: in.Description,
		Visibility: v, OwnerID: c.UserID, CreatedAt: time.Now(),
	}
	f.projects = append(f.projects, p)
	f.slugMap[p.Slug] = p
	f.idMap[p.ID] = p
	return p, nil
}
func (f *fakeProjectSvc) Get(_ context.Context, _ project.Caller, slug string) (*project.Project, error) {
	if p, ok := f.slugMap[slug]; ok {
		return p, nil
	}
	return nil, errs.New(errs.CodeNotFound, "project")
}
func (f *fakeProjectSvc) GetByID(_ context.Context, _ project.Caller, id uuid.UUID) (*project.Project, error) {
	if p, ok := f.idMap[id]; ok {
		return p, nil
	}
	return nil, errs.New(errs.CodeNotFound, "project")
}
func (f *fakeProjectSvc) List(_ context.Context, _ project.Caller, _ bool) ([]*project.Project, error) {
	return f.projects, nil
}
func (f *fakeProjectSvc) Update(ctx context.Context, c project.Caller, slug string, in project.UpdateProjectInput) (*project.Project, error) {
	p, err := f.Get(ctx, c, slug)
	if err != nil {
		return nil, err
	}
	if in.Name != nil {
		p.Name = *in.Name
	}
	if in.Description != nil {
		p.Description = *in.Description
	}
	if in.Visibility != nil {
		p.Visibility = *in.Visibility
	}
	return p, nil
}
func (f *fakeProjectSvc) Archive(ctx context.Context, c project.Caller, slug string) error {
	p, err := f.Get(ctx, c, slug)
	if err != nil {
		return err
	}
	now := time.Now()
	p.ArchivedAt = &now
	return nil
}
func (f *fakeProjectSvc) Unarchive(ctx context.Context, c project.Caller, slug string) error {
	p, err := f.Get(ctx, c, slug)
	if err != nil {
		return err
	}
	p.ArchivedAt = nil
	return nil
}
func (f *fakeProjectSvc) ListMembers(_ context.Context, _ project.Caller, _ string) ([]*project.Member, error) {
	return nil, nil
}
func (f *fakeProjectSvc) AddMember(_ context.Context, _ project.Caller, _ string, _ project.AddMemberInput) (*project.Member, error) {
	return nil, nil
}
func (f *fakeProjectSvc) RemoveMember(_ context.Context, _ project.Caller, _ string, _ uuid.UUID) error {
	return nil
}
func (f *fakeProjectSvc) MemberRole(_ context.Context, _, _ uuid.UUID) (project.Role, error) {
	return project.RoleNone, nil
}

// fakeRequirementSvc implements project.RequirementService.
type fakeRequirementSvc struct {
	items []*project.Requirement
}

func (f *fakeRequirementSvc) Create(_ context.Context, _ project.Caller, _ string, in project.CreateRequirementInput) (*project.Requirement, error) {
	r := &project.Requirement{
		ID: uuid.New(), Number: len(f.items) + 1,
		Title: in.Title, Description: in.Description,
		Phase: project.PhasePlanning, Status: project.StatusOpen,
		CreatedAt: time.Now(),
	}
	f.items = append(f.items, r)
	return r, nil
}
func (f *fakeRequirementSvc) Get(_ context.Context, _ project.Caller, slug string, number int) (*project.Requirement, error) {
	for _, r := range f.items {
		if r.Number == number {
			return r, nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "requirement")
}
func (f *fakeRequirementSvc) List(_ context.Context, _ project.Caller, _ string, _ project.RequirementFilter) ([]*project.Requirement, error) {
	return f.items, nil
}
func (f *fakeRequirementSvc) Update(_ context.Context, _ project.Caller, slug string, num int, in project.UpdateRequirementInput) (*project.Requirement, error) {
	r, err := f.Get(context.Background(), project.Caller{}, slug, num)
	if err != nil {
		return nil, err
	}
	if in.Title != nil {
		r.Title = *in.Title
	}
	if in.Description != nil {
		r.Description = *in.Description
	}
	return r, nil
}
func (f *fakeRequirementSvc) ChangePhase(_ context.Context, _ project.Caller, slug string, num int, to project.Phase) (*project.Requirement, error) {
	r, err := f.Get(context.Background(), project.Caller{}, slug, num)
	if err != nil {
		return nil, err
	}
	r.Phase = to
	return r, nil
}
func (f *fakeRequirementSvc) Close(_ context.Context, _ project.Caller, slug string, num int) error {
	r, err := f.Get(context.Background(), project.Caller{}, slug, num)
	if err != nil {
		return err
	}
	r.Status = project.StatusClosed
	return nil
}
func (f *fakeRequirementSvc) Reopen(_ context.Context, _ project.Caller, slug string, num int) error {
	r, err := f.Get(context.Background(), project.Caller{}, slug, num)
	if err != nil {
		return err
	}
	r.Status = project.StatusOpen
	return nil
}

// fakeArtifactSvc implements project.ArtifactService.
type fakeArtifactSvc struct {
	items []*project.Artifact
}

func (f *fakeArtifactSvc) Create(_ context.Context, c project.Caller, _ string, reqNumber int, in project.CreateArtifactInput) (*project.Artifact, error) {
	a := &project.Artifact{
		ID: uuid.New(), Phase: in.Phase, Version: len(f.items) + 1,
		Content: in.Content, Note: in.Note, CreatedBy: c.UserID,
		CreatedAt: time.Now(), Verdict: project.VerdictNone,
	}
	f.items = append(f.items, a)
	return a, nil
}
func (f *fakeArtifactSvc) List(_ context.Context, _ project.Caller, _ string, _ int, phase project.Phase) ([]*project.Artifact, error) {
	out := make([]*project.Artifact, 0, len(f.items))
	for _, a := range f.items {
		if phase == "" || a.Phase == phase {
			out = append(out, a)
		}
	}
	return out, nil
}
func (f *fakeArtifactSvc) GetByVersion(_ context.Context, _ project.Caller, _ string, _ int, phase project.Phase, version int) (*project.Artifact, error) {
	for _, a := range f.items {
		if a.Phase == phase && a.Version == version {
			return a, nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "artifact")
}
func (f *fakeArtifactSvc) Approve(_ context.Context, _ project.Caller, _ string, _ int, phase project.Phase, version int, verdict project.Verdict) (*project.Artifact, error) {
	for _, a := range f.items {
		if a.Phase == phase && a.Version == version {
			a.Verdict = verdict
			return a, nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "artifact")
}

// fakeIssueSvc implements project.IssueService.
type fakeIssueSvc struct {
	items []*project.Issue
	deps  []*project.Dependency
}

func (f *fakeIssueSvc) Create(_ context.Context, _ project.Caller, _ string, in project.CreateIssueInput) (*project.Issue, error) {
	prio := 0
	if in.Priority != nil {
		prio = *in.Priority
	}
	i := &project.Issue{
		ID: uuid.New(), Number: len(f.items) + 1,
		Title: in.Title, Description: in.Description,
		Status:    project.StatusOpen,
		Priority:  prio,
		CreatedAt: time.Now(),
	}
	f.items = append(f.items, i)
	return i, nil
}
func (f *fakeIssueSvc) Get(_ context.Context, _ project.Caller, slug string, number int) (*project.Issue, error) {
	for _, i := range f.items {
		if i.Number == number {
			return i, nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "issue")
}
func (f *fakeIssueSvc) List(_ context.Context, _ project.Caller, _ string, _ project.IssueFilter) ([]*project.Issue, error) {
	return f.items, nil
}
func (f *fakeIssueSvc) Update(_ context.Context, _ project.Caller, slug string, num int, in project.UpdateIssueInput) (*project.Issue, error) {
	i, err := f.Get(context.Background(), project.Caller{}, slug, num)
	if err != nil {
		return nil, err
	}
	if in.Title != nil {
		i.Title = *in.Title
	}
	if in.Description != nil {
		i.Description = *in.Description
	}
	return i, nil
}
func (f *fakeIssueSvc) Assign(_ context.Context, _ project.Caller, slug string, num int, assignee *uuid.UUID) (*project.Issue, error) {
	i, err := f.Get(context.Background(), project.Caller{}, slug, num)
	if err != nil {
		return nil, err
	}
	i.AssigneeID = assignee
	return i, nil
}
func (f *fakeIssueSvc) Close(_ context.Context, _ project.Caller, slug string, num int) error {
	i, err := f.Get(context.Background(), project.Caller{}, slug, num)
	if err != nil {
		return err
	}
	i.Status = project.StatusClosed
	return nil
}
func (f *fakeIssueSvc) Reopen(_ context.Context, _ project.Caller, slug string, num int) error {
	i, err := f.Get(context.Background(), project.Caller{}, slug, num)
	if err != nil {
		return err
	}
	i.Status = project.StatusOpen
	return nil
}
func (f *fakeIssueSvc) CreateSubtask(ctx context.Context, c project.Caller, slug string, parentNumber int, in project.CreateIssueInput) (*project.Issue, error) {
	parent, err := f.Get(ctx, c, slug, parentNumber)
	if err != nil {
		return nil, err
	}
	i, err := f.Create(ctx, c, slug, in)
	if err != nil {
		return nil, err
	}
	pid := parent.ID
	i.ParentID = &pid
	return i, nil
}
func (f *fakeIssueSvc) AddDependency(ctx context.Context, c project.Caller, slug string, in project.AddDependencyInput) (*project.Dependency, error) {
	if in.BlockedNumber == in.BlockerNumber {
		return nil, errs.New(errs.CodeInvalidInput, "issue 不能依赖自身")
	}
	blocked, err := f.Get(ctx, c, slug, in.BlockedNumber)
	if err != nil {
		return nil, err
	}
	blocker, err := f.Get(ctx, c, slug, in.BlockerNumber)
	if err != nil {
		return nil, err
	}
	d := &project.Dependency{ID: uuid.New(), BlockedID: blocked.ID, BlockerID: blocker.ID, CreatedAt: time.Now()}
	f.deps = append(f.deps, d)
	return d, nil
}
func (f *fakeIssueSvc) RemoveDependency(ctx context.Context, c project.Caller, slug string, blockedNumber, blockerNumber int) error {
	blocked, err := f.Get(ctx, c, slug, blockedNumber)
	if err != nil {
		return err
	}
	blocker, err := f.Get(ctx, c, slug, blockerNumber)
	if err != nil {
		return err
	}
	for idx, d := range f.deps {
		if d.BlockedID == blocked.ID && d.BlockerID == blocker.ID {
			f.deps = append(f.deps[:idx], f.deps[idx+1:]...)
			return nil
		}
	}
	return errs.New(errs.CodeNotFound, "依赖不存在")
}
func (f *fakeIssueSvc) ListDependencies(ctx context.Context, c project.Caller, slug string, number int) ([]*project.Issue, []*project.Issue, error) {
	iss, err := f.Get(ctx, c, slug, number)
	if err != nil {
		return nil, nil, err
	}
	byID := func(id uuid.UUID) *project.Issue {
		for _, x := range f.items {
			if x.ID == id {
				return x
			}
		}
		return nil
	}
	var blockedBy, blocks []*project.Issue
	for _, d := range f.deps {
		if d.BlockedID == iss.ID {
			if b := byID(d.BlockerID); b != nil {
				blockedBy = append(blockedBy, b)
			}
		}
		if d.BlockerID == iss.ID {
			if b := byID(d.BlockedID); b != nil {
				blocks = append(blocks, b)
			}
		}
	}
	return blockedBy, blocks, nil
}

// fakeWorkspaceSvc implements workspace.WorkspaceService.
type fakeWorkspaceSvc struct {
	items    []*workspace.Workspace
	branches []string
}

func (f *fakeWorkspaceSvc) Create(_ context.Context, _ workspace.Caller, _ string, in workspace.CreateWorkspaceInput) (*workspace.Workspace, error) {
	w := &workspace.Workspace{
		ID: uuid.New(), ProjectID: testPID,
		Slug: in.Slug, Name: in.Name, Description: in.Description,
		GitRemoteURL: in.GitRemoteURL, DefaultBranch: in.DefaultBranch,
		SyncStatus: workspace.SyncStatusIdle, CreatedAt: time.Now(),
	}
	f.items = append(f.items, w)
	return w, nil
}
func (f *fakeWorkspaceSvc) Get(_ context.Context, _ workspace.Caller, id uuid.UUID) (*workspace.Workspace, error) {
	for _, w := range f.items {
		if w.ID == id {
			return w, nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "workspace")
}
func (f *fakeWorkspaceSvc) List(_ context.Context, _ workspace.Caller, _ string) ([]*workspace.Workspace, error) {
	return f.items, nil
}
func (f *fakeWorkspaceSvc) Update(ctx context.Context, c workspace.Caller, id uuid.UUID, in workspace.UpdateWorkspaceInput) (*workspace.Workspace, error) {
	w, err := f.Get(ctx, c, id)
	if err != nil {
		return nil, err
	}
	if in.Name != nil {
		w.Name = *in.Name
	}
	if in.Description != nil {
		w.Description = *in.Description
	}
	if in.DefaultBranch != nil {
		w.DefaultBranch = *in.DefaultBranch
	}
	return w, nil
}
func (f *fakeWorkspaceSvc) Delete(ctx context.Context, c workspace.Caller, id uuid.UUID) error {
	if _, err := f.Get(ctx, c, id); err != nil {
		return err
	}
	kept := f.items[:0]
	for _, w := range f.items {
		if w.ID != id {
			kept = append(kept, w)
		}
	}
	f.items = kept
	return nil
}
func (f *fakeWorkspaceSvc) Sync(ctx context.Context, c workspace.Caller, id uuid.UUID) (*workspace.Workspace, error) {
	return f.Get(ctx, c, id)
}
func (f *fakeWorkspaceSvc) UpsertCredential(_ context.Context, _ workspace.Caller, _ uuid.UUID, _ workspace.UpsertCredentialInput) (*workspace.Credential, error) {
	panic("not implemented")
}
func (f *fakeWorkspaceSvc) GetCredentialMeta(_ context.Context, _ workspace.Caller, _ uuid.UUID) (*workspace.Credential, error) {
	panic("not implemented")
}
func (f *fakeWorkspaceSvc) ListBranches(_ context.Context, _ workspace.Caller, _ uuid.UUID) ([]string, error) {
	return f.branches, nil
}

// ===== test helpers =====

var (
	testUID  = uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
	testPID  = uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
	testWSID = uuid.MustParse("cccccccc-cccc-cccc-cccc-cccccccccccc")
)

// testDeps assembles ServerDeps with fake services.
func testDeps() ServerDeps {
	proj := &project.Project{
		ID: testPID, Slug: "test-project", Name: "Test Project",
		Visibility: project.VisibilityPrivate, OwnerID: testUID,
		CreatedAt: time.Now(),
	}
	uSvc := &fakeUserSvc{users: map[uuid.UUID]*user.User{
		testUID: {ID: testUID, IsAdmin: true},
	}}
	return ServerDeps{
		Caller:     NewCallerResolver(uSvc),
		Projects:   newFakeProjectSvc(proj),
		Reqs:       &fakeRequirementSvc{},
		Issues:     &fakeIssueSvc{},
		Artifacts:  &fakeArtifactSvc{},
		Workspaces: &fakeWorkspaceSvc{},
	}
}

// callTool wires up an MCP server+client via in-memory transport,
// injects an AuthSession into the server's receiving middleware,
// and calls a tool by name.
// Each call creates a fresh server to avoid middleware accumulation.
func callTool(ctx context.Context, deps ServerDeps, toolName string, args map[string]any) (*mcpsdk.CallToolResult, error) {
	// Extract AuthSession from caller's ctx and inject into server-side via middleware.
	sess, _ := FromContext(ctx)

	srv := NewMCPServer(deps)
	srv.AddReceivingMiddleware(func(next mcpsdk.MethodHandler) mcpsdk.MethodHandler {
		return func(ctx context.Context, method string, req mcpsdk.Request) (mcpsdk.Result, error) {
			if sess != nil {
				ctx = context.WithValue(ctx, AuthContextKey, sess)
			}
			return next(ctx, method, req)
		}
	})

	client := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "test-client", Version: "0.0.1"}, nil)
	t1, t2 := mcpsdk.NewInMemoryTransports()
	if _, err := srv.Connect(ctx, t1, nil); err != nil {
		return nil, err
	}
	session, err := client.Connect(ctx, t2, nil)
	if err != nil {
		return nil, err
	}
	defer session.Close()

	return session.CallTool(ctx, &mcpsdk.CallToolParams{
		Name:      toolName,
		Arguments: args,
	})
}

// unmarshalStructured extracts the StructuredContent from a CallToolResult.
func unmarshalStructured(t *testing.T, result *mcpsdk.CallToolResult, out any) {
	t.Helper()
	data, err := json.Marshal(result.StructuredContent)
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(data, out))
}

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

func TestListProjects(t *testing.T) {
	deps := testDeps()
	result, err := callTool(ctxWithAuth(Scope{}), deps, "list_projects", map[string]any{})
	require.NoError(t, err)
	assert.False(t, result.IsError)

	var out listProjectsOut
	unmarshalStructured(t, result, &out)
	require.Len(t, out.Projects, 1)
	assert.Equal(t, "test-project", out.Projects[0].Slug)
}

func TestListWorkspaces(t *testing.T) {
	ws := &workspace.Workspace{
		ID: testWSID, Slug: "ws-1", Name: "Workspace 1",
		DefaultBranch: "main", SyncStatus: workspace.SyncStatusIdle,
	}
	deps := testDeps()
	deps.Workspaces = &fakeWorkspaceSvc{items: []*workspace.Workspace{ws}}

	result, err := callTool(ctxWithAuth(Scope{}), deps, "list_workspaces", map[string]any{
		"project_slug": "test-project",
	})
	require.NoError(t, err)
	assert.False(t, result.IsError)

	var out listWorkspacesOut
	unmarshalStructured(t, result, &out)
	require.Len(t, out.Workspaces, 1)
	assert.Equal(t, "ws-1", out.Workspaces[0].Slug)
}

func TestCreateAndGetIssue(t *testing.T) {
	deps := testDeps()

	result, err := callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
		"project_slug": "test-project",
		"title":        "Bug report",
		"description":  "Something broke",
	})
	require.NoError(t, err)
	assert.False(t, result.IsError)

	var createOut issueCreateOut
	unmarshalStructured(t, result, &createOut)
	assert.True(t, createOut.OK)
	assert.Equal(t, "Bug report", createOut.Issue.Title)

	// Get
	result, err = callTool(ctxWithAuth(Scope{}), deps, "get_issue", map[string]any{
		"project_slug": "test-project",
		"number":       float64(1),
	})
	require.NoError(t, err)
	var getOut issueDetail
	unmarshalStructured(t, result, &getOut)
	assert.Equal(t, "Bug report", getOut.Title)
}

func TestUpdateIssue(t *testing.T) {
	deps := testDeps()

	_, err := callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
		"project_slug": "test-project", "title": "Original",
	})
	require.NoError(t, err)

	result, err := callTool(ctxWithAuth(Scope{}), deps, "update_issue", map[string]any{
		"project_slug": "test-project", "number": float64(1), "title": "Updated",
	})
	require.NoError(t, err)
	var out issueUpdateOut
	unmarshalStructured(t, result, &out)
	assert.True(t, out.OK)
	assert.Equal(t, "Updated", out.Issue.Title)
}

func TestCloseReopenIssue(t *testing.T) {
	deps := testDeps()

	_, err := callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
		"project_slug": "test-project", "title": "To close",
	})
	require.NoError(t, err)

	result, err := callTool(ctxWithAuth(Scope{}), deps, "close_issue", map[string]any{
		"project_slug": "test-project", "number": float64(1),
	})
	require.NoError(t, err)
	var out okOut
	unmarshalStructured(t, result, &out)
	assert.True(t, out.OK)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "reopen_issue", map[string]any{
		"project_slug": "test-project", "number": float64(1),
	})
	require.NoError(t, err)
	unmarshalStructured(t, result, &out)
	assert.True(t, out.OK)
}

func TestAssignIssue(t *testing.T) {
	deps := testDeps()

	_, err := callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
		"project_slug": "test-project", "title": "Assign me",
	})
	require.NoError(t, err)

	assigneeID := uuid.New().String()
	result, err := callTool(ctxWithAuth(Scope{}), deps, "assign_issue", map[string]any{
		"project_slug": "test-project", "number": float64(1), "assignee_id": assigneeID,
	})
	require.NoError(t, err)
	var out okOut
	unmarshalStructured(t, result, &out)
	assert.True(t, out.OK)
}

func TestCreateAndGetRequirement(t *testing.T) {
	deps := testDeps()

	result, err := callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
		"project_slug": "test-project", "title": "Auth module", "description": "Need auth",
	})
	require.NoError(t, err)
	var createOut requirementCreateOut
	unmarshalStructured(t, result, &createOut)
	assert.True(t, createOut.OK)
	assert.Equal(t, "Auth module", createOut.Requirement.Title)
	assert.Equal(t, "planning", createOut.Requirement.Phase)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "get_requirement", map[string]any{
		"project_slug": "test-project", "number": float64(1),
	})
	require.NoError(t, err)
	var getOut requirementDetail
	unmarshalStructured(t, result, &getOut)
	assert.Equal(t, "Auth module", getOut.Title)
}

func TestUpdateRequirement(t *testing.T) {
	deps := testDeps()

	_, err := callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
		"project_slug": "test-project", "title": "V1",
	})
	require.NoError(t, err)

	result, err := callTool(ctxWithAuth(Scope{}), deps, "update_requirement", map[string]any{
		"project_slug": "test-project", "number": float64(1), "title": "V2",
	})
	require.NoError(t, err)
	var out requirementCreateOut
	unmarshalStructured(t, result, &out)
	assert.Equal(t, "V2", out.Requirement.Title)
}

func TestCloseReopenRequirement(t *testing.T) {
	deps := testDeps()

	_, err := callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
		"project_slug": "test-project", "title": "Close me",
	})
	require.NoError(t, err)

	result, err := callTool(ctxWithAuth(Scope{}), deps, "close_requirement", map[string]any{
		"project_slug": "test-project", "number": float64(1),
	})
	require.NoError(t, err)
	var out okOut
	unmarshalStructured(t, result, &out)
	assert.True(t, out.OK)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "reopen_requirement", map[string]any{
		"project_slug": "test-project", "number": float64(1),
	})
	require.NoError(t, err)
	unmarshalStructured(t, result, &out)
	assert.True(t, out.OK)
}

func TestSetRequirementPhase(t *testing.T) {
	deps := testDeps()

	_, err := callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
		"project_slug": "test-project", "title": "Phase test",
	})
	require.NoError(t, err)

	result, err := callTool(ctxWithAuth(Scope{}), deps, "set_requirement_phase", map[string]any{
		"project_slug": "test-project", "number": float64(1), "phase": "auditing",
	})
	require.NoError(t, err)
	var out requirementCreateOut
	unmarshalStructured(t, result, &out)
	assert.Equal(t, "auditing", out.Requirement.Phase)
}

func TestApproveRequirementArtifact(t *testing.T) {
	deps := testDeps()

	_, err := callTool(ctxWithAuth(Scope{}), deps, "add_requirement_artifact", map[string]any{
		"project_slug": "test-project", "number": float64(1), "phase": "auditing", "content": "# audit",
	})
	require.NoError(t, err)

	result, err := callTool(ctxWithAuth(Scope{}), deps, "approve_requirement_artifact", map[string]any{
		"project_slug": "test-project", "number": float64(1), "phase": "auditing",
		"version": float64(1), "verdict": "pass",
	})
	require.NoError(t, err)
	var out artifactCreateOut
	unmarshalStructured(t, result, &out)
	assert.True(t, out.OK)
	assert.Equal(t, "pass", out.Artifact.Verdict)
	assert.True(t, out.Artifact.Approved)
}

func TestApproveRequirementArtifact_ScopeGated(t *testing.T) {
	deps := testDeps()
	// scope 不含 approve_requirement_artifact → CheckToolFromCtx 拒绝（IsError）。
	result, err := callTool(ctxWithAuth(Scope{Tools: []string{"add_requirement_artifact"}}), deps,
		"approve_requirement_artifact", map[string]any{
			"project_slug": "test-project", "number": float64(1), "phase": "auditing",
			"version": float64(1), "verdict": "pass",
		})
	require.NoError(t, err)
	assert.True(t, result.IsError, "token scope without approve_requirement_artifact must deny")
}

func TestListRequirements(t *testing.T) {
	deps := testDeps()

	_, _ = callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
		"project_slug": "test-project", "title": "R1",
	})
	_, _ = callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
		"project_slug": "test-project", "title": "R2",
	})

	result, err := callTool(ctxWithAuth(Scope{}), deps, "list_requirements", map[string]any{
		"project_slug": "test-project",
	})
	require.NoError(t, err)
	var out listRequirementsOut
	unmarshalStructured(t, result, &out)
	assert.Len(t, out.Requirements, 2)
}

func TestListIssues(t *testing.T) {
	deps := testDeps()

	_, _ = callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
		"project_slug": "test-project", "title": "I1",
	})
	_, _ = callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
		"project_slug": "test-project", "title": "I2",
	})

	result, err := callTool(ctxWithAuth(Scope{}), deps, "list_issues", map[string]any{
		"project_slug": "test-project",
	})
	require.NoError(t, err)
	var out listIssuesOut
	unmarshalStructured(t, result, &out)
	assert.Len(t, out.Issues, 2)
}

func TestToolScopeCheck(t *testing.T) {
	deps := testDeps()

	// Restricted scope: only list_projects
	restricted := Scope{Tools: []string{"list_projects"}}
	result, err := callTool(ctxWithAuth(restricted), deps, "list_projects", map[string]any{})
	require.NoError(t, err)
	assert.False(t, result.IsError)

	result, err = callTool(ctxWithAuth(restricted), deps, "create_issue", map[string]any{
		"project_slug": "test-project", "title": "Blocked",
	})
	require.NoError(t, err)
	assert.True(t, result.IsError)
}

// ctxWithAuth returns a context with an AuthSession for the test admin user.
func ctxWithAuth(scope Scope) context.Context {
	return context.WithValue(context.Background(), AuthContextKey, &AuthSession{
		UserID:  testUID.String(),
		TokenID: uuid.NewString(),
		Scope:   scope,
	})
}
