package message

import (
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"testing"

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

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

	repoRoot := messageRepoRootForTest(t)
	for _, match := range grepRepoFilesForTest(t, repoRoot, regexp.MustCompile(`(?is)INSERT\s+INTO\s+scope_messages\b`), ".go", ".sql") {
		rel := filepath.ToSlash(match.rel)
		if strings.HasSuffix(rel, "_test.go") {
			continue
		}
		if rel == "internal/store/message/scope_store.go" {
			continue
		}
		if strings.HasPrefix(rel, "internal/tidb/") {
			continue
		}
		t.Fatalf("scope_messages insert must stay behind the message store primitive and ApplyMessagePlanTx boundary; unexpected direct insert in %s", rel)
	}

	for _, match := range grepRepoFilesForTest(t, repoRoot, regexp.MustCompile(`(?m)\.Messages\.Insert\s*\(`), ".go") {
		rel := filepath.ToSlash(match.rel)
		if strings.HasSuffix(rel, "_test.go") {
			continue
		}
		t.Fatalf("production scope message writes must use MessagePlan + ApplyMessagePlanTx, not direct repository insertion in %s", rel)
	}
}

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

	repoRoot := messageRepoRootForTest(t)
	productionPlannerConstructor := regexp.MustCompile(`\bNewMessagePlanner(?:WithClock)?\s*\(`)
	for _, match := range grepRepoFilesForTest(t, repoRoot, productionPlannerConstructor, ".go") {
		rel := filepath.ToSlash(match.rel)
		if strings.HasSuffix(rel, "_test.go") || rel == "internal/store/message/scope_planner.go" {
			continue
		}
		t.Fatalf("MessagePlanner is currently a plan compiler/test surface, not the production write owner; unexpected production constructor in %s", rel)
	}
}

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

	repoRoot := messageRepoRootForTest(t)
	var missing []string
	require.NoError(t, filepath.WalkDir(repoRoot, func(path string, entry os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		name := entry.Name()
		if entry.IsDir() {
			if messageInvariantShouldSkipRepoDir(name) {
				return filepath.SkipDir
			}
			return nil
		}
		if filepath.Ext(name) != ".go" || strings.HasSuffix(name, "_test.go") {
			return nil
		}

		fset := token.NewFileSet()
		parsed, err := parser.ParseFile(fset, path, nil, 0)
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(repoRoot, path)
		if err != nil {
			return err
		}
		rel = filepath.ToSlash(rel)
		if messageInvariantShouldSkipRepoPath(rel) {
			return nil
		}
		ast.Inspect(parsed, func(node ast.Node) bool {
			literal, ok := node.(*ast.CompositeLit)
			if !ok || messagePlanLiteralType(literal.Type) == "" {
				return true
			}
			if messagePlanLiteralHasKey(literal, "SideEffectProfile") {
				return true
			}
			position := fset.Position(literal.Pos())
			missing = append(missing, rel+":"+strconv.Itoa(position.Line))
			return true
		})
		return nil
	}))
	require.Empty(t, missing, "production MessagePlan literals must explicitly declare SideEffectProfile")
}

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

	require.True(t, messageInvariantShouldSkipRepoDir(".claude"))
	require.True(t, messageInvariantShouldSkipRepoPath(".claude/worktrees/other/internal/store/message/scope_store.go"))
	require.True(t, messageInvariantShouldSkipRepoDir(".data"))
	require.True(t, messageInvariantShouldSkipRepoPath("meshx/.data/meshx/kb-code-wiki/repo-snapshots/other/internal/store/message/scope_store.go"))
	require.True(t, messageInvariantShouldSkipRepoDir(".worktrees"))
	require.True(t, messageInvariantShouldSkipRepoPath(".worktrees/other/internal/store/message/scope_store.go"))
	require.False(t, messageInvariantShouldSkipRepoPath("internal/store/message/scope_store.go"))
}

func messagePlanLiteralType(expr ast.Expr) string {
	switch typed := expr.(type) {
	case *ast.Ident:
		if typed.Name == "MessagePlan" {
			return typed.Name
		}
	case *ast.SelectorExpr:
		if typed.Sel.Name == "MessagePlan" {
			return typed.Sel.Name
		}
	}
	return ""
}

func messagePlanLiteralHasKey(literal *ast.CompositeLit, key string) bool {
	for _, element := range literal.Elts {
		keyed, ok := element.(*ast.KeyValueExpr)
		if !ok {
			continue
		}
		ident, ok := keyed.Key.(*ast.Ident)
		if ok && ident.Name == key {
			return true
		}
	}
	return false
}

type repoGrepMatch struct {
	rel string
}

func messageRepoRootForTest(t *testing.T) string {
	t.Helper()
	_, file, _, ok := runtime.Caller(0)
	require.True(t, ok)
	return filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", ".."))
}

func grepRepoFilesForTest(t *testing.T, repoRoot string, pattern *regexp.Regexp, extensions ...string) []repoGrepMatch {
	t.Helper()
	extensionSet := make(map[string]struct{}, len(extensions))
	for _, extension := range extensions {
		extensionSet[extension] = struct{}{}
	}

	var matches []repoGrepMatch
	require.NoError(t, filepath.WalkDir(repoRoot, func(path string, entry os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		name := entry.Name()
		if entry.IsDir() {
			if messageInvariantShouldSkipRepoDir(name) {
				return filepath.SkipDir
			}
			return nil
		}
		if _, ok := extensionSet[filepath.Ext(name)]; !ok {
			return nil
		}
		content, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		if !pattern.Match(content) {
			return nil
		}
		rel, err := filepath.Rel(repoRoot, path)
		if err != nil {
			return err
		}
		if messageInvariantShouldSkipRepoPath(filepath.ToSlash(rel)) {
			return nil
		}
		matches = append(matches, repoGrepMatch{rel: rel})
		return nil
	}))
	return matches
}

func messageInvariantShouldSkipRepoDir(name string) bool {
	switch name {
	case ".claude", ".data", ".git", ".worktrees", "node_modules", "dist", "coverage":
		return true
	default:
		return false
	}
}

func messageInvariantShouldSkipRepoPath(rel string) bool {
	return rel == ".claude" ||
		strings.HasPrefix(rel, ".claude/") ||
		rel == ".data" ||
		strings.HasPrefix(rel, ".data/") ||
		strings.Contains(rel, "/.data/") ||
		rel == ".worktrees" ||
		strings.HasPrefix(rel, ".worktrees/")
}
