package exec

import (
	"encoding/json"
	"go/ast"
	"go/importer"
	"go/parser"
	"go/token"
	"go/types"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"testing"

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

func TestRemoteClientMethodsHaveTypedBoundaries(t *testing.T) {
	fset := token.NewFileSet()
	files := make([]*ast.File, 0)
	entries, err := filepath.Glob("*.go")
	require.NoError(t, err)
	for _, path := range entries {
		if strings.HasSuffix(path, "_test.go") {
			continue
		}
		if strings.HasSuffix(path, "_windows.go") || strings.HasSuffix(path, "_unsupported.go") {
			continue
		}
		file, parseErr := parser.ParseFile(fset, path, nil, 0)
		require.NoError(t, parseErr)
		files = append(files, file)
	}
	config := types.Config{Importer: boundaryImporter(t)}
	pkg, err := config.Check("github.com/tidbcloud/AgentTeam/desktop/internal/api/exec", fset, files, nil)
	require.NoError(t, err)

	remoteNames := map[string]struct{}{
		"agentRemote": {}, "approvalRemote": {}, "profileRemote": {}, "projectRemote": {}, "reviewRemote": {},
		"scheduledMessageRemote": {}, "taskRemote": {},
		"workflowRemote": {}, "workspaceRemote": {},
	}
	var problems []string
	for _, name := range pkg.Scope().Names() {
		object, ok := pkg.Scope().Lookup(name).(*types.TypeName)
		if !ok {
			continue
		}
		named, ok := object.Type().(*types.Named)
		if !ok {
			continue
		}
		if _, ok := remoteNames[name]; !ok {
			continue
		}
		for index := 0; index < named.NumMethods(); index++ {
			method := named.Method(index)
			// Transport binders are assembly helpers, not public wire
			// boundaries; their concrete client-bearing return is intentionally
			// inspected by route capability validation instead.
			if strings.HasSuffix(method.Name(), "Transport") {
				continue
			}
			signature := method.Type().(*types.Signature)
			checkRemoteTuple(&problems, name+"."+method.Name()+" params", signature.Params())
			checkRemoteTuple(&problems, name+"."+method.Name()+" results", signature.Results())
		}
	}
	sort.Strings(problems)
	require.Empty(t, problems, strings.Join(problems, "\n"))
}

func boundaryImporter(t *testing.T) types.Importer {
	t.Helper()
	command := exec.Command("go", "list", "-deps", "-export", "-json", ".")
	output, err := command.Output()
	require.NoError(t, err)
	exports := make(map[string]string)
	decoder := json.NewDecoder(strings.NewReader(string(output)))
	for decoder.More() {
		var item struct {
			ImportPath string
			Export     string
		}
		require.NoError(t, decoder.Decode(&item))
		if item.ImportPath != "" && item.Export != "" {
			exports[item.ImportPath] = item.Export
		}
	}
	return importer.ForCompiler(token.NewFileSet(), "gc", func(path string) (io.ReadCloser, error) {
		file, err := os.Open(exports[path])
		return file, err
	})
}

func checkRemoteTuple(problems *[]string, owner string, tuple *types.Tuple) {
	for index := 0; index < tuple.Len(); index++ {
		if dynamicRemoteBoundary(tuple.At(index).Type(), map[types.Type]bool{}) {
			*problems = append(*problems, owner+": "+types.TypeString(tuple.At(index).Type(), nil))
		}
	}
}

func dynamicRemoteBoundary(value types.Type, seen map[types.Type]bool) bool {
	if seen[value] {
		return false
	}
	seen[value] = true
	switch value := value.(type) {
	case *types.Basic:
		return value.Kind() == types.UntypedNil
	case *types.Interface:
		return value.Empty()
	case *types.Map:
		return true
	case *types.Pointer:
		return dynamicRemoteBoundary(value.Elem(), seen)
	case *types.Slice:
		return dynamicRemoteBoundary(value.Elem(), seen)
	case *types.Array:
		return dynamicRemoteBoundary(value.Elem(), seen)
	case *types.Named:
		if value.Obj().Pkg() != nil && value.Obj().Pkg().Path() == "encoding/json" && value.Obj().Name() == "RawMessage" {
			return true
		}
		return dynamicRemoteBoundary(value.Underlying(), seen)
	case *types.Struct:
		for index := 0; index < value.NumFields(); index++ {
			field := value.Field(index)
			if field.Pkg() != nil && field.Pkg().Path() == "github.com/tidbcloud/AgentTeam/desktop/internal/api/exec" && field.Name() == "Output" {
				// Workflow provider output is an intentionally model-visible raw leaf.
				continue
			}
			if dynamicRemoteBoundary(field.Type(), seen) {
				return true
			}
		}
	}
	return false
}

func TestWireAPIClientOwnsNoExecDomainFiles(t *testing.T) {
	for _, path := range []string{
		"../../wire/apiclient/approvals.go",
		"../../wire/apiclient/approvals_test.go",
	} {
		_, err := os.Stat(path)
		require.ErrorIs(t, err, os.ErrNotExist, path)
	}
}
