package project

import (
	"context"
	"sort"
	"sync"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgerrcode"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/stretchr/testify/require"

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

// fakeRepo 是内存版 Repository，仅用于 service 单测；按 ID/slug 查找用 map，
// number 自增手动维护。并发安全（一把大锁就够）。
type fakeRepo struct {
	mu            sync.Mutex
	projects      map[uuid.UUID]*Project
	requirements  map[uuid.UUID]*Requirement
	issues        map[uuid.UUID]*Issue
	artifacts     map[uuid.UUID]*Artifact
	phasePointers map[string]*PhasePointer // key: requirementID + "|" + phase
	dependencies  map[uuid.UUID]*Dependency
	members       map[string]*Member // key: projectID + "|" + userID

	// 置 true 时，下一次 CreateArtifact 返回一次真实的 PG 唯一约束冲突后自动复位，
	// 用于覆盖 artifactService.createWithRetry 的 version 竞争重试分支。
	createArtifactFailOnce bool
}

func newFakeRepo() *fakeRepo {
	return &fakeRepo{
		projects:      map[uuid.UUID]*Project{},
		requirements:  map[uuid.UUID]*Requirement{},
		issues:        map[uuid.UUID]*Issue{},
		artifacts:     map[uuid.UUID]*Artifact{},
		phasePointers: map[string]*PhasePointer{},
		dependencies:  map[uuid.UUID]*Dependency{},
		members:       map[string]*Member{},
	}
}

func memberKey(projectID, userID uuid.UUID) string {
	return projectID.String() + "|" + userID.String()
}

func phasePointerKey(reqID uuid.UUID, phase Phase) string {
	return reqID.String() + "|" + string(phase)
}

func (r *fakeRepo) cloneProject(p *Project) *Project     { v := *p; return &v }
func (r *fakeRepo) cloneReq(p *Requirement) *Requirement { v := *p; return &v }
func (r *fakeRepo) cloneIssue(p *Issue) *Issue           { v := *p; return &v }
func (r *fakeRepo) cloneArtifact(a *Artifact) *Artifact  { v := *a; return &v }

func (r *fakeRepo) CreateProject(_ context.Context, p *Project) (*Project, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, x := range r.projects {
		if x.Slug == p.Slug {
			return nil, errs.New(errs.CodeProjectSlugTaken, "slug 已被使用")
		}
	}
	now := time.Now()
	p.CreatedAt, p.UpdatedAt = now, now
	r.projects[p.ID] = r.cloneProject(p)
	return r.cloneProject(p), nil
}

func (r *fakeRepo) GetProjectBySlug(_ context.Context, slug string) (*Project, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, p := range r.projects {
		if p.Slug == slug {
			return r.cloneProject(p), nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "project 不存在")
}

func (r *fakeRepo) GetProjectByID(_ context.Context, id uuid.UUID) (*Project, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	p, ok := r.projects[id]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "project 不存在")
	}
	return r.cloneProject(p), nil
}

func (r *fakeRepo) ListProjects(_ context.Context, includeArchived bool) ([]*Project, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]*Project, 0, len(r.projects))
	for _, p := range r.projects {
		if !includeArchived && p.ArchivedAt != nil {
			continue
		}
		out = append(out, r.cloneProject(p))
	}
	return out, nil
}

func (r *fakeRepo) UpdateProject(_ context.Context, p *Project) (*Project, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	cur, ok := r.projects[p.ID]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "project 不存在")
	}
	cur.Name, cur.Description, cur.Visibility = p.Name, p.Description, p.Visibility
	cur.UpdatedAt = time.Now()
	return r.cloneProject(cur), nil
}

func (r *fakeRepo) ArchiveProject(_ context.Context, id uuid.UUID) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	p, ok := r.projects[id]
	if !ok {
		return nil
	}
	now := time.Now()
	p.ArchivedAt, p.UpdatedAt = &now, now
	return nil
}

func (r *fakeRepo) UnarchiveProject(_ context.Context, id uuid.UUID) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	p, ok := r.projects[id]
	if !ok {
		return nil
	}
	p.ArchivedAt, p.UpdatedAt = nil, time.Now()
	return nil
}

// ===== ProjectMember =====

func (r *fakeRepo) UpsertMember(_ context.Context, projectID, userID uuid.UUID, role Role, addedBy *uuid.UUID) (*Member, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	m := &Member{ProjectID: projectID, UserID: userID, Role: role, AddedBy: addedBy, CreatedAt: time.Now()}
	r.members[memberKey(projectID, userID)] = m
	cp := *m
	return &cp, nil
}

func (r *fakeRepo) DeleteMember(_ context.Context, projectID, userID uuid.UUID) (int64, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	k := memberKey(projectID, userID)
	if _, ok := r.members[k]; !ok {
		return 0, nil
	}
	delete(r.members, k)
	return 1, nil
}

func (r *fakeRepo) GetMemberRole(_ context.Context, projectID, userID uuid.UUID) (Role, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	m, ok := r.members[memberKey(projectID, userID)]
	if !ok {
		return RoleNone, nil
	}
	return m.Role, nil
}

func (r *fakeRepo) ListMembers(_ context.Context, projectID uuid.UUID) ([]*Member, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]*Member, 0)
	for _, m := range r.members {
		if m.ProjectID == projectID {
			cp := *m
			out = append(out, &cp)
		}
	}
	return out, nil
}

// ===== Requirement =====

func (r *fakeRepo) nextRequirementNumber(projectID uuid.UUID) int {
	maxNum := 0
	for _, x := range r.requirements {
		if x.ProjectID == projectID && x.Number > maxNum {
			maxNum = x.Number
		}
	}
	return maxNum + 1
}

func (r *fakeRepo) CreateRequirement(_ context.Context, in *Requirement) (*Requirement, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	in.Number = r.nextRequirementNumber(in.ProjectID)
	now := time.Now()
	in.CreatedAt, in.UpdatedAt = now, now
	if in.Phase == "" {
		in.Phase = PhasePlanning
	}
	if in.Status == "" {
		in.Status = StatusOpen
	}
	r.requirements[in.ID] = r.cloneReq(in)
	return r.cloneReq(in), nil
}

func (r *fakeRepo) GetRequirementByNumber(_ context.Context, projectID uuid.UUID, number int) (*Requirement, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, x := range r.requirements {
		if x.ProjectID == projectID && x.Number == number {
			return r.cloneReq(x), nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
}

func (r *fakeRepo) GetRequirementByID(_ context.Context, id uuid.UUID) (*Requirement, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	x, ok := r.requirements[id]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
	}
	return r.cloneReq(x), nil
}

func (r *fakeRepo) ListRequirements(_ context.Context, projectID uuid.UUID, phase Phase, status Status) ([]*Requirement, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]*Requirement, 0)
	for _, x := range r.requirements {
		if x.ProjectID != projectID {
			continue
		}
		if phase != "" && x.Phase != phase {
			continue
		}
		if status != "" && x.Status != status {
			continue
		}
		out = append(out, r.cloneReq(x))
	}
	return out, nil
}

func (r *fakeRepo) UpdateRequirement(_ context.Context, in *Requirement) (*Requirement, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	cur, ok := r.requirements[in.ID]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
	}
	cur.Title, cur.Description, cur.OwnerID = in.Title, in.Description, in.OwnerID
	cur.WorkspaceID = in.WorkspaceID
	cur.UpdatedAt = time.Now()
	return r.cloneReq(cur), nil
}

func (r *fakeRepo) UpdateRequirementPhase(_ context.Context, id uuid.UUID, to Phase) (*Requirement, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	cur, ok := r.requirements[id]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
	}
	cur.Phase = to
	cur.UpdatedAt = time.Now()
	return r.cloneReq(cur), nil
}

func (r *fakeRepo) CloseRequirement(_ context.Context, id uuid.UUID) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	cur, ok := r.requirements[id]
	if !ok || cur.Status != StatusOpen {
		return nil
	}
	now := time.Now()
	cur.Status, cur.ClosedAt, cur.UpdatedAt = StatusClosed, &now, now
	return nil
}

func (r *fakeRepo) ReopenRequirement(_ context.Context, id uuid.UUID) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	cur, ok := r.requirements[id]
	if !ok || cur.Status != StatusClosed {
		return nil
	}
	cur.Status, cur.ClosedAt, cur.UpdatedAt = StatusOpen, nil, time.Now()
	return nil
}

// ===== Issue =====

func (r *fakeRepo) nextIssueNumber(projectID uuid.UUID) int {
	maxNum := 0
	for _, x := range r.issues {
		if x.ProjectID == projectID && x.Number > maxNum {
			maxNum = x.Number
		}
	}
	return maxNum + 1
}

func (r *fakeRepo) CreateIssue(_ context.Context, in *Issue) (*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	in.Number = r.nextIssueNumber(in.ProjectID)
	now := time.Now()
	in.CreatedAt, in.UpdatedAt = now, now
	if in.Status == "" {
		in.Status = StatusOpen
	}
	r.issues[in.ID] = r.cloneIssue(in)
	return r.cloneIssue(in), nil
}

func (r *fakeRepo) GetIssueByNumber(_ context.Context, projectID uuid.UUID, number int) (*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, x := range r.issues {
		if x.ProjectID == projectID && x.Number == number {
			return r.cloneIssue(x), nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "issue 不存在")
}

func (r *fakeRepo) GetIssueByID(_ context.Context, id uuid.UUID) (*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, x := range r.issues {
		if x.ID == id {
			return r.cloneIssue(x), nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "issue 不存在")
}

func (r *fakeRepo) ListIssues(_ context.Context, projectID uuid.UUID, filter IssueFilter, requirementID *uuid.UUID) ([]*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]*Issue, 0)
	for _, x := range r.issues {
		if x.ProjectID != projectID {
			continue
		}
		if filter.Status != "" && x.Status != filter.Status {
			continue
		}
		if filter.Requirement != nil {
			if *filter.Requirement == 0 {
				if x.RequirementID != nil {
					continue
				}
			} else if x.RequirementID == nil || *x.RequirementID != *requirementID {
				continue
			}
		}
		if filter.AssigneeID != nil {
			if x.AssigneeID == nil || *x.AssigneeID != *filter.AssigneeID {
				continue
			}
		}
		out = append(out, r.cloneIssue(x))
	}
	return out, nil
}

func (r *fakeRepo) ListIssuesByRequirement(_ context.Context, requirementID uuid.UUID) ([]*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]*Issue, 0)
	for _, x := range r.issues {
		if x.RequirementID != nil && *x.RequirementID == requirementID {
			out = append(out, r.cloneIssue(x))
		}
	}
	return out, nil
}

func (r *fakeRepo) UpdateIssueCore(_ context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID, workspaceID *uuid.UUID) (*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	cur, ok := r.issues[id]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "issue 不存在")
	}
	cur.Title, cur.Description, cur.RequirementID = title, description, requirementID
	cur.WorkspaceID = workspaceID
	cur.UpdatedAt = time.Now()
	return r.cloneIssue(cur), nil
}

func (r *fakeRepo) UpdateIssueAssignee(_ context.Context, id uuid.UUID, assignee *uuid.UUID) (*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	cur, ok := r.issues[id]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "issue 不存在")
	}
	cur.AssigneeID = assignee
	cur.UpdatedAt = time.Now()
	return r.cloneIssue(cur), nil
}

func (r *fakeRepo) CloseIssue(_ context.Context, id uuid.UUID) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	cur, ok := r.issues[id]
	if !ok || cur.Status != StatusOpen {
		return nil
	}
	now := time.Now()
	cur.Status, cur.ClosedAt, cur.UpdatedAt = StatusClosed, &now, now
	return nil
}

func (r *fakeRepo) ReopenIssue(_ context.Context, id uuid.UUID) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	cur, ok := r.issues[id]
	if !ok || cur.Status != StatusClosed {
		return nil
	}
	cur.Status, cur.ClosedAt, cur.UpdatedAt = StatusOpen, nil, time.Now()
	return nil
}

func (r *fakeRepo) UpdateIssuePriority(_ context.Context, id uuid.UUID, priority int) (*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	cur, ok := r.issues[id]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "issue 不存在")
	}
	cur.Priority = priority
	cur.UpdatedAt = time.Now()
	return r.cloneIssue(cur), nil
}

func (r *fakeRepo) CreateDependency(_ context.Context, d *Dependency) (*Dependency, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, x := range r.dependencies {
		if x.BlockedID == d.BlockedID && x.BlockerID == d.BlockerID {
			return nil, errs.New(errs.CodeInvalidInput, "依赖已存在")
		}
	}
	d.CreatedAt = time.Now()
	v := *d
	r.dependencies[d.ID] = &v
	out := *d
	return &out, nil
}

func (r *fakeRepo) DeleteDependency(_ context.Context, projectID, blockedID, blockerID uuid.UUID) (int64, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for id, x := range r.dependencies {
		if x.ProjectID == projectID && x.BlockedID == blockedID && x.BlockerID == blockerID {
			delete(r.dependencies, id)
			return 1, nil
		}
	}
	return 0, nil
}

func (r *fakeRepo) ListBlockers(_ context.Context, blockedID uuid.UUID) ([]*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]*Issue, 0)
	for _, d := range r.dependencies {
		if d.BlockedID == blockedID {
			if iss, ok := r.issues[d.BlockerID]; ok {
				out = append(out, r.cloneIssue(iss))
			}
		}
	}
	return out, nil
}

func (r *fakeRepo) ListBlocked(_ context.Context, blockerID uuid.UUID) ([]*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]*Issue, 0)
	for _, d := range r.dependencies {
		if d.BlockerID == blockerID {
			if iss, ok := r.issues[d.BlockedID]; ok {
				out = append(out, r.cloneIssue(iss))
			}
		}
	}
	return out, nil
}

func (r *fakeRepo) ListBlockerIDs(_ context.Context, blockedID uuid.UUID) ([]uuid.UUID, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]uuid.UUID, 0)
	for _, d := range r.dependencies {
		if d.BlockedID == blockedID {
			out = append(out, d.BlockerID)
		}
	}
	return out, nil
}

func (r *fakeRepo) ListSubtasks(_ context.Context, parentID uuid.UUID) ([]*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]*Issue, 0)
	for _, x := range r.issues {
		if x.ParentID != nil && *x.ParentID == parentID {
			out = append(out, r.cloneIssue(x))
		}
	}
	return out, nil
}

func (r *fakeRepo) ListReadyLeafSubtasks(_ context.Context, projectID uuid.UUID, limit int) ([]*Issue, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]*Issue, 0)
	for _, x := range r.issues {
		if x.ProjectID != projectID || x.Status != StatusOpen || x.ParentID == nil {
			continue
		}
		blocked := false
		for _, d := range r.dependencies {
			if d.BlockedID == x.ID {
				if b, ok := r.issues[d.BlockerID]; ok && b.Status != StatusClosed {
					blocked = true
					break
				}
			}
		}
		if blocked {
			continue
		}
		out = append(out, r.cloneIssue(x))
	}
	if limit > 0 && len(out) > limit {
		out = out[:limit]
	}
	return out, nil
}

func (r *fakeRepo) ListSchedulableProjects(_ context.Context) ([]uuid.UUID, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	seen := map[uuid.UUID]struct{}{}
	out := make([]uuid.UUID, 0)
	for _, x := range r.issues {
		if x.Status == StatusOpen && x.ParentID != nil {
			if _, ok := seen[x.ProjectID]; !ok {
				seen[x.ProjectID] = struct{}{}
				out = append(out, x.ProjectID)
			}
		}
	}
	return out, nil
}

func (r *fakeRepo) CountActiveSessionsForProject(_ context.Context, _ uuid.UUID) (int, error) {
	return 0, nil
}

func (r *fakeRepo) GetMaxArtifactVersion(_ context.Context, requirementID uuid.UUID, phase Phase) (int, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	maxVer := 0
	for _, x := range r.artifacts {
		if x.RequirementID == requirementID && x.Phase == phase && x.Version > maxVer {
			maxVer = x.Version
		}
	}
	return maxVer, nil
}

func (r *fakeRepo) CreateArtifact(_ context.Context, in *Artifact) (*Artifact, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.createArtifactFailOnce {
		r.createArtifactFailOnce = false
		// 模拟真实 PG 唯一约束冲突，触发 IsUniqueViolation 重试分支。
		return nil, &pgconn.PgError{Code: pgerrcode.UniqueViolation}
	}
	for _, x := range r.artifacts {
		if x.RequirementID == in.RequirementID && x.Phase == in.Phase && x.Version == in.Version {
			return nil, &pgconn.PgError{Code: pgerrcode.UniqueViolation}
		}
	}
	in.CreatedAt = time.Now()
	if in.Verdict == "" {
		in.Verdict = VerdictNone // 模拟 DB 列默认值
	}
	r.artifacts[in.ID] = r.cloneArtifact(in)
	return r.cloneArtifact(in), nil
}

func (r *fakeRepo) ListArtifactsByRequirement(_ context.Context, requirementID uuid.UUID, phase Phase) ([]*Artifact, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]*Artifact, 0)
	for _, x := range r.artifacts {
		if x.RequirementID == requirementID && (phase == "" || x.Phase == phase) {
			out = append(out, r.cloneArtifact(x))
		}
	}
	sort.Slice(out, func(i, j int) bool {
		if out[i].Phase != out[j].Phase {
			return out[i].Phase < out[j].Phase
		}
		return out[i].Version < out[j].Version
	})
	return out, nil
}

func (r *fakeRepo) GetArtifactByVersion(_ context.Context, requirementID uuid.UUID, phase Phase, version int) (*Artifact, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, x := range r.artifacts {
		if x.RequirementID == requirementID && x.Phase == phase && x.Version == version {
			return r.cloneArtifact(x), nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "artifact 不存在")
}

func (r *fakeRepo) GetArtifactByID(_ context.Context, id uuid.UUID) (*Artifact, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if x, ok := r.artifacts[id]; ok {
		return r.cloneArtifact(x), nil
	}
	return nil, errs.New(errs.CodeNotFound, "artifact 不存在")
}

func (r *fakeRepo) ApproveArtifact(_ context.Context, artifactID uuid.UUID, verdict Verdict) (*Artifact, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	x, ok := r.artifacts[artifactID]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "artifact 不存在")
	}
	x.Verdict = verdict
	return r.cloneArtifact(x), nil
}

func (r *fakeRepo) UpsertPhasePointer(_ context.Context, requirementID uuid.UUID, phase Phase, artifactID, approvedBy uuid.UUID) (*PhasePointer, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	now := time.Now()
	aid, by := artifactID, approvedBy
	ptr := &PhasePointer{
		RequirementID:      requirementID,
		Phase:              phase,
		ApprovedArtifactID: &aid,
		ApprovedBy:         &by,
		ApprovedAt:         &now,
	}
	r.phasePointers[phasePointerKey(requirementID, phase)] = ptr
	v := *ptr
	return &v, nil
}

func (r *fakeRepo) GetPhasePointer(_ context.Context, requirementID uuid.UUID, phase Phase) (*PhasePointer, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	ptr, ok := r.phasePointers[phasePointerKey(requirementID, phase)]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "phase pointer 不存在")
	}
	v := *ptr
	return &v, nil
}

func (r *fakeRepo) ListPhasePointers(_ context.Context, requirementID uuid.UUID) ([]*PhasePointer, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]*PhasePointer, 0)
	for _, ptr := range r.phasePointers {
		if ptr.RequirementID == requirementID {
			v := *ptr
			out = append(out, &v)
		}
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Phase < out[j].Phase })
	return out, nil
}

// spyAudit 收集所有写入的 audit entry，方便断言。
type spyAudit struct {
	mu      sync.Mutex
	entries []audit.Entry
}

func (s *spyAudit) Record(_ context.Context, e audit.Entry) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.entries = append(s.entries, e)
	return nil
}

func (s *spyAudit) actions() []string {
	s.mu.Lock()
	defer s.mu.Unlock()
	out := make([]string, 0, len(s.entries))
	for _, e := range s.entries {
		out = append(out, e.Action)
	}
	return out
}

// seedProject 写一条测试用 Project 并返回。caller 默认为 owner。
func seedProject(t *testing.T, r *fakeRepo, ownerID uuid.UUID) *Project {
	t.Helper()
	p := &Project{
		ID: uuid.New(), Slug: "demo", Name: "Demo",
		Visibility: VisibilityPrivate, OwnerID: ownerID,
	}
	out, err := r.CreateProject(context.Background(), p)
	require.NoError(t, err)
	return out
}

func TestProjectService_Create_HappyPath(t *testing.T) {
	repo := newFakeRepo()
	spy := &spyAudit{}
	svc := NewProjectService(repo, spy)
	owner := uuid.New()
	p, err := svc.Create(context.Background(), Caller{UserID: owner, IsAdmin: false}, CreateProjectInput{
		Slug: "demo", Name: "Demo", Visibility: VisibilityPrivate,
	})
	require.NoError(t, err)
	require.Equal(t, "demo", p.Slug)
	require.Equal(t, owner, p.OwnerID)
	require.Equal(t, []string{"project.create"}, spy.actions())
}

func TestProjectService_Create_SlugTaken(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	seedProject(t, repo, owner)
	svc := NewProjectService(repo, nil)
	_, err := svc.Create(context.Background(), Caller{UserID: owner}, CreateProjectInput{
		Slug: "demo", Name: "Other", Visibility: VisibilityPrivate,
	})
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeProjectSlugTaken, ae.Code)
}

func TestProjectService_Get_PrivateRejectsStranger(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	seedProject(t, repo, owner)
	svc := NewProjectService(repo, nil)
	_, err := svc.Get(context.Background(), Caller{UserID: uuid.New()}, "demo")
	ae, ok := errs.As(err)
	require.True(t, ok)
	// stranger 看 private project：屏蔽存在性，按 not_found 处理。
	require.Equal(t, errs.CodeNotFound, ae.Code)
}

func TestProjectService_Get_InternalAllowsLoggedIn(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	p := seedProject(t, repo, owner)
	p.Visibility = VisibilityInternal
	_, err := repo.UpdateProject(context.Background(), p)
	require.NoError(t, err)
	svc := NewProjectService(repo, nil)
	got, err := svc.Get(context.Background(), Caller{UserID: uuid.New()}, "demo")
	require.NoError(t, err)
	require.Equal(t, "demo", got.Slug)
}

func TestProjectService_Update_RequiresOwnerOrAdmin(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	seedProject(t, repo, owner)
	svc := NewProjectService(repo, nil)
	name := "Renamed"
	_, err := svc.Update(context.Background(), Caller{UserID: uuid.New()}, "demo", UpdateProjectInput{Name: &name})
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeNotFound, ae.Code, "stranger 看 private 直接 not_found")
}

func TestProjectService_Archive_BlocksWrites(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	seedProject(t, repo, owner)
	svc := NewProjectService(repo, nil)
	require.NoError(t, svc.Archive(context.Background(), Caller{UserID: owner}, "demo"))
	name := "X"
	_, err := svc.Update(context.Background(), Caller{UserID: owner}, "demo", UpdateProjectInput{Name: &name})
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeProjectArchived, ae.Code)
}
