package brief

import (
	"context"
	"strings"
	"testing"

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

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

func newReq(phase project.Phase) *project.Requirement {
	return &project.Requirement{
		ID:          uuid.New(),
		Number:      7,
		Title:       "导出报表",
		Description: "支持导出 Excel",
		Phase:       phase,
	}
}

func newIssue() *project.Issue {
	return &project.Issue{
		ID:          uuid.New(),
		Number:      12,
		Title:       "修复导出乱码",
		Description: "UTF-8 BOM 缺失",
	}
}

func art(phase project.Phase, version int, content string) *project.Artifact {
	return &project.Artifact{
		ID:            uuid.New(),
		RequirementID: uuid.New(),
		Phase:         phase,
		Version:       version,
		Content:       content,
	}
}

func TestComposeTaskBrief_FullBriefAllSectionsInPriorityOrder(t *testing.T) {
	t.Parallel()
	c := NewComposer(nil, Config{DefaultTokenBudget: 100000})

	res, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Requirement: newReq(project.PhaseAuditing),
		Artifacts: []*project.Artifact{
			art(project.PhasePlanning, 1, "# 规划\n## 验收标准\n- 必须支持 Excel"),
			art(project.PhasePrototyping, 1, "# 原型内容"),
			art(project.PhaseAuditing, 1, "# 评审内容"),
		},
		UserPrompt: "请开始实现",
		Kind:       KindACPSession,
	})
	require.NoError(t, err)
	assert.False(t, res.Truncated)

	// 角色（auditing 阶段用阶段角色）+ 交互协议 + 需求核心 + 验收标准 + 三个产物 + 用户指令。
	wantOrder := []string{
		"role", "interactive_protocol", "requirement_core", "acceptance_criteria",
		"artifact_auditing", "artifact_prototyping", "artifact_planning", "user_prompt",
	}
	assert.Equal(t, wantOrder, res.Sections)

	// 验收标准必须出现在 auditing 产物之前（优先级）。
	idxAC := strings.Index(res.Text, "验收标准")
	idxAudit := strings.Index(res.Text, "评审内容")
	require.GreaterOrEqual(t, idxAC, 0)
	require.GreaterOrEqual(t, idxAudit, 0)
	assert.Less(t, idxAC, idxAudit)

	// 用户指令永远在最后。
	assert.True(t, strings.HasSuffix(strings.TrimSpace(res.Text), "请开始实现"))
	// 角色提示与需求标题都在。
	assert.Contains(t, res.Text, PhaseRolePrompts[project.PhaseAuditing])
	assert.Contains(t, res.Text, "需求 #7：导出报表")
}

func TestComposeTaskBrief_TightBudgetDropsLowPrioritySections(t *testing.T) {
	t.Parallel()
	c := NewComposer(nil, Config{})

	bigPlanning := strings.Repeat("规", 4000) // ~1000 tokens
	res, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Requirement: newReq(project.PhasePlanning),
		Artifacts: []*project.Artifact{
			art(project.PhasePlanning, 1, bigPlanning),
			art(project.PhaseAuditing, 1, strings.Repeat("评", 4000)),
		},
		UserPrompt:  "做",
		Kind:        KindACPSession,
		TokenBudget: 300, // 紧预算
	})
	require.NoError(t, err)
	assert.True(t, res.Truncated)
	// 高优先级（角色/需求核心）保留，用户指令永远保留。
	assert.Contains(t, res.Sections, "role")
	assert.Contains(t, res.Sections, "requirement_core")
	assert.Contains(t, res.Sections, "user_prompt")
	// 用户指令永不丢。
	assert.Contains(t, res.Text, "## 操作者指令\n做")
}

func TestComposeTaskBrief_RequirementOnly_IssueOnly_Both(t *testing.T) {
	t.Parallel()
	c := NewComposer(nil, Config{DefaultTokenBudget: 100000})

	// requirement-only
	res1, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Requirement: newReq(project.PhasePlanning), Kind: KindChatRequirement,
	})
	require.NoError(t, err)
	assert.Contains(t, res1.Sections, "requirement_core")
	assert.NotContains(t, res1.Sections, "issue_core")

	// issue-only
	res2, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Issue: newIssue(), Kind: KindChatIssue,
	})
	require.NoError(t, err)
	assert.Contains(t, res2.Sections, "issue_core")
	assert.NotContains(t, res2.Sections, "requirement_core")
	assert.Contains(t, res2.Text, "Issue #12：修复导出乱码")

	// both
	res3, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Requirement: newReq(project.PhasePlanning), Issue: newIssue(), Kind: KindACPSession,
	})
	require.NoError(t, err)
	assert.Contains(t, res3.Sections, "requirement_core")
	assert.Contains(t, res3.Sections, "issue_core")
}

func TestComposeTaskBrief_MissingDataYieldsNoErrorAndOmitsSections(t *testing.T) {
	t.Parallel()
	c := NewComposer(nil, Config{DefaultTokenBudget: 100000})

	// 空描述 + 无产物。
	req := newReq(project.PhasePlanning)
	req.Description = ""
	res, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Requirement: req, Kind: KindChatRequirement,
	})
	require.NoError(t, err)
	assert.NotContains(t, res.Text, "需求描述")
	assert.NotContains(t, res.Sections, "artifact_planning")
	assert.NotContains(t, res.Sections, "acceptance_criteria")

	// 完全空输入（无 PM 上下文，仅 ACP 角色）。
	resEmpty, err := c.ComposeTaskBrief(context.Background(), BriefInput{Kind: KindACPSession})
	require.NoError(t, err)
	// ACP 场景仍给角色 + 交互协议，但无 PM section。
	assert.Contains(t, resEmpty.Sections, "role")
	assert.NotContains(t, resEmpty.Sections, "requirement_core")
}

func TestComposeTaskBrief_AcceptanceCriteriaExtraction(t *testing.T) {
	t.Parallel()
	c := NewComposer(nil, Config{DefaultTokenBudget: 100000})

	planning := art(project.PhasePlanning, 3, `# 规划文档
## 背景与目标
做导出。
## 验收标准
- 能导出 Excel
- 中文不乱码
## 风险
无`)
	res, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Requirement: newReq(project.PhasePlanning),
		Artifacts:   []*project.Artifact{planning},
		Kind:        KindChatRequirement,
	})
	require.NoError(t, err)
	assert.Contains(t, res.Sections, "acceptance_criteria")
	assert.Contains(t, res.Text, "能导出 Excel")
	assert.Contains(t, res.Text, "中文不乱码")
	// 验收标准段落不应吞掉后面的"风险"小节。
	acStart := strings.Index(res.Text, "## 验收标准")
	require.GreaterOrEqual(t, acStart, 0)
}

func TestComposeTaskBrief_AcceptanceCriteriaFallbackOmitsWhenAbsent(t *testing.T) {
	t.Parallel()
	c := NewComposer(nil, Config{DefaultTokenBudget: 100000})

	planning := art(project.PhasePlanning, 1, "# 规划\n## 背景\n仅有背景，没有验收标准小节")
	res, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Requirement: newReq(project.PhasePlanning),
		Artifacts:   []*project.Artifact{planning},
		Kind:        KindChatRequirement,
	})
	require.NoError(t, err)
	assert.NotContains(t, res.Sections, "acceptance_criteria")
}

func TestComposeTaskBrief_UserPromptNeverTruncatedEvenWhenExceedsBudget(t *testing.T) {
	t.Parallel()
	c := NewComposer(nil, Config{})

	huge := strings.Repeat("做", 5000) // 远超预算
	res, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Requirement: newReq(project.PhasePlanning),
		UserPrompt:  huge,
		Kind:        KindACPSession,
		TokenBudget: 50,
	})
	require.NoError(t, err)
	assert.Contains(t, res.Sections, "user_prompt")
	// 用户指令整段保留（不含截断标记）。
	assert.Contains(t, res.Text, huge)
	assert.NotContains(t, res.Text, "操作者指令\n"+huge+truncateMarker)
}

func TestComposeTaskBrief_LatestArtifactPerPhase(t *testing.T) {
	t.Parallel()
	c := NewComposer(nil, Config{DefaultTokenBudget: 100000})

	// 同阶段多版本，应取 version 最大的。
	res, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Requirement: newReq(project.PhasePrototyping),
		Artifacts: []*project.Artifact{
			art(project.PhasePrototyping, 1, "旧原型 v1"),
			art(project.PhasePrototyping, 3, "新原型 v3"),
			art(project.PhasePrototyping, 2, "中原型 v2"),
		},
		Kind: KindChatRequirement,
	})
	require.NoError(t, err)
	assert.Contains(t, res.Text, "新原型 v3")
	assert.NotContains(t, res.Text, "旧原型 v1")
	assert.Contains(t, res.Text, "prototyping 阶段产物 v3")
}

// fakeOrienter 用于验证 ACP 场景纳入仓库定向 section。
type fakeOrienter struct{ ori RepoOrientation }

func (f fakeOrienter) Orient(_ context.Context, _, _ string, _, _ int) RepoOrientation {
	return f.ori
}

func TestComposeTaskBrief_RepoOrientationIncludedForACP(t *testing.T) {
	t.Parallel()
	c := NewComposer(fakeOrienter{ori: RepoOrientation{
		Branch:        "feat/x",
		RecentCommits: []string{"abc1234 init"},
		DirtyFiles:    []string{"main.go"},
	}}, Config{DefaultTokenBudget: 100000})

	res, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Requirement: newReq(project.PhasePlanning),
		RepoCwd:     "/tmp/repo",
		Branch:      "feat/x",
		Kind:        KindACPSession,
	})
	require.NoError(t, err)
	assert.Contains(t, res.Sections, "repo_orientation")
	assert.Contains(t, res.Text, "当前分支：feat/x")
	assert.Contains(t, res.Text, "abc1234 init")
	assert.Contains(t, res.Text, "main.go")
}

func TestComposeTaskBrief_NoOrientationWhenCwdEmpty(t *testing.T) {
	t.Parallel()
	c := NewComposer(fakeOrienter{ori: RepoOrientation{Branch: "x"}}, Config{DefaultTokenBudget: 100000})
	res, err := c.ComposeTaskBrief(context.Background(), BriefInput{
		Requirement: newReq(project.PhasePlanning),
		Kind:        KindACPSession, // RepoCwd 为空 → 跳过定向
	})
	require.NoError(t, err)
	assert.NotContains(t, res.Sections, "repo_orientation")
}

func TestExtractSection_NumberedHeadingAndCaseInsensitive(t *testing.T) {
	t.Parallel()
	md := "## 6. Acceptance Criteria\n- foo\n## next\nbar"
	got := extractSection(md, acceptanceCriteriaHeadings)
	assert.Equal(t, "- foo", got)
}
