//go:build windows

package winpwn

import (
	"testing"
	"time"
)

// TestDebuggerBreakpointAtEntry composes SpawnSuspended + Attach +
// ResumeMainThread (the pattern documented on Debugger) to break at the
// real entry point of the checked-in fixture binary, confirming against a
// live process that: the loader/CreateProcess/LoadDll/CreateThread events
// decode without error, ResolveModuleBase eventually succeeds once the
// loader has run, SetBreakpoint+the breakpoint-hit path leaves the
// thread's Rip exactly equal to the breakpoint address (the INT3
// rewind-by-one fixup), and Continue can step back past a still-armed
// breakpoint without hanging.
//
// This exercises the exact sequence validated manually against a live
// process while building debugger_windows.go (see ROADMAP.md's Phase 4
// notes) -- a hermetic version of that same proof, run by `go test`.
func TestDebuggerBreakpointAtEntry(t *testing.T) {
	requireFixturePE(t)

	diskPE, err := OpenPE(testFixturePE)
	if err != nil {
		t.Fatal(err)
	}
	diskBase, err := diskPE.ImageBase()
	if err != nil {
		diskPE.Close()
		t.Fatal(err)
	}
	diskEntry, err := diskPE.EntryPoint()
	if err != nil {
		diskPE.Close()
		t.Fatal(err)
	}
	diskPE.Close()
	entryRVA := diskEntry - diskBase

	tube, pid, err := SpawnSuspended(testFixturePE)
	if err != nil {
		t.Fatalf("SpawnSuspended: %v", err)
	}
	defer tube.Close()

	dbg, err := Attach(pid)
	if err != nil {
		t.Fatalf("Attach: %v", err)
	}
	defer dbg.Close()

	if err := ResumeMainThread(pid); err != nil {
		t.Fatalf("ResumeMainThread: %v", err)
	}

	var bpAddr uintptr
	var entrySet, hit bool

	deadline := time.After(10 * time.Second)
	for !hit {
		select {
		case ev, ok := <-dbg.Events():
			if !ok {
				t.Fatal("debugger event channel closed before hitting the entry breakpoint")
			}

			if !entrySet {
				if base, rerr := ResolveModuleBase(pid, "bof_win.c.exe"); rerr == nil {
					bpAddr = uintptr(base) + uintptr(entryRVA)
					if err := dbg.SetBreakpoint(bpAddr); err != nil {
						t.Fatalf("SetBreakpoint(0x%x): %v", bpAddr, err)
					}
					entrySet = true
				}
				// Not yet resolvable (loader hasn't populated PEB.Ldr yet) --
				// expected on the first couple of events, see
				// ResolveModuleBase's doc comment in procmem_windows.go.
			}

			if ev.Kind == EventBreakpoint {
				regs, gerr := dbg.GetContext(ev.ThreadID)
				if gerr != nil {
					t.Fatalf("GetContext(%d): %v", ev.ThreadID, gerr)
				}
				if regs.Rip != uint64(ev.Addr) {
					t.Errorf("Rip = 0x%x, want 0x%x (breakpoint address, after the INT3 rewind)", regs.Rip, ev.Addr)
				}
				if ev.Addr != bpAddr {
					t.Errorf("breakpoint fired at 0x%x, want entry point 0x%x", ev.Addr, bpAddr)
				}
				hit = true
			}

			// Continue every event, including the still-armed breakpoint --
			// proves Continue's internal restore/rewind/single-step/re-arm
			// dance (stepPastBreakpoint) completes without hanging.
			if err := dbg.Continue(ev); err != nil {
				t.Fatalf("Continue: %v", err)
			}

		case <-deadline:
			t.Fatal("timed out waiting for the entry breakpoint to fire")
		}
	}
}
