package api

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

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

// TestNoNewRawGinContextHandlers prevents API handler methods from adding or
// resurrecting direct *gin.Context dependencies. The baseline is migration
// debt and must exactly match the current set: every migration removes its
// entry, so a previously migrated method cannot later return unnoticed.
func TestNoNewRawGinContextHandlers(t *testing.T) {
	t.Parallel()

	_, currentFile, _, ok := runtime.Caller(0)
	require.True(t, ok)
	apiRoot := filepath.Dir(currentFile)

	baseline := readRawGinHandlerBaseline(t, filepath.Join(apiRoot, "testdata", "raw_gin_handler_baseline.txt"))
	current := findRawGinContextHandlers(t, apiRoot)

	require.Equal(t, sortedRawGinHandlers(baseline), sortedRawGinHandlers(current),
		"raw Gin handler baseline must exactly match the current migration debt; new handlers must use typed requests, and migrated entries must be removed from the baseline")
}

func sortedRawGinHandlers(entries map[string]struct{}) []string {
	result := make([]string, 0, len(entries))
	for entry := range entries {
		result = append(result, entry)
	}
	sort.Strings(result)
	return result
}

func readRawGinHandlerBaseline(t *testing.T, path string) map[string]struct{} {
	t.Helper()
	file, err := os.Open(path)
	require.NoError(t, err)
	defer file.Close()

	entries := make(map[string]struct{})
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		entry := strings.TrimSpace(scanner.Text())
		if entry != "" && !strings.HasPrefix(entry, "#") {
			entries[entry] = struct{}{}
		}
	}
	require.NoError(t, scanner.Err())
	return entries
}

func findRawGinContextHandlers(t *testing.T, root string) map[string]struct{} {
	t.Helper()
	fset := token.NewFileSet()
	entries := make(map[string]struct{})

	err := filepath.Walk(root, func(path string, info os.FileInfo, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if info.IsDir() {
			if info.Name() == "testdata" {
				return filepath.SkipDir
			}
			return nil
		}
		if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
			return nil
		}

		file, err := parser.ParseFile(fset, path, nil, 0)
		if err != nil {
			return err
		}
		ginAliases := ginImportAliases(file)
		if len(ginAliases) == 0 {
			return nil
		}
		for _, declaration := range file.Decls {
			function, ok := declaration.(*ast.FuncDecl)
			if !ok || function.Recv == nil || len(function.Recv.List) == 0 {
				continue
			}
			receiver := receiverTypeName(function.Recv.List[0].Type)
			if !strings.Contains(receiver, "Handler") || !hasGinContextParameter(function.Type.Params, ginAliases) {
				continue
			}
			relative, err := filepath.Rel(root, path)
			if err != nil {
				return err
			}
			entry := filepath.ToSlash(relative) + ":" + receiver + "." + function.Name.Name
			entries[entry] = struct{}{}
		}
		return nil
	})
	require.NoError(t, err)
	return entries
}

func ginImportAliases(file *ast.File) map[string]struct{} {
	aliases := make(map[string]struct{})
	for _, spec := range file.Imports {
		path, err := strconv.Unquote(spec.Path.Value)
		if err != nil || path != "github.com/gin-gonic/gin" {
			continue
		}
		alias := "gin"
		if spec.Name != nil {
			alias = spec.Name.Name
		}
		if alias != "." && alias != "_" {
			aliases[alias] = struct{}{}
		}
	}
	return aliases
}

func receiverTypeName(expression ast.Expr) string {
	switch typed := expression.(type) {
	case *ast.Ident:
		return typed.Name
	case *ast.StarExpr:
		return receiverTypeName(typed.X)
	case *ast.IndexExpr:
		return receiverTypeName(typed.X)
	case *ast.IndexListExpr:
		return receiverTypeName(typed.X)
	default:
		return ""
	}
}

func hasGinContextParameter(fields *ast.FieldList, aliases map[string]struct{}) bool {
	if fields == nil {
		return false
	}
	for _, field := range fields.List {
		pointer, ok := field.Type.(*ast.StarExpr)
		if !ok {
			continue
		}
		selector, ok := pointer.X.(*ast.SelectorExpr)
		if !ok || selector.Sel.Name != "Context" {
			continue
		}
		identifier, ok := selector.X.(*ast.Ident)
		if !ok {
			continue
		}
		if _, found := aliases[identifier.Name]; found {
			return true
		}
	}
	return false
}
