package vs

import (
	"bytes"
	_ "embed"
	"fmt"
	"testing"

	"github.com/tetratelabs/wazero/internal/testing/require"
)

var (
	// allocationWasm is compiled from ../../../examples/allocation/tinygo/testdata/src/greet.go
	// We can't use go:embed as it is outside this directory. Copying it isn't ideal due to size and drift.
	allocationWasmPath = "../../../examples/allocation/tinygo/testdata/greet.wasm"
	allocationWasm     []byte
	allocationParam    = "wazero"
	allocationResult   = []byte("wasm >> Hello, wazero!")
	allocationConfig   *RuntimeConfig
)

func init() {
	allocationWasm = readRelativeFile(allocationWasmPath)
	allocationConfig = &RuntimeConfig{
		ModuleName: "greet",
		ModuleWasm: allocationWasm,
		FuncNames:  []string{"malloc", "free", "greet"},
		NeedsWASI:  true, // Needed for TinyGo
		LogFn: func(buf []byte) error {
			if !bytes.Equal(allocationResult, buf) {
				return fmt.Errorf("expected %q, but was %q", allocationResult, buf)
			}
			return nil
		},
	}
}

func allocationCall(m Module, _ int) error {
	nameSize := uint32(len(allocationParam))
	// Instead of an arbitrary memory offset, use TinyGo's allocator. Notice
	// there is nothing string-specific in this allocation function. The same
	// function could be used to pass binary serialized data to Wasm.
	namePtr, err := m.CallI32_I32(testCtx, "malloc", nameSize)
	if err != nil {
		return err
	}

	// The pointer is a linear memory offset, which is where we write the name.
	if err = m.WriteMemory(namePtr, []byte(allocationParam)); err != nil {
		return err
	}

	// Now, we can call "greeting", which reads the string we wrote to memory!
	fnErr := m.CallI32I32_V(testCtx, "greet", namePtr, nameSize)
	if fnErr != nil {
		return fnErr
	}

	// This pointer was allocated by TinyGo, but owned by Go, So, we have to
	// deallocate it when finished
	if err := m.CallI32_V(testCtx, "free", namePtr); err != nil {
		return err
	}

	return nil
}

func RunTestAllocation(t *testing.T, runtime func() Runtime) {
	testCall(t, runtime, allocationConfig, testAllocationCall)
}

func testAllocationCall(t *testing.T, m Module, instantiation, iteration int) {
	err := allocationCall(m, iteration)
	require.NoError(t, err, "instantiation[%d] iteration[%d] failed: %v", instantiation, iteration, err)
}

func RunTestBenchmarkAllocation_Call_CompilerFastest(t *testing.T, vsRuntime Runtime) {
	runTestBenchmark_Call_CompilerFastest(t, allocationConfig, "Allocation", allocationCall, vsRuntime)
}

func RunBenchmarkAllocation(b *testing.B, runtime func() Runtime) {
	benchmark(b, runtime, allocationConfig, allocationCall)
}
