package packs

import (
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"testing"
	"unicode"

	kernelseedrows "github.com/nexuscrm/kernel/pkg/seedrows"
)

var tsImportPathPattern = regexp.MustCompile(`(?m)\bimport(?:\s+type)?(?:[\s\S]*?\sfrom\s*)?['"]([^'"]+)['"]`)

func TestFrontendGenericZonesDoNotImportOrNameAppPackActionRenderers(t *testing.T) {
	requireNexusCRMRepoRootForGuardrail(t)
	allowedAppPackImports := map[string]map[string]struct{}{
		"frontend/src/index.tsx": {
			"./app-packs/registerBundledAppPacks": {},
		},
	}
	forbiddenFragments := appPackActionRendererFactsForFrontendBoundary(t)

	var violations []string
	for _, path := range frontendGenericProductionSourceFiles(t) {
		rel := relRepoPath(t, path)
		rawBytes, err := os.ReadFile(path)
		if err != nil {
			t.Fatalf("read %s: %v", rel, err)
		}
		raw := string(rawBytes)
		for _, importPath := range tsImportPaths(raw) {
			if !strings.Contains(importPath, "app-packs") {
				continue
			}
			if _, ok := allowedAppPackImports[rel][importPath]; ok {
				continue
			}
			violations = append(violations, rel+" imports app-pack module "+importPath)
		}
		for _, fragment := range forbiddenFragments {
			if strings.Contains(raw, fragment) {
				violations = append(violations, rel+" contains CRM action renderer fragment "+strconvQuote(fragment))
			}
		}
	}

	if len(violations) > 0 {
		sort.Strings(violations)
		t.Fatalf("frontend generic zones contain CRM action renderer facts:\n  - %s", strings.Join(violations, "\n  - "))
	}
}

func TestFrontendGenericZonesDoNotRenderAppPackObjectLiterals(t *testing.T) {
	requireNexusCRMRepoRootForGuardrail(t)
	appObjectNames := appPackObjectNamesForL0GeneratedGuardrail(t)
	for _, ambiguous := range []string{"Case", "Event", "Lead", "Order", "Quote", "Task"} {
		delete(appObjectNames, ambiguous)
	}
	var violations []string
	for _, path := range frontendGenericProductionSourceFiles(t) {
		rel := relRepoPath(t, path)
		if isFrontendTransitionalAppSurface(rel) {
			continue
		}
		rawBytes, err := os.ReadFile(path)
		if err != nil {
			t.Fatalf("read %s: %v", rel, err)
		}
		for _, fragment := range frontendUserVisibleFragments(string(rawBytes)) {
			for appObject := range appObjectNames {
				if containsIdentifier(fragment, appObject) {
					violations = append(violations, rel+" contains app-pack object literal "+strconvQuote(appObject)+" in user-visible text: "+strconvQuote(strings.TrimSpace(fragment)))
				}
			}
		}
	}

	if len(violations) > 0 {
		sort.Strings(violations)
		t.Fatalf("frontend generic zones contain app-pack object literals:\n  - %s", strings.Join(violations, "\n  - "))
	}
}

func TestFrontendAppPackObjectLiteralMatcherCoversCompositeExamples(t *testing.T) {
	positive := map[string]string{
		"e.g. High_Value_Opportunity": "Opportunity",
		"e.g. Account_Automation":     "Account",
		"e.g. Get Accounts":           "Account",
		"e.g. Get_Accounts":           "Account",
		"Open Opportunities":          "Opportunity",
	}
	for text, ident := range positive {
		if !containsIdentifier(text, ident) {
			t.Fatalf("expected %q to contain app object %q", text, ident)
		}
	}

	negative := map[string]string{
		"Accountable owner review": "Account",
		"OpportunityCost review":   "Opportunity",
	}
	for text, ident := range negative {
		if containsIdentifier(text, ident) {
			t.Fatalf("did not expect %q to contain app object %q", text, ident)
		}
	}
}

func frontendGenericProductionSourceFiles(t *testing.T) []string {
	t.Helper()
	root := filepath.Join(requireNexusCRMRepoRootForGuardrail(t), "frontend", "src")
	var files []string
	err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if entry.IsDir() {
			switch entry.Name() {
			case "app-packs", "__tests__", "__fixtures__":
				return filepath.SkipDir
			}
			return nil
		}
		if !isProductionTypeScriptSource(path) {
			return nil
		}
		if filepath.Base(path) == "generated-schema.ts" {
			return nil
		}
		files = append(files, path)
		return nil
	})
	if err != nil {
		t.Fatalf("walk frontend sources: %v", err)
	}
	sort.Strings(files)
	return files
}

func isProductionTypeScriptSource(path string) bool {
	if strings.HasSuffix(path, ".test.ts") ||
		strings.HasSuffix(path, ".test.tsx") ||
		strings.HasSuffix(path, ".spec.ts") ||
		strings.HasSuffix(path, ".spec.tsx") ||
		strings.HasSuffix(path, ".d.ts") {
		return false
	}
	return strings.HasSuffix(path, ".ts") || strings.HasSuffix(path, ".tsx")
}

func isFrontendTransitionalAppSurface(rel string) bool {
	return strings.HasPrefix(rel, "frontend/src/features/activity/") ||
		strings.HasPrefix(rel, "frontend/src/features/approvals/components/EmailAlertRecipientRow.tsx") ||
		strings.HasPrefix(rel, "frontend/src/features/approvals/components/ProcessActionsSection.tsx") ||
		strings.HasPrefix(rel, "frontend/src/features/approvals/approvalTypes.ts") ||
		rel == "frontend/src/infrastructure/api/schemas.ts"
}

func tsImportPaths(raw string) []string {
	matches := tsImportPathPattern.FindAllStringSubmatch(raw, -1)
	out := make([]string, 0, len(matches))
	for _, match := range matches {
		if len(match) == 2 {
			out = append(out, match[1])
		}
	}
	return out
}

func frontendUserVisibleFragments(raw string) []string {
	var fragments []string
	for i := 0; i < len(raw); {
		switch raw[i] {
		case '/':
			if i+1 < len(raw) && raw[i+1] == '/' {
				i += 2
				for i < len(raw) && raw[i] != '\n' {
					i++
				}
				continue
			}
			if i+1 < len(raw) && raw[i+1] == '*' {
				i += 2
				for i+1 < len(raw) && !(raw[i] == '*' && raw[i+1] == '/') {
					i++
				}
				if i+1 < len(raw) {
					i += 2
				}
				continue
			}
		case '\'', '"', '`':
			quote := raw[i]
			start := i + 1
			i = start
			var builder strings.Builder
			for i < len(raw) {
				if raw[i] == '\\' && i+1 < len(raw) {
					builder.WriteByte(raw[i+1])
					i += 2
					continue
				}
				if raw[i] == quote {
					break
				}
				builder.WriteByte(raw[i])
				i++
			}
			if text := strings.TrimSpace(builder.String()); text != "" {
				fragments = append(fragments, text)
			}
			if i < len(raw) {
				i++
			}
			continue
		case '>':
			start := i + 1
			end := strings.IndexByte(raw[start:], '<')
			if end >= 0 {
				text := strings.TrimSpace(raw[start : start+end])
				if text != "" && !strings.ContainsAny(text, "{}") {
					fragments = append(fragments, text)
				}
				i = start + end
				continue
			}
		}
		i++
	}
	return fragments
}

func appPackActionRendererFactsForFrontendBoundary(t *testing.T) []string {
	t.Helper()
	root := officialPackRootFromThisFile(t)
	seen := map[string]struct{}{}
	add := func(value string) {
		value = strings.TrimSpace(value)
		if value == "" {
			return
		}
		seen[value] = struct{}{}
	}

	err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if entry.IsDir() || filepath.Base(path) != "invocable_standard_actions.json" {
			return nil
		}
		raw, readErr := os.ReadFile(path)
		if readErr != nil {
			return readErr
		}
		defs, parseErr := kernelseedrows.ParseStandardActionDefs(raw)
		if parseErr != nil {
			return parseErr
		}
		for _, def := range defs {
			if strings.TrimSpace(def.RendererID) == "" {
				continue
			}
			add(def.RendererID)
			add(def.Name)
			add("dataAPI." + def.Name)
			exportedName := exportedNameFromActionName(def.Name)
			add(exportedName + "Modal")
			add(exportedName + "Options")
			add(exportedName + "Result")
			add(exportedName + "ActionResult")
		}
		return nil
	})
	if err != nil {
		t.Fatalf("collect app-pack action renderer facts: %v", err)
	}
	if len(seen) == 0 {
		t.Fatal("no app-pack action renderer facts found for frontend boundary guardrail")
	}
	out := make([]string, 0, len(seen))
	for value := range seen {
		out = append(out, value)
	}
	sort.Strings(out)
	return out
}

func exportedNameFromActionName(actionName string) string {
	var out strings.Builder
	upperNext := true
	for _, r := range actionName {
		if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
			upperNext = true
			continue
		}
		if out.Len() == 0 || upperNext {
			out.WriteRune(unicode.ToUpper(r))
		} else {
			out.WriteRune(r)
		}
		upperNext = false
	}
	return out.String()
}

func strconvQuote(value string) string {
	return `"` + strings.ReplaceAll(value, `"`, `\"`) + `"`
}
