package proc_test

import (
	"go/constant"
	"path/filepath"
	"runtime"
	"testing"

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

func TestStepInstructionOnBreakpoint(t *testing.T) {
	// StepInstruction should step one instruction forward when
	// PC is on a 1 byte instruction with a software breakpoint.
	protest.AllowRecording(t)
	withTestProcess("break/", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
		setFileBreakpoint(p, t, filepath.ToSlash(filepath.Join(fixture.BuildDir, "break_amd64.s")), 4)

		assertNoError(grp.Continue(), t, "Continue()")

		pc := getRegisters(p, t).PC()
		assertNoError(grp.StepInstruction(false), t, "StepInstruction()")
		if pc == getRegisters(p, t).PC() {
			t.Fatal("Could not step a single instruction")
		}
	})
}

func TestNextUnknownInstr(t *testing.T) {
	if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 10) {
		t.Skip("versions of Go before 1.10 can't assemble the instruction VPUNPCKLWD")
	}
	withTestProcess("nodisasm/", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
		setFunctionBreakpoint(p, t, "main.asmFunc")
		assertNoError(grp.Continue(), t, "Continue()")
		assertNoError(grp.Next(), t, "Next()")
	})
}

func TestIssue1656(t *testing.T) {
	withTestProcess("issue1656/", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
		setFileBreakpoint(p, t, filepath.ToSlash(filepath.Join(fixture.BuildDir, "main.s")), 5)
		assertNoError(grp.Continue(), t, "Continue()")
		t.Logf("step1\n")
		assertNoError(grp.Step(), t, "Step()")
		assertLineNumber(p, t, 8, "wrong line number after first step")
		t.Logf("step2\n")
		assertNoError(grp.Step(), t, "Step()")
		assertLineNumber(p, t, 9, "wrong line number after second step")
	})
}

func TestBreakpointConfusionOnResume(t *testing.T) {
	// Checks that SetCurrentBreakpoint, (*Thread).StepInstruction and
	// native.(*Thread).singleStep all agree on which breakpoint the thread is
	// stopped at.
	// This test checks for a regression introduced when fixing Issue #1656
	withTestProcess("nopbreakpoint/", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
		maindots := filepath.ToSlash(filepath.Join(fixture.BuildDir, "main.s"))
		maindotgo := filepath.ToSlash(filepath.Join(fixture.BuildDir, "main.go"))
		setFileBreakpoint(p, t, maindots, 5) // line immediately after the NOP
		assertNoError(grp.Continue(), t, "First Continue")
		assertLineNumber(p, t, 5, "not on main.s:5")
		setFileBreakpoint(p, t, maindots, 4)   // sets a breakpoint on the NOP line, which will be one byte before the breakpoint we currently are stopped at.
		setFileBreakpoint(p, t, maindotgo, 18) // set one extra breakpoint so that we can recover execution and check the global variable g
		assertNoError(grp.Continue(), t, "Second Continue")
		gvar := evalVariable(p, t, "g")
		if n, _ := constant.Int64Val(gvar.Value); n != 1 {
			t.Fatalf("wrong value of global variable 'g': %v (expected 1)", gvar.Value)
		}
	})
}

func TestCallInjectionFlagCorruption(t *testing.T) {
	// debugCallV2 has a bug in amd64 where its tail corrupts the FLAGS register by running an ADD instruction.
	// Since this problem exists in many versions of Go, instead of fixing
	// debugCallV2, we work around this problem by restoring FLAGS, one extra
	// time, after stepping out of debugCallV2.
	// Fixes issue https://github.com/go-delve/delve/issues/2985
	protest.MustSupportFunctionCalls(t, testBackend)

	withTestProcessArgs("badflags", t, ".", []string{"0"}, 0, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
		mainfn := p.BinInfo().LookupFunc()["main.main"][0]

		// Find JNZ instruction on line :14
		var addr uint64
		text, err := proc.Disassemble(p.Memory(), nil, p.Breakpoints(), p.BinInfo(), mainfn.Entry, mainfn.End)
		assertNoError(err, t, "Disassemble")
		for _, instr := range text {
			if instr.Loc.Line != 14 {
				continue
			}
			if proc.IsJNZ(instr.Inst) {
				addr = instr.Loc.PC
			}
		}
		if addr == 0 {
			t.Fatalf("Could not find JNZ instruction at line :14")
		}

		// Create breakpoint
		_, err = p.SetBreakpoint(0, addr, proc.UserBreakpoint, nil)
		assertNoError(err, t, "SetBreakpoint")

		// Continue to breakpoint
		assertNoError(grp.Continue(), t, "Continue()")
		assertLineNumber(p, t, 14, "expected line :14")

		// Save RFLAGS register
		rflagsBeforeCall := p.BinInfo().Arch.RegistersToDwarfRegisters(0, getRegisters(p, t)).Uint64Val(regnum.AMD64_Rflags)
		t.Logf("rflags before = %#x", rflagsBeforeCall)

		// Inject call to main.g()
		assertNoError(proc.EvalExpressionWithCalls(grp, p.SelectedGoroutine(), "g()", normalLoadConfig, true), t, "Call")

		// Check RFLAGS register after the call
		rflagsAfterCall := p.BinInfo().Arch.RegistersToDwarfRegisters(0, getRegisters(p, t)).Uint64Val(regnum.AMD64_Rflags)
		t.Logf("rflags after = %#x", rflagsAfterCall)

		if rflagsBeforeCall != rflagsAfterCall {
			t.Errorf("mismatched rflags value")
		}

		// Single step and check where we end up
		assertNoError(grp.Step(), t, "Step()")
		assertLineNumber(p, t, 17, "expected line :17") // since we passed "0" as argument we should be going into the false branch at line :17
	})
}
