package acp

import (
	"context"
	"sync"
	"testing"

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

	"github.com/yan1h/agent-coding-workflow/internal/audit"
	"github.com/yan1h/agent-coding-workflow/internal/brief"
	"github.com/yan1h/agent-coding-workflow/internal/project"
)

// recordingAudit 捕获 Record 调用，供断言 brief_composed 是否写入。
type recordingAudit struct {
	mu      sync.Mutex
	entries []audit.Entry
}

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

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

// fakeComposer 是 brief.Composer 的测试替身：返回固定文本并记录是否被调用。
type fakeComposer struct {
	res    brief.BriefResult
	err    error
	called bool
	gotIn  brief.BriefInput
}

func (f *fakeComposer) ComposeTaskBrief(_ context.Context, in brief.BriefInput) (brief.BriefResult, error) {
	f.called = true
	f.gotIn = in
	return f.res, f.err
}

// fakeArtifactAccess 是 ArtifactAccess 的测试替身。
type fakeArtifactAccess struct {
	arts []*project.Artifact
}

func (f fakeArtifactAccess) ListLatestArtifacts(_ context.Context, _ uuid.UUID) ([]*project.Artifact, error) {
	return f.arts, nil
}

func testSession() *Session {
	return &Session{
		ID:        uuid.New(),
		ProjectID: uuid.New(),
		Branch:    "req/demo-7",
		CwdPath:   "/tmp/wt",
	}
}

func TestComposeInitialPrompt_RequirementContextComposed(t *testing.T) {
	t.Parallel()
	rec := &recordingAudit{}
	fc := &fakeComposer{res: brief.BriefResult{
		Text:     "你是一名资深产品规划助手。\n\n## 当前需求\n需求 #7：导出报表",
		Tokens:   20,
		Sections: []string{"role", "requirement_core"},
	}}
	s := &sessionService{
		audit:    rec,
		composer: fc,
		aa:       fakeArtifactAccess{arts: []*project.Artifact{{Phase: project.PhasePlanning, Version: 2}}},
		cfg:      SessionServiceConfig{BriefTokenBudget: 6000},
	}

	req := &project.Requirement{ID: uuid.New(), Number: 7, Title: "导出报表", Phase: project.PhasePlanning}
	c := Caller{UserID: uuid.New()}
	out := s.composeInitialPrompt(context.Background(), c, testSession(), nil, req, nil, "请开始")

	require.True(t, fc.called)
	assert.Contains(t, out, "需求 #7：导出报表")
	assert.Contains(t, out, "资深产品规划助手")
	// 组装入参带上了 requirement + artifacts + cwd + 用户文本 + budget。
	assert.Equal(t, req, fc.gotIn.Requirement)
	assert.Equal(t, "请开始", fc.gotIn.UserPrompt)
	assert.Equal(t, "/tmp/wt", fc.gotIn.RepoCwd)
	assert.Equal(t, 6000, fc.gotIn.TokenBudget)
	assert.Equal(t, brief.KindACPSession, fc.gotIn.Kind)
	assert.Len(t, fc.gotIn.Artifacts, 1)
	// 写了 brief_composed audit。
	assert.Contains(t, rec.actions(), "acp.session.brief_composed")
}

func TestComposeInitialPrompt_IssueContextComposed(t *testing.T) {
	t.Parallel()
	rec := &recordingAudit{}
	fc := &fakeComposer{res: brief.BriefResult{Text: "## 当前 Issue\nIssue #3：bug", Tokens: 8}}
	s := &sessionService{audit: rec, composer: fc}

	iss := &project.Issue{ID: uuid.New(), Number: 3, Title: "bug"}
	out := s.composeInitialPrompt(context.Background(), Caller{UserID: uuid.New()}, testSession(), nil, nil, iss, "fix it")

	require.True(t, fc.called)
	assert.Contains(t, out, "Issue #3：bug")
	assert.Equal(t, iss, fc.gotIn.Issue)
	// issue-only 时不调用 ArtifactAccess（aa 为 nil 也不 panic）。
}

func TestComposeInitialPrompt_NoPMContextFallsBackToRaw(t *testing.T) {
	t.Parallel()
	fc := &fakeComposer{res: brief.BriefResult{Text: "SHOULD NOT BE USED"}}
	s := &sessionService{audit: &recordingAudit{}, composer: fc}

	out := s.composeInitialPrompt(context.Background(), Caller{UserID: uuid.New()}, testSession(), nil, nil, nil, "raw text only")
	assert.Equal(t, "raw text only", out)
	assert.False(t, fc.called, "composer must not be called without PM context")
}

func TestComposeInitialPrompt_NilComposerFallsBackToRaw(t *testing.T) {
	t.Parallel()
	s := &sessionService{audit: &recordingAudit{}, composer: nil}
	req := &project.Requirement{ID: uuid.New(), Number: 7, Title: "x", Phase: project.PhasePlanning}
	out := s.composeInitialPrompt(context.Background(), Caller{UserID: uuid.New()}, testSession(), nil, req, nil, "raw")
	assert.Equal(t, "raw", out)
}

func TestComposeInitialPrompt_EmptyComposedTextFallsBackToRaw(t *testing.T) {
	t.Parallel()
	rec := &recordingAudit{}
	fc := &fakeComposer{res: brief.BriefResult{Text: ""}}
	s := &sessionService{audit: rec, composer: fc}
	req := &project.Requirement{ID: uuid.New(), Number: 1, Title: "t", Phase: project.PhasePlanning}
	out := s.composeInitialPrompt(context.Background(), Caller{UserID: uuid.New()}, testSession(), nil, req, nil, "fallback raw")
	assert.Equal(t, "fallback raw", out)
	// 空简报不写 brief_composed audit。
	assert.NotContains(t, rec.actions(), "acp.session.brief_composed")
}
