package winpwn

import (
	"errors"
	"testing"
)

var errOutOfRange = errors.New("byteReaderAt: out of range")

// TestDecodeHeapEntryMatchesLiveGroundTruth pins DecodeHeapEntry against
// three real _HEAP_ENTRY headers + their heap's real Encoding key,
// captured byte-for-byte from a live process (a tiny HeapAlloc(256)/
// HeapAlloc(4)/HeapAlloc(16) probe on this machine, Windows build 10.0.26100)
// and cross-checked against WinDbg's own `!heap -a` ground-truth listing
// before being trusted -- see heap.go's top comment for the full story.
//
// This is the test that caught the real bug worth remembering: the first
// version of UserSize subtracted HeapEntrySize *again* on top of
// UnusedBytes (i.e. assumed UnusedBytes was padding *after* a separately-
// accounted-for header), which silently produced a UserSize 16 bytes
// smaller than reality for every entry. !heap -a's own "(requested size)"
// column is what caught it -- UnusedBytes already bakes the header in.
func TestDecodeHeapEntryMatchesLiveGroundTruth(t *testing.T) {
	// Heap.Encoding, captured at heap+0x80 on the live probe.
	encoding := [16]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x1e, 0xb6, 0x56, 0xf8, 0xe5, 0xd3, 0x00, 0x00}

	cases := []struct {
		name         string
		addr         uint64
		raw          [16]byte
		wantSize     uint16 // granularity units, cross-checked against `!heap -a`'s byte-size column / HeapEntrySize
		wantFlags    uint8
		wantPrevSize uint16
		wantUnused   uint8
		wantUserSize uint64 // the requested size `!heap -a` printed in parens
	}{
		{
			// !heap -a: 00000000000d0850: 00110 . 00110 [101] - busy (100)
			name:         "256-byte allocation, no slack",
			addr:         0xd0850,
			raw:          [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0x0f, 0xb6, 0x57, 0xe8, 0xf4, 0xd3, 0x00, 0x10},
			wantSize:     17, // 17*0x10 = 0x110 = 272
			wantFlags:    HeapEntryBusy,
			wantPrevSize: 17, // previous entry (0xd0740) was also 0x110 bytes per !heap -a
			wantUnused:   16,
			wantUserSize: 0x100,
		},
		{
			// !heap -a: 00000000000d0d70: 00050 . 00020 [101] - busy (4)
			name:         "4-byte allocation, lots of slack",
			addr:         0xd0d70,
			raw:          [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0x1c, 0xb6, 0x57, 0xfb, 0xe0, 0xd3, 0x00, 0x1c},
			wantSize:     2, // 2*0x10 = 0x20
			wantFlags:    HeapEntryBusy,
			wantPrevSize: 5, // previous entry (0xd0d20) was 0x50 bytes per !heap -a
			wantUnused:   28,
			wantUserSize: 4,
		},
		{
			// !heap -a: 00000000000d0d90: 00020 . 00020 [101] - busy (10)
			name:         "16-byte allocation, header-only slack",
			addr:         0xd0d90,
			raw:          [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0x1c, 0xb6, 0x57, 0xfb, 0xe7, 0xd3, 0x00, 0x10},
			wantSize:     2,
			wantFlags:    HeapEntryBusy,
			wantPrevSize: 2, // previous entry (0xd0d70) was also 0x20 bytes
			wantUnused:   16,
			wantUserSize: 16,
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			e := DecodeHeapEntry(c.addr, c.raw, &encoding)
			if e.Size != c.wantSize {
				t.Errorf("Size = %d, want %d", e.Size, c.wantSize)
			}
			if e.Flags != c.wantFlags {
				t.Errorf("Flags = 0x%x, want 0x%x", e.Flags, c.wantFlags)
			}
			if e.PreviousSize != c.wantPrevSize {
				t.Errorf("PreviousSize = %d, want %d", e.PreviousSize, c.wantPrevSize)
			}
			if e.UnusedBytes != c.wantUnused {
				t.Errorf("UnusedBytes = %d, want %d", e.UnusedBytes, c.wantUnused)
			}
			if got := e.UserSize(); got != c.wantUserSize {
				t.Errorf("UserSize() = 0x%x, want 0x%x", got, c.wantUserSize)
			}
			if !e.Busy() {
				t.Error("Busy() = false, want true (all three live samples were busy)")
			}
			if e.UserData() != c.addr+HeapEntrySize {
				t.Errorf("UserData() = 0x%x, want 0x%x", e.UserData(), c.addr+HeapEntrySize)
			}
		})
	}
}

func TestDetectHeapKind(t *testing.T) {
	ntHeap := make([]byte, 0x20)
	binaryLEPutUint32(ntHeap[0x10:], heapSignatureNT)

	segHeap := make([]byte, 0x20)
	binaryLEPutUint32(segHeap[0x10:], heapSignatureSegment)

	garbage := make([]byte, 0x20)

	cases := []struct {
		name string
		buf  []byte
		want HeapKind
		ok   bool
	}{
		{"nt heap", ntHeap, HeapKindNT, true},
		{"segment heap", segHeap, HeapKindSegment, true},
		{"garbage", garbage, HeapKindUnknown, false},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			kind, err := DetectHeapKind(newByteReaderAt(c.buf), 0)
			if c.ok && err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if !c.ok && err == nil {
				t.Fatal("expected an error for an unrecognized signature")
			}
			if kind != c.want {
				t.Errorf("kind = %v, want %v", kind, c.want)
			}
		})
	}
}

// TestReadHeapDecodesRealCapturedFields pins ReadHeap against the actual
// field bytes captured from the same live probe heap.go's top comment and
// heap_test.go's ground-truth test describe (Flags=0x2/HEAP_GROWABLE,
// EncodeFlagMask=0x100000, Signature=0xeeffeeff -- deliberately distinct
// from _HEAP_SEGMENT's 0xffeeffee, confirmed with `db` against the live
// process rather than assumed equal).
func TestReadHeapDecodesRealCapturedFields(t *testing.T) {
	buf := make([]byte, 0x300)
	binaryLEPutUint32(buf[0x10:], heapSignatureNT) // _HEAP_SEGMENT.SegmentSignature
	binaryLEPutUint32(buf[0x70:], 0x00000002)      // Flags = HEAP_GROWABLE
	binaryLEPutUint32(buf[0x7c:], 0x00100000)      // EncodeFlagMask
	copy(buf[0x80:0x90], []byte{0, 0, 0, 0, 0, 0, 0, 0, 0x1e, 0xb6, 0x56, 0xf8, 0xe5, 0xd3, 0x00, 0x00})
	binaryLEPutUint32(buf[0x98:], 0xeeffeeff) // _HEAP.Signature
	buf[0x1a2] = FrontEndHeapNone
	binaryLEPutUint64(buf[0x30:], 0x6f0000) // BaseAddress
	binaryLEPutUint64(buf[0x40:], 0x6f0740) // FirstEntry
	binaryLEPutUint64(buf[0x48:], 0x7ef000) // LastValidEntry

	h, err := ReadHeap(newByteReaderAt(buf), 0)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if h.Signature != 0xeeffeeff {
		t.Errorf("Signature = 0x%x, want 0xeeffeeff", h.Signature)
	}
	if h.Flags != 0x2 {
		t.Errorf("Flags = 0x%x, want 0x2", h.Flags)
	}
	if !h.EncodingActive() {
		t.Error("EncodingActive() = false, want true (EncodeFlagMask is nonzero)")
	}
	if h.FrontEndHeapType != FrontEndHeapNone {
		t.Errorf("FrontEndHeapType = %d, want %d", h.FrontEndHeapType, FrontEndHeapNone)
	}
	if h.BaseAddress != 0x6f0000 || h.FirstEntry != 0x6f0740 || h.LastValidEntry != 0x7ef000 {
		t.Errorf("BaseAddress/FirstEntry/LastValidEntry = 0x%x/0x%x/0x%x", h.BaseAddress, h.FirstEntry, h.LastValidEntry)
	}
}

func TestReadHeapRejectsSegmentHeap(t *testing.T) {
	buf := make([]byte, 0x20)
	binaryLEPutUint32(buf[0x10:], heapSignatureSegment)
	if _, err := ReadHeap(newByteReaderAt(buf), 0); err == nil {
		t.Fatal("expected ReadHeap to reject a Segment Heap signature")
	}
}

func TestHeapSegmentsSingleSegmentIsEmpty(t *testing.T) {
	// A heap with only the embedded Segment0: SegmentList's Flink/Blink
	// both point back at the list head itself (heapOffSegmentList), the
	// standard "empty list" LIST_ENTRY representation.
	buf := make([]byte, 0x200)
	headAddr := uint64(heapOffSegmentList)
	binaryLEPutUint64(buf[heapOffSegmentList:], headAddr)
	binaryLEPutUint64(buf[heapOffSegmentList+8:], headAddr)

	h := &Heap{Addr: 0}
	segs, err := h.Segments(newByteReaderAt(buf))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(segs) != 0 {
		t.Errorf("got %d segments, want 0 for an empty SegmentList", len(segs))
	}
}

func TestWalkSegmentEntriesStopsAtLastEntry(t *testing.T) {
	// Three unencoded entries (no Encoding key, EncodeFlagMask=0 case):
	// 32 bytes busy, 16 bytes free, 16 bytes busy+LastEntry.
	buf := make([]byte, 64)
	putEntry := func(off int, size uint16, flags uint8, prevSize uint16) {
		buf[off+8] = byte(size)
		buf[off+9] = byte(size >> 8)
		buf[off+10] = flags
		buf[off+12] = byte(prevSize)
		buf[off+13] = byte(prevSize >> 8)
	}
	putEntry(0, 2, HeapEntryBusy, 0)
	putEntry(32, 1, 0, 2)
	putEntry(48, 1, HeapEntryBusy|HeapEntryLastEntry, 1)

	entries, err := WalkSegmentEntries(newByteReaderAt(buf), 0, 64, nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(entries) != 3 {
		t.Fatalf("got %d entries, want 3", len(entries))
	}
	if entries[0].Addr != 0 || entries[0].BlockSize() != 32 || !entries[0].Busy() {
		t.Errorf("entry 0 = %+v", entries[0])
	}
	if entries[1].Addr != 32 || entries[1].BlockSize() != 16 || entries[1].Busy() {
		t.Errorf("entry 1 = %+v", entries[1])
	}
	if entries[2].Addr != 48 || !entries[2].LastEntry() {
		t.Errorf("entry 2 = %+v", entries[2])
	}
}

func TestWalkSegmentEntriesZeroSizeIsAnError(t *testing.T) {
	buf := make([]byte, 32) // entry at 0 decodes to Size=0 -- can't make progress
	_, err := WalkSegmentEntries(newByteReaderAt(buf), 0, 32, nil)
	if err == nil {
		t.Fatal("expected an error for a zero-size entry")
	}
}

// byteReaderAt adapts a plain []byte to io.ReaderAt for synthetic tests,
// the same role buildSyntheticMinidump's bytes.Reader plays in
// minidump_test.go.
type byteReaderAt struct{ buf []byte }

func newByteReaderAt(buf []byte) *byteReaderAt { return &byteReaderAt{buf: buf} }

func (b *byteReaderAt) ReadAt(p []byte, off int64) (int, error) {
	if off < 0 || int(off) > len(b.buf) {
		return 0, errOutOfRange
	}
	n := copy(p, b.buf[off:])
	if n < len(p) {
		return n, errOutOfRange
	}
	return n, nil
}

func binaryLEPutUint32(b []byte, v uint32) {
	b[0] = byte(v)
	b[1] = byte(v >> 8)
	b[2] = byte(v >> 16)
	b[3] = byte(v >> 24)
}

func binaryLEPutUint64(b []byte, v uint64) {
	for i := 0; i < 8; i++ {
		b[i] = byte(v >> (8 * i))
	}
}
