package mcp

import (
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

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

func TestMCPDependencyDispatcherHasNoMCPInversion(t *testing.T) {
	dispatcherDir := filepath.Join("..", "..", "dispatcher")
	entries, err := os.ReadDir(dispatcherDir)
	require.NoError(t, err)
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
			continue
		}
		file, parseErr := parser.ParseFile(token.NewFileSet(), filepath.Join(dispatcherDir, entry.Name()), nil, parser.ImportsOnly)
		require.NoError(t, parseErr)
		for _, imported := range file.Imports {
			require.NotContains(t, imported.Path.Value, "/internal/api/mcp", entry.Name())
		}
	}
}

func TestMCPDependencyHasNoProcessGlobalKnowledgeRegistry(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)
		text := string(source)
		for _, retired := range []string{
			"knowledgeQASessionContexts",
			"RegisterKnowledgeQASessionContext",
			"UnregisterKnowledgeQASessionContext",
			"LoadKnowledgeQAAnswerSubmission",
			"ReportKnowledgeQASessionActivity",
		} {
			require.NotContains(t, text, retired, entry.Name())
		}
	}
}

func TestMCPDependencyKBKnowledgePolicyBoundariesAreTyped(t *testing.T) {
	for _, path := range []string{"kb_handler.go"} {
		source, err := os.ReadFile(path)
		require.NoError(t, err)
		require.NotContains(t, string(source), "map[string]any", path)
	}
	source, err := os.ReadFile("kb_input.go")
	require.NoError(t, err)
	file, err := parser.ParseFile(token.NewFileSet(), "kb_input.go", source, 0)
	require.NoError(t, err)
	ast.Inspect(file, func(node ast.Node) bool {
		field, ok := node.(*ast.Field)
		if !ok {
			return true
		}
		if len(field.Names) == 0 || field.Names[0].Name != "extra" {
			return true
		}
		mapType, ok := field.Type.(*ast.MapType)
		if !ok {
			return true
		}
		key, keyOK := mapType.Key.(*ast.Ident)
		selector, valueOK := mapType.Value.(*ast.SelectorExpr)
		require.True(t, keyOK && valueOK && key.Name == "string" && selector.Sel.Name == "RawMessage")
		require.Len(t, field.Names, 1)
		require.Equal(t, "extra", field.Names[0].Name)
		return true
	})
}

func TestMCPDependencyPortsAreExact(t *testing.T) {
	require.Equal(t, []string{
		"AcknowledgeRuntimeMessageToolDeliveriesForSession",
		"ConfirmReactionReply",
		"InboxMessagesForFreshnessPage",
		"InboxMessagesForFreshnessPageForRuntime",
		"MessageFreshnessSnapshot",
		"PendingRuntimeInputCoversFreshness",
		"PrepareRuntimeMessageToolDeliveries",
		"PrepareVisibleReply",
		"ReconcileDeferredDeliveriesForAgent",
		"ReconcileInboxMessages",
		"ReconcileRuntimeAckedDeliveries",
		"RememberMessageFreshnessHeld",
		"RememberMessageFreshnessObserved",
		"Session",
		"SetActiveTaskTarget",
	}, interfaceMethodNames(reflect.TypeOf((*MessageRuntime)(nil)).Elem()))
	require.Equal(t, []string{"CurrentTurnMessageID", "Session"}, interfaceMethodNames(reflect.TypeOf((*KBRouteContext)(nil)).Elem()))

	for _, value := range []any{MessageHandlerConfig{}, KBHandlerConfig{}} {
		typ := reflect.TypeOf(value)
		for index := 0; index < typ.NumField(); index++ {
			field := typ.Field(index)
			require.NotEqual(t, reflect.TypeOf((*any)(nil)).Elem(), field.Type, "%s.%s must be concrete", typ.Name(), field.Name)
		}
	}
}

func TestMCPDependencyConfigsHaveNoCapabilityAssertions(t *testing.T) {
	for _, path := range []string{"mcp_handler.go", "kb_handler.go", "message_send.go", "message_reaction.go"} {
		source, err := os.ReadFile(path)
		require.NoError(t, err)
		file, err := parser.ParseFile(token.NewFileSet(), path, source, 0)
		require.NoError(t, err)
		ast.Inspect(file, func(node ast.Node) bool {
			assertion, ok := node.(*ast.TypeAssertExpr)
			if !ok {
				return true
			}
			selector, ok := assertion.X.(*ast.SelectorExpr)
			if !ok {
				return true
			}
			identifier, ok := selector.X.(*ast.Ident)
			if ok && identifier.Name == "h" && (selector.Sel.Name == "runtime" || selector.Sel.Name == "config") {
				t.Errorf("%s contains capability assertion on handler dependency at %s", path, token.NewFileSet().Position(assertion.Pos()))
			}
			return true
		})
		require.NotContains(t, string(source), ".runtime.(", path)
	}
}

func TestMCPProductionFilesHaveSinglePackageComment(t *testing.T) {
	entries, err := os.ReadDir(".")
	require.NoError(t, err)
	var owners []string
	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)
		if strings.Contains(string(source), "// Package mcp ") {
			owners = append(owners, entry.Name())
		}
	}
	require.Equal(t, []string{"doc.go"}, owners)
}

func TestMCPProductionFilesStayWithinResponsibilityLimit(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 TestMCPRetiredResponsibilityFilesStayDeleted(t *testing.T) {
	for _, path := range []string{
		"protocol.go", "mcp_tools_chat.go", "mcp_tool_defs_chat.go",
		"mcp_tools_reactions.go", "attachments.go", "send_limiter.go",
		"execution_tools.go", "kb_agentteam_handler.go", "knowledge_mcp_handler.go",
		"execution_budget.go", "execution_call.go", "execution_diagnostics.go",
		"execution_format.go", "execution_handler.go", "execution_result.go",
		"execution_schema.go",
		"runtime_attachments.go",
	} {
		_, err := os.Stat(path)
		require.ErrorIs(t, err, os.ErrNotExist, path)
	}
}

func interfaceMethodNames(typ reflect.Type) []string {
	result := make([]string, 0, typ.NumMethod())
	for index := 0; index < typ.NumMethod(); index++ {
		result = append(result, typ.Method(index).Name)
	}
	return result
}

// sendScopeAllowedFiles is the exact file-level allowlist for consuming the
// send-scope port: the Process producer, the dispatcher bridge, the shared
// vocabulary package, the mcp port definition, and the message_send guard.
// Anything else — including new tools in allowed directories — fails.
var sendScopeAllowedFiles = map[string]bool{
	"agent/process_message_send_scope.go":       true,
	"agent/process_message_send_scope_test.go":  true,
	"dispatcher/dispatcher.go":                  true,
	"messagesend/scope.go":                      true,
	"messagesend/bearer.go":                     true,
	"api/mcp/mcp_handler.go":                    true,
	"api/mcp/message_send.go":                   true,
	"api/mcp/message_send_cross_target_test.go": true,
}

// sendScopeViolation is a byte slice that any AST-level selector check must
// flag; used to prove the guard fails on a violating source.
const sendScopeViolation = "package other\n\nfunc f(p Provider) { p.SendScope(nil, \"a\") }\n"

const sendScopeViolationAccessors = "package other\n\nfunc g(p Provider) bool { return p.ValidateSendScope(1) }\n"

// containsSendScopeCall reports whether the source calls or references the
// SendScope method (selector or method value), not just mentions it in a
// comment-free context.
// sendScopeAccessorNames are the method names exposing send-scope state.
var sendScopeAccessorNames = map[string]bool{
	"SendScope":               true,
	"SendScopeWithGeneration": true,
	"ValidateSendScope":       true,
}

func containsSendScopeCall(source string) bool {
	fileSet := token.NewFileSet()
	file, err := parser.ParseFile(fileSet, "fixture.go", source, 0)
	if err != nil {
		for _, name := range []string{".SendScope(", ".SendScopeWithGeneration(", ".ValidateSendScope("} {
			if strings.Contains(source, name) {
				return true
			}
		}
		return false
	}
	found := false
	ast.Inspect(file, func(node ast.Node) bool {
		if sel, ok := node.(*ast.SelectorExpr); ok && sendScopeAccessorNames[sel.Sel.Name] {
			found = true
		}
		return !found
	})
	return found
}

// TestSendScopeHasSingleConsumer walks the desktop tree and fails when any
// production file outside the exact allowlist consumes SendScope.
func TestSendScopeHasSingleConsumer(t *testing.T) {
	require.True(t, containsSendScopeCall(sendScopeViolation),
		"guard must detect a violating consumer (fixture self-check)")
	require.True(t, containsSendScopeCall(sendScopeViolationAccessors),
		"guard must detect scope accessor misuse (fixture self-check)")
	root, absErr := filepath.Abs("../..")
	require.NoError(t, absErr)
	err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
		if err != nil || entry.IsDir() {
			return err
		}
		if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
			return nil
		}
		relative, relErr := filepath.Rel(root, path)
		if relErr != nil {
			return relErr
		}
		relative = filepath.ToSlash(relative)
		if sendScopeAllowedFiles[relative] {
			return nil
		}
		data, readErr := os.ReadFile(path)
		if readErr != nil {
			return readErr
		}
		require.False(t, containsSendScopeCall(string(data)),
			"%s must not consume the send-scope port", relative)
		return nil
	})
	require.NoError(t, err)
}
