package winpwn

import "testing"

// These tests pin heap_lfh.go's decoding against field *values* captured
// from live runs of examples/heap_lfh.exe (Windows build 10.0.26100) rather
// than raw byte dumps, the same style heap_test.go's
// TestReadHeapDecodesRealCapturedFields uses -- the offsets themselves were
// confirmed via `dt ntdll!_LFH_HEAP`/`dt ntdll!_HEAP_SUBSEGMENT` and the
// values via cdb reads cross-checked against winpwn's own output, not
// assumed.

func TestReadLFHBuckets(t *testing.T) {
	buf := make([]byte, lfhOffBuckets+lfhBucketCount*lfhBucketStride)
	// Bucket 2: 48-byte blocks (3 granularity units), the bucket that
	// actually served examples/heap_lfh's 32-byte Note/buffer allocations
	// once LFH took over that size class -- see FindLFHBucket's doc comment
	// for why 32-byte requests land in the 48-byte bucket, not the 32-byte
	// one.
	off := lfhOffBuckets + 2*lfhBucketStride
	buf[off] = 3
	buf[off+1] = 0
	buf[off+2] = 2 // SizeIndex

	buckets, err := ReadLFHBuckets(newByteReaderAt(buf), 0)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if buckets[2].BlockUnits != 3 || buckets[2].BlockSize() != 48 {
		t.Errorf("bucket[2] = %+v, want BlockUnits=3 BlockSize=48", buckets[2])
	}
	if buckets[0].BlockUnits != 0 {
		t.Errorf("bucket[0] should be unused, got %+v", buckets[0])
	}
}

func TestFindLFHBucketAccountsForBlockHeader(t *testing.T) {
	var buckets [lfhBucketCount]HeapBucket
	buckets[1] = HeapBucket{Index: 1, BlockUnits: 2} // 32 bytes total, 16 usable -- too small
	buckets[2] = HeapBucket{Index: 2, BlockUnits: 3} // 48 bytes total, 32 usable -- the real fit

	got, err := FindLFHBucket(buckets, 32)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got.Index != 2 {
		t.Errorf("FindLFHBucket(32) chose bucket %d, want 2 (the naive BlockSize()>=wantSize check would wrongly pick bucket 1)", got.Index)
	}
}

func TestFindLFHBucketNoneActive(t *testing.T) {
	var buckets [lfhBucketCount]HeapBucket
	if _, err := FindLFHBucket(buckets, 32); err == nil {
		t.Fatal("expected an error when no bucket has BlockUnits set")
	}
}

func TestActiveSubsegment(t *testing.T) {
	const lfhHeapAddr = 0x1000
	buf := make([]byte, 0x2000)
	const segInfoAddr = 0x1500
	const subsegAddr = 0x1700
	binaryLEPutUint64(buf[lfhHeapAddr+lfhOffSegmentInfoArrays+2*8:], segInfoAddr)
	binaryLEPutUint64(buf[segInfoAddr+hlsiOffActiveSubsegment:], subsegAddr)

	got, err := ActiveSubsegment(newByteReaderAt(buf), lfhHeapAddr, 2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != subsegAddr {
		t.Errorf("ActiveSubsegment = 0x%x, want 0x%x", got, subsegAddr)
	}

	if _, err := ActiveSubsegment(newByteReaderAt(buf), lfhHeapAddr, 5); err == nil {
		t.Fatal("expected an error for a bucket with no segment info")
	}
}

// TestReadLFHSubsegmentBusyBitmap pins ReadLFHSubsegment against the exact
// shape captured from a live run: bucket 2 (48-byte blocks), a 19-block
// subsegment, with one bit clear (an early, pre-LFH-activation allocation
// that never got a tracked slot -- see heap_lfh.go's package comment) and
// the rest busy.
func TestReadLFHSubsegmentBusyBitmap(t *testing.T) {
	const subsegAddr = 0x713480
	const userBlocksAddr = 0x713050
	buf := make([]byte, 0x800000)

	binaryLEPutUint64(buf[subsegAddr+subsegOffUserBlocks:], userBlocksAddr)
	buf[subsegAddr+subsegOffBlockSize] = 3 // granularity units -> 48 bytes
	buf[subsegAddr+subsegOffBlockCount] = 19

	const bitmapBufferAddr = 0x713200
	binaryLEPutUint64(buf[userBlocksAddr+userDataOffBitmapSize:], 19)
	binaryLEPutUint64(buf[userBlocksAddr+userDataOffBitmapBuffer:], bitmapBufferAddr)
	// bits: every slot busy except slot 5 (0-indexed) -- matches a real
	// captured run's BusyBitmap word.
	var word uint64 = 0
	for i := 0; i < 19; i++ {
		if i != 5 {
			word |= 1 << uint(i)
		}
	}
	binaryLEPutUint64(buf[bitmapBufferAddr:], word)

	subseg, err := ReadLFHSubsegment(newByteReaderAt(buf), subsegAddr)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if subseg.BlockSize != 48 || subseg.BlockCount != 19 {
		t.Errorf("BlockSize/BlockCount = %d/%d, want 48/19", subseg.BlockSize, subseg.BlockCount)
	}
	if subseg.UserBlocksAddr != userBlocksAddr {
		t.Errorf("UserBlocksAddr = 0x%x, want 0x%x", subseg.UserBlocksAddr, userBlocksAddr)
	}
	for i, busy := range subseg.Busy {
		want := i != 5
		if busy != want {
			t.Errorf("Busy[%d] = %v, want %v", i, busy, want)
		}
	}
}

// TestCalibrateLFHFirstBlockOffsetMatchesLiveAddress pins
// CalibrateLFHFirstBlockOffset/BlockAddress/SlotOf against a real
// before/after-free pair captured live: freeing the note at 0x7132b0
// flipped exactly Busy[11] from true to false, and slot 11's address by
// BlockAddress must equal 0x7132b0 -- not 0x713280 (one block short), which
// is what the naive "(addr-UserBlocksAddr) % BlockSize" formula gives
// before the +BlockSize correction (see CalibrateLFHFirstBlockOffset's doc
// comment for why that's wrong: it lands on _HEAP_USERDATA_HEADER's own
// reserved region, one block before any real, bit-tracked slot).
func TestCalibrateLFHFirstBlockOffsetMatchesLiveAddress(t *testing.T) {
	subseg := &LFHSubsegment{
		UserBlocksAddr: 0x713050,
		BlockSize:      48,
		BlockCount:     19,
	}
	const victimAddr = 0x7132b0

	off := CalibrateLFHFirstBlockOffset(subseg, victimAddr)
	if off != 80 {
		t.Fatalf("CalibrateLFHFirstBlockOffset = %d, want 80", off)
	}
	if got := subseg.BlockAddress(off, 11); got != victimAddr {
		t.Errorf("BlockAddress(80, 11) = 0x%x, want 0x%x", got, victimAddr)
	}
	idx, ok := subseg.SlotOf(off, victimAddr)
	if !ok || idx != 11 {
		t.Errorf("SlotOf = (%d, %v), want (11, true)", idx, ok)
	}
	if _, ok := subseg.SlotOf(off, 0xdeadbeef); ok {
		t.Error("SlotOf should reject an address outside the subsegment")
	}
}
