package persistence

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

var catalogObjectIdentityWriterInventory = []string{
	"nexuscrm-persistence/schema_metadata_persist.go#SchemaRepository.InsertObjectMetadata",
	"nexuscrm-persistence/schema_metadata_persist.go#SchemaRepository.SaveObjectMetadata",
	"nexuscrm-persistence/schema_metadata_persist.go#SchemaRepository.batchSaveObjectMetadata",
}

var catalogObjectIdentityInsertSiteInventory = []string{
	"nexuscrm-persistence/schema_metadata_persist.go#SchemaRepository.InsertObjectMetadata",
	"nexuscrm-persistence/schema_metadata_persist.go#SchemaRepository.SaveObjectMetadata",
	"nexuscrm-persistence/schema_metadata_persist.go#SchemaRepository.batchSaveObjectMetadata",
}

var catalogObjectIdentityInsertBuilderCallCount = map[string]int{
	"nexuscrm-persistence/schema_metadata_persist.go#SchemaRepository.InsertObjectMetadata":    1,
	"nexuscrm-persistence/schema_metadata_persist.go#SchemaRepository.SaveObjectMetadata":      2,
	"nexuscrm-persistence/schema_metadata_persist.go#SchemaRepository.batchSaveObjectMetadata": 2,
}

func TestCatalogObjectIdentityWriterInventoryIsClosedAndGuarded(t *testing.T) {
	root := catalogObjectIdentityRepositoryRoot(t)
	fileSet := token.NewFileSet()
	writerPositions := make(map[string][]token.Pos)
	functions := make(map[string]*ast.FuncDecl)

	err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			switch entry.Name() {
			case ".git", "node_modules", "vendor":
				return filepath.SkipDir
			}
			return nil
		}
		if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
			return nil
		}

		source, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		parsed, err := parser.ParseFile(fileSet, path, source, 0)
		if err != nil {
			return err
		}
		relativePath, err := filepath.Rel(root, path)
		if err != nil {
			return err
		}
		relativePath = filepath.ToSlash(relativePath)

		for _, declaration := range parsed.Decls {
			function, ok := declaration.(*ast.FuncDecl)
			if !ok || function.Body == nil {
				continue
			}
			writerID := catalogObjectIdentityFunctionID(relativePath, function)
			ast.Inspect(function.Body, func(node ast.Node) bool {
				call, ok := node.(*ast.CallExpr)
				if !ok || !isCatalogObjectInsertBuilderCall(call) {
					return true
				}
				writerPositions[writerID] = append(writerPositions[writerID], call.Pos())
				functions[writerID] = function
				return true
			})
		}
		return nil
	})
	if err != nil {
		t.Fatal(err)
	}

	got := make([]string, 0, len(writerPositions))
	for writerID := range writerPositions {
		got = append(got, writerID)
	}
	sort.Strings(got)
	if !reflect.DeepEqual(got, catalogObjectIdentityInsertSiteInventory) {
		t.Fatalf(
			"_System_Object insert/upsert writer inventory =\n%q\nwant =\n%q",
			got,
			catalogObjectIdentityInsertSiteInventory,
		)
	}

	for _, writerID := range catalogObjectIdentityWriterInventory {
		t.Run(writerID, func(t *testing.T) {
			positions := writerPositions[writerID]
			wantCalls, ok := catalogObjectIdentityInsertBuilderCallCount[writerID]
			if !ok {
				t.Fatalf("missing frozen insert/upsert builder count")
			}
			if len(positions) != wantCalls {
				t.Fatalf("insert/upsert builder calls = %d, want %d", len(positions), wantCalls)
			}
			firstInsertPosition := positions[0]
			guardPositions := map[string][]token.Pos{
				"prepareCatalogObjectIdentityCandidates": nil,
				"assertCatalogObjectIdentityMappings":    nil,
			}
			ast.Inspect(functions[writerID].Body, func(node ast.Node) bool {
				call, ok := node.(*ast.CallExpr)
				if !ok {
					return true
				}
				name := catalogObjectIdentityCallName(call.Fun)
				if _, guarded := guardPositions[name]; guarded {
					guardPositions[name] = append(guardPositions[name], call.Pos())
				}
				return true
			})
			for _, guardName := range []string{
				"prepareCatalogObjectIdentityCandidates",
				"assertCatalogObjectIdentityMappings",
			} {
				positions := guardPositions[guardName]
				if len(positions) != 1 {
					t.Errorf("%s calls = %d, want 1", guardName, len(positions))
					continue
				}
				if positions[0] >= firstInsertPosition {
					t.Errorf("%s is not called before the first _System_Object write", guardName)
				}
			}
			if prepared := guardPositions["prepareCatalogObjectIdentityCandidates"]; len(prepared) == 1 {
				if asserted := guardPositions["assertCatalogObjectIdentityMappings"]; len(asserted) == 1 &&
					prepared[0] >= asserted[0] {
					t.Error("identity candidates must be prepared before their mappings are asserted")
				}
			}
		})
	}
}

func TestCatalogObjectIdentityWritersDoNotUseRawSQLInserts(t *testing.T) {
	root := catalogObjectIdentityRepositoryRoot(t)
	err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			switch entry.Name() {
			case ".git", "node_modules", "vendor":
				return filepath.SkipDir
			}
			return nil
		}
		if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
			return nil
		}
		parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
		if err != nil {
			return err
		}
		ast.Inspect(parsed, func(node ast.Node) bool {
			literal, ok := node.(*ast.BasicLit)
			if !ok || literal.Kind != token.STRING {
				return true
			}
			value, err := strconv.Unquote(literal.Value)
			if err != nil {
				return true
			}
			normalized := strings.ToLower(strings.Join(strings.Fields(value), " "))
			if strings.Contains(normalized, "insert into _system_object") ||
				strings.Contains(normalized, "insert into `_system_object`") {
				t.Errorf("raw _System_Object insert in %s; use the guarded writer chokepoints", path)
			}
			return true
		})
		return nil
	})
	if err != nil {
		t.Fatal(err)
	}
}

func isCatalogObjectInsertBuilderCall(call *ast.CallExpr) bool {
	selector, ok := call.Fun.(*ast.SelectorExpr)
	if !ok || selector.Sel.Name != "Insert" {
		return false
	}
	pkg, ok := selector.X.(*ast.Ident)
	if !ok || pkg.Name != "query" || len(call.Args) == 0 {
		return false
	}
	table, ok := call.Args[0].(*ast.SelectorExpr)
	if !ok || table.Sel.Name != "TableObject" {
		return false
	}
	constantsPackage, ok := table.X.(*ast.Ident)
	return ok && constantsPackage.Name == "constants"
}

func catalogObjectIdentityFunctionID(path string, function *ast.FuncDecl) string {
	symbol := function.Name.Name
	if function.Recv != nil && len(function.Recv.List) == 1 {
		receiver := function.Recv.List[0].Type
		if pointer, ok := receiver.(*ast.StarExpr); ok {
			receiver = pointer.X
		}
		if identifier, ok := receiver.(*ast.Ident); ok {
			symbol = identifier.Name + "." + symbol
		}
	}
	return path + "#" + symbol
}

func catalogObjectIdentityCallName(expression ast.Expr) string {
	switch value := expression.(type) {
	case *ast.Ident:
		return value.Name
	case *ast.SelectorExpr:
		return value.Sel.Name
	default:
		return ""
	}
}

func catalogObjectIdentityRepositoryRoot(t *testing.T) string {
	t.Helper()
	_, current, _, ok := runtime.Caller(0)
	if !ok {
		t.Fatal("resolve catalog object identity repository root")
	}
	return filepath.Dir(filepath.Dir(current))
}
