package testkit

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

var serializedRollbackGlobalSQL = regexp.MustCompile(`(?i)\b(?:create|alter|drop|truncate)\s+(?:database|table|index|view)\b|\bset\s+(?:@@)?global\b`)

// AssertSerializedRollbackTestBoundary keeps a test package limited to effects
// that SerializedRollbackDB can isolate. The package-wide scan covers helper
// functions as well as tests. It is intentionally a small syntax check;
// integration tests still own the behavioral guarantee.
func AssertSerializedRollbackTestBoundary(t *testing.T) {
	t.Helper()
	violations, err := serializedRollbackBoundaryViolations(".")
	if err != nil {
		t.Fatal(err)
	}
	for _, violation := range violations {
		t.Error(violation)
	}
}

func serializedRollbackBoundaryViolations(dir string) ([]string, error) {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, err
	}
	var violations []string
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), "_test.go") {
			continue
		}

		fset := token.NewFileSet()
		path := filepath.Join(dir, entry.Name())
		file, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution)
		if err != nil {
			return nil, fmt.Errorf("parse %s: %w", path, err)
		}

		failpointAliases := make(map[string]struct{})
		for _, spec := range file.Imports {
			importPath, err := strconv.Unquote(spec.Path.Value)
			if err != nil {
				return nil, fmt.Errorf("parse import in %s: %w", path, err)
			}
			if !strings.Contains(importPath, "failpoint") {
				continue
			}
			alias := filepath.Base(importPath)
			if spec.Name != nil {
				alias = spec.Name.Name
			}
			failpointAliases[alias] = struct{}{}
		}

		ast.Inspect(file, func(node ast.Node) bool {
			switch value := node.(type) {
			case *ast.CallExpr:
				selector, ok := value.Fun.(*ast.SelectorExpr)
				if !ok {
					return true
				}
				switch selector.Sel.Name {
				case "Parallel":
					violations = append(violations, fmt.Sprintf("%s: t.Parallel is incompatible with the serialized rollback fixture", fset.Position(value.Pos())))
				case "Setenv", "Unsetenv":
					violations = append(violations, fmt.Sprintf("%s: environment mutation is not rollback-isolated", fset.Position(value.Pos())))
				}
				if ident, ok := selector.X.(*ast.Ident); ok {
					if _, isFailpoint := failpointAliases[ident.Name]; isFailpoint {
						violations = append(violations, fmt.Sprintf("%s: failpoint state is not rollback-isolated", fset.Position(value.Pos())))
					}
				}
			case *ast.BasicLit:
				if value.Kind != token.STRING {
					return true
				}
				literal, err := strconv.Unquote(value.Value)
				if err == nil && serializedRollbackGlobalSQL.MatchString(literal) {
					violations = append(violations, fmt.Sprintf("%s: DDL or global SQL is not rollback-isolated", fset.Position(value.Pos())))
				}
			}
			return true
		})
	}
	return violations, nil
}
