package wasi_snapshot_preview1_test

import (
	"bytes"
	"context"
	_ "embed"
	"testing"
	"time"

	"github.com/tetratelabs/wazero"
	"github.com/tetratelabs/wazero/api"
	"github.com/tetratelabs/wazero/experimental"
	"github.com/tetratelabs/wazero/experimental/logging"
	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
	"github.com/tetratelabs/wazero/internal/testing/proxy"
	"github.com/tetratelabs/wazero/internal/testing/require"
	"github.com/tetratelabs/wazero/internal/wasip1"
	"github.com/tetratelabs/wazero/sys"
)

// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")

const testMemoryPageSize = 1

// exitOnStartUnstableWasm was generated by the following:
//
//	cd testdata; wat2wasm --debug-names exit_on_start_unstable.wat
//
//go:embed testdata/exit_on_start_unstable.wasm
var exitOnStartUnstableWasm []byte

func TestNewFunctionExporter(t *testing.T) {
	t.Run("export as wasi_unstable", func(t *testing.T) {
		r := wazero.NewRuntime(testCtx)
		defer r.Close(testCtx)

		// Instantiate the current WASI functions under the wasi_unstable
		// instead of wasi_snapshot_preview1.
		wasiBuilder := r.NewHostModuleBuilder("wasi_unstable")
		wasi_snapshot_preview1.NewFunctionExporter().ExportFunctions(wasiBuilder)
		_, err := wasiBuilder.Instantiate(testCtx)
		require.NoError(t, err)

		// Instantiate our test binary, but using the old import names.
		_, err = r.Instantiate(testCtx, exitOnStartUnstableWasm)

		// Ensure the test binary worked. It should return exit code 2.
		require.Equal(t, uint32(2), err.(*sys.ExitError).ExitCode())
	})

	t.Run("override function", func(t *testing.T) {
		r := wazero.NewRuntime(testCtx)
		defer r.Close(testCtx)

		// Export the default WASI functions
		wasiBuilder := r.NewHostModuleBuilder(wasi_snapshot_preview1.ModuleName)
		wasi_snapshot_preview1.NewFunctionExporter().ExportFunctions(wasiBuilder)

		// Override proc_exit to prove the point that you can add or replace
		// functions like this.
		wasiBuilder.NewFunctionBuilder().
			WithFunc(func(ctx context.Context, mod api.Module, exitCode uint32) {
				require.Equal(t, uint32(2), exitCode)
				// ignore the code instead!
				mod.Close(ctx)
			}).Export("proc_exit")

		_, err := wasiBuilder.Instantiate(testCtx)
		require.NoError(t, err)

		// Instantiate our test binary which will use our modified WASI.
		_, err = r.Instantiate(testCtx, exitOnStartWasm)

		// Ensure the modified function was used!
		require.Nil(t, err)
	})
}

// maskMemory sets the first memory in the store to '?' * size, so tests can see what's written.
func maskMemory(t *testing.T, mod api.Module, size int) {
	for i := uint32(0); i < uint32(size); i++ {
		require.True(t, mod.Memory().WriteByte(i, '?'))
	}
}

func requireProxyModule(t *testing.T, config wazero.ModuleConfig) (api.Module, api.Closer, *bytes.Buffer) {
	return requireProxyModuleWithContext(testCtx, t, config)
}

func requireProxyModuleWithContext(ctx context.Context, t *testing.T, config wazero.ModuleConfig) (api.Module, api.Closer, *bytes.Buffer) {
	var log bytes.Buffer

	// Set context to one that has an experimental listener
	ctx = context.WithValue(ctx, experimental.FunctionListenerFactoryKey{},
		proxy.NewLoggingListenerFactory(&log, logging.LogScopeAll))

	r := wazero.NewRuntime(ctx)

	wasiModuleCompiled, err := wasi_snapshot_preview1.NewBuilder(r).Compile(ctx)
	require.NoError(t, err)

	_, err = r.InstantiateModule(ctx, wasiModuleCompiled, config)
	require.NoError(t, err)

	proxyBin := proxy.NewModuleBinary(wasi_snapshot_preview1.ModuleName, wasiModuleCompiled)

	proxyCompiled, err := r.CompileModule(ctx, proxyBin)
	require.NoError(t, err)

	mod, err := r.InstantiateModule(ctx, proxyCompiled, config)
	require.NoError(t, err)

	return mod, r, &log
}

// requireErrnoNosys ensures a call of the given function returns errno. The log
// message returned can verify the output is wasm `-->` or a host `==>`
// function.
func requireErrnoNosys(t *testing.T, funcName string, params ...uint64) string {
	var log bytes.Buffer

	// Set context to one that has an experimental listener
	ctx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{},
		proxy.NewLoggingListenerFactory(&log, logging.LogScopeAll))

	r := wazero.NewRuntime(ctx)
	defer r.Close(ctx)

	// Instantiate the wasi module.
	wasiModuleCompiled, err := wasi_snapshot_preview1.NewBuilder(r).Compile(ctx)
	require.NoError(t, err)

	_, err = r.InstantiateModule(ctx, wasiModuleCompiled, wazero.NewModuleConfig())
	require.NoError(t, err)

	proxyBin := proxy.NewModuleBinary(wasi_snapshot_preview1.ModuleName, wasiModuleCompiled)

	proxyCompiled, err := r.CompileModule(ctx, proxyBin)
	require.NoError(t, err)

	mod, err := r.InstantiateModule(ctx, proxyCompiled, wazero.NewModuleConfig())
	require.NoError(t, err)

	requireErrnoResult(t, wasip1.ErrnoNosys, mod, funcName, params...)
	return "\n" + log.String()
}

func requireErrnoResult(t *testing.T, expectedErrno wasip1.Errno, mod api.Closer, funcName string, params ...uint64) {
	results, err := mod.(api.Module).ExportedFunction(funcName).Call(testCtx, params...)
	require.NoError(t, err)
	errno := wasip1.Errno(results[0])
	require.Equal(t, expectedErrno, errno, "want %s but have %s", wasip1.ErrnoName(expectedErrno), wasip1.ErrnoName(errno))
}

func newBlockingReader(t *testing.T) blockingReader {
	timeout, cancelFunc := context.WithTimeout(testCtx, 5*time.Second)
	t.Cleanup(cancelFunc)
	return blockingReader{ctx: timeout}
}

// blockingReader is an io.Reader that never terminates its read
// unless the embedded context is Done()
type blockingReader struct {
	ctx context.Context
}

// Read implements io.Reader
func (b blockingReader) Read(buf []byte) (n int, err error) {
	<-b.ctx.Done()
	return 0, nil
}
