package promptcompiler

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
	"unicode/utf8"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/identitynotice"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/desktop/prompts"
	"github.com/tidbcloud/AgentTeam/internal/core/domain"
)

func TestRenderRuntimeInputCodexUsesSplitPayload(t *testing.T) {
	baseBlock, err := DefaultPromptBlockRegistry().Construct("platform.identity", newIdentityPromptInput(IdentityTemplateData{AgentName: "base", ChannelDisplayName: "AgentTeam"}))
	require.NoError(t, err)
	developerBlock, err := DefaultPromptBlockRegistry().Construct("agent_config.agents_md", newWorkspaceDocumentPromptInput(WorkspaceDocumentTemplateData{Content: "developer instructions"}))
	require.NoError(t, err)
	env := RuntimeInputEnvelope{
		SchemaVersion: "prompt-runtime-input/v1",
		EventID:       "evt-codex-render",
		EventType:     EventThreadStart,
		AgentID:       "agt-1",
		EventKind:     EventThreadContract,
		Bundle: &PromptBundle{
			SchemaVersion: "prompt-bundle/v1",
			EventID:       "evt-codex-render",
			EventType:     EventThreadStart,
			AgentID:       "agt-1",
			Blocks:        []PromptBlock{baseBlock, developerBlock},
		},
	}

	rendered, err := RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	require.Equal(t, "codex", rendered.Runtime)
	require.NotEmpty(t, ExtractStringSlot(rendered, "baseInstructions"))
	require.NotEmpty(t, ExtractStringSlot(rendered, "developerInstructions"))
	require.Empty(t, ExtractStringSlot(rendered, "runtimeCarrier"))
	require.Equal(t, []string{"baseInstructions", "developerInstructions"}, rendered.RenderOrder)
}

func TestCompileGeneratedTurnProducesOneTypedAttributionBlock(t *testing.T) {
	compiler := New(DefaultPolicy())
	envelope, err := compiler.CompileGeneratedTurn(
		context.Background(),
		EventFinalVisibleReplyCheck,
		"evt-final-check",
		GeneratedPromptInput{
			Kind: GeneratedFinalReplyCheck,
			FinalReply: FinalVisibleReplyCheckTemplateData{
				Wakes:           []FinalVisibleReplyCheckTemplateWake{{Target: "#agentteam", HasTarget: true}},
				SendMessageTool: "message_send",
			},
		},
	)
	require.NoError(t, err)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), envelope, "codex", RenderOptions{})
	require.NoError(t, err)
	require.Contains(t, ExtractStringSlot(rendered, "turnInput"), "exact target \"#agentteam\"")
	require.Len(t, rendered.AttributionBlocks, 1)
	require.Equal(t, "agentteam.final_visible_reply_check", rendered.AttributionBlocks[0].ID)
	require.Equal(t, BlockTurnContext, rendered.AttributionBlocks[0].Type)
	require.Equal(t, LayerRuntimeGuidance, rendered.AttributionBlocks[0].Layer)
	require.Equal(t, DurabilityTurnOnly, rendered.AttributionBlocks[0].Durability)
	require.Positive(t, rendered.AttributionBlocks[0].TokenEstimate)
}

func TestCompileRuntimeInputReturnsCanceledContextBeforeWork(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	_, err := CompileRuntimeInput(ctx, CompileDescriptor{EventKind: EventThreadContract}, CompileInput{
		AgentID:   "agt-1",
		AgentName: "agent",
	})
	require.ErrorIs(t, err, context.Canceled)
}

func TestRenderRuntimeInputReturnsCanceledContextBeforeValidation(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	_, err := RenderRuntimeInput(ctx, RuntimeInputEnvelope{}, "codex", RenderOptions{})
	require.ErrorIs(t, err, context.Canceled)
}

func TestCompileThreadContract_IncludesBaseAndDeveloperSources(t *testing.T) {
	workDir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(workDir, "memory", "system"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "memory", "system", "identity.md"), []byte("# Identity\nPersistent reviewer\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte("Always validate migrations before shipping."), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "MEMORY.md"), []byte("# Memory\n\nDO NOT RAW INJECT MEMORY SENTINEL\n"), 0o644))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-thread-start", CompileInput{
		AgentID:                  "agt-1",
		AgentName:                "reviewer",
		AgentDescription:         "Review backend changes carefully",
		Runtime:                  "codex",
		WorkDir:                  workDir,
		ExternalSkillCorePayload: "## Core rules\n\nUse the MCP tools only.",
	})
	require.NoError(t, err)
	require.Equal(t, EventThreadContract, env.EventKind)
	require.NotNil(t, env.Bundle)
	require.NotEmpty(t, env.Bundle.Fingerprint)

	var (
		foundCooperation      bool
		foundAgentTeamReview  bool
		foundAgentTeamStartup bool
		foundAgentTeamTasks   bool
		foundIdentityDoc      bool
		foundIdentityNote     bool
		foundAgents           bool
		foundRole             bool
	)
	for _, block := range env.Bundle.Blocks {
		switch block.ID {
		case "platform.identity":
			require.Equal(t, SourceKindMarkdownTemplate, block.Source.SourceKind)
			require.Equal(t, "desktop/prompts/platform/identity.md", block.Source.Path)
		case "platform.agent_identity":
			foundIdentityNote = true
			require.Equal(t, SlotBase, block.Slot)
			require.Equal(t, LayerPlatform, block.Layer)
			require.Contains(t, block.text, "## Agent identity")
			require.Contains(t, block.text, "memory/system/identity.md")
		case "workspace.identity":
			foundIdentityDoc = true
			require.Equal(t, SourceKindWorkspaceDoc, block.Source.SourceKind)
			require.Equal(t, "desktop/prompts/events/workspace_identity.md", block.Source.Path)
			require.Equal(t, SlotBase, block.Slot)
			require.Equal(t, LayerWorkspaceDoc, block.Layer)
			require.Contains(t, block.text, "## System identity")
			require.Contains(t, block.text, "# Identity\nPersistent reviewer")
		case "platform.workspace_memory":
			require.Equal(t, SourceKindMarkdownTemplate, block.Source.SourceKind)
			require.Equal(t, "desktop/prompts/platform/workspace_memory.md", block.Source.Path)
		case "platform.cooperation":
			foundCooperation = true
			require.Equal(t, SlotBase, block.Slot)
			require.Equal(t, LayerPlatform, block.Layer)
			require.Contains(t, block.text, "## Cooperation")
			require.Contains(t, block.text, "Check existing context and ownership")
			require.Contains(t, block.text, "`[@name](/mentions/agent/<agent-id>)`")
			require.Contains(t, block.text, "plain `@name` is name-based and may be ambiguous")
		case "platform.agentteam.startup":
			foundAgentTeamStartup = true
			require.Equal(t, SlotBase, block.Slot)
			require.Equal(t, LayerPlatform, block.Layer)
			require.Equal(t, "desktop/prompts/platform/agentteam/startup.md", block.Source.Path)
			require.Contains(t, block.text, "## Startup")
			require.Contains(t, block.text, "concrete incoming message first")
			require.Contains(t, block.text, "it is authoritative")
			require.Contains(t, block.text, "Actionable feedback creates work")
			require.Contains(t, block.text, "accepted but unimplemented work remains pending")
			require.Contains(t, block.text, "to its exact target")
			require.Contains(t, block.text, "Never poll for messages")
			// 190: startup now makes actionable-feedback ownership and the
			// wake-start-only stop condition explicit.
			require.LessOrEqual(t, block.TokenEstimate, 190)
		case "platform.agentteam.task_boards":
			foundAgentTeamTasks = true
			require.Equal(t, "desktop/prompts/platform/agentteam/task_boards.md", block.Source.Path)
			require.Contains(t, block.text, "## Task boards")
			require.Contains(t, block.text, "Use tasks as the ledger for substantial work")
			require.Contains(t, block.text, "do not search broadly just to decide")
			require.Contains(t, block.text, "run `agentteam exec task claim`")
			require.Contains(t, block.text, "`agentteam exec task checkpoint`")
			require.Contains(t, block.text, "Task refs are `#tN`")
			require.Contains(t, block.text, "`agentteam exec task update`")
			require.Contains(t, block.text, "thread as the task origin")
			require.Contains(t, block.text, "`in_review` → `in_progress`")
			require.Contains(t, block.text, "then return to `in_review`")
			// 340: the block absorbed thread-first placement (create defaults,
			// origin lineage), task-discussion routing, and the complete
			// in_review -> in_progress -> in_review rework discipline.
			require.LessOrEqual(t, block.TokenEstimate, 340)
		case "agent_config.agents_md":
			foundAgents = true
			require.Equal(t, SlotDeveloper, block.Slot)
			require.Contains(t, block.text, "Always validate migrations")
		case "agent_config.initial_role":
			foundRole = true
			require.Equal(t, SlotBase, block.Slot)
			require.Contains(t, block.text, "Review backend changes carefully")
		case "platform.agentteam.review_context":
			foundAgentTeamReview = true
			require.Equal(t, "desktop/prompts/platform/agentteam/review_context.md", block.Source.Path)
			require.Contains(t, block.text, "## Review context")
			require.Contains(t, block.text, "`agentteam exec review add`")
			require.Contains(t, block.text, "branch/commit `head`")
			require.Contains(t, block.text, "worktree path only for uncommitted state")
			require.Contains(t, block.text, "Exclude memory, notes")
			require.LessOrEqual(t, block.TokenEstimate, 120)
		}
	}
	require.True(t, foundCooperation)
	require.True(t, foundAgentTeamStartup)
	require.True(t, foundAgentTeamTasks)
	require.True(t, foundAgentTeamReview)
	require.True(t, foundIdentityNote)
	require.True(t, foundIdentityDoc)
	require.True(t, foundAgents)
	require.True(t, foundRole)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	baseInstructions := ExtractStringSlot(rendered, "baseInstructions")
	require.Contains(t, baseInstructions, `You are "reviewer", an AI agent in AgentTeam.`)
	require.Contains(t, baseInstructions, "AgentTeam is a multi-agent chat platform where humans and agents collaborate through channels, threads, and tasks.")
	require.Contains(t, baseInstructions, "## Initial role")
	require.Contains(t, baseInstructions, "Review backend changes carefully")
	require.Contains(t, baseInstructions, "You work alongside humans and other agents in a shared workspace.")
	require.Contains(t, baseInstructions, "## Cooperation")
	require.Contains(t, baseInstructions, "## Startup")
	require.Contains(t, baseInstructions, "Never poll for messages")
	require.NotContains(t, baseInstructions, "queued_messages")
	require.Contains(t, baseInstructions, "do not defer it to a scheduled task or a future wake")
	require.NotContains(t, baseInstructions, "collect only the small amount of context needed to define the packet")
	require.Less(t, strings.Index(baseInstructions, "## Startup"), strings.Index(baseInstructions, "\n## Workspace & Memory\n"))
	require.Contains(t, baseInstructions, "## Task boards")
	require.Contains(t, baseInstructions, "## Review context")
	require.NotContains(t, baseInstructions, "### Shared memory skill")
	require.NotContains(t, baseInstructions, "DO NOT RAW INJECT MEMORY SENTINEL")
	require.Contains(t, baseInstructions, "## Agent identity")
	require.Contains(t, baseInstructions, "## System identity")
	require.Contains(t, baseInstructions, "# Identity\nPersistent reviewer")
	require.Less(t, strings.Index(baseInstructions, "## Review context"), strings.Index(baseInstructions, "## Workspace & Memory"))
	require.Contains(t, ExtractStringSlot(rendered, "developerInstructions"), "Always validate migrations")
	require.NotContains(t, ExtractStringSlot(rendered, "developerInstructions"), "DO NOT RAW INJECT MEMORY SENTINEL")
	require.NotContains(t, ExtractStringSlot(rendered, "developerInstructions"), "## Initial role")

	var foundIdentityBlock bool
	for _, block := range env.Bundle.Blocks {
		require.NotEqual(t, "workspace.memory_md", block.ID)
		if block.ID != "platform.identity" {
			continue
		}
		foundIdentityBlock = true
		require.Equal(t, "desktop/prompts/platform/identity.md", block.Source.Path)
		require.NotEmpty(t, block.Source.Version)
	}
	require.True(t, foundIdentityBlock)
}

func TestCompileThreadContract_RendersDelegatedSubagentLeaderOnlyPolicy(t *testing.T) {
	toolName := func(name string) string { return "mcp__agentteam__" + name }
	parts := AgentTeamSkillDocParts(toolName)

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-delegated-subagent-policy", CompileInput{
		AgentID:                  "agt-1",
		AgentName:                "reviewer",
		Runtime:                  "codex",
		WorkDir:                  t.TempDir(),
		ExternalSkillCorePayload: parts.Core,
	})
	require.NoError(t, err)

	var coreBlock *PromptBlock
	for i := range env.Bundle.Blocks {
		if env.Bundle.Blocks[i].ID == "platform.agentteam.external_skill_core" {
			coreBlock = &env.Bundle.Blocks[i]
			break
		}
	}
	require.NotNil(t, coreBlock)
	require.Equal(t, "desktop/prompts/wire/external_skill_core.md", coreBlock.Source.Path)
	require.Contains(t, coreBlock.text, parts.Core)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	base := ExtractStringSlot(rendered, "baseInstructions")
	require.Contains(t, base, "Delegated subagents return only to their leader")
	require.Contains(t, base, "current prompt explicitly authorizes communication to one exact target")
	require.Contains(t, base, "Merely naming or inheriting an ambient, historical/recovered, or Owner-DM target is never authorization")
	require.Contains(t, base, "independent of retired execution mechanisms")
	require.Contains(t, base, "exact target of the current inbound delivery")
	require.Contains(t, base, "For direct human work, send a brief plan")
	require.NotContains(t, base, "agentteam_execution_")
	require.NotContains(t, base, "Execution Packet")
	require.NotContains(t, base, "Result Packet")
}

func TestCompileThreadContract_ComposesAgentDescriptionAndRoleIdentity(t *testing.T) {
	workDir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(workDir, "memory", "system"), 0o755))
	require.NoError(t, os.WriteFile(
		filepath.Join(workDir, "memory", "system", "identity.md"),
		[]byte("# Release reviewer\n\nFollow the reusable review checklist."),
		0o644,
	))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-composed-identity", CompileInput{
		AgentID:          "agt-1",
		AgentName:        "reviewer",
		AgentDescription: "Focus this agent on the current release train.",
		Runtime:          "codex",
		WorkDir:          workDir,
	})
	require.NoError(t, err)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	baseInstructions := ExtractStringSlot(rendered, "baseInstructions")
	descriptionIndex := strings.Index(baseInstructions, "Focus this agent on the current release train.")
	identityIndex := strings.Index(baseInstructions, "Follow the reusable review checklist.")
	require.GreaterOrEqual(t, descriptionIndex, 0)
	require.Greater(t, identityIndex, descriptionIndex)
}

func TestRenderedPromptsExcludeRetiredAgentTeamToolNames(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-retired-tools-thread", CompileInput{
		AgentID: "agt-1", AgentName: "reviewer", Runtime: "codex", WorkDir: t.TempDir(),
	})
	require.NoError(t, err)
	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	assertNoRetiredAgentTeamToolNames(t, ExtractStringSlot(rendered, "baseInstructions"))

	env, err = compiler.CompileTurnContext(context.Background(), EventWake, "evt-retired-tools-turn", "", CompileInput{
		AgentID: "agt-1", Runtime: "codex",
		Binding: ConversationBinding{ChannelID: "agentteam", TargetID: "#general"},
		Message: types.InboxMessage{MessageID: "msg-2", Target: "#general", AuthorProfile: types.AuthorProfile{ActorType: "human", DisplayName: "alice"}, Content: "Please review this PR."},
	})
	require.NoError(t, err)
	rendered, err = compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	assertNoRetiredAgentTeamToolNames(t, ExtractStringSlot(rendered, "turnInput"))
}

func assertNoRetiredAgentTeamToolNames(t *testing.T, text string) {
	t.Helper()
	for _, retired := range []string{
		"task_set_claim", "task_create", "task_update", "task_get", "task_list", "task_checkpoint",
	} {
		require.NotContains(t, text, retired)
	}
}

func TestCompileThreadContract_OmitsDeliveryContractBlock(t *testing.T) {
	compiler := New(DefaultPolicy())
	baseInput := CompileInput{
		AgentID:   "agt-1",
		AgentName: "reviewer",
		Runtime:   "codex",
		WorkDir:   t.TempDir(),
	}

	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-thread-delivery-contract", baseInput)
	require.NoError(t, err)
	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "baseInstructions")
	// The participant-delivery contract is a turn-scoped block and must not
	// leak into the thread/base instructions slot.
	require.NotContains(t, text, "## Participant delivery contract")
}

func TestCompileThreadContract_PrefersRemoteMemoryPromptWhenMounted(t *testing.T) {
	workDir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(workDir, "memory"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "memory", "MEMORY.md"), []byte("# Memory\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "MEMORY.md"), []byte("# Local fallback\n"), 0o644))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-remote-memory", CompileInput{
		AgentID:   "agt-1",
		AgentName: "reviewer",
		Runtime:   "codex",
		WorkDir:   workDir,
	})
	require.NoError(t, err)

	var memoryBlock *PromptBlock
	for i := range env.Bundle.Blocks {
		if env.Bundle.Blocks[i].ID == "platform.workspace_memory" {
			memoryBlock = &env.Bundle.Blocks[i]
			break
		}
	}
	require.NotNil(t, memoryBlock)
	require.Equal(t, SourceKindMarkdownTemplate, memoryBlock.Source.SourceKind)
	require.Equal(t, "desktop/prompts/platform/workspace_memory.md", memoryBlock.Source.Path)
	require.Contains(t, memoryBlock.text, "memory/MEMORY.md")
	require.Contains(t, memoryBlock.text, filepath.Join(workDir, "memory"))
	require.Contains(t, memoryBlock.text, "Memory root:")
	require.Contains(t, memoryBlock.text, "reusable facts")
	require.Contains(t, memoryBlock.text, "Consolidate")
	require.NotContains(t, memoryBlock.text, "kb/index.md")
}

func TestCompileThreadContract_UsesCloudMemorySystemIdentity(t *testing.T) {
	workDir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(workDir, "memory", "system"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "memory", "MEMORY.md"), []byte("# Memory\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "memory", "system", "identity.md"), []byte("# Remote Identity\nUse remote identity.\n"), 0o644))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-remote-identity", CompileInput{
		AgentID:   "agt-1",
		AgentName: "reviewer",
		Runtime:   "codex",
		WorkDir:   workDir,
	})
	require.NoError(t, err)

	var identityBlock *PromptBlock
	for i := range env.Bundle.Blocks {
		if env.Bundle.Blocks[i].ID == "workspace.identity" {
			identityBlock = &env.Bundle.Blocks[i]
			break
		}
	}
	require.NotNil(t, identityBlock)
	require.Equal(t, "System identity", identityBlock.Title)
	require.Equal(t, "desktop/prompts/events/workspace_identity.md", identityBlock.Source.Path)
	require.Contains(t, identityBlock.text, "## System identity")
	require.Contains(t, identityBlock.text, "Use remote identity.")
	require.NotContains(t, identityBlock.text, "Do not inject this")
}

func TestThreadContractInstructionsFingerprint_IgnoresThreadLifecycleSubtype(t *testing.T) {
	workDir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte("Always validate migrations before shipping."), 0o644))

	compiler := New(DefaultPolicy())
	input := CompileInput{
		AgentID:                  "agt-1",
		AgentName:                "reviewer",
		AgentDescription:         "Review backend changes carefully",
		Runtime:                  "opencode",
		WorkDir:                  workDir,
		ExternalSkillCorePayload: "## Core rules\n\nUse the MCP tools only.",
	}

	startEnv, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-thread-start", input)
	require.NoError(t, err)
	resumeEnv, err := compiler.CompileThreadContract(context.Background(), EventThreadResume, "evt-thread-resume", input)
	require.NoError(t, err)

	startFingerprint, err := ThreadContractInstructionsFingerprint(startEnv)
	require.NoError(t, err)
	resumeFingerprint, err := ThreadContractInstructionsFingerprint(resumeEnv)
	require.NoError(t, err)

	require.NotEmpty(t, startFingerprint)
	require.Equal(t, startFingerprint, resumeFingerprint)
}

func TestCompileThreadContract_ExecGuidanceUsesTypedCatalogFacts(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-exec-guidance", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Exec: ExecCompileInput{
			Enabled: true,
			Guidance: ExecGuidanceTemplateData{
				Domains:       []string{"agent", "task"},
				CommonActions: []ExecGuidanceAction{{Domain: "task", Name: "get", ExampleJSON: `{"number":1}`}},
			},
		},
	})
	require.NoError(t, err)
	var block *PromptBlock
	for index := range env.Bundle.Blocks {
		if env.Bundle.Blocks[index].ID == "platform.agentteam.exec" {
			block = &env.Bundle.Blocks[index]
			break
		}
	}
	require.NotNil(t, block)
	require.Contains(t, block.RenderedText(), "AgentTeam Exec actions are discoverable on demand:")
	require.Contains(t, block.RenderedText(), "agentteam exec task get")
	require.NotContains(t, block.RenderedText(), "RuntimeGuidanceTemplateData")
	require.NotContains(t, block.RenderedText(), "Content:")
}

func TestCompileThreadContract_ExtensionGuidanceIsPersistentAndFingerprintBound(t *testing.T) {
	compiler := New(DefaultPolicy())
	base := CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
	}
	withoutExtension, err := compiler.CompileThreadContract(
		context.Background(),
		EventThreadStart,
		"evt-no-extension",
		base,
	)
	require.NoError(t, err)
	for _, block := range withoutExtension.Bundle.Blocks {
		require.NotEqual(t, "platform.agentteam.extension", block.ID)
	}

	withExtension := base
	withExtension.Extension = ExtensionCompileInput{
		Enabled:  true,
		Guidance: ExtensionCatalogGuidanceTemplateData{Actions: []ExtensionCatalogGuidanceAction{{ExtensionName: "github", ActionName: "list-issues"}}},
	}
	first, err := compiler.CompileThreadContract(
		context.Background(),
		EventThreadStart,
		"evt-extension-one",
		withExtension,
	)
	require.NoError(t, err)
	var extensionBlock *PromptBlock
	for index := range first.Bundle.Blocks {
		if first.Bundle.Blocks[index].ID == "platform.agentteam.extension" {
			extensionBlock = &first.Bundle.Blocks[index]
			break
		}
	}
	require.NotNil(t, extensionBlock)
	require.Equal(t, LayerRuntimeGuidance, extensionBlock.Layer)
	require.Equal(t, DurabilityPersistent, extensionBlock.Durability)
	require.Contains(t, extensionBlock.text, "github list-issues")
	require.Contains(t, extensionBlock.text, "- github list-issues")

	secondInput := withExtension
	secondInput.Extension.Guidance = ExtensionCatalogGuidanceTemplateData{Actions: []ExtensionCatalogGuidanceAction{{ExtensionName: "github", ActionName: "create-issue"}}}
	second, err := compiler.CompileThreadContract(
		context.Background(),
		EventThreadResume,
		"evt-extension-two",
		secondInput,
	)
	require.NoError(t, err)
	firstFingerprint, err := ThreadContractInstructionsFingerprint(first)
	require.NoError(t, err)
	secondFingerprint, err := ThreadContractInstructionsFingerprint(second)
	require.NoError(t, err)
	require.NotEqual(t, firstFingerprint, secondFingerprint)
}

func TestCompileThreadContract_NativeCarrierRuntimeIgnoresWorkspaceAgentsDoc(t *testing.T) {
	t.Parallel()

	for _, runtime := range []string{"opencode"} {
		runtime := runtime
		t.Run(runtime, func(t *testing.T) {
			t.Parallel()

			workDir := t.TempDir()
			require.NoError(t, os.MkdirAll(filepath.Join(workDir, "memory", "system"), 0o755))
			require.NoError(t, os.WriteFile(filepath.Join(workDir, "memory", "system", "identity.md"), []byte("# Identity\nNative carrier identity\n"), 0o644))
			require.NoError(t, os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte("Should not be re-read from workspace."), 0o644))

			compiler := New(DefaultPolicy())
			env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-thread-start", CompileInput{
				AgentID:                  "agt-1",
				AgentName:                "reviewer",
				AgentDescription:         "Review backend changes carefully",
				Runtime:                  runtime,
				WorkDir:                  workDir,
				ExternalSkillCorePayload: "## Core rules\n\nUse the MCP tools only.",
			})
			require.NoError(t, err)

			for _, block := range env.Bundle.Blocks {
				require.NotEqual(t, "agent_config.agents_md", block.ID)
			}

			rendered, err := compiler.RenderRuntimeInput(context.Background(), env, runtime, RenderOptions{})
			require.NoError(t, err)
			require.Contains(t, ExtractStringSlot(rendered, "runtimeCarrier"), `You are "reviewer", an AI agent in AgentTeam.`)
			require.Contains(t, ExtractStringSlot(rendered, "runtimeCarrier"), "Native carrier identity")
			require.NotContains(t, ExtractStringSlot(rendered, "runtimeCarrier"), "Should not be re-read from workspace.")
			require.NotContains(t, rendered.RenderOrder, "prompt")
		})
	}
}

func TestRenderRuntimeInput_NativeCarrierRuntimesUseRuntimeCarrierSlot(t *testing.T) {
	t.Parallel()

	for _, runtime := range []string{"opencode"} {
		runtime := runtime
		t.Run(runtime, func(t *testing.T) {
			t.Parallel()

			compiler := New(DefaultPolicy())
			env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-thread-start", CompileInput{
				AgentID:                  "agt-1",
				AgentName:                "reviewer",
				AgentDescription:         "Review backend changes carefully",
				Runtime:                  runtime,
				ExternalSkillCorePayload: "## Core rules\n\nUse the MCP tools only.",
			})
			require.NoError(t, err)

			rendered, err := compiler.RenderRuntimeInput(context.Background(), env, runtime, RenderOptions{})
			require.NoError(t, err)
			require.Equal(t, []string{"runtimeCarrier"}, rendered.RenderOrder)
			require.Contains(t, ExtractStringSlot(rendered, "runtimeCarrier"), `You are "reviewer", an AI agent in AgentTeam.`)
			require.Contains(t, ExtractStringSlot(rendered, "runtimeCarrier"), "## Initial role")
		})
	}
}

func TestCompileTurnContext_FreshStartSeparatesReferenceAndTurn(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileTurnContext(context.Background(), EventFreshStart, "evt-fresh", "", CompileInput{
		AgentID:                       "agt-1",
		Runtime:                       "codex",
		Binding:                       ConversationBinding{ChannelID: "agentteam", TargetID: "#general"},
		Message:                       types.InboxMessage{MessageID: "msg-1", AuthorProfile: types.AuthorProfile{ActorType: "human", DisplayName: "alice"}, Content: "Please inspect the queue."},
		ExternalSkillReferencePayload: "## Reference\n\nRead this when needed.",
	})
	require.NoError(t, err)
	require.NotNil(t, env.Bundle)
	require.Len(t, env.Bundle.Blocks, 3)

	var foundChannelReference bool
	for _, block := range env.Bundle.Blocks {
		switch block.ID {
		case "platform.agentteam.external_skill_reference":
			foundChannelReference = true
			require.Equal(t, SlotReference, block.Slot)
			require.Equal(t, SourceKindMarkdownTemplate, block.Source.SourceKind)
			require.Equal(t, "desktop/prompts/wire/external_skill_reference.md", block.Source.Path)
		}
	}
	require.True(t, foundChannelReference)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "turnInput")
	require.Contains(t, text, "## Reference")
	require.Contains(t, text, "## Current message")
	require.Contains(t, text, "[target=#general msg=msg-1 time=-] @alice: Please inspect the queue.")
}

func TestCompileTurnContextNonReplyDeliveriesHaveNoReplyEligibility(t *testing.T) {
	tests := []struct {
		name    string
		message types.InboxMessage
	}{
		{name: "reaction", message: types.InboxMessage{MessageType: "reaction"}},
		{
			name: "workflow callback",
			message: types.InboxMessage{WorkflowContext: types.WorkflowContext{
				RunID: "wfr-1", EffectID: "wfs-1", NodeID: "agent",
				CallbackExpected: true,
			}},
		},
		{
			name:    "system author",
			message: types.InboxMessage{AuthorProfile: types.AuthorProfile{ActorType: "system", DisplayName: "daemon"}},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			compiler := New(DefaultPolicy())
			message := test.message
			message.MessageID = "msg-signal"
			message.Target = "#ops:deadbeef"
			message.Content = "Handle through the owning signal path."
			env, err := compiler.CompileTurnContext(context.Background(), EventDeferredContinuation, "evt-signal", "", CompileInput{
				AgentID: "agt-1",
				Runtime: "codex",
				Binding: ConversationBinding{ChannelID: "ops", TargetID: "#ops:deadbeef"},
				Message: message,
			})
			require.NoError(t, err)
			require.Nil(t, env.Bundle.ReplyContext)

			rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
			require.NoError(t, err)
			text := ExtractStringSlot(rendered, "turnInput")
			require.Contains(t, text, "Handle through the owning signal path.")
			require.NotContains(t, text, "Reply context:")
			require.NotContains(t, text, "message_send(target=")
		})
	}
}

func TestCompileTurnContext_WakeIncludesReferenceBlocksWhenProvided(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileTurnContext(context.Background(), EventWake, "evt-wake-reference", "", CompileInput{
		AgentID:                       "agt-1",
		Runtime:                       "codex",
		Binding:                       ConversationBinding{ChannelID: "agentteam", TargetID: "#general"},
		Message:                       types.InboxMessage{MessageID: "msg-2", Target: "#general", AuthorProfile: types.AuthorProfile{ActorType: "human", DisplayName: "alice"}, Content: "Please continue the rollout."},
		UnreadSummary:                 map[string]int{"#general": 3},
		ExternalSkillReferencePayload: "## Reference\n\nRead this when needed.",
	})
	require.NoError(t, err)
	require.NotNil(t, env.Bundle)

	blockByID := map[string]PromptBlock{}
	for _, block := range env.Bundle.Blocks {
		blockByID[block.ID] = block
	}

	require.Contains(t, blockByID, "platform.agentteam.external_skill_reference")
	require.Contains(t, blockByID, "event.wake_context")

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "turnInput")
	require.Contains(t, text, "## Reference")
	require.Contains(t, text, "Please continue the rollout.")
}

func TestCompileTurnContext_WakeOmitsUnreadSummary(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileTurnContext(context.Background(), EventWake, "evt-wake-unread-summary", "", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Binding: ConversationBinding{ChannelID: "agentteam", TargetID: "#general"},
		Message: types.InboxMessage{MessageID: "msg-2", Target: "#general", AuthorProfile: types.AuthorProfile{ActorType: "human", DisplayName: "alice"}, Content: "Please continue the rollout."},
		UnreadSummary: map[string]int{
			"#general": 3,
			"#ops":     2,
		},
	})
	require.NoError(t, err)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "turnInput")
	require.Contains(t, text, "New message received:")
	require.Contains(t, text, "[target=#general msg=msg-2 time=-] @alice: Please continue the rollout.")
	require.NotContains(t, text, "You also have unread messages in other channels")
	require.NotContains(t, text, "#ops")
	require.NotContains(t, text, "Those other unread counts are backlog hints only")
}

func TestUnreadSummaryTemplateGoldenCasesAndDeterministicFacts(t *testing.T) {
	cases := []struct {
		name   string
		counts map[string]int
		golden string
	}{
		{name: "zero", counts: map[string]int{"#ignored": 0, " ": 3}, golden: "unread_summary.zero.md"},
		{name: "one", counts: map[string]int{"#general": 1}, golden: "unread_summary.one.md"},
		{name: "multiple", counts: map[string]int{"#zeta": 2, "#alpha": 1, "#ignored": -1}, golden: "unread_summary.multiple.md"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			data := unreadSummaryTemplateData(tc.counts)
			block, err := DefaultPromptBlockRegistry().Construct("event.unread_summary", newUnreadSummaryPromptInput(data))
			require.NoError(t, err)
			assertGoldenText(t, tc.golden, block.RenderedText()+"\n")
		})
	}

	first := unreadSummaryTemplateData(map[string]int{"#zeta": 2, "#alpha": 1})
	second := unreadSummaryTemplateData(map[string]int{"#alpha": 1, "#zeta": 2})
	require.Equal(t, first, second)
	require.Equal(t, []UnreadSummaryChannel{{Channel: "#alpha", Count: 1}, {Channel: "#zeta", Count: 2}}, first.Channels)
}

func TestCompileTurnContext_WakeAlwaysIncludesDeliveryContract(t *testing.T) {
	compiler := New(DefaultPolicy())
	input := CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Binding: ConversationBinding{ChannelID: "agentteam", TargetID: "#general"},
		Message: types.InboxMessage{MessageID: "msg-2", Target: "#general", AuthorProfile: types.AuthorProfile{ActorType: "human", DisplayName: "alice"}, Content: "Please continue the rollout."},
	}

	env, err := compiler.CompileTurnContext(context.Background(), EventWake, "evt-wake-delivery-contract", "", input)
	require.NoError(t, err)
	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "turnInput")
	// The participant-delivery contract is always compiled (no experiment gate).
	require.Contains(t, text, "## Participant delivery contract")
	require.Contains(t, text, "mcp__agentteam__message_send")
	require.Contains(t, text, "is local output only and is not posted to the conversation")
}

func TestCompileTurnContext_WakeOmitsMemoryResumeReminder(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileTurnContext(context.Background(), EventWake, "evt-wake-memory-reminder", "", CompileInput{
		AgentID:              "agt-1",
		Runtime:              "codex",
		Binding:              ConversationBinding{ChannelID: "agentteam", TargetID: "#general"},
		Message:              types.InboxMessage{MessageID: "msg-2", Target: "#general", AuthorProfile: types.AuthorProfile{ActorType: "human", DisplayName: "alice"}, Content: "Please continue the rollout."},
		MemoryResumeReminder: "Memory: your previous context may be incomplete. Before resuming, recover only the context needed.",
	})
	require.NoError(t, err)
	require.NotNil(t, env.Bundle)

	blockByID := map[string]PromptBlock{}
	for _, block := range env.Bundle.Blocks {
		blockByID[block.ID] = block
	}
	require.Contains(t, blockByID, "event.wake_context")
	require.NotContains(t, blockByID, "event.resume_context")
	require.Equal(t, "Wake context", blockByID["event.wake_context"].Title)
	require.Equal(t, "desktop/prompts/events/wake_message.md", blockByID["event.wake_context"].Source.Path)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "turnInput")
	require.Contains(t, text, "Please continue the rollout.")
	require.NotContains(t, text, "Memory: your previous context may be incomplete")
	require.NotContains(t, text, "Before resuming")
}

func TestCompileTurnContext_ResumeIncludesMemoryResumeReminder(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileTurnContext(context.Background(), EventResume, "evt-resume-memory-reminder", "", CompileInput{
		AgentID:              "agt-1",
		Runtime:              "codex",
		Binding:              ConversationBinding{ChannelID: "agentteam", TargetID: "#general"},
		Message:              types.InboxMessage{MessageID: "msg-2", Target: "#general", AuthorProfile: types.AuthorProfile{ActorType: "human", DisplayName: "alice"}, Content: "Please continue the rollout."},
		MemoryResumeReminder: "Memory: your previous context may be incomplete. Before resuming, recover only the context needed.",
	})
	require.NoError(t, err)
	require.NotNil(t, env.Bundle)

	blockByID := map[string]PromptBlock{}
	for _, block := range env.Bundle.Blocks {
		blockByID[block.ID] = block
	}
	require.Contains(t, blockByID, "event.resume_context")
	require.NotContains(t, blockByID, "event.wake_context")

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "turnInput")
	require.Contains(t, text, "Please continue the rollout.")
	require.Contains(t, text, "Memory: your previous context may be incomplete")
	require.Contains(t, text, "Before resuming")
}

func TestCompileTurnContext_WakeAttachmentOnlyMessageRendersAttachmentDetails(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileTurnContext(context.Background(), EventWake, "evt-wake-attachment", "", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Binding: ConversationBinding{ChannelID: "agentteam", TargetID: "#general"},
		Message: types.InboxMessage{
			MessageID:       "msg-att-1",
			Target:          "#general",
			AuthorProfile:   types.AuthorProfile{ActorType: "human", DisplayName: "alice"},
			AttachmentIDs:   "att-1",
			AttachmentNames: "runbook.pdf",
		},
		UnreadSummary: map[string]int{"#general": 1},
	})
	require.NoError(t, err)
	require.NotNil(t, env.Bundle)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "turnInput")
	require.Contains(t, text, "[target=#general msg=msg-att-1 time=-] @alice:")
	require.Contains(t, text, "attachments:")
	require.Contains(t, text, "- runbook.pdf (id:att-1)")
	require.NotContains(t, text, "No new messages while you were away. Nothing to do - just stop.")
}

func TestCompileTurnContext_TracksChannelReferenceSources(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileTurnContext(context.Background(), EventFreshStart, "evt-source-split", "", CompileInput{
		AgentID:                       "agt-1",
		Runtime:                       "codex",
		Binding:                       ConversationBinding{ChannelID: "agentteam", TargetID: "#ops"},
		Message:                       types.InboxMessage{MessageID: "msg-2", AuthorProfile: types.AuthorProfile{ActorType: "human", DisplayName: "alice"}, Content: "Check the deployment status."},
		ExternalSkillReferencePayload: "## AgentTeam Tools\n\nUse message_send for visible replies.",
	})
	require.NoError(t, err)
	require.NotNil(t, env.Bundle)

	blockByID := map[string]PromptBlock{}
	for _, block := range env.Bundle.Blocks {
		blockByID[block.ID] = block
	}

	channelReference, ok := blockByID["platform.agentteam.external_skill_reference"]
	require.True(t, ok)
	require.Equal(t, SourceKindMarkdownTemplate, channelReference.Source.SourceKind)
	require.Equal(t, "desktop/prompts/wire/external_skill_reference.md", channelReference.Source.Path)
}

func TestCompileDeliveryEnvelope_RendersIdentityNoticeAndReactionMetadata(t *testing.T) {
	workDir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte("Prefer terse incident updates."), 0o644))
	require.NoError(t, identitynotice.Save(workDir, identitynotice.Pending{
		CurrentName: "responder",
		OldName:     "planner",
	}))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventReactionDelivery, "evt-delivery", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		WorkDir: workDir,
		Delivery: DeliveryCompileInput{
			MessageID:      "rx-1",
			Target:         "#ops",
			ChannelType:    "channel",
			ChannelName:    "ops",
			AuthorIdentity: types.AuthorIdentity{ActorType: "human", DisplayName: "alice"},
			Content:        "reacted ❌ to your message",
			Timestamp:      "2026-04-20T12:00:00Z",
			MessageType:    "reaction",
			Metadata: map[string]any{
				"reaction": map[string]any{
					"emoji":                "❌",
					"targetMessageId":      "msg-123",
					"targetMessagePreview": "Please review the API shape",
				},
			},
		},
	})
	require.NoError(t, err)
	require.NotNil(t, env.Delivery)
	require.Equal(t, DeliverySourceReaction, env.Delivery.SourceKind)
	require.Equal(t, "msg-123", env.Delivery.Correlation.SourceMessageID)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, "Identity update:")
	require.Contains(t, text, "reacted ❌ to your message")
	require.Contains(t, text, "targetMessageId: msg-123")
	require.Contains(t, text, "Reply unavailable")
	require.NotContains(t, text, "message_send(target=")
	require.NotNil(t, env.Delivery)
	require.Equal(t, "responder", env.Delivery.IdentityNoticeData.CurrentName)
	require.Equal(t, "planner", env.Delivery.IdentityNoticeData.OldName)
	require.Contains(t, env.Delivery.Metadata[identityNoticeVersionMetadataKey], prompts.Version(identitynotice.PromptTemplatePath))
}

func TestCompileDeliveryEnvelope_RendersAttachmentDetails(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-delivery-attachments", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "msg-att",
			Target:         "#ops",
			ChannelType:    "channel",
			AuthorIdentity: types.AuthorIdentity{ActorType: "human", DisplayName: "alice"},
			Timestamp:      "2026-04-20T12:00:00Z",
			MessageType:    "text",
			Attachments: []types.MessageAttachment{
				{ID: "att-1", Filename: "deploy.log"},
				{ID: "att-2", Filename: "trace.json"},
			},
		},
	})
	require.NoError(t, err)
	require.NotNil(t, env.Delivery)
	require.Len(t, env.Delivery.Attachments, 2)
	require.Equal(t, "att-1", env.Delivery.Attachments[0].ID)
	require.Equal(t, "deploy.log", env.Delivery.Attachments[0].Filename)
	require.Equal(t, "att-2", env.Delivery.Attachments[1].ID)
	require.Equal(t, "trace.json", env.Delivery.Attachments[1].Filename)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, "[target=#ops msg=msg-att time=2026-04-20T12:00:00Z type=text] @alice:")
	require.Contains(t, text, "attachments:")
	require.Contains(t, text, "- deploy.log (id:att-1)")
	require.Contains(t, text, "- trace.json (id:att-2)")
}

func TestCompileDeliveryEnvelope_RendersTeamContexts(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-delivery-team", CompileInput{
		AgentID: "agt-leader",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "msg-team",
			Target:         "#product:abcd1234",
			ChannelType:    "thread",
			AuthorIdentity: types.AuthorIdentity{ActorType: "human", DisplayName: "alice"},
			Content:        "@product-engineering1 please prepare a complete responsibility proposal for this Team and submit it for approval",
			Timestamp:      "2026-06-23T14:00:00Z",
			MessageType:    "text",
			TeamContexts: []types.TeamContext{{
				TeamID:            "team_product",
				TeamHandle:        "product-engineering1",
				TeamDisplayName:   "Product Engineering",
				TeamDescription:   "Coordinate the team before implementation.",
				TeamMemberID:      "member_leader",
				Role:              "leader",
				RoleLabel:         "Coordinator",
				RoleOverlayPrompt: "Coordinate members. Do not implement features yourself.",
				MemberRoster: []types.TeamRosterMember{{
					TeamMemberID:      "wtm_leader",
					ActorKind:         "agent",
					ActorID:           "agt-leader",
					DisplayName:       "Leader Agent",
					Handle:            "agentteam4-codex-gmh",
					Role:              "leader",
					RoleLabel:         "Coordinator",
					RoleOverlayPrompt: "Coordinate members. Do not implement features yourself.",
					WakeOnTeamMention: true,
					DeliveryKind:      "wake",
					Self:              true,
				}, {
					TeamMemberID:      "wtm_builder",
					ActorKind:         "agent",
					ActorID:           "agt-builder",
					DisplayName:       "Builder Agent",
					Handle:            "builder-agent",
					Role:              "member",
					RoleLabel:         "builder",
					RoleOverlayPrompt: "",
					WakeOnTeamMention: false,
					DeliveryKind:      "agent",
				}},
			}},
		},
	})
	require.NoError(t, err)
	require.NotNil(t, env.Delivery)
	require.Len(t, env.Delivery.TeamContexts, 1)
	require.Equal(t, "team_product", env.Delivery.TeamContexts[0].TeamID)
	require.Equal(t, "wtm_leader", env.Delivery.TeamContexts[0].MemberRoster[0].TeamMemberID)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, "team contexts:")
	require.Contains(t, text, "- team: @product-engineering1")
	require.Contains(t, text, "team id: team_product")
	require.Contains(t, text, "team invocation: you were awakened as this Team member. Other Team members may not have been awakened automatically.")
	require.NotContains(t, text, "collaboration rules:")
	require.NotContains(t, text, "team_invoke_team_id")
	require.NotContains(t, text, "workflow-plan-author Skill")
	require.Contains(t, text, "Team responsibility updates:")
	require.Contains(t, text, "with `agentteam exec team update`; it will request Human approval")
	require.Contains(t, text, "team duty:\n    Coordinate the team before implementation.")
	require.Contains(t, text, "your Team role: leader")
	require.Contains(t, text, "your role label: Coordinator")
	require.Contains(t, text, "your duty:\n    Coordinate members. Do not implement features yourself.")
	require.Contains(t, text, "team member roster:\n    - @agentteam4-codex-gmh (Leader Agent) (agent, Coordinator, active participant, self)")
	require.Contains(t, text, "      teamMemberId: wtm_leader")
	require.Contains(t, text, "      Team role: leader")
	require.Contains(t, text, "      teamMemberId: wtm_builder")
	require.Contains(t, text, "      Team role: member")
	require.Contains(t, text, "      duty:\n        Coordinate members. Do not implement features yourself.")
	require.Contains(t, text, "    - @builder-agent (Builder Agent) (agent, builder, as-needed participant)")
	require.Contains(t, text, "      duty: not configured")
}

func TestCompileDeliveryEnvelope_ReactionUsesNestedCorrelationMetadata(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventReactionDelivery, "evt-reaction-correlation", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "fallback-message-id",
			Target:         "#ops",
			ChannelType:    "channel",
			AuthorIdentity: types.AuthorIdentity{ActorType: "human", DisplayName: "alice"},
			Content:        "reacted 👍 to your message",
			Timestamp:      "2026-04-20T12:00:00Z",
			MessageType:    "reaction",
			Metadata: map[string]any{
				"reaction": map[string]any{
					"deliveryId":      "rxdlv_123",
					"targetMessageId": "msg_123",
					"emoji":           "👍",
				},
			},
		},
	})
	require.NoError(t, err)
	require.NotNil(t, env.Delivery)
	require.Equal(t, "rxdlv_123", env.Delivery.Correlation.DeliveryID)
	require.Equal(t, "msg_123", env.Delivery.Correlation.SourceMessageID)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, "delivery=rxdlv_123")
	require.Contains(t, text, "source=msg_123")
}

func TestCompileTurnContext_UsesPromptPackForIdentityNotice(t *testing.T) {
	workDir := t.TempDir()
	require.NoError(t, identitynotice.Save(workDir, identitynotice.Pending{
		CurrentName: "reviewer",
		OldName:     "planner",
	}))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileTurnContext(context.Background(), EventWake, "evt-identity-notice", "", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		WorkDir: workDir,
		Binding: ConversationBinding{ChannelID: "agentteam", TargetID: "#general"},
		Message: types.InboxMessage{
			MessageID:     "msg-1",
			AuthorProfile: types.AuthorProfile{ActorType: "human", DisplayName: "alice"},
			Content:       "Please continue.",
		},
	})
	require.NoError(t, err)
	require.NotNil(t, env.Bundle)

	var noticeBlock *PromptBlock
	for i := range env.Bundle.Blocks {
		if env.Bundle.Blocks[i].ID == "notice.identity" {
			noticeBlock = &env.Bundle.Blocks[i]
			break
		}
	}
	require.NotNil(t, noticeBlock)
	require.Equal(t, SourceKindNotice, noticeBlock.Source.SourceKind)
	require.Equal(t, "desktop/prompts/notices/identity_notice.md", noticeBlock.Source.Path)
	require.Contains(t, noticeBlock.Source.Version, prompts.Version(identitynotice.PromptTemplatePath))
	require.Contains(t, noticeBlock.text, "`reviewer`")
	require.Contains(t, noticeBlock.text, "`planner`")

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "turnInput")
	noticeIndex := strings.Index(text, "Identity update:")
	messageIndex := strings.Index(text, "New message received:")
	require.NotEqual(t, -1, noticeIndex)
	require.NotEqual(t, -1, messageIndex)
	require.Less(t, noticeIndex, messageIndex)
}

func TestCompileTurnContext_RendersNoticesAheadOfReferenceMaterial(t *testing.T) {
	workDir := t.TempDir()
	require.NoError(t, identitynotice.Save(workDir, identitynotice.Pending{
		CurrentName: "reviewer",
		OldName:     "planner",
	}))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileTurnContext(context.Background(), EventWake, "evt-notice-order", "", CompileInput{
		AgentID:                       "agt-1",
		Runtime:                       "codex",
		WorkDir:                       workDir,
		Binding:                       ConversationBinding{ChannelID: "agentteam", TargetID: "#general"},
		Message:                       types.InboxMessage{MessageID: "msg-1", AuthorProfile: types.AuthorProfile{ActorType: "human", DisplayName: "alice"}, Content: "Please continue."},
		ExternalSkillReferencePayload: "## Reference\n\nRead this when needed.",
	})
	require.NoError(t, err)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "turnInput")

	noticeIndex := strings.Index(text, "Identity update:")
	referenceIndex := strings.Index(text, "## Reference")
	wakeIndex := strings.Index(text, "New message received:")
	require.NotEqual(t, -1, noticeIndex)
	require.NotEqual(t, -1, referenceIndex)
	require.NotEqual(t, -1, wakeIndex)
	require.Less(t, noticeIndex, referenceIndex)
	require.Less(t, referenceIndex, wakeIndex)
}

func TestCompileDeliveryEnvelope_FingerprintIncludesDeterministicMetadata(t *testing.T) {
	compiler := New(DefaultPolicy())
	baseInput := CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:         "msg-1",
			Target:            "#ops",
			ChannelType:       "channel",
			AuthorIdentity:    types.AuthorIdentity{DisplayName: "alice"},
			Content:           "please review",
			MessageType:       "text",
			DeliveryMode:      "turn_direct",
			RuntimeSessionID:  "thread-1",
			ThreadID:          "thread-1",
			ActiveTurnID:      "turn-1",
			WakeToken:         "wake-1",
			Attempt:           2,
			DeferredBatchID:   "batch-1",
			ReplayState:       "deferred_replay",
			DeferredReason:    "busy_runtime",
			AcceptedAt:        time.Date(2026, 4, 20, 12, 0, 0, 0, time.UTC),
			OriginalOrderKey:  "order-1",
			PriorAttemptCount: 1,
			EvidenceClass:     EvidenceReplayStarted,
			Metadata: map[string]any{
				"targetMessageId": "target-1",
				"reaction": map[string]any{
					"emoji": "eyes",
				},
			},
		},
	}

	baseEnv, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-base", baseInput)
	require.NoError(t, err)
	require.NotNil(t, baseEnv.Delivery)

	testCases := []struct {
		name   string
		mutate func(*CompileInput)
	}{
		{
			name: "attempt",
			mutate: func(in *CompileInput) {
				in.Delivery.Attempt++
			},
		},
		{
			name: "deferred batch",
			mutate: func(in *CompileInput) {
				in.Delivery.DeferredBatchID = "batch-2"
			},
		},
		{
			name: "active turn",
			mutate: func(in *CompileInput) {
				in.Delivery.ActiveTurnID = "turn-2"
			},
		},
		{
			name: "replay metadata",
			mutate: func(in *CompileInput) {
				in.Delivery.OriginalOrderKey = "order-2"
			},
		},
		{
			name: "evidence class",
			mutate: func(in *CompileInput) {
				in.Delivery.EvidenceClass = EvidenceVisibleReplySent
			},
		},
		{
			name: "author display name",
			mutate: func(in *CompileInput) {
				in.Delivery.AuthorIdentity.DisplayName = "bob"
			},
		},
		{
			name: "metadata",
			mutate: func(in *CompileInput) {
				in.Delivery.Metadata = map[string]any{
					"targetMessageId": "target-2",
				}
			},
		},
		{
			name: "target key",
			mutate: func(in *CompileInput) {
				in.Delivery.TargetKey = "scp_ops"
			},
		},
		{
			name: "target seq",
			mutate: func(in *CompileInput) {
				in.Delivery.TargetKey = "scp_ops"
				in.Delivery.TargetSeq = 42
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			mutated := baseInput
			tc.mutate(&mutated)

			env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-"+tc.name, mutated)
			require.NoError(t, err)
			require.NotNil(t, env.Delivery)
			require.NotEqual(t, baseEnv.Delivery.Fingerprint, env.Delivery.Fingerprint)
			require.NotEqual(t, baseEnv.Fingerprint, env.Fingerprint)
		})
	}
}

func TestRenderDeliveryText_IncludesDeterministicEnvelopeMetadata(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-render-delivery", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:         "msg-1",
			TargetKey:         "scp_ops",
			TargetSeq:         42,
			Target:            "#ops",
			ChannelType:       "channel",
			AuthorIdentity:    types.AuthorIdentity{DisplayName: "alice"},
			Content:           "please review",
			MessageType:       "text",
			DeliveryMode:      "turn_direct",
			RuntimeSessionID:  "thread-1",
			ThreadID:          "thread-1",
			ActiveTurnID:      "turn-1",
			WakeToken:         "wake-1",
			Attempt:           2,
			DeferredBatchID:   "batch-1",
			ReplayState:       "deferred_replay",
			DeferredReason:    "busy_runtime",
			AcceptedAt:        time.Date(2026, 4, 20, 12, 0, 0, 0, time.UTC),
			OriginalOrderKey:  "order-1",
			PriorAttemptCount: 1,
			EvidenceClass:     EvidenceReplayStarted,
			Timestamp:         "2026-04-20T12:00:00Z",
			Metadata: map[string]any{
				"targetMessageId": "target-1",
			},
		},
	})
	require.NoError(t, err)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, "[target=#ops msg=msg-1 source=target-1 time=2026-04-20T12:00:00Z type=text] @alice: please review")
	require.Contains(t, text, "Deferred delivery (state=deferred_replay, reason=busy_runtime); handle this message once now.")
	require.NotContains(t, text, "session=thread-1")
	require.NotContains(t, text, "providerTurn=turn-1")
	require.NotContains(t, text, "targetKey=scp_ops")
	require.NotContains(t, text, "accepted=2026-04-20T12:00:00Z")
	require.Contains(t, text, `mcp__agentteam__message_send(target="#ops", content="...")`)
	require.NotContains(t, text, "replyTurn")
	require.NotContains(t, text, "Respond as appropriate")
}

func TestCompileDeliveryEnvelopeRendersWorkflowStepSelectors(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-render-workflow-context", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "msg-1",
			TargetKey:      "chn_ops",
			TargetSeq:      42,
			Target:         "#ops",
			ChannelType:    "channel",
			AuthorIdentity: types.AuthorIdentity{DisplayName: "workflow"},
			Content:        "please update state",
			MessageType:    "text",
			Timestamp:      "2026-07-08T08:45:00Z",
			Metadata: map[string]any{
				"custom": "visible",
			},
			WorkflowContext: types.WorkflowContext{
				WorkspaceID:   "wsp_1",
				PlanID:        "wf_1",
				PlanVersionID: "wfv_1",
				RunID:         "wfr_1",
				EffectID:      "wfs_1",
				NodeID:        "notify_agent",
				SourceKind:    "schedule",
				DeliveryScope: domain.MessageScopeValue{
					Kind: domain.MessageScopeKindChannel, ScopeID: "chn_hidden_delivery",
				},
				CallbackExpected: true,
			},
		},
	})
	require.NoError(t, err)
	require.NotNil(t, env.Delivery)
	require.True(t, env.Delivery.WorkflowContext.Available)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, "Workflow context:")
	require.NotContains(t, text, "workflow variable write tool")
	require.Contains(t, text, "ordinary chat reply does not advance the Run")
	require.Contains(t, text, `workflow step '{"action":"get","runId":"wfr_1","stepId":"wfs_1"}'`)
	require.NotContains(t, text, "get_current")
	require.Contains(t, text, "Every `complete` or `fail` call must include a non-blank `idempotencyKey`")
	require.Contains(t, text, "reuse the same key only when retrying that exact terminal call")
	require.Contains(t, text, "`deliveryScope`, first-class `workScope`, and `relatedScopes`")
	require.Contains(t, text, "`relatedScopes` are read-only context")
	require.Contains(t, text, "If `workScope` is null")
	require.Contains(t, text, "top-level `workScope` on successful `complete`")
	require.Contains(t, text, "If `get` includes `workItemsContract`")
	require.Contains(t, text, "do not claim them and do not assign them to the responder")
	require.Contains(t, text, "Leave a new Task unassigned")
	require.Contains(t, text, "`workItemsContract.dependencyValueContract` is the exact ArtifactContract")
	require.Contains(t, text, "author every business output path relative to that contract")
	require.Contains(t, text, "$<dependencyKey>.<businessOutputPath>")
	require.Contains(t, text, "$op08.result")
	require.Contains(t, text, "Bare `$key` and unknown/non-direct keys are invalid")
	require.Contains(t, text, "server rejects statically provable contract errors before callback commit")
	require.Contains(t, text, "an undeclared top-level field")
	require.Contains(t, text, "traversal past a declared `number` or `boolean`")
	require.Contains(t, text, "A top-level `object` reference must continue to an internal field")
	require.Contains(t, text, "presence of an optional field are verified by the runtime resolver")
	require.Contains(t, text, "write `$$` for a literal dollar sign")
	require.Contains(t, text, "^[a-z][a-z0-9_-]{0,255}$")
	require.Contains(t, text, "at most 64 references")
	require.Contains(t, text, "at most 8 ASCII identifier segments of 64 bytes")
	require.Contains(t, text, "`__proto__`, `prototype`, or `constructor`")
	require.Contains(t, text, "only `number`, `boolean`, or `null` leaves")
	require.Contains(t, text, "never `string`, object, or array")
	require.Contains(t, text, "limited to 8 KiB")
	require.Contains(t, text, "resolved instruction to 32 KiB")
	require.Contains(t, text, "remain available in typed callback input")
	require.Contains(t, text, "receiving Agent does not parse")
	require.Contains(t, text, "business output fields whose declared type is `scope`")
	require.NotContains(t, text, "chn_hidden_delivery")
	require.Contains(t, text, "wfr_1")
	require.Contains(t, text, "wfs_1")
	require.NotContains(t, text, types.WorkflowContextMetadataRunIDKey)
	require.NotContains(t, text, types.WorkflowContextMetadataEffectIDKey)
	require.Contains(t, text, "Reply unavailable")
	require.NotContains(t, text, "message_send(target=")
}

func TestCompileDeliveryEnvelope_SystemAuthorHasNoReplyEligibility(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-system", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "msg-system",
			Target:         "#ops",
			ChannelType:    "channel",
			AuthorIdentity: types.AuthorIdentity{ActorType: "system", DisplayName: "daemon"},
			Content:        "System signal.",
			MessageType:    "text",
		},
	})
	require.NoError(t, err)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, "Reply unavailable")
	require.NotContains(t, text, "message_send(target=")
}

func TestRenderDeliveryText_AlwaysEmitsStrongReplyInstruction(t *testing.T) {
	compiler := New(DefaultPolicy())
	input := CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "msg-1",
			TargetKey:      "scp_ops",
			TargetSeq:      7,
			Target:         "#ops:abcd1234",
			ChannelType:    "thread",
			AuthorIdentity: types.AuthorIdentity{ActorType: "human", DisplayName: "alice"},
			Content:        "please review",
			DeliveryMode:   "turn_direct",
			Timestamp:      "2026-04-20T12:00:00Z",
		},
	}

	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-strong-reply-instruction", input)
	require.NoError(t, err)
	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	// The strong participant-delivery reply instruction is always emitted.
	require.Contains(t, text, `Reply to exact target "#ops:abcd1234": call mcp__agentteam__message_send(target="#ops:abcd1234", content="...") before writing local/final prose; otherwise stop.`)
	require.Contains(t, text, "Do not write it as plain output, which is not posted.")
	require.NotContains(t, text, "replyTurn")
	require.NotContains(t, text, "routing_kind")
	require.Equal(t, 1, strings.Count(text, `mcp__agentteam__message_send(target="#ops:abcd1234"`))
}

func TestRenderDeliveryText_ContextRecoveryUsesExactTarget(t *testing.T) {
	text := renderDeliveryTextForRuntime(&DeliveryEnvelope{
		Binding:           ConversationBindingRef{Target: "#ops:deadbeef"},
		Correlation:       DeliveryCorrelation{MessageID: "ctx-recovery-1"},
		MessageType:       "context_recovery",
		PayloadText:       "Recover local context only.",
		AuthorMentionName: "daemon",
	}, "codex")

	require.Contains(t, text, `Reply to exact target "#ops:deadbeef": call mcp__agentteam__message_send(target="#ops:deadbeef", content="...")`)
}

func TestRenderDeliveryText_UsesRuntimeSpecificSendTool(t *testing.T) {
	delivery := &DeliveryEnvelope{
		Binding: ConversationBindingRef{Target: "#ops"},
		Correlation: DeliveryCorrelation{
			MessageID: "msg-1",
		},
		PayloadText: "continue",
	}
	for _, test := range []struct {
		runtime string
		tool    string
	}{
		{runtime: "codex", tool: "mcp__agentteam__message_send"},
		{runtime: "claude", tool: "mcp__agentteam__message_send"},
		{runtime: "kimi", tool: "mcp__agentteam__message_send"},
		{runtime: "opencode", tool: "mcp_agentteam_message_send"},
		{runtime: "stdin", tool: "message_send"},
	} {
		t.Run(test.runtime, func(t *testing.T) {
			text := renderDeliveryTextForRuntime(delivery, test.runtime)
			require.Contains(t, text, test.tool+`(target="#ops", content="...")`)
			require.NotContains(t, text, "replyTurn")
		})
	}
}

func TestRenderDeliveryText_IncludesTaskDiscussionDetails(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-render-task-discussion", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "tsk-1",
			Target:         "#agentteam",
			ChannelType:    "channel",
			AuthorIdentity: types.AuthorIdentity{ActorType: "human", DisplayName: "wink"},
			Content:        "Task discussion update for #t1",
			MessageType:    "task_dispatch",
			DeliveryMode:   "stdin_delivery",
			Timestamp:      "2026-04-22T14:53:57Z",
			Metadata: map[string]any{
				"task": map[string]any{
					"taskId":     "tsk_1",
					"taskNumber": 1,
					"channelRef": "#agentteam",
					"title":      "输出 markdown 制品",
				},
				"taskDiscussion": map[string]any{
					"entryId": "tds_1",
					"taskId":  "tsk_1",
					"body":    "@test1 你能不能输出的结论变成 markdown 的制品？",
					"createdBy": map[string]any{
						"type": "human",
						"id":   "usr_wink",
						"name": "wink",
					},
				},
			},
		},
	})
	require.NoError(t, err)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, `task discussion: #t1 "输出 markdown 制品"`)
	require.Contains(t, text, "channel: #agentteam")
	require.Contains(t, text, "comment by @wink:\n@test1 你能不能输出的结论变成 markdown 的制品？")
	require.Contains(t, text, `reply: use mcp__agentteam__message_send with the exact task scope target from the current message header`)
	require.Contains(t, text, "Active task target:")
	require.Contains(t, text, "- task: #t1 in #agentteam")
	require.Contains(t, text, "- source: task_dispatch")
	require.NotContains(t, text, `target="#tsk_..."`)
}

func TestRenderDeliveryText_TaskScopeDiscussionUsesSendMessageTarget(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-render-task-scope-discussion", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "msg-task-scope",
			Target:         "#tsk_tk8msx2pmcjpw9ujlqn0",
			ChannelType:    "task",
			ChannelName:    "tsk_tk8msx2pmcjpw9ujlqn0",
			AuthorIdentity: types.AuthorIdentity{ActorType: "human", DisplayName: "wink"},
			Content:        "test3",
			MessageType:    "task_discussion",
			DeliveryMode:   "stdin_delivery",
			Timestamp:      "2026-05-20T09:22:00Z",
		},
	})
	require.NoError(t, err)
	require.Equal(t, "task", env.Delivery.Binding.ScopeKind)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, `[target=#tsk_tk8msx2pmcjpw9ujlqn0 msg=msg-task-scope time=2026-05-20T09:22:00Z type=task_discussion] @wink: test3`)
	require.Contains(t, text, "task discussion: scope-native message")
	require.Contains(t, text, "scope: #tsk_tk8msx2pmcjpw9ujlqn0")
	require.Contains(t, text, `reply: use mcp__agentteam__message_send(target="#tsk_tk8msx2pmcjpw9ujlqn0", content="...") to continue the task discussion`)
	require.NotContains(t, text, "replyTurn")
	require.NotContains(t, text, "add_discussion_entry")
}

func TestBuildConversationBindingUsesSharedMessageTargetKinds(t *testing.T) {
	t.Parallel()

	for _, test := range []struct {
		name        string
		target      string
		channelType string
		wantKind    string
		wantThread  string
	}{
		{name: "channel", target: "#ops", wantKind: "channel"},
		{name: "channel reply", target: "#ops:AbCd1234", wantKind: "thread", wantThread: "#ops:abcd1234"},
		{name: "dm", target: "DM:@alpha-agent", wantKind: "dm"},
		{name: "dm reply", target: "dm:@alpha-agent:AbCd1234", wantKind: "thread", wantThread: "dm:@alpha-agent:abcd1234"},
		{name: "task", target: "#tsk_abc123", wantKind: "task"},
		{name: "workflow", target: "workflow:pln_abc123", wantKind: "workflow"},
		{name: "team", target: "#team_abcdefghijklmn", wantKind: "channel"},
		{name: "team reply", target: "#team_abcdefghijklmn:AbCd1234", wantKind: "thread", wantThread: "#team_abcdefghijklmn:abcd1234"},
		{name: "resolved task fallback", target: "tsk_abc123", channelType: "task", wantKind: "task"},
		{name: "resolved thread fallback", target: "thr_abc123", channelType: "thread", wantKind: "thread", wantThread: "thr_abc123"},
		{name: "malformed workflow fails closed", target: "workflow:Bad", channelType: "workflow", wantKind: ""},
		{name: "malformed task does not use projection fallback", target: "#tsk_Bad", channelType: "task", wantKind: "channel"},
		{name: "malformed team does not use projection fallback", target: "#team_short:deadbeef", channelType: "thread", wantKind: "channel"},
	} {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			got := buildConversationBinding(test.target, test.channelType)
			require.Equal(t, test.wantKind, got.ScopeKind)
			require.Equal(t, test.wantThread, got.ThreadRef)
		})
	}
}

func TestRenderDeliveryText_DirectThreadWakeWithActiveTaskTargetKeepsThreadContext(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-render-thread-active-task", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "msg-1",
			Target:         "#agentteam:00000034",
			ChannelType:    "thread",
			AuthorIdentity: types.AuthorIdentity{ActorType: "human", DisplayName: "wink"},
			Content:        "@codex1 提一个 PR",
			ActiveTaskTarget: types.ActiveTaskTarget{
				Channel:      "#agentteam",
				TaskNumber:   13,
				Source:       "thread_focus",
				ThreadTarget: "#agentteam:00000034",
			},
		},
	})
	require.NoError(t, err)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, "This wake is a normal channel/DM/thread message, not a task scope message.")
	require.Contains(t, text, `message_history_read(target="#agentteam:00000034", limit=20)`)
	require.Contains(t, text, `message_send(target="#agentteam:00000034", content="...")`)
	require.Contains(t, text, `Proactive task discussion updates use mcp__agentteam__message_send with the exact task scope target`)
	require.NotContains(t, text, "replyTurn")
}

func TestRenderDeliveryText_ThreadDeliveryGuidesHistoryWhenMessageIsUnclear(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-render-thread-guidance", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "msg-1",
			Target:         "#agentteam:00000034",
			ChannelType:    "thread",
			AuthorIdentity: types.AuthorIdentity{ActorType: "human", DisplayName: "wink"},
			Content:        "@codex1",
			DeliveryMode:   "turn_direct",
		},
	})
	require.NoError(t, err)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, "Thread context: call mcp__agentteam__message_history_read")
	require.Contains(t, text, `mcp__agentteam__message_history_read(target="#agentteam:00000034", limit=20)`)
	require.Contains(t, text, "if needed")
}

func TestRenderDeliveryText_ChannelDeliveryDoesNotRenderThreadContextGuidance(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-render-channel-guidance", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "msg-1",
			Target:         "#agentteam",
			ChannelType:    "channel",
			AuthorIdentity: types.AuthorIdentity{ActorType: "human", DisplayName: "wink"},
			Content:        "@codex1 提一个 PR",
			DeliveryMode:   "turn_direct",
		},
	})
	require.NoError(t, err)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.NotContains(t, text, "Thread context:")
}

func TestRenderDeliveryText_DirectThreadWakeDoesNotRenderStickyClaimedTaskTarget(t *testing.T) {
	compiler := New(DefaultPolicy())
	env, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-render-thread-claimed-task", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "msg-1",
			Target:         "#agentteam:00000034",
			ChannelType:    "thread",
			AuthorIdentity: types.AuthorIdentity{ActorType: "human", DisplayName: "wink"},
			Content:        "@codex1 这个普通问题不需要写 task discussion",
			Metadata: map[string]any{
				"replyThreadRoot": map[string]any{
					"messageId":  "49ac6c63bc45e2bf8e1e08868239e247",
					"authorName": "andc5vq",
					"authorProfile": map[string]any{
						"actorType":   "human",
						"displayName": "andc5vq",
					},
					"messageType": "text",
					"content":     "@codex1 请基于 ~/agentteam 最新的 origin/main，定位 agent 看不到 thread 里根消息的问题",
					"timestamp":   "2026-05-17T15:09:05+08:00",
				},
			},
			ActiveTaskTarget: types.ActiveTaskTarget{
				Channel:      "#agentteam",
				TaskNumber:   13,
				Source:       "claimed_task",
				ThreadTarget: "#agentteam:00000034",
			},
		},
	})
	require.NoError(t, err)

	rendered, err := compiler.RenderRuntimeInput(context.Background(), env, "codex", RenderOptions{})
	require.NoError(t, err)
	text := ExtractStringSlot(rendered, "deliveryText")
	require.Contains(t, text, "[target=#agentteam:00000034 msg=msg-1] @wink: @codex1 这个普通问题不需要写 task discussion")
	require.Contains(t, text, "thread root:")
	require.Contains(t, text, "@andc5vq: @codex1 请基于 ~/agentteam 最新的 origin/main，定位 agent 看不到 thread 里根消息的问题")
	require.NotContains(t, text, "thread context:")
	require.NotContains(t, text, "Active task target:")
	require.NotContains(t, text, "add_discussion_entry(channel=")
}

func TestCompileOversizedIdentityDocTruncatesAndNotices(t *testing.T) {
	workDir := t.TempDir()
	tooLong := strings.Repeat("界", DefaultMaxIdentityDocCharacters+1)
	require.NoError(t, os.MkdirAll(filepath.Join(workDir, "memory", "system"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "memory", "system", "identity.md"), []byte(tooLong), 0o644))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-too-large", CompileInput{
		AgentID:          "agt-3",
		AgentName:        "reviewer",
		AgentDescription: "Review backend changes carefully",
		Runtime:          "codex",
		WorkDir:          workDir,
	})
	require.NoError(t, err)
	require.NotNil(t, env.Bundle)

	identityBlock, hasIdentity := blockByID(env.Bundle.Blocks, "workspace.identity")
	noticeBlock, hasNotice := blockByID(env.Bundle.Blocks, "notice.workspace.identity_oversize")
	require.True(t, hasIdentity, "identity block should still be injected after truncation")
	require.True(t, hasNotice, "oversize notice block should be injected when the identity doc is over budget")

	// The identity content was truncated back to exactly the character budget.
	require.Equal(t, DefaultMaxIdentityDocCharacters, strings.Count(identityBlock.text, "界"),
		"identity content should be truncated to exactly the character budget")
	// The notice tells the agent how far over budget it is and which file to fix.
	require.Contains(t, noticeBlock.text, fmt.Sprintf("%d characters", DefaultMaxIdentityDocCharacters+1))
	require.Contains(t, noticeBlock.text, fmt.Sprintf("limit %d", DefaultMaxIdentityDocCharacters))
	require.Contains(t, noticeBlock.text, "memory/system/identity.md")
}

// TestTruncateDocBudgetKeepsMostContent guards against regressing on a marginally
// over-budget document: a newline early in the file must not cause truncation to
// discard almost the entire body (the old line-boundary backscan bug).
func TestTruncateDocBudgetKeepsMostContent(t *testing.T) {
	workDir := t.TempDir()
	// "# Identity\n" (11 runes) + 4096 'a' = 4107 runes, only 11 over budget.
	tooLong := "# Identity\n" + strings.Repeat("a", DefaultMaxIdentityDocCharacters)
	require.NoError(t, os.MkdirAll(filepath.Join(workDir, "memory", "system"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "memory", "system", "identity.md"), []byte(tooLong), 0o644))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-margin", CompileInput{
		AgentID: "agt-margin", AgentName: "m", AgentDescription: "d", Runtime: "codex", WorkDir: workDir,
	})
	require.NoError(t, err)

	identityBlock, ok := blockByID(env.Bundle.Blocks, "workspace.identity")
	require.True(t, ok)
	// The body must survive: the old behavior kept only "# Identity" (0 'a's).
	require.GreaterOrEqual(t, strings.Count(identityBlock.text, "a"), DefaultMaxIdentityDocCharacters-64,
		"a marginally over-budget identity must keep almost all of its body, not collapse to its header")
}

// TestOversizedAgentsDocAllWhitespaceStillNotices guards the empty-trim path: a
// document whose first budget-bytes are all whitespace still had content truncated
// away, so it must emit a notice even though no doc block is injected.
func TestOversizedAgentsDocAllWhitespaceStillNotices(t *testing.T) {
	workDir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "AGENTS.md"), append(bytes.Repeat([]byte(" "), DefaultMaxWorkspaceDocBytes), []byte("MUST KEEP")...), 0o644))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-ws", CompileInput{
		AgentID: "agt-ws", AgentName: "m", AgentDescription: "d", Runtime: "codex", WorkDir: workDir,
	})
	require.NoError(t, err)

	_, hasAgents := blockByID(env.Bundle.Blocks, "agent_config.agents_md")
	require.False(t, hasAgents, "no usable AGENTS.md content should remain after truncation")
	noticeBlock, hasNotice := blockByID(env.Bundle.Blocks, "notice.agent_config.agents_md_oversize")
	require.True(t, hasNotice, "an all-whitespace oversized doc must still produce a notice, not vanish silently")
	require.Contains(t, noticeBlock.text, "bytes")
}

// TestOversizedIdentityListsAllConstraints guards the notice-merge behavior: a
// document exceeding multiple budgets (bytes and characters) must list every
// binding constraint, not just the outermost one.
func TestOversizedIdentityListsAllConstraints(t *testing.T) {
	workDir := t.TempDir()
	// 65537 ASCII runes: over the 65536-byte budget AND the 4096-character budget.
	required := DefaultMaxWorkspaceDocBytes + 1
	require.Less(t, DefaultMaxIdentityDocCharacters, required)
	require.NoError(t, os.MkdirAll(filepath.Join(workDir, "memory", "system"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "memory", "system", "identity.md"), []byte(strings.Repeat("a", required)), 0o644))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-multi", CompileInput{
		AgentID: "agt-multi", AgentName: "m", AgentDescription: "d", Runtime: "codex", WorkDir: workDir,
	})
	require.NoError(t, err)

	noticeBlock, hasNotice := blockByID(env.Bundle.Blocks, "notice.workspace.identity_oversize")
	require.True(t, hasNotice)
	require.Contains(t, noticeBlock.text, "characters", "notice must report the character constraint")
	require.Contains(t, noticeBlock.text, "bytes", "notice must also report the byte constraint")
}

// TestCompileOversizedAgentsDocTruncatesToByteBudget guards the unified
// graceful-degradation path for the other always-on workspace doc: an AGENTS.md
// larger than the workspace-doc byte budget must be truncated to fit, kept under
// the per-block token budget, and emit a notice instead of failing startup.
func TestCompileOversizedAgentsDocTruncatesToByteBudget(t *testing.T) {
	workDir := t.TempDir()
	required := DefaultMaxWorkspaceDocBytes + 4096
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte(strings.Repeat("a", required)), 0o644))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-agents-too-large", CompileInput{
		AgentID:          "agt-agents",
		AgentName:        "builder",
		AgentDescription: "Build things",
		Runtime:          "codex",
		WorkDir:          workDir,
	})
	require.NoError(t, err)
	require.NotNil(t, env.Bundle)

	agentsBlock, hasAgents := blockByID(env.Bundle.Blocks, "agent_config.agents_md")
	noticeBlock, hasNotice := blockByID(env.Bundle.Blocks, "notice.agent_config.agents_md_oversize")
	require.True(t, hasAgents, "AGENTS.md block should still be injected after truncation")
	require.True(t, hasNotice, "oversize notice should be injected when AGENTS.md is over the byte budget")
	require.LessOrEqual(t, agentsBlock.TokenEstimate, DefaultMaxBlockTokens,
		"AGENTS.md block must be trimmed back under the token budget so it never hard-fails")
	require.Contains(t, noticeBlock.text, "AGENTS.md")
	require.Contains(t, noticeBlock.text, "bytes")
	require.Contains(t, noticeBlock.text, fmt.Sprintf("%d", required))
}

// TestEnforceDocBudgetImpossibleWrapper guards the F5 boundary at the unit
// level: when the wrapper's own overhead exceeds the token budget, the document
// cannot be materialized. enforceDocBudget must report ok=false (so the caller
// drops the block instead of emitting one that would trip mustBlock) while still
// returning the overrun constraint for the notice. (This cannot be exercised via
// CompileThreadContract because a budget that small fails the platform blocks
// first; enforceDocBudget is tested directly with a pathological wrapper.)
func TestEnforceDocBudgetImpossibleWrapper(t *testing.T) {
	compiler := New(DefaultPolicy())
	// A wrapper whose output always far exceeds MaxBlockTokens regardless of input.
	huge := func(string) string { return strings.Repeat("x", 100_000) }
	_, constraints, ok := compiler.enforceDocBudget("hello", huge, docBudget{TokenBudget: DefaultMaxBlockTokens})
	require.False(t, ok, "a wrapper whose overhead exceeds the budget must report not-ok")
	require.NotEmpty(t, constraints, "the token overrun must still be reported for the notice")
	require.Equal(t, "tokens", constraints[0].Unit)
}

// TestEnforceDocBudgetGuaranteesTokenPostCondition verifies the happy-path
// contract: whenever ok is true and a token budget is set, the returned wrapped
// text fits the budget.
func TestEnforceDocBudgetGuaranteesTokenPostCondition(t *testing.T) {
	compiler := New(DefaultPolicy())
	wrap := func(content string) string { return "prefix\n" + content + "\nsuffix" }
	content := strings.Repeat("line\n", 8000) // large enough to exceed MaxBlockTokens once wrapped
	text, _, ok := compiler.enforceDocBudget(content, wrap, docBudget{TokenBudget: DefaultMaxBlockTokens})
	require.True(t, ok)
	require.LessOrEqual(t, EstimateTokens(text), DefaultMaxBlockTokens)
}

// TestWorkspaceDocNormalizesInvalidUTF8 guards F6: invalid UTF-8 in a workspace
// doc must be normalized before it reaches the prompt, so block text is always
// valid UTF-8 and consistent with the source version/fingerprint.
func TestWorkspaceDocNormalizesInvalidUTF8(t *testing.T) {
	workDir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte{0xff, 0xfe, 'A', 'B'}, 0o644))

	compiler := New(DefaultPolicy())
	env, err := compiler.CompileThreadContract(context.Background(), EventThreadStart, "evt-utf8", CompileInput{
		AgentID: "agt-u", AgentName: "m", AgentDescription: "d", Runtime: "codex", WorkDir: workDir,
	})
	require.NoError(t, err)

	agentsBlock, ok := blockByID(env.Bundle.Blocks, "agent_config.agents_md")
	require.True(t, ok)
	require.True(t, utf8.ValidString(agentsBlock.text), "block text must be valid UTF-8 after read-time normalization")
}

func blockByID(blocks []PromptBlock, id string) (PromptBlock, bool) {
	for _, block := range blocks {
		if block.ID == id {
			return block, true
		}
	}
	return PromptBlock{}, false
}
func TestPromptCompileError_ErrorIncludesBlockDetails(t *testing.T) {
	err := (&PromptCompileError{
		Kind:      ErrBlockTooLarge,
		BlockID:   "event.current_message",
		Tokens:    9000,
		Limit:     4096,
		EventID:   "evt-block-too-large",
		EventType: EventFreshStart,
		SourceID:  "event.current_message",
	}).Error()

	require.Contains(t, err, `materialized block "event.current_message"`)
	require.Contains(t, err, "source=event.current_message")
	require.Contains(t, err, "9000 tokens")
	require.Contains(t, err, "limit 4096")
	require.Contains(t, err, "fresh_start")
}

func TestPromptCompileError_ErrorIncludesRuntimeInputDetails(t *testing.T) {
	err := (&PromptCompileError{
		Kind:      ErrInvalidRuntimeInput,
		EventID:   "evt-invalid-runtime-input",
		EventType: EventThreadStart,
		Reason:    `thread contract compile must not set turn id`,
	}).Error()

	require.Contains(t, err, "runtime input envelope has invalid semantics")
	require.Contains(t, err, "evt-invalid-runtime-input")
	require.Contains(t, err, "thread_start")
	require.Contains(t, err, "must not set turn id")
}

func TestCompileRuntimeInput_RejectsThreadContractTurnID(t *testing.T) {
	compiler := New(DefaultPolicy())

	_, err := compiler.CompileRuntimeInput(context.Background(), CompileDescriptor{
		EventID:   "evt-thread-turn-id",
		EventKind: EventThreadContract,
		EventType: EventThreadStart,
		TurnID:    "turn-1",
	}, CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
	})
	require.Error(t, err)
	var compileErr *PromptCompileError
	require.ErrorAs(t, err, &compileErr)
	require.Equal(t, ErrInvalidRuntimeInput, compileErr.Kind)
	require.Contains(t, compileErr.Error(), "thread contract compile must not set turn id")
}

func TestCompileDeliveryEnvelope_RejectsRoutedReactionMismatch(t *testing.T) {
	compiler := New(DefaultPolicy())

	_, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-routed-reaction-mismatch", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			MessageID:      "msg-1",
			Target:         "#ops",
			ChannelType:    "channel",
			AuthorIdentity: types.AuthorIdentity{DisplayName: "alice"},
			Content:        "reacted 👍 to your message",
			MessageType:    "reaction",
			Metadata: map[string]any{
				"reaction": map[string]any{
					"deliveryId":      "rxdlv_123",
					"targetMessageId": "msg_123",
					"emoji":           "👍",
				},
			},
		},
	})
	require.Error(t, err)
	var compileErr *PromptCompileError
	require.ErrorAs(t, err, &compileErr)
	require.Equal(t, ErrInvalidRuntimeInput, compileErr.Kind)
	require.Contains(t, compileErr.Error(), `routed-message delivery cannot use message type "reaction"`)
}

func TestCompileDeliveryEnvelope_RejectsMissingRoutedMessageID(t *testing.T) {
	compiler := New(DefaultPolicy())

	_, err := compiler.CompileDeliveryEnvelope(context.Background(), EventRoutedMessageDelivery, "evt-routed-missing-message-id", CompileInput{
		AgentID: "agt-1",
		Runtime: "codex",
		Delivery: DeliveryCompileInput{
			Target:         "#ops",
			ChannelType:    "channel",
			AuthorIdentity: types.AuthorIdentity{DisplayName: "alice"},
			Content:        "please review",
			MessageType:    "text",
		},
	})
	require.Error(t, err)
	var compileErr *PromptCompileError
	require.ErrorAs(t, err, &compileErr)
	require.Equal(t, ErrInvalidRuntimeInput, compileErr.Kind)
	require.Contains(t, compileErr.Error(), "routed-message delivery must include correlation message id")
}

func TestRenderRejectsInvalidRuntimeInputEnvelope(t *testing.T) {
	compiler := New(DefaultPolicy())

	baseBundle := &PromptBundle{
		SchemaVersion: SchemaVersion,
		EventID:       "evt-invalid-runtime-input",
		EventType:     EventThreadStart,
		AgentID:       "agt-1",
		Blocks: []PromptBlock{{
			ID:         "platform.identity",
			Type:       BlockInstruction,
			Layer:      LayerPlatform,
			Slot:       SlotBase,
			Durability: DurabilityPersistent,
			Source:     PromptSourceRef{ID: "platform.identity"},
			text:       "identity",
		}},
	}
	baseDelivery := &DeliveryEnvelope{
		Correlation: DeliveryCorrelation{AgentID: "agt-1"},
	}

	testCases := []struct {
		name          string
		input         RuntimeInputEnvelope
		wantSubstring string
	}{
		{
			name: "thread contract cannot carry turn id",
			input: RuntimeInputEnvelope{
				SchemaVersion: SchemaVersion,
				EventID:       "evt-thread-turn-id",
				EventType:     EventThreadStart,
				TurnID:        "turn-1",
				AgentID:       "agt-1",
				EventKind:     EventThreadContract,
				Bundle: &PromptBundle{
					SchemaVersion: SchemaVersion,
					EventID:       "evt-thread-turn-id",
					EventType:     EventThreadStart,
					TurnID:        "turn-1",
					AgentID:       "agt-1",
				},
			},
			wantSubstring: "thread contract compile must not set turn id",
		},
		{
			name: "envelope cannot carry both bundle and delivery",
			input: RuntimeInputEnvelope{
				SchemaVersion: SchemaVersion,
				EventID:       "evt-both-payloads",
				EventType:     EventThreadStart,
				AgentID:       "agt-1",
				EventKind:     EventThreadContract,
				Bundle:        baseBundle,
				Delivery:      baseDelivery,
			},
			wantSubstring: "exactly one of bundle or delivery must be set",
		},
		{
			name: "turn context cannot use delivery event type",
			input: RuntimeInputEnvelope{
				SchemaVersion: SchemaVersion,
				EventID:       "evt-wrong-type",
				EventType:     EventRoutedMessageDelivery,
				AgentID:       "agt-1",
				EventKind:     EventTurnContext,
				Bundle: &PromptBundle{
					SchemaVersion: SchemaVersion,
					EventID:       "evt-wrong-type",
					EventType:     EventRoutedMessageDelivery,
					AgentID:       "agt-1",
				},
			},
			wantSubstring: `event kind "turn_context" cannot use event type "routed_message_delivery"`,
		},
		{
			name: "bundle metadata must match envelope metadata",
			input: RuntimeInputEnvelope{
				SchemaVersion: SchemaVersion,
				EventID:       "evt-envelope",
				EventType:     EventFreshStart,
				AgentID:       "agt-1",
				EventKind:     EventTurnContext,
				Bundle: &PromptBundle{
					SchemaVersion: SchemaVersion,
					EventID:       "evt-bundle",
					EventType:     EventFreshStart,
					AgentID:       "agt-1",
				},
			},
			wantSubstring: `bundle event id "evt-bundle" does not match envelope event id "evt-envelope"`,
		},
		{
			name: "delivery correlation agent id must match envelope agent id",
			input: RuntimeInputEnvelope{
				SchemaVersion: SchemaVersion,
				EventID:       "evt-delivery-agent-mismatch",
				EventType:     EventRoutedMessageDelivery,
				AgentID:       "agt-1",
				EventKind:     EventDeliveryEnvelope,
				Delivery: &DeliveryEnvelope{
					Correlation: DeliveryCorrelation{AgentID: "agt-2"},
				},
			},
			wantSubstring: `delivery correlation agent id "agt-2" does not match envelope agent id "agt-1"`,
		},
		{
			name: "delivery wake token must match transport wake token",
			input: RuntimeInputEnvelope{
				SchemaVersion: SchemaVersion,
				EventID:       "evt-delivery-wake-mismatch",
				EventType:     EventRoutedMessageDelivery,
				AgentID:       "agt-1",
				EventKind:     EventDeliveryEnvelope,
				Delivery: &DeliveryEnvelope{
					SourceKind: DeliverySourceRoutedMessage,
					Transport: DeliveryTransportBinding{
						WakeToken: "wake-1",
					},
					Correlation: DeliveryCorrelation{
						AgentID:    "agt-1",
						MessageID:  "msg-1",
						WakeToken:  "wake-2",
						DeliveryID: "msg-1",
					},
					MessageType: "text",
				},
			},
			wantSubstring: `delivery correlation wake token "wake-2" does not match transport wake token "wake-1"`,
		},
		{
			name: "delivery active turn must match envelope turn id",
			input: RuntimeInputEnvelope{
				SchemaVersion: SchemaVersion,
				EventID:       "evt-delivery-turn-mismatch",
				EventType:     EventRoutedMessageDelivery,
				TurnID:        "turn-1",
				AgentID:       "agt-1",
				EventKind:     EventDeliveryEnvelope,
				Delivery: &DeliveryEnvelope{
					SourceKind: DeliverySourceRoutedMessage,
					Transport: DeliveryTransportBinding{
						ActiveTurnID: "turn-2",
					},
					Correlation: DeliveryCorrelation{
						AgentID:    "agt-1",
						MessageID:  "msg-1",
						DeliveryID: "msg-1",
					},
					MessageType: "text",
				},
			},
			wantSubstring: `delivery transport active turn id "turn-2" does not match envelope turn id "turn-1"`,
		},
		{
			name: "delivery metadata cannot expose workflow delivery scope",
			input: RuntimeInputEnvelope{
				SchemaVersion: SchemaVersion,
				EventID:       "evt-delivery-workflow-scope-leak",
				EventType:     EventRoutedMessageDelivery,
				AgentID:       "agt-1",
				EventKind:     EventDeliveryEnvelope,
				Delivery: &DeliveryEnvelope{
					SourceKind: DeliverySourceRoutedMessage,
					Correlation: DeliveryCorrelation{
						AgentID:    "agt-1",
						MessageID:  "msg-1",
						DeliveryID: "msg-1",
					},
					MessageType:     "text",
					WorkflowContext: DeliveryWorkflowContext{Available: true},
					Metadata: map[string]any{
						types.WorkflowContextMetadataDeliveryScopeKey: map[string]any{
							"kind": "channel", "scopeId": "chn_hidden",
						},
					},
				},
			},
			wantSubstring: `delivery metadata must not expose workflow context key "workflow_delivery_scope"`,
		},
		{
			name: "reaction delivery requires delivery id",
			input: RuntimeInputEnvelope{
				SchemaVersion: SchemaVersion,
				EventID:       "evt-reaction-missing-delivery-id",
				EventType:     EventReactionDelivery,
				AgentID:       "agt-1",
				EventKind:     EventDeliveryEnvelope,
				Delivery: &DeliveryEnvelope{
					SourceKind: DeliverySourceReaction,
					Correlation: DeliveryCorrelation{
						AgentID: "agt-1",
					},
					MessageType: "reaction",
				},
			},
			wantSubstring: "reaction delivery must include correlation delivery id",
		},
		{
			name: "routed delivery event type cannot use reaction source kind",
			input: RuntimeInputEnvelope{
				SchemaVersion: SchemaVersion,
				EventID:       "evt-routed-source-mismatch",
				EventType:     EventRoutedMessageDelivery,
				AgentID:       "agt-1",
				EventKind:     EventDeliveryEnvelope,
				Delivery: &DeliveryEnvelope{
					SourceKind: DeliverySourceReaction,
					Correlation: DeliveryCorrelation{
						AgentID:    "agt-1",
						DeliveryID: "rxdlv_1",
					},
					MessageType: "reaction",
				},
			},
			wantSubstring: `event type "routed_message_delivery" must use delivery source kind "routed_message", got "reaction_signal"`,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := compiler.RenderRuntimeInput(context.Background(), tc.input, "codex", RenderOptions{})
			require.Error(t, err)
			var compileErr *PromptCompileError
			require.ErrorAs(t, err, &compileErr)
			require.Equal(t, ErrInvalidRuntimeInput, compileErr.Kind)
			require.Contains(t, compileErr.Error(), tc.wantSubstring)

		})
	}
}

func TestInvalidBlockSemanticCombinations(t *testing.T) {

	testCases := []struct {
		name          string
		block         PromptBlock
		wantSubstring string
	}{
		{
			name: "reference block cannot enter base slot",
			block: PromptBlock{
				ID:         "platform.agentteam.reference",
				Type:       BlockReference,
				Slot:       SlotBase,
				Durability: DurabilityCompactable,
				Source:     PromptSourceRef{ID: "platform.agentteam.reference"},
				text:       "Reference text",
			},
			wantSubstring: `reference blocks must target "reference", got "base"`,
		},
		{
			name: "turn context cannot be persistent",
			block: PromptBlock{
				ID:         "event.current_message",
				Type:       BlockTurnContext,
				Slot:       SlotTurn,
				Durability: DurabilityPersistent,
				Source:     PromptSourceRef{ID: "event.current_message"},
				text:       "Current message",
			},
			wantSubstring: `turn-context blocks must use "turn_only" durability, got "persistent"`,
		},
		{
			name: "instruction cannot target turn slot",
			block: PromptBlock{
				ID:         "platform.identity",
				Type:       BlockInstruction,
				Slot:       SlotTurn,
				Durability: DurabilityPersistent,
				Source:     PromptSourceRef{ID: "platform.identity"},
				text:       "Identity",
			},
			wantSubstring: `instruction blocks must target "base" or "developer", got "turn"`,
		},
		{
			name: "notice cannot be compactable reference",
			block: PromptBlock{
				ID:         "notice.identity",
				Type:       BlockNotice,
				Slot:       SlotTurn,
				Durability: DurabilityCompactable,
				Source:     PromptSourceRef{ID: "notice.identity"},
				text:       "Notice",
			},
			wantSubstring: `notice blocks must use "turn_only" or "until_acknowledged" durability, got "compactable"`,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			require.Equal(t, tc.wantSubstring, invalidBlockSemanticReason(tc.block))
		})
	}
}

func recoveredPromptCompileError(fn func()) (err error) {
	defer func() {
		recovered := recover()
		if recovered == nil {
			return
		}
		recoveredErr, ok := recovered.(error)
		if !ok {
			panic(recovered)
		}
		err = recoveredErr
	}()
	fn()
	return nil
}
