package boundary_test

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

func TestL0StreamingDoesNotOwnSalesforceTopicTaxonomy(t *testing.T) {
	root := repoRoot(t)
	streamingDir := filepath.Join(root, "backend/internal/features/streaming")
	for _, file := range productionGoFiles(t, streamingDir) {
		rel := toRepoRelative(t, root, file)
		value, pos, ok := streamingTopicLiteral(t, file)
		if ok {
			t.Fatalf("%s contains Salesforce /topic channel literal %q at %s; product topic grammar must be registered through the streaming seam", rel, value, pos)
		}
	}
}

func TestProductionTopicLiteralsStayInProductRegistration(t *testing.T) {
	root := repoRoot(t)
	allowed := map[string]struct{}{
		"backend/distributions/crm/streaming_topics.go": {},
	}
	for _, dir := range []string{
		"backend/internal",
		"backend/pkg",
		"backend/distributions",
		"shared",
	} {
		for _, file := range productionGoFilesRecursive(t, filepath.Join(root, dir)) {
			rel := toRepoRelative(t, root, file)
			value, pos, ok := streamingTopicLiteral(t, file)
			if !ok {
				continue
			}
			if _, ok := allowed[rel]; !ok {
				t.Fatalf("%s contains Salesforce /topic literal %q at %s; topic taxonomy must be supplied through product registration", rel, value, pos)
			}
		}
	}
}

func TestStreamingTopicLiteralMatcherCoversGoStringForms(t *testing.T) {
	dir := t.TempDir()

	rawFile := filepath.Join(dir, "raw.go")
	if err := os.WriteFile(rawFile, []byte("package sample\n\nconst topic = `/topic/records/created`\n"), 0o600); err != nil {
		t.Fatalf("write raw fixture: %v", err)
	}
	if value, _, ok := streamingTopicLiteral(t, rawFile); !ok || value != "/topic/records/created" {
		t.Fatalf("raw string topic literal not detected, got value %q ok %v", value, ok)
	}

	interpretedFile := filepath.Join(dir, "interpreted.go")
	if err := os.WriteFile(interpretedFile, []byte("package sample\n\nconst topic = \"/topic/records/created\"\n"), 0o600); err != nil {
		t.Fatalf("write interpreted fixture: %v", err)
	}
	if value, _, ok := streamingTopicLiteral(t, interpretedFile); !ok || value != "/topic/records/created" {
		t.Fatalf("interpreted string topic literal not detected, got value %q ok %v", value, ok)
	}

	splitFile := filepath.Join(dir, "split.go")
	if err := os.WriteFile(splitFile, []byte("package sample\n\nconst prefix = \"/topic\"\nconst topic = prefix + \"/records/created\"\n"), 0o600); err != nil {
		t.Fatalf("write split fixture: %v", err)
	}
	if value, _, ok := streamingTopicLiteral(t, splitFile); !ok || value != "/topic/records/created" {
		t.Fatalf("split string topic literal not detected, got value %q ok %v", value, ok)
	}

	crossFilePrefix := filepath.Join(dir, "cross_file_prefix.go")
	if err := os.WriteFile(crossFilePrefix, []byte("package sample\n\nconst topicBoundaryProbePrefix = \"/topic\"\n"), 0o600); err != nil {
		t.Fatalf("write cross-file prefix fixture: %v", err)
	}
	crossFileTopic := filepath.Join(dir, "cross_file_topic.go")
	if err := os.WriteFile(crossFileTopic, []byte("package sample\n\nconst topicBoundaryProbeCrossFile = topicBoundaryProbePrefix + \"/records/created\"\n"), 0o600); err != nil {
		t.Fatalf("write cross-file topic fixture: %v", err)
	}
	if value, _, ok := streamingTopicLiteral(t, crossFileTopic); !ok || value != "/topic/records/created" {
		t.Fatalf("cross-file string topic literal not detected, got value %q ok %v", value, ok)
	}
}

func TestProductionGoFilesRecursiveIncludesGeneratedProductionGo(t *testing.T) {
	dir := t.TempDir()
	generated := filepath.Join(dir, "z_generated.go")
	if err := os.WriteFile(generated, []byte("// Code generated by test. DO NOT EDIT.\npackage sample\n"), 0o600); err != nil {
		t.Fatalf("write generated fixture: %v", err)
	}
	if err := os.WriteFile(filepath.Join(dir, "sample_test.go"), []byte("package sample\n"), 0o600); err != nil {
		t.Fatalf("write test fixture: %v", err)
	}

	files := productionGoFilesRecursive(t, dir)
	if len(files) != 1 || files[0] != generated {
		t.Fatalf("productionGoFilesRecursive() = %#v, want only generated production Go file %s", files, generated)
	}
}

func streamingTopicLiteral(t *testing.T, path string) (string, token.Position, bool) {
	t.Helper()
	fset := token.NewFileSet()
	files := parsePackageFiles(t, fset, path)
	file := files[path]
	constStrings := collectPackageStringConstants(t, path, files)
	var foundValue string
	var foundPos token.Position
	ast.Inspect(file, func(node ast.Node) bool {
		expr, ok := node.(ast.Expr)
		if !ok {
			return true
		}
		value, ok := stringConstantValue(t, path, expr, constStrings)
		if ok && strings.Contains(value, "/topic/") {
			foundValue = value
			foundPos = fset.Position(expr.Pos())
			return false
		}
		return true
	})
	return foundValue, foundPos, foundValue != ""
}

func parsePackageFiles(t *testing.T, fset *token.FileSet, path string) map[string]*ast.File {
	t.Helper()
	files := map[string]*ast.File{}
	for _, filePath := range productionGoFiles(t, filepath.Dir(path)) {
		file, err := parser.ParseFile(fset, filePath, nil, 0)
		if err != nil {
			t.Fatalf("parse %s: %v", filePath, err)
		}
		files[filePath] = file
	}
	if _, ok := files[path]; !ok {
		t.Fatalf("parsed package for %s did not include target file", path)
	}
	return files
}

func collectPackageStringConstants(t *testing.T, path string, files map[string]*ast.File) map[string]string {
	t.Helper()
	constants := map[string]string{}
	for {
		changed := false
		for _, file := range files {
			for _, decl := range file.Decls {
				gen, ok := decl.(*ast.GenDecl)
				if !ok || gen.Tok != token.CONST {
					continue
				}
				for _, spec := range gen.Specs {
					valueSpec, ok := spec.(*ast.ValueSpec)
					if !ok {
						continue
					}
					for i, name := range valueSpec.Names {
						if i >= len(valueSpec.Values) {
							continue
						}
						if _, exists := constants[name.Name]; exists {
							continue
						}
						value, ok := stringConstantValue(t, path, valueSpec.Values[i], constants)
						if ok {
							constants[name.Name] = value
							changed = true
						}
					}
				}
			}
		}
		if !changed {
			return constants
		}
	}
}

func stringConstantValue(t *testing.T, path string, expr ast.Expr, constants map[string]string) (string, bool) {
	t.Helper()
	switch expr := expr.(type) {
	case *ast.BasicLit:
		if expr.Kind != token.STRING {
			return "", false
		}
		value, err := strconv.Unquote(expr.Value)
		if err != nil {
			t.Fatalf("unquote %s literal %s: %v", path, expr.Value, err)
		}
		return value, true
	case *ast.BinaryExpr:
		if expr.Op != token.ADD {
			return "", false
		}
		left, ok := stringConstantValue(t, path, expr.X, constants)
		if !ok {
			return "", false
		}
		right, ok := stringConstantValue(t, path, expr.Y, constants)
		if !ok {
			return "", false
		}
		return left + right, true
	case *ast.Ident:
		value, ok := constants[expr.Name]
		return value, ok
	case *ast.ParenExpr:
		return stringConstantValue(t, path, expr.X, constants)
	}
	return "", false
}

func productionGoFilesRecursive(t *testing.T, dir string) []string {
	t.Helper()
	var files []string
	err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if entry.IsDir() {
			return nil
		}
		name := entry.Name()
		if strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") {
			files = append(files, path)
		}
		return nil
	})
	if err != nil {
		t.Fatalf("walk %s: %v", dir, err)
	}
	return files
}
