package promptcompiler

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

	"github.com/stretchr/testify/require"
)

func TestPromptBlockRegistryIsComplete(t *testing.T) {
	require.NoError(t, ValidatePromptBlockRegistry())
	entries := RegisteredPromptBlocks()
	require.NotEmpty(t, entries)
	seen := make(map[string]struct{}, len(entries))
	for _, entry := range entries {
		_, duplicate := seen[entry.ID]
		require.False(t, duplicate, "duplicate registry id %q", entry.ID)
		seen[entry.ID] = struct{}{}
		require.NotEmpty(t, entry.InputType)
		require.NotEmpty(t, entry.ExpectedType)
		require.NotEmpty(t, entry.Title)
		require.NotEmpty(t, entry.Type)
		require.NotEmpty(t, entry.Layer)
		require.NotEmpty(t, entry.Owner)
		require.NotEmpty(t, entry.Description)
		require.NotEmpty(t, entry.DebugLabel)
		require.NotEmpty(t, entry.TemplatePath)
		require.NotNil(t, entry.Renderer)
		require.NotNil(t, entry.Constructor)
		require.NotEmpty(t, entry.Slot)
		require.Positive(t, entry.Priority)
		require.NotEmpty(t, entry.Durability)
		require.Positive(t, entry.BudgetTokens)
	}
}

func TestEveryRegisteredIDConstructsItsDeclaredFixtureAndRejectsWrongDomain(t *testing.T) {
	registry := DefaultPromptBlockRegistry()
	for _, spec := range RegisteredPromptBlocks() {
		t.Run(spec.ID, func(t *testing.T) {
			fixture, err := promptBlockFixture(spec)
			require.NoError(t, err)
			block, err := registry.Construct(spec.ID, fixture)
			require.NoError(t, err)
			require.Equal(t, spec.ID, block.ID)
			require.Equal(t, spec.Title, block.Title)
			require.Equal(t, spec.Type, block.Type)
			require.Equal(t, spec.Layer, block.Layer)
			require.Equal(t, spec.Slot, block.Slot)
			require.Equal(t, spec.Priority, block.Priority)
			require.Equal(t, spec.Durability, block.Durability)
			require.Equal(t, "desktop/prompts/"+spec.TemplatePath, block.Source.Path)
			require.Equal(t, specTemplateVersion(spec), block.Source.Version)
			require.Equal(t, spec.Owner, block.Source.Owner)
			require.Equal(t, spec.Description, block.Source.Description)
			wrong := wrongPromptFixture(spec)
			require.NotNil(t, wrong)
			_, err = registry.Construct(spec.ID, wrong)
			require.Error(t, err)
		})
	}
}

func wrongPromptFixture(spec PromptBlockSpec) PromptBlockInput {
	switch spec.InputType {
	case PromptInputIdentity:
		if spec.ExpectedType == "IdentityNoticeTemplateData" {
			return newIdentityPromptInput(IdentityTemplateData{AgentName: "wrong"})
		}
		return newIdentityNoticePromptInput(IdentityNoticeTemplateData{CurrentName: "wrong"})
	case PromptInputAgentConfig:
		return newEmptyPromptInput(PromptInputAgentConfig)
	case PromptInputSkillPlan:
		if spec.ExpectedType == "AgentTeamCoreTemplateData" {
			return newPlatformLoopPromptInput(PlatformLoopTemplateData{SendMessageTool: "wrong"})
		}
		return newAgentTeamCorePromptInput(AgentTeamCoreTemplateData{SendMessageTool: "wrong"})
	case PromptInputMessage:
		if spec.ExpectedType == "ReactionPromptData" {
			return newMessagePromptInput(MessageTemplateData{Content: "wrong"})
		}
		return newReactionPromptInput(ReactionPromptData{Emoji: "wrong"})
	case PromptInputTask:
		if spec.ExpectedType == "TaskScopeContextTemplateData" {
			return newTaskTemplatePromptInput(TaskPromptTemplateData{Label: "wrong"})
		}
		return newTaskScopeContextPromptInput(TaskScopeContextTemplateData{Target: "wrong"})
	case PromptInputWorkflow:
		if spec.ExpectedType == "WorkflowEditPromptData" {
			return newWorkflowPromptInput(WorkflowTemplateData{Trusted: true})
		}
		return newWorkflowEditPromptInput(WorkflowEditPromptData{PlanID: "wrong"})
	case PromptInputTeam:
		if spec.ExpectedType == "WorkflowTeamPromptData" {
			return newTeamPromptInput(TeamPromptTemplateData{Teams: []TeamPromptTemplateContext{{Label: "wrong"}}})
		}
		return newWorkflowTeamPromptInput(WorkflowTeamPromptData{Teams: []WorkflowTeamPromptContext{{Label: "wrong"}}})
	case PromptInputActiveTask:
		if spec.ExpectedType == "ActiveTaskNormalRoutingTemplateData" {
			return newActiveTaskPromptInput(ActiveTaskTemplateData{TaskNumber: 1})
		}
		return newActiveTaskNormalPromptInput(ActiveTaskNormalRoutingTemplateData{Target: "wrong"})
	case PromptInputAttachment:
		if spec.ExpectedType == "AttachmentDetailsTemplateData" {
			return newAttachmentPromptInput(AttachmentTemplateData{})
		}
		return newAttachmentDetailsPromptInput(AttachmentDetailsTemplateData{})
	case PromptInputRecovery:
		if spec.ExpectedType == "RuntimeRetryTemplateData" {
			return newRecoveryPromptInput(RecoveryTemplateData{Text: "wrong"})
		}
		return newRuntimeRetryPromptInput(RuntimeRetryTemplateData{Reason: "wrong"})
	case PromptInputVisibleReplyCheck:
		if spec.ExpectedType == "FinalVisibleReplyCheckTemplateData" {
			return newVisibleReplyPromptInput(VisibleReplyContractTemplateData{SendMessageTool: "wrong"})
		}
		return newFinalReplyCheckPromptInput(FinalVisibleReplyCheckTemplateData{})
	case PromptInputExecGuidance:
		return newExtensionCatalogGuidancePromptInput(ExtensionCatalogGuidanceTemplateData{Actions: []ExtensionCatalogGuidanceAction{{ExtensionName: "wrong", ActionName: "action"}}})
	case PromptInputExtensionCatalogGuidance:
		return newExecGuidancePromptInput(ExecGuidanceTemplateData{Domains: []string{"wrong"}})
	case PromptInputWorkspaceDocument:
		if spec.ExpectedType == "WorkspaceDocOversizeTemplateData" {
			return newWorkspaceDocumentPromptInput(WorkspaceDocumentTemplateData{Content: "wrong"})
		}
		return newOversizePromptInput(WorkspaceDocOversizeTemplateData{})
	case PromptInputRenderer:
		return newAttributionPromptInput(AttributionTemplateData{})
	case PromptInputRuntimePolicy:
		return newEmptyPromptInput(PromptInputRuntimePolicy)
	default:
		return nil
	}
}

func TestUnreadSummaryRegistryRejectsWrongTypedGuidance(t *testing.T) {
	registry := DefaultPromptBlockRegistry()
	_, err := registry.Construct("event.unread_summary", newExecGuidancePromptInput(ExecGuidanceTemplateData{Domains: []string{"wrong"}}))
	require.Error(t, err)
	block, err := registry.Construct("event.unread_summary", newUnreadSummaryPromptInput(UnreadSummaryTemplateData{Channels: []UnreadSummaryChannel{{Channel: "#agentteam", Count: 1}}}))
	require.NoError(t, err)
	require.Contains(t, block.RenderedText(), "#agentteam: 1 unread message")
}

func TestRegistryValidationRejectsMissingFixtureOrRenderer(t *testing.T) {
	registry := DefaultPromptBlockRegistry()
	spec, ok := registry.Lookup("event.current_message")
	require.True(t, ok)
	spec.ExpectedType = ""
	registry.entries[spec.ID] = spec
	require.Error(t, registry.Validate())

	registry = DefaultPromptBlockRegistry()
	spec, ok = registry.Lookup("event.current_message")
	require.True(t, ok)
	spec.Renderer = nil
	registry.entries[spec.ID] = spec
	require.Error(t, registry.Validate())
}

func TestCompilePromptProducesSealedTypedCarrier(t *testing.T) {
	compiler := New(DefaultPolicy())
	carrier, err := compiler.CompilePrompt(context.Background(), CompileDescriptor{
		EventID:   "evt-sealed",
		EventKind: EventTurnContext,
		EventType: EventFreshStart,
	}, CompileInput{}, "codex")
	require.NoError(t, err)
	require.True(t, carrier.Valid())
	require.Equal(t, "codex", carrier.Runtime())
	require.NotNil(t, carrier.Envelope().Bundle)
	require.NotEmpty(t, carrier.SlotText("turnInput"))
	wire, err := json.Marshal(carrier)
	require.NoError(t, err)
	require.Contains(t, string(wire), `"envelope"`)
	require.Contains(t, string(wire), `"rendered"`)
}

func TestRegistryRejectsArbitraryAndWrongTypedInputs(t *testing.T) {
	registry := DefaultPromptBlockRegistry()
	spec, ok := registry.Lookup("agentteam.context_compaction_continuation")
	require.True(t, ok)
	_, err := registry.Construct(spec.ID, newEmptyPromptInput(PromptInputMessage))
	require.Error(t, err)
	_, err = registry.Construct(spec.ID, newExecGuidancePromptInput(ExecGuidanceTemplateData{Domains: []string{"FORGED SEMANTIC PROMPT"}}))
	require.Error(t, err, "a registered ID must render its declared typed input, not accept another guidance domain")
	_, err = registry.Construct("not.registered", newRecoveryPromptInput(RecoveryTemplateData{}))
	require.Error(t, err)
}

func TestRegistryTemplateRendererUsesDeclaredTemplate(t *testing.T) {
	registry := DefaultPromptBlockRegistry()
	spec, ok := registry.Lookup("event.runtime_retry")
	require.True(t, ok)
	block, err := registry.Construct(spec.ID, newRuntimeRetryPromptInput(RuntimeRetryTemplateData{Reason: "overloaded", Attempt: 1, MaxAttempts: 2, Delay: "1s"}))
	require.NoError(t, err)
	require.Contains(t, block.RenderedText(), "overloaded")
	require.Equal(t, "desktop/prompts/"+spec.TemplatePath, block.Source.Path)
	require.NotEqual(t, "FORGED SEMANTIC PROMPT", block.RenderedText())
}

func TestRegistryRejectsForgedRegisteredBlockIdentityAndProvenance(t *testing.T) {
	compiler := New(DefaultPolicy())
	desc := CompileDescriptor{EventID: "evt-forged", EventKind: EventTurnContext, EventType: EventFreshStart}
	require.Panics(t, func() {
		compiler.validateConstructedBlock(desc, PromptBlock{ID: "event.current_message", Source: PromptSourceRef{ID: "event.current_message", Path: "not-the-registered-template"}, text: "FORGED REGISTERED PROMPT"})
	})
	spec, ok := DefaultPromptBlockRegistry().Lookup("event.current_message")
	require.True(t, ok)
	require.Panics(t, func() {
		compiler.validateConstructedBlock(desc, PromptBlock{ID: spec.ID, Title: spec.Title, Type: spec.Type, Layer: spec.Layer, Slot: spec.Slot, Durability: spec.Durability, Priority: spec.Priority, Source: PromptSourceRef{ID: spec.ID, Path: "desktop/prompts/events/current_message.md", Version: specTemplateVersion(spec), Owner: spec.Owner, Description: spec.Description}, text: "FORGED VALID-PATH PROMPT"})
	})
}

func TestJSONRestoredForgedRegisteredBlockCannotRender(t *testing.T) {
	var input RuntimeInputEnvelope
	data := []byte(`{"schemaVersion":"prompt-compiler/v1","eventId":"evt-json-forged","eventType":"fresh_start","agentId":"agt","eventKind":"turn_context","fingerprint":"fingerprint","bundle":{"schemaVersion":"prompt-compiler/v1","eventId":"evt-json-forged","eventType":"fresh_start","agentId":"agt","blocks":[{"id":"event.current_message","title":"Current message","type":"turn_context","layer":"event","slot":"turn","durability":"turn_only","priority":30,"source":{"id":"event.current_message","sourceKind":"markdown_template","path":"not-the-registered-template","version":"forged","owner":"promptcompiler","description":"forged"},"text":"FORGED REGISTERED PROMPT","tokenEstimate":1}]}}`)
	require.NoError(t, json.Unmarshal(data, &input))
	_, err := New(DefaultPolicy()).RenderRuntimeInput(context.Background(), input, "codex", RenderOptions{})
	require.Error(t, err)
	require.Contains(t, err.Error(), "not materialized by its registered constructor")
}

func TestCompileGeneratedTurnRejectsUnregisteredBlock(t *testing.T) {
	_, err := New(DefaultPolicy()).CompileGeneratedTurn(
		context.Background(),
		EventFinalVisibleReplyCheck,
		"evt-unregistered",
		GeneratedPromptInput{Kind: GeneratedPromptKind("future.unregistered")},
	)
	require.Error(t, err)
	var compileErr *PromptCompileError
	require.ErrorAs(t, err, &compileErr)
	require.Equal(t, ErrUnregisteredPromptBlock, compileErr.Kind)
}
