package proc_test

import (
	"errors"
	"fmt"
	"go/constant"
	"go/parser"
	"go/token"
	"math"
	"path/filepath"
	"reflect"
	"runtime"
	"strconv"
	"strings"
	"testing"

	"github.com/go-delve/delve/pkg/goversion"
	"github.com/go-delve/delve/pkg/proc"
	protest "github.com/go-delve/delve/pkg/proc/test"
)

func TestScopeWithEscapedVariable(t *testing.T) {
	if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 9}) {
		return
	}

	withTestProcess("scopeescapevareval", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
		assertNoError(grp.Continue(), t, "Continue")

		// On the breakpoint there are two 'a' variables in scope, the one that
		// isn't shadowed is a variable that escapes to the heap and figures in
		// debug_info as '&a'. Evaluating 'a' should yield the escaped variable.

		avar := evalVariable(p, t, "a")
		if aval, _ := constant.Int64Val(avar.Value); aval != 3 {
			t.Errorf("wrong value for variable a: %d", aval)
		}

		if avar.Flags&proc.VariableEscaped == 0 {
			t.Errorf("variable a isn't escaped to the heap")
		}
	})
}

// TestScope will:
//   - run _fixtures/scopetest.go
//   - set a breakpoint on all lines containing a comment
//   - continue until the program ends
//   - every time a breakpoint is hit it will check that
//     scope.FunctionArguments+scope.LocalVariables and scope.EvalExpression
//     return what the corresponding comment describes they should return and
//     removes the breakpoint.
//
// Each comment is a comma separated list of variable declarations, with
// each variable declaration having the following format:
//
//	name type = initialvalue
//
// the = and the initial value are optional and can only be specified if the
// type is an integer type, float32, float64 or bool.
//
// If multiple variables with the same name are specified:
//  1. LocalVariables+FunctionArguments should return them in the same order and
//     every variable except the last one should be marked as shadowed
//  2. EvalExpression should return the last one.
func TestScope(t *testing.T) {
	if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 9, Rev: -1}) {
		return
	}

	fixturesDir := protest.FindFixturesDir()
	scopetestPath := filepath.Join(fixturesDir, "scopetest.go")

	scopeChecks := getScopeChecks(scopetestPath, t)

	withTestProcess("scopetest", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
		for i := range scopeChecks {
			setFileBreakpoint(p, t, fixture.Source, scopeChecks[i].line)
		}

		t.Logf("%d breakpoints set", len(scopeChecks))

		for {
			err := grp.Continue()
			if errors.As(err, &proc.ErrProcessExited{}) {
				break
			}
			assertNoError(err, t, "Continue()")
			bp := p.CurrentThread().Breakpoint()

			scopeCheck := findScopeCheck(scopeChecks, bp.Line)
			if scopeCheck == nil {
				t.Errorf("unknown stop position %s:%d %#x", bp.File, bp.Line, bp.Addr)
			}

			scope, _ := scopeCheck.checkLocalsAndArgs(p, t)

			for i := range scopeCheck.varChecks {
				vc := &scopeCheck.varChecks[i]
				if vc.shdw {
					continue
				}
				vc.checkInScope(scopeCheck.line, scope, t)
			}

			scopeCheck.ok = true
			err = p.ClearBreakpoint(bp.Addr)
			assertNoError(err, t, "ClearBreakpoint")
		}
	})

	for i := range scopeChecks {
		if !scopeChecks[i].ok {
			t.Errorf("breakpoint at line %d not hit", scopeChecks[i].line)
		}
	}
}

type scopeCheck struct {
	line      int
	varChecks []varCheck
	ok        bool // this scope check was passed
}

type varCheck struct {
	name     string
	typ      string
	kind     reflect.Kind
	shdw     bool // this variable should be shadowed
	hasVal   bool
	intVal   int64
	uintVal  uint64
	floatVal float64
	boolVal  bool

	ok bool // this variable check was passed
}

func getScopeChecks(path string, t *testing.T) []scopeCheck {
	var fset token.FileSet
	root, err := parser.ParseFile(&fset, path, nil, parser.ParseComments)
	if err != nil {
		t.Fatalf("could not parse %s: %v", path, err)
	}

	scopeChecks := []scopeCheck{}

	for _, cmtg := range root.Comments {
		for _, cmt := range cmtg.List {
			pos := fset.Position(cmt.Slash)

			scopeChecks = append(scopeChecks, scopeCheck{line: pos.Line})
			scopeChecks[len(scopeChecks)-1].Parse(cmt.Text[2:], t)
		}
	}

	return scopeChecks
}

func findScopeCheck(scopeChecks []scopeCheck, line int) *scopeCheck {
	for i := range scopeChecks {
		if scopeChecks[i].line == line {
			return &scopeChecks[i]
		}
	}
	return nil
}

func (check *scopeCheck) Parse(descr string, t *testing.T) {
	decls := strings.Split(descr, ",")
	check.varChecks = make([]varCheck, len(decls))
	for i, decl := range decls {
		varcheck := &check.varChecks[i]
		value := ""
		if equal := strings.Index(decl, "="); equal >= 0 {
			value = strings.TrimSpace(decl[equal+1:])
			decl = strings.TrimSpace(decl[:equal])
			varcheck.hasVal = true
		} else {
			decl = strings.TrimSpace(decl)
		}

		space := strings.Index(decl, " ")
		if space < 0 {
			t.Fatalf("could not parse scope comment %q (%q)", descr, decl)
		}
		varcheck.name = strings.TrimSpace(decl[:space])
		varcheck.typ = strings.TrimSpace(decl[space+1:])
		if strings.Contains(varcheck.typ, " ") {
			t.Fatalf("could not parse scope comment %q (%q)", descr, decl)
		}

		if !varcheck.hasVal {
			continue
		}

		switch varcheck.typ {
		case "int", "int8", "int16", "int32", "int64":
			var err error
			varcheck.kind = reflect.Int
			varcheck.intVal, err = strconv.ParseInt(value, 10, 64)
			if err != nil {
				t.Fatalf("could not parse scope comment %q: %v", descr, err)
			}

		case "uint", "uint8", "uint16", "uint32", "uint64", "uintptr":
			var err error
			varcheck.kind = reflect.Uint
			varcheck.uintVal, err = strconv.ParseUint(value, 10, 64)
			if err != nil {
				t.Fatalf("could not parse scope comment %q: %v", descr, err)
			}

		case "float32", "float64":
			var err error
			varcheck.kind = reflect.Float64
			varcheck.floatVal, err = strconv.ParseFloat(value, 64)
			if err != nil {
				t.Fatalf("could not parse scope comment %q: %v", descr, err)
			}

		case "bool":
			var err error
			varcheck.kind = reflect.Bool
			varcheck.boolVal, err = strconv.ParseBool(value)
			if err != nil {
				t.Fatalf("could not parse scope comment %q: %v", descr, err)
			}
		}
	}

	for i := 1; i < len(check.varChecks); i++ {
		if check.varChecks[i-1].name == check.varChecks[i].name {
			check.varChecks[i-1].shdw = true
		}
	}
}

func (check *scopeCheck) checkLocalsAndArgs(p *proc.Target, t *testing.T) (*proc.EvalScope, bool) {
	scope, err := proc.GoroutineScope(p, p.CurrentThread())
	assertNoError(err, t, "GoroutineScope()")

	ok := true

	args, err := scope.FunctionArguments(normalLoadConfig)
	assertNoError(err, t, "FunctionArguments()")
	locals, err := scope.LocalVariables(normalLoadConfig)
	assertNoError(err, t, "LocalVariables()")

	for _, arg := range args {
		check.checkVar(arg, t)
	}

	for _, local := range locals {
		check.checkVar(local, t)
	}

	for i := range check.varChecks {
		if !check.varChecks[i].ok {
			t.Errorf("%d: variable %s not found", check.line, check.varChecks[i].name)
			ok = false
		}
	}

	return scope, ok
}

func (check *scopeCheck) checkVar(v *proc.Variable, t *testing.T) {
	var varCheck *varCheck
	for i := range check.varChecks {
		if !check.varChecks[i].ok && (check.varChecks[i].name == v.Name) {
			varCheck = &check.varChecks[i]
			break
		}
	}

	if varCheck == nil {
		t.Errorf("%d: unexpected variable %s", check.line, v.Name)
		return
	}

	varCheck.check(check.line, v, t, "FunctionArguments+LocalVariables")
	varCheck.ok = true
}

func (varCheck *varCheck) checkInScope(line int, scope *proc.EvalScope, t *testing.T) {
	v, err := scope.EvalExpression(varCheck.name, normalLoadConfig)
	assertNoError(err, t, fmt.Sprintf("EvalVariable(%s)", varCheck.name))
	varCheck.check(line, v, t, "EvalExpression")
}

func (varCheck *varCheck) check(line int, v *proc.Variable, t *testing.T, ctxt string) {
	typ := v.DwarfType.String()
	typ = strings.ReplaceAll(typ, " ", "")
	if typ != varCheck.typ {
		t.Errorf("%d: wrong type for %s (%s), got %s, expected %s", line, v.Name, ctxt, typ, varCheck.typ)
	}

	if varCheck.shdw && v.Flags&proc.VariableShadowed == 0 {
		t.Errorf("%d: expected shadowed %s variable", line, v.Name)
	}

	if !varCheck.hasVal {
		return
	}

	switch varCheck.kind {
	case reflect.Int:
		if vv, _ := constant.Int64Val(v.Value); vv != varCheck.intVal {
			t.Errorf("%d: wrong value for %s (%s), got %d expected %d", line, v.Name, ctxt, vv, varCheck.intVal)
		}
	case reflect.Uint:
		if vv, _ := constant.Uint64Val(v.Value); vv != varCheck.uintVal {
			t.Errorf("%d: wrong value for %s (%s), got %d expected %d", line, v.Name, ctxt, vv, varCheck.uintVal)
		}
	case reflect.Float64:
		if vv, _ := constant.Float64Val(v.Value); math.Abs(vv-varCheck.floatVal) > 0.001 {
			t.Errorf("%d: wrong value for %s (%s), got %g expected %g", line, v.Name, ctxt, vv, varCheck.floatVal)
		}
	case reflect.Bool:
		if vv := constant.BoolVal(v.Value); vv != varCheck.boolVal {
			t.Errorf("%d: wrong value for %s (%s), got %v expected %v", line, v.Name, ctxt, vv, varCheck.boolVal)
		}
	}
}
