package winpwn

import (
	"bytes"
	"encoding/binary"
	"testing"
	"unicode/utf16"
)

// buildSyntheticMinidump assembles a minimal but structurally real
// MINIDUMP_HEADER + MINIDUMP_DIRECTORY[2] + ModuleListStream (one module)
// + ExceptionStream, laid out at arbitrary offsets the directory points to
// (deliberately not in stream order, to exercise the Rva indirection
// rather than relying on everything being contiguous). This is the payoff
// of parsing the format natively instead of via dbghelp.dll: a synthetic
// fixture like this lets the parser be tested without a real crash dump.
func buildSyntheticMinidump(t *testing.T) []byte {
	t.Helper()

	const (
		headerSize    = 32
		directorySize = 12 * 2 // two streams
	)

	moduleName := "ntdll.dll"
	nameUTF16 := utf16.Encode([]rune(moduleName))
	nameBytes := make([]byte, 2*len(nameUTF16))
	for i, u := range nameUTF16 {
		binary.LittleEndian.PutUint16(nameBytes[i*2:], u)
	}
	// MINIDUMP_STRING: Length (byte count, no terminator) + UTF-16 buffer + NUL.
	moduleNameStream := make([]byte, 4+len(nameBytes)+2)
	binary.LittleEndian.PutUint32(moduleNameStream[0:4], uint32(len(nameBytes)))
	copy(moduleNameStream[4:], nameBytes)

	moduleNameOff := uint32(headerSize + directorySize)
	moduleListOff := moduleNameOff + uint32(len(moduleNameStream))

	// MINIDUMP_MODULE_LIST: NumberOfModules(4) + one MINIDUMP_MODULE(108).
	moduleList := make([]byte, 4+sizeofMinidumpModule)
	binary.LittleEndian.PutUint32(moduleList[0:4], 1)
	mod := moduleList[4:]
	binary.LittleEndian.PutUint64(mod[0:8], 0x00007FFE12340000) // BaseOfImage
	binary.LittleEndian.PutUint32(mod[8:12], 0x00200000)        // SizeOfImage
	binary.LittleEndian.PutUint32(mod[16:20], 0x5F000000)       // TimeDateStamp
	binary.LittleEndian.PutUint32(mod[20:24], moduleNameOff)    // ModuleNameRva

	exceptionOff := moduleListOff + uint32(len(moduleList))
	exception := make([]byte, sizeofMinidumpExceptionStream)
	binary.LittleEndian.PutUint32(exception[0:4], 1337) // ThreadId
	exc := exception[8:]
	binary.LittleEndian.PutUint32(exc[0:4], 0xC0000005)          // ExceptionCode (access violation)
	binary.LittleEndian.PutUint64(exc[16:24], 0x00007FFE12341234) // ExceptionAddress
	binary.LittleEndian.PutUint32(exc[24:28], 2)                  // NumberParameters
	binary.LittleEndian.PutUint64(exc[32:40], 1)                  // Parameters[0]: write access
	binary.LittleEndian.PutUint64(exc[40:48], 0xDEADBEEF)         // Parameters[1]: faulting address

	total := int(exceptionOff) + len(exception)
	out := make([]byte, total)

	binary.LittleEndian.PutUint32(out[0:4], minidumpSignature)
	binary.LittleEndian.PutUint32(out[8:12], 2)         // NumberOfStreams
	binary.LittleEndian.PutUint32(out[12:16], headerSize) // StreamDirectoryRva

	dir := out[headerSize:]
	binary.LittleEndian.PutUint32(dir[0:4], uint32(StreamModuleList))
	binary.LittleEndian.PutUint32(dir[4:8], uint32(len(moduleList)))
	binary.LittleEndian.PutUint32(dir[8:12], moduleListOff)
	binary.LittleEndian.PutUint32(dir[12:16], uint32(StreamException))
	binary.LittleEndian.PutUint32(dir[16:20], uint32(len(exception)))
	binary.LittleEndian.PutUint32(dir[20:24], exceptionOff)

	copy(out[moduleNameOff:], moduleNameStream)
	copy(out[moduleListOff:], moduleList)
	copy(out[exceptionOff:], exception)

	return out
}

func TestMinidumpModules(t *testing.T) {
	raw := buildSyntheticMinidump(t)
	m, err := newMinidump(bytes.NewReader(raw))
	if err != nil {
		t.Fatal(err)
	}
	defer m.Close()

	mods, err := m.Modules()
	if err != nil {
		t.Fatal(err)
	}
	if len(mods) != 1 {
		t.Fatalf("got %d modules, want 1", len(mods))
	}
	if mods[0].Name != "ntdll.dll" {
		t.Errorf("module name = %q, want %q", mods[0].Name, "ntdll.dll")
	}
	if mods[0].BaseOfImage != 0x00007FFE12340000 {
		t.Errorf("BaseOfImage = 0x%X, want 0x7FFE12340000", mods[0].BaseOfImage)
	}
	if mods[0].SizeOfImage != 0x00200000 {
		t.Errorf("SizeOfImage = 0x%X, want 0x200000", mods[0].SizeOfImage)
	}
}

func TestMinidumpException(t *testing.T) {
	raw := buildSyntheticMinidump(t)
	m, err := newMinidump(bytes.NewReader(raw))
	if err != nil {
		t.Fatal(err)
	}
	defer m.Close()

	exc, err := m.Exception()
	if err != nil {
		t.Fatal(err)
	}
	if exc.ThreadID != 1337 {
		t.Errorf("ThreadID = %d, want 1337", exc.ThreadID)
	}
	if exc.ExceptionCode != 0xC0000005 {
		t.Errorf("ExceptionCode = 0x%X, want 0xC0000005", exc.ExceptionCode)
	}
	if exc.ExceptionAddress != 0x00007FFE12341234 {
		t.Errorf("ExceptionAddress = 0x%X, want 0x7FFE12341234", exc.ExceptionAddress)
	}
	if len(exc.Parameters) != 2 || exc.Parameters[0] != 1 || exc.Parameters[1] != 0xDEADBEEF {
		t.Errorf("Parameters = %v, want [1 0xDEADBEEF]", exc.Parameters)
	}
}

func TestMinidumpRejectsBadSignature(t *testing.T) {
	raw := buildSyntheticMinidump(t)
	binary.LittleEndian.PutUint32(raw[0:4], 0xDEADBEEF)
	if _, err := newMinidump(bytes.NewReader(raw)); err == nil {
		t.Error("expected an error for a bad minidump signature")
	}
}

func TestMinidumpRawStream(t *testing.T) {
	raw := buildSyntheticMinidump(t)
	m, err := newMinidump(bytes.NewReader(raw))
	if err != nil {
		t.Fatal(err)
	}
	defer m.Close()

	data, err := m.RawStream(StreamModuleList)
	if err != nil {
		t.Fatal(err)
	}
	if got := binary.LittleEndian.Uint32(data[0:4]); got != 1 {
		t.Errorf("RawStream(StreamModuleList) NumberOfModules = %d, want 1", got)
	}

	if _, err := m.RawStream(StreamSystemInfo); err == nil {
		t.Error("expected an error for an absent stream type")
	}
}
