package exec

import (
	"errors"
	"go/ast"
	"go/parser"
	"go/token"
	"net/http"
	"os"
	"path/filepath"
	"slices"
	"sort"
	"strconv"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
)

func TestRegisterRoutesUsesExactTypedBuiltinActionSet(t *testing.T) {
	require.Equal(t, expectedExecActionIdentities(), registeredLiteralActionIdentities(t))
}

func TestSealedProductionInvokersDoNotStoreOperationFunctions(t *testing.T) {
	contents, err := os.ReadFile("production_invokers.go")
	require.NoError(t, err)
	require.NotContains(t, string(contents), "invoke    func")
	require.NotContains(t, string(contents), "invoke func")
}

func TestRegisterRoutesInstallsMergedCatalogAndSingleDynamicActionRoute(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	RegisterRoutes(router.Group("/api/internal/exec"), RouteDependencies{})

	const prefix = "/api/internal/exec/servers/:serverID/agents/:agentID/"
	routes := router.Routes()
	require.Len(t, routes, 2)
	require.Equal(t, http.MethodGet, routes[0].Method)
	require.Equal(t, prefix+"catalog", routes[0].Path)
	require.Equal(t, http.MethodPost, routes[1].Method)
	require.Equal(t, prefix+":domain/:action", routes[1].Path)

	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	expected := make([]string, 0, catalog.len())
	for _, descriptor := range catalog.actionsSnapshot() {
		expected = append(expected, descriptor.identity())
	}
	sort.Strings(expected)
	require.Equal(t, expected, registeredLiteralActionIdentities(t))
}

func TestCatalogIsSingleSourceForHelpSchemaOpenAPIAndPrompt(t *testing.T) {
	descriptor, err := describe(actionSpec[contractTestInput, contractTestOutput]{
		Meta: actionMeta{
			Domain:      "sentinel",
			Name:        "inspect",
			Summary:     "Projection sentinel",
			Description: "Only the supplied catalog contains this action.",
			Permission:  permissionReadOnly,
		},
	})
	require.NoError(t, err)
	catalog, err := newDescriptorCatalog(descriptor)
	require.NoError(t, err)

	rootHelp := catalog.RenderRootHelp()
	require.Contains(t, rootHelp, "sentinel")
	require.NotContains(t, rootHelp, "workspace")
	domainHelp, ok := catalog.RenderDomainHelp("sentinel")
	require.True(t, ok)
	require.Contains(t, domainHelp, "inspect")
	actionHelp, ok := catalog.RenderActionHelp("sentinel", "inspect")
	require.True(t, ok)
	require.Contains(t, actionHelp, "Projection sentinel")

	schema, ok := catalog.RenderActionInputSchemaJSON("sentinel", "inspect")
	require.True(t, ok)
	require.Contains(t, schema, `"channel"`)
	require.NotContains(t, schema, `"domain"`)

	openAPI, err := catalog.openAPI()
	require.NoError(t, err)
	require.Len(t, openAPI.Paths, 1)
	require.Contains(t, openAPI.Paths, "/sentinel/inspect")

	prompt := catalog.RenderActionSummary()
	require.Contains(t, prompt, "`sentinel`")
	require.Contains(t, prompt, "`agentteam exec <domain> --help`")
	require.NotContains(t, prompt, "`agentteam exec sentinel inspect`")
	require.NotContains(t, prompt, "workspace")
}

func TestExecDynamicDispatcherKeepsTypedBuiltinBoundary(t *testing.T) {
	files := parseExecProductionFiles(t)
	var problems []string
	for path, file := range files {
		ast.Inspect(file, func(node ast.Node) bool {
			switch node := node.(type) {
			case *ast.TypeSpec:
				if strings.Contains(strings.ToLower(node.Name.Name), "registry") {
					problems = append(problems, path+": executable registry declaration "+node.Name.Name)
				}
			case *ast.ValueSpec:
				for _, name := range node.Names {
					if strings.Contains(strings.ToLower(name.Name), "registry") {
						problems = append(problems, path+": executable registry value "+name.Name)
					}
				}
			case *ast.Field:
				for _, name := range node.Names {
					if name.Name == "Raw" && isJSONRawMessage(node.Type) {
						problems = append(problems, path+": raw action field")
					}
				}
			case *ast.FuncDecl:
				lowerName := strings.ToLower(node.Name.Name)
				if strings.Contains(lowerName, "registry") {
					problems = append(problems, path+": executable registry function "+node.Name.Name)
				}
				if strings.Contains(lowerName, "wirehandlers") || strings.Contains(lowerName, "wireall") {
					problems = append(problems, path+": central handler wiring "+node.Name.Name)
				}
			}
			return true
		})
	}
	sort.Strings(problems)
	require.Empty(t, problems, strings.Join(problems, "\n"))

	routesFile := files["routes.go"]
	require.NotNil(t, routesFile)
	adapterType := typeDeclaration(routesFile, "adapter")
	require.NotNil(t, adapterType)
	require.True(t, structHasBuiltinInvokerMap(adapterType.Type),
		"adapter must own exactly the type-erased map populated by typed registerJSON declarations")

	runtimeActionType := typeDeclaration(files["runtime_catalog.go"], "RuntimeAction")
	require.NotNil(t, runtimeActionType)
	require.False(t, structContainsHandler(runtimeActionType.Type),
		"Runtime Catalog descriptors must never expose executable handlers")
}

func TestExecProductionFilesStayWithinResponsibilityLimit(t *testing.T) {
	entries, err := os.ReadDir(".")
	require.NoError(t, err)
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
			continue
		}
		source, readErr := os.ReadFile(entry.Name())
		require.NoError(t, readErr)
		lines := strings.Count(string(source), "\n") + 1
		require.LessOrEqualf(t, lines, 500, "%s has %d lines", entry.Name(), lines)
	}
}

func TestWorkflowCatalogOwnsBusinessInputShape(t *testing.T) {
	var problems []string
	if _, err := os.Stat("workflow_validation.go"); !errors.Is(err, os.ErrNotExist) {
		problems = append(problems, "workflow_validation.go must stay deleted")
	}

	handler, err := parser.ParseFile(token.NewFileSet(), "workflow_handler.go", nil, 0)
	require.NoError(t, err)
	for _, name := range []string{"plan", "trigger", "run"} {
		function := methodDeclaration(handler, "workflowHandler", name)
		require.NotNil(t, function, name)
		calls := functionCallNames(function)
		for _, forbidden := range []string{
			"typedWorkflowAction",
			"validateWorkflowPlanAction",
			"validateWorkflowTriggerAction",
			"validateWorkflowRunAction",
		} {
			if slices.Contains(calls, forbidden) {
				problems = append(problems, name+" must not call "+forbidden)
			}
		}
	}
	step := methodDeclaration(handler, "workflowHandler", "step")
	require.NotNil(t, step)
	stepCalls := functionCallNames(step)
	for _, required := range []string{"typedWorkflowAction", "validateWorkflowStep"} {
		if !slices.Contains(stepCalls, required) {
			problems = append(problems, "step must call "+required)
		}
	}
	require.Empty(t, problems, strings.Join(problems, "\n"))
}

func methodDeclaration(file *ast.File, receiver, name string) *ast.FuncDecl {
	for _, declaration := range file.Decls {
		function, ok := declaration.(*ast.FuncDecl)
		if !ok || function.Name.Name != name || function.Recv == nil || len(function.Recv.List) != 1 {
			continue
		}
		identifier, ok := function.Recv.List[0].Type.(*ast.Ident)
		if ok && identifier.Name == receiver {
			return function
		}
	}
	return nil
}

func functionCallNames(function *ast.FuncDecl) []string {
	var names []string
	ast.Inspect(function.Body, func(node ast.Node) bool {
		call, ok := node.(*ast.CallExpr)
		if !ok {
			return true
		}
		identifier, ok := call.Fun.(*ast.Ident)
		if ok {
			names = append(names, identifier.Name)
		}
		return true
	})
	return names
}

func TestExecRetiredResponsibilityFilesStayDeleted(t *testing.T) {
	for _, path := range []string{"typed_contract.go", "workflow_contract.go", "workflow_schema.go"} {
		_, err := os.Stat(path)
		require.ErrorIs(t, err, os.ErrNotExist, path)
	}
}

func expectedExecActionIdentities() []string {
	return []string{
		"agent create",
		"agent delete",
		"agent duplicate",
		"agent get",
		"agent list",
		"agent restart",
		"agent start",
		"agent stop",
		"agent update",
		"channel agent-add",
		"channel agent-remove",
		"channel create",
		"channel get",
		"channel list",
		"channel members",
		"channel update",
		"profile set-avatar",
		"project create",
		"project delete",
		"project list",
		"review add",
		"review delete",
		"review list",
		"role apply",
		"role create",
		"role delete",
		"role get",
		"role list",
		"role request-get",
		"role update",
		"scheduled-message create",
		"scheduled-message delete",
		"scheduled-message get",
		"scheduled-message list",
		"scheduled-message pause",
		"scheduled-message resume",
		"scheduled-message runs",
		"scheduled-message update",
		"scheduled-message validate",
		"skill create",
		"skill delete",
		"skill get",
		"skill import",
		"skill list",
		"skill update",
		"task checkpoint",
		"task claim",
		"task create",
		"task delete",
		"task get",
		"task list",
		"task unclaim",
		"task update",
		"team get",
		"team list",
		"team update",
		"workflow plan",
		"workflow run",
		"workflow step",
		"workflow trigger",
		"workspace list",
	}
}

func registeredLiteralActionIdentities(t *testing.T) []string {
	t.Helper()
	file, err := parser.ParseFile(token.NewFileSet(), "invoker_registry.go", nil, 0)
	require.NoError(t, err)
	registry := topLevelFunction(file, "builtinRouteRegistrations")
	require.NotNil(t, registry)

	var identities []string
	ast.Inspect(registry.Body, func(node ast.Node) bool {
		call, ok := node.(*ast.CallExpr)
		if !ok {
			return true
		}
		var functionName string
		switch function := call.Fun.(type) {
		case *ast.IndexListExpr:
			ident, ok := function.X.(*ast.Ident)
			if !ok {
				return true
			}
			functionName = ident.Name
			require.Len(t, function.Indices, 2, "builtinRoute must retain typed input/output parameters")
		case *ast.IndexExpr:
			ident, ok := function.X.(*ast.Ident)
			if !ok {
				return true
			}
			functionName = ident.Name
		default:
			return true
		}
		if functionName != "builtinRoute" {
			return true
		}
		require.Len(t, call.Args, 3, "builtinRoute must receive one sealed executable object")
		domain := requireStringLiteral(t, call.Args[0])
		action := requireStringLiteral(t, call.Args[1])
		executableLiteral, ok := call.Args[2].(*ast.CompositeLit)
		require.True(t, ok, "builtinRoute must receive one concrete invoker object")
		invokerType, ok := executableLiteral.Type.(*ast.Ident)
		require.True(t, ok, "builtinRoute invoker must be a concrete sealed operation type")
		require.True(t, strings.HasSuffix(invokerType.Name, "Object"))
		require.Len(t, executableLiteral.Elts, 1, "sealed invoker must own one family transport object")
		identities = append(identities, domain+" "+action)
		return true
	})
	sort.Strings(identities)
	require.Len(t, identities, 61)
	return identities
}

func topLevelFunction(file *ast.File, name string) *ast.FuncDecl {
	for _, declaration := range file.Decls {
		function, ok := declaration.(*ast.FuncDecl)
		if ok && function.Name.Name == name {
			return function
		}
	}
	return nil
}

func typeDeclaration(file *ast.File, name string) *ast.TypeSpec {
	if file == nil {
		return nil
	}
	for _, declaration := range file.Decls {
		general, ok := declaration.(*ast.GenDecl)
		if !ok || general.Tok != token.TYPE {
			continue
		}
		for _, candidate := range general.Specs {
			spec, ok := candidate.(*ast.TypeSpec)
			if ok && spec.Name.Name == name {
				return spec
			}
		}
	}
	return nil
}

func structHasBuiltinInvokerMap(expression ast.Expr) bool {
	structure, ok := expression.(*ast.StructType)
	if !ok {
		return false
	}
	for _, field := range structure.Fields.List {
		if len(field.Names) != 1 || field.Names[0].Name != "builtin" {
			continue
		}
		mapping, ok := field.Type.(*ast.MapType)
		if !ok {
			return false
		}
		key, keyOK := mapping.Key.(*ast.Ident)
		value, valueOK := mapping.Value.(*ast.Ident)
		return keyOK && key.Name == "string" &&
			valueOK && value.Name == "builtinInvoker"
	}
	return false
}

func structContainsHandler(expression ast.Expr) bool {
	structure, ok := expression.(*ast.StructType)
	if !ok {
		return true
	}
	for _, field := range structure.Fields.List {
		if isHandlerType(field.Type) {
			return true
		}
	}
	return false
}

func requireStringLiteral(t *testing.T, expression ast.Expr) string {
	t.Helper()
	literal, ok := expression.(*ast.BasicLit)
	require.True(t, ok, "route domain and action must be string literals")
	require.Equal(t, token.STRING, literal.Kind)
	value, err := strconv.Unquote(literal.Value)
	require.NoError(t, err)
	return value
}

func parseExecProductionFiles(t *testing.T) map[string]*ast.File {
	t.Helper()
	paths, err := filepath.Glob("*.go")
	require.NoError(t, err)
	files := make(map[string]*ast.File)
	fset := token.NewFileSet()
	for _, path := range paths {
		if strings.HasSuffix(path, "_test.go") {
			continue
		}
		file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
		require.NoError(t, err)
		files[path] = file
	}
	return files
}

func isHandlerType(expression ast.Expr) bool {
	switch expression := expression.(type) {
	case *ast.FuncType, *ast.InterfaceType:
		return true
	case *ast.Ident:
		return strings.HasSuffix(expression.Name, "Handler") || strings.HasSuffix(expression.Name, "HandlerFunc")
	case *ast.SelectorExpr:
		return strings.HasSuffix(expression.Sel.Name, "Handler") ||
			strings.HasSuffix(expression.Sel.Name, "HandlerFunc")
	default:
		return false
	}
}

func isCentralHandlerMap(expression ast.Expr) bool {
	mapType, ok := expression.(*ast.MapType)
	if !ok {
		return false
	}
	key, ok := mapType.Key.(*ast.Ident)
	return ok && key.Name == "string" && isHandlerType(mapType.Value)
}

func expressionsContainCentralHandlerMap(expressions []ast.Expr) bool {
	for _, expression := range expressions {
		found := false
		ast.Inspect(expression, func(node ast.Node) bool {
			if candidate, ok := node.(ast.Expr); ok && isCentralHandlerMap(candidate) {
				found = true
				return false
			}
			return !found
		})
		if found {
			return true
		}
	}
	return false
}

func isJSONRawMessage(expression ast.Expr) bool {
	selector, ok := expression.(*ast.SelectorExpr)
	if !ok || selector.Sel.Name != "RawMessage" {
		return false
	}
	pkg, ok := selector.X.(*ast.Ident)
	return ok && pkg.Name == "json"
}

func fieldListContainsJSONRawMessage(fields *ast.FieldList) bool {
	if fields == nil {
		return false
	}
	for _, field := range fields.List {
		if isJSONRawMessage(field.Type) {
			return true
		}
	}
	return false
}

func isHTTPRouteMethod(name string) bool {
	switch name {
	case "Any", "DELETE", "GET", "Handle", "PATCH", "POST", "PUT":
		return true
	default:
		return false
	}
}
