package exec

import (
	"encoding/json"
	"testing"

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

func TestDefaultCatalogHasOneExamplePerAction(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	actions := catalog.actionsSnapshot()
	require.Len(t, actions, len(builtinActionInventory))
	for _, action := range actions {
		require.Lenf(t, action.Examples, 1, "%s must have exactly one canonical example", action.identity())
	}
}

func TestDefaultCatalogProjectionBudgets(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	var (
		helpBytes, inputBytes, outputBytes                               int
		structuralHelpBytes, structuralInputBytes, structuralOutputBytes int
		roleHelpBytes, roleInputBytes, roleOutputBytes                   int
		skillHelpBytes, skillInputBytes, skillOutputBytes                int
		workflowWorkScopeBytes, workflowWorkItemsBytes                   int
		workflowWorkItemsContractBytes                                   int
		taskCreateParentScopeBytes                                       int
	)
	for _, action := range catalog.actionsSnapshot() {
		help, ok := catalog.RenderActionHelp(action.Domain, action.Name)
		require.True(t, ok)
		input, ok := catalog.RenderActionInputSchemaJSON(action.Domain, action.Name)
		require.True(t, ok)
		output, ok := catalog.RenderActionOutputSchemaJSON(action.Domain, action.Name)
		require.Truef(t, ok, "output schema %s", action.identity())
		helpBytes += len(help)
		inputBytes += len(input)
		outputBytes += len(output)
		if action.Domain == "agent" {
			switch action.Name {
			case "create", "update", "delete", "duplicate":
				structuralHelpBytes += len(help)
				structuralInputBytes += len(input)
				structuralOutputBytes += len(output)
			}
		}
		if action.Domain == "role" {
			roleHelpBytes += len(help)
			roleInputBytes += len(input)
			roleOutputBytes += len(output)
		}
		if action.Domain == "skill" {
			skillHelpBytes += len(help)
			skillInputBytes += len(input)
			skillOutputBytes += len(output)
		}
		if action.Domain == "workflow" && action.Name == "step" {
			workflowWorkItemsBytes = projectedWorkflowRunCompleteFieldBytes(t, input, "workItems")
			workflowWorkScopeBytes = projectedWorkflowRunCompleteFieldBytes(t, input, "workScope")
			workflowWorkItemsContractBytes = projectedSchemaPropertyBytes(t, output, "workItemsContract")
		}
		if action.Domain == "task" && action.Name == "create" {
			taskCreateParentScopeBytes = projectedTaskCreateParentScopeBytes(t, input)
		}
	}
	t.Logf(
		"projection bytes: help=%d (role=%d skill=%d) input=%d (role=%d skill=%d workScope=%d workItems=%d taskCreateParentScope=%d) output=%d (role=%d skill=%d workItemsContract=%d)",
		helpBytes,
		roleHelpBytes,
		skillHelpBytes,
		inputBytes,
		roleInputBytes,
		skillInputBytes,
		workflowWorkScopeBytes,
		workflowWorkItemsBytes,
		taskCreateParentScopeBytes,
		outputBytes,
		roleOutputBytes,
		skillOutputBytes,
		workflowWorkItemsContractBytes,
	)
	require.LessOrEqual(t, structuralHelpBytes, 1024)
	require.LessOrEqual(t, structuralInputBytes, 2*1024)
	require.LessOrEqual(t, structuralOutputBytes, 2*1024)
	require.Positive(t, roleHelpBytes)
	require.LessOrEqual(t, roleHelpBytes, 2*1024)
	require.Positive(t, roleInputBytes)
	require.LessOrEqual(t, roleInputBytes, 4*1024)
	require.Positive(t, roleOutputBytes)
	require.LessOrEqual(t, roleOutputBytes, 8*1024)
	require.Positive(t, skillHelpBytes)
	require.LessOrEqual(t, skillHelpBytes, 2*1024)
	require.Positive(t, skillInputBytes)
	require.LessOrEqual(t, skillInputBytes, 4*1024)
	require.Positive(t, skillOutputBytes)
	require.LessOrEqual(t, skillOutputBytes, 10*1024)
	require.LessOrEqual(t, helpBytes-structuralHelpBytes-roleHelpBytes-skillHelpBytes, 16*1024)
	// Keep the aggregate input projection bounded. The current Workflow artifact
	// continuation and transport-governance descriptors add a small amount of
	// closed metadata, so this ratchet is intentionally 25 KiB rather than
	// silently allowing unbounded growth.
	require.Positive(t, workflowWorkScopeBytes)
	require.LessOrEqual(t, workflowWorkScopeBytes, 256)
	require.Positive(t, workflowWorkItemsBytes)
	require.LessOrEqual(t, workflowWorkItemsBytes, 2*1024)
	require.Positive(t, taskCreateParentScopeBytes)
	require.LessOrEqual(t, taskCreateParentScopeBytes, 1024)
	require.LessOrEqual(
		t,
		inputBytes-structuralInputBytes-roleInputBytes-skillInputBytes-workflowWorkScopeBytes-workflowWorkItemsBytes-taskCreateParentScopeBytes,
		25*1024,
	)
	require.Positive(t, workflowWorkItemsContractBytes)
	require.LessOrEqual(t, workflowWorkItemsContractBytes, 1024)
	// Output schemas retain business properties whose names collide with JSON
	// Schema annotation keywords (e.g. description on agent/team) now that
	// compactModelSchema no longer strips them from property containers, plus
	// the compact stable assignee identity on Task results. Workflow payload
	// continuation adds immutable version/layout refs and bounded page metadata;
	// keep that exact read contract within its own explicit aggregate ratchet.
	require.LessOrEqual(t, outputBytes-structuralOutputBytes-roleOutputBytes-skillOutputBytes-workflowWorkItemsContractBytes, 49*1024)
}

func projectedSchemaUnionBranchBytes(t *testing.T, schemaJSON, markerField string) int {
	t.Helper()
	var schema map[string]any
	require.NoError(t, json.Unmarshal([]byte(schemaJSON), &schema))
	require.Truef(t, removeFirstSchemaUnionBranchWithProperty(schema, markerField), "schema has no union branch with %s", markerField)
	without, err := json.Marshal(schema)
	require.NoError(t, err)
	return len(schemaJSON) - len(without) - 1 // standalone schema JSON appends '\n'
}

func removeFirstSchemaUnionBranchWithProperty(value any, field string) bool {
	switch typed := value.(type) {
	case map[string]any:
		if branches, ok := typed["oneOf"].([]any); ok {
			for index, branch := range branches {
				object, _ := branch.(map[string]any)
				properties, _ := object["properties"].(map[string]any)
				if _, exists := properties[field]; exists {
					typed["oneOf"] = append(branches[:index], branches[index+1:]...)
					return true
				}
			}
		}
		for _, child := range typed {
			if removeFirstSchemaUnionBranchWithProperty(child, field) {
				return true
			}
		}
	case []any:
		for _, child := range typed {
			if removeFirstSchemaUnionBranchWithProperty(child, field) {
				return true
			}
		}
	}
	return false
}

func projectedSchemaPropertyBytes(t *testing.T, schemaJSON, field string) int {
	t.Helper()
	var schema map[string]any
	require.NoError(t, json.Unmarshal([]byte(schemaJSON), &schema))
	require.Truef(t, removeFirstSchemaProperty(schema, field), "schema has no %s property", field)
	without, err := json.Marshal(schema)
	require.NoError(t, err)
	return len(schemaJSON) - len(without) - 1 // standalone schema JSON appends '\n'
}

func removeFirstSchemaProperty(value any, field string) bool {
	switch typed := value.(type) {
	case map[string]any:
		if properties, ok := typed["properties"].(map[string]any); ok {
			if _, exists := properties[field]; exists {
				delete(properties, field)
				if required, ok := typed["required"].([]any); ok {
					filtered := required[:0]
					for _, item := range required {
						if item != field {
							filtered = append(filtered, item)
						}
					}
					typed["required"] = filtered
				}
				return true
			}
		}
		for _, child := range typed {
			if removeFirstSchemaProperty(child, field) {
				return true
			}
		}
	case []any:
		for _, child := range typed {
			if removeFirstSchemaProperty(child, field) {
				return true
			}
		}
	}
	return false
}

func projectedWorkflowRunCompleteFieldBytes(t *testing.T, input, field string) int {
	t.Helper()
	var schema map[string]any
	require.NoError(t, json.Unmarshal([]byte(input), &schema))
	for _, rawBranch := range schema["oneOf"].([]any) {
		branch := rawBranch.(map[string]any)
		properties, _ := branch["properties"].(map[string]any)
		action, _ := properties["action"].(map[string]any)
		if action["const"] != "complete" {
			continue
		}
		require.Contains(t, properties, field)
		delete(properties, field)
		without, err := json.Marshal(schema)
		require.NoError(t, err)
		return len(input) - len(without) - 1 // standaloneSchemaJSON appends '\n'
	}
	t.Fatalf("workflow run complete projection has no %s", field)
	return 0
}

func projectedTaskCreateParentScopeBytes(t *testing.T, input string) int {
	t.Helper()
	var schema map[string]any
	require.NoError(t, json.Unmarshal([]byte(input), &schema))
	properties, ok := schema["properties"].(map[string]any)
	require.True(t, ok)
	require.Contains(t, properties, "parentTaskScope")
	delete(properties, "parentTaskScope")
	require.Contains(t, schema, "oneOf")
	delete(schema, "oneOf")
	required, ok := schema["required"].([]any)
	require.True(t, ok)
	required = append(required, "channel")
	schema["required"] = required
	without, err := json.Marshal(schema)
	require.NoError(t, err)
	return len(input) - len(without) - 1 // standaloneSchemaJSON appends '\n'
}
