package emscripten

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

	"github.com/tetratelabs/wazero"
	"github.com/tetratelabs/wazero/experimental"
	"github.com/tetratelabs/wazero/experimental/logging"
	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
	internal "github.com/tetratelabs/wazero/internal/emscripten"
	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
	"github.com/tetratelabs/wazero/internal/testing/require"
	"github.com/tetratelabs/wazero/internal/wasm"
)

const (
	i64 = wasm.ValueTypeI64
	f32 = wasm.ValueTypeF32
	f64 = wasm.ValueTypeF64
)

// growWasm was compiled from testdata/grow.cc
//
//go:embed testdata/grow.wasm
var growWasm []byte

// invokeWasm was generated by the following:
//
//	cd testdata; wasm-tools parse invoke.wat -o invoke.wasm
//
//go:embed testdata/invoke.wasm
var invokeWasm []byte

type arbitrary struct{}

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

// TestGrow is an integration test until we have an Emscripten example.
func TestGrow(t *testing.T) {
	var log bytes.Buffer

	// Set context to one that has an experimental listener
	ctx := experimental.WithFunctionListenerFactory(testCtx,
		logging.NewHostLoggingListenerFactory(&log, logging.LogScopeMemory))

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

	wasi_snapshot_preview1.MustInstantiate(ctx, r)

	_, err := Instantiate(ctx, r)
	require.NoError(t, err)

	// Emscripten exits main with zero by default, which coerces to nul.
	_, err = r.Instantiate(ctx, growWasm)
	require.Nil(t, err)

	// We expect the memory no-op memory growth hook to be invoked as wasm.
	require.Contains(t, log.String(), "==> env.emscripten_notify_memory_growth(memory_index=0)")
}

func TestNewFunctionExporterForModule(t *testing.T) {
	tests := []struct {
		name     string
		input    *wasm.Module
		expected emscriptenFns
	}{
		{
			name:     "empty",
			input:    &wasm.Module{},
			expected: emscriptenFns{},
		},
		{
			name: internal.FunctionNotifyMemoryGrowth,
			input: &wasm.Module{
				TypeSection: []wasm.FunctionType{
					{Params: []wasm.ValueType{i32}},
				},
				ImportSection: []wasm.Import{
					{
						Module: "env", Name: internal.FunctionNotifyMemoryGrowth,
						Type:     wasm.ExternTypeFunc,
						DescFunc: 0,
					},
				},
			},
			expected: []*wasm.HostFunc{internal.NotifyMemoryGrowth},
		},
		{
			name: "all result types",
			input: &wasm.Module{
				TypeSection: []wasm.FunctionType{
					{Params: []wasm.ValueType{i32}},
					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}},
					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i64}},
					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{f32}},
					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{f64}},
				},
				ImportSection: []wasm.Import{
					{
						Module: "env", Name: "invoke_v",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 0,
					},
					{
						Module: "env", Name: "invoke_i",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 1,
					},
					{
						Module: "env", Name: "invoke_p",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 1,
					},
					{
						Module: "env", Name: "invoke_j",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 2,
					},
					{
						Module: "env", Name: "invoke_f",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 3,
					},
					{
						Module: "env", Name: "invoke_d",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 4,
					},
				},
			},
			expected: []*wasm.HostFunc{
				{
					ExportName: "invoke_v",
					ParamTypes: []wasm.ValueType{i32},
					ParamNames: []string{"index"},
					Code:       wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}},
				},
				{
					ExportName:  "invoke_i",
					ParamTypes:  []wasm.ValueType{i32},
					ParamNames:  []string{"index"},
					ResultTypes: []wasm.ValueType{i32},
					Code:        wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []wasm.ValueType{i32}}}},
				},
				{
					ExportName:  "invoke_p",
					ParamTypes:  []wasm.ValueType{i32},
					ParamNames:  []string{"index"},
					ResultTypes: []wasm.ValueType{i32},
					Code:        wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []wasm.ValueType{i32}}}},
				},
				{
					ExportName:  "invoke_j",
					ParamTypes:  []wasm.ValueType{i32},
					ParamNames:  []string{"index"},
					ResultTypes: []wasm.ValueType{i64},
					Code:        wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []wasm.ValueType{i64}}}},
				},
				{
					ExportName:  "invoke_f",
					ParamTypes:  []wasm.ValueType{i32},
					ParamNames:  []string{"index"},
					ResultTypes: []wasm.ValueType{f32},
					Code:        wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []wasm.ValueType{f32}}}},
				},
				{
					ExportName:  "invoke_d",
					ParamTypes:  []wasm.ValueType{i32},
					ParamNames:  []string{"index"},
					ResultTypes: []wasm.ValueType{f64},
					Code:        wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []wasm.ValueType{f64}}}},
				},
			},
		},
		{
			name: "ignores other imports",
			input: &wasm.Module{
				TypeSection: []wasm.FunctionType{
					{Params: []wasm.ValueType{i32}},
				},
				ImportSection: []wasm.Import{
					{
						Module: "anv", Name: "invoke_v",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 0,
					},
					{
						Module: "env", Name: "invoke_v",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 0,
					},
					{
						Module: "env", Name: "grow",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 0,
					},
				},
			},
			expected: []*wasm.HostFunc{
				{
					ExportName: "invoke_v",
					ParamTypes: []wasm.ValueType{i32},
					ParamNames: []string{"index"},
					Code:       wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}},
				},
			},
		},
		{
			name: "invoke_v and " + internal.FunctionNotifyMemoryGrowth,
			input: &wasm.Module{
				TypeSection: []wasm.FunctionType{{Params: []wasm.ValueType{i32}}},
				ImportSection: []wasm.Import{
					{
						Module: "env", Name: "invoke_v",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 0,
					},
					{
						Module: "env", Name: internal.FunctionNotifyMemoryGrowth,
						Type:     wasm.ExternTypeFunc,
						DescFunc: 0,
					},
				},
			},
			expected: []*wasm.HostFunc{
				{
					ExportName: "invoke_v",
					ParamTypes: []wasm.ValueType{i32},
					ParamNames: []string{"index"},
					Code:       wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}},
				},
				internal.NotifyMemoryGrowth,
			},
		},
		{
			name: "invoke_vi",
			input: &wasm.Module{
				TypeSection: []wasm.FunctionType{
					{Params: []wasm.ValueType{i32, i32}},
				},
				ImportSection: []wasm.Import{
					{
						Module: "env", Name: "invoke_vi",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 0,
					},
				},
			},
			expected: []*wasm.HostFunc{
				{
					ExportName: "invoke_vi",
					ParamTypes: []wasm.ValueType{i32, i32},
					ParamNames: []string{"index", "a1"},
					Code:       wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Params: []wasm.ValueType{i32}}}},
				},
			},
		},
		{
			name: "invoke_iiiii",
			input: &wasm.Module{
				TypeSection: []wasm.FunctionType{
					{
						Params:  []wasm.ValueType{i32, i32, i32, i32, i32},
						Results: []wasm.ValueType{i32},
					},
				},
				ImportSection: []wasm.Import{
					{
						Module: "env", Name: "invoke_iiiii",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 0,
					},
				},
			},
			expected: []*wasm.HostFunc{
				{
					ExportName:  "invoke_iiiii",
					ParamTypes:  []wasm.ValueType{i32, i32, i32, i32, i32},
					ParamNames:  []string{"index", "a1", "a2", "a3", "a4"},
					ResultTypes: []wasm.ValueType{i32},
					Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{
						Params:  []wasm.ValueType{i32, i32, i32, i32},
						Results: []wasm.ValueType{i32},
					}}},
				},
			},
		},
		{
			name: "invoke_viiiddiiiiii",
			input: &wasm.Module{
				TypeSection: []wasm.FunctionType{
					{
						Params: []wasm.ValueType{i32, i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32},
					},
				},
				ImportSection: []wasm.Import{
					{
						Module: "env", Name: "invoke_viiiddiiiiii",
						Type:     wasm.ExternTypeFunc,
						DescFunc: 0,
					},
				},
			},
			expected: []*wasm.HostFunc{
				{
					ExportName: "invoke_viiiddiiiiii",
					ParamTypes: []wasm.ValueType{i32, i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32},
					ParamNames: []string{"index", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10", "a11"},
					Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{
						Params: []wasm.ValueType{i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32},
					}}},
				},
			},
		},
	}

	for _, tt := range tests {
		tc := tt

		t.Run(tc.name, func(t *testing.T) {
			r := wazero.NewRuntime(testCtx)
			defer r.Close(testCtx)

			guest, err := r.CompileModule(testCtx, binaryencoding.EncodeModule(tc.input))
			require.NoError(t, err)

			exporter, err := NewFunctionExporterForModule(guest)
			require.NoError(t, err)
			actual := exporter.(emscriptenFns)

			require.Equal(t, len(tc.expected), len(actual))
			for i, expected := range tc.expected {
				require.Equal(t, expected, actual[i], actual[i].ExportName)
			}
		})
	}
}

func TestInstantiateForModule(t *testing.T) {
	var log bytes.Buffer

	// Set context to one that has an experimental listener
	ctx := experimental.WithFunctionListenerFactory(testCtx, logging.NewLoggingListenerFactory(&log))

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

	compiled, err := r.CompileModule(ctx, invokeWasm)
	require.NoError(t, err)

	_, err = InstantiateForModule(ctx, r, compiled)
	require.NoError(t, err)

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

	tests := []struct {
		name, funcName          string
		tableOffset             int
		params, expectedResults []uint64
		expectedLog             string
	}{
		{
			name:            "invoke_i",
			funcName:        "call_v_i32",
			expectedResults: []uint64{42},
			expectedLog: `--> .call_v_i32(0)
	==> env.invoke_i(index=0)
		--> .stackSave()
		<-- 65536
		--> .v_i32()
		<-- 42
	<== 42
<-- 42
`,
		},
		{
			name:            "invoke_ii",
			funcName:        "call_i32_i32",
			tableOffset:     2,
			params:          []uint64{42},
			expectedResults: []uint64{42},
			expectedLog: `--> .call_i32_i32(2,42)
	==> env.invoke_ii(index=2,a1=42)
		--> .stackSave()
		<-- 65536
		--> .i32_i32(42)
		<-- 42
	<== 42
<-- 42
`,
		},
		{
			name:            "invoke_iii",
			funcName:        "call_i32i32_i32",
			tableOffset:     4,
			params:          []uint64{1, 2},
			expectedResults: []uint64{3},
			expectedLog: `--> .call_i32i32_i32(4,1,2)
	==> env.invoke_iii(index=4,a1=1,a2=2)
		--> .stackSave()
		<-- 65536
		--> .i32i32_i32(1,2)
		<-- 3
	<== 3
<-- 3
`,
		},
		{
			name:            "invoke_iiii",
			funcName:        "call_i32i32i32_i32",
			tableOffset:     6,
			params:          []uint64{1, 2, 4},
			expectedResults: []uint64{7},
			expectedLog: `--> .call_i32i32i32_i32(6,1,2,4)
	==> env.invoke_iiii(index=6,a1=1,a2=2,a3=4)
		--> .stackSave()
		<-- 65536
		--> .i32i32i32_i32(1,2,4)
		<-- 7
	<== 7
<-- 7
`,
		},
		{
			name:            "invoke_iiiii",
			funcName:        "calli32_i32i32i32i32_i32",
			tableOffset:     8,
			params:          []uint64{1, 2, 4, 8},
			expectedResults: []uint64{15},
			expectedLog: `--> .calli32_i32i32i32i32_i32(8,1,2,4,8)
	==> env.invoke_iiiii(index=8,a1=1,a2=2,a3=4,a4=8)
		--> .stackSave()
		<-- 65536
		--> .i32i32i32i32_i32(1,2,4,8)
		<-- 15
	<== 15
<-- 15
`,
		},
		{
			name:        "invoke_v",
			funcName:    "call_v_v",
			tableOffset: 10,
			expectedLog: `--> .call_v_v(10)
	==> env.invoke_v(index=10)
		--> .stackSave()
		<-- 65536
		--> .v_v()
		<--
	<==
<--
`,
		},
		{
			name:        "invoke_vi",
			funcName:    "call_i32_v",
			tableOffset: 12,
			params:      []uint64{42},
			expectedLog: `--> .call_i32_v(12,42)
	==> env.invoke_vi(index=12,a1=42)
		--> .stackSave()
		<-- 65536
		--> .i32_v(42)
		<--
	<==
<--
`,
		},
		{
			name:        "invoke_vii",
			funcName:    "call_i32i32_v",
			tableOffset: 14,
			params:      []uint64{1, 2},
			expectedLog: `--> .call_i32i32_v(14,1,2)
	==> env.invoke_vii(index=14,a1=1,a2=2)
		--> .stackSave()
		<-- 65536
		--> .i32i32_v(1,2)
		<--
	<==
<--
`,
		},
		{
			name:        "invoke_viii",
			funcName:    "call_i32i32i32_v",
			tableOffset: 16,
			params:      []uint64{1, 2, 4},
			expectedLog: `--> .call_i32i32i32_v(16,1,2,4)
	==> env.invoke_viii(index=16,a1=1,a2=2,a3=4)
		--> .stackSave()
		<-- 65536
		--> .i32i32i32_v(1,2,4)
		<--
	<==
<--
`,
		},
		{
			name:        "invoke_viiii",
			funcName:    "calli32_i32i32i32i32_v",
			tableOffset: 18,
			params:      []uint64{1, 2, 4, 8},
			expectedLog: `--> .calli32_i32i32i32i32_v(18,1,2,4,8)
	==> env.invoke_viiii(index=18,a1=1,a2=2,a3=4,a4=8)
		--> .stackSave()
		<-- 65536
		--> .i32i32i32i32_v(1,2,4,8)
		<--
	<==
<--
`,
		},
		{
			name:        "invoke_v_with_longjmp",
			funcName:    "call_invoke_v_with_longjmp_throw",
			tableOffset: 20,
			params:      []uint64{},
			expectedLog: `--> .call_invoke_v_with_longjmp_throw(20)
	==> env.invoke_v(index=20)
		--> .stackSave()
		<-- 42
		--> .call_longjmp_throw()
			==> env._emscripten_throw_longjmp()
		--> .stackRestore(42)
		<--
		--> .setThrew(1,0)
		<--
	<==
<--
`,
		},
	}

	for _, tt := range tests {
		tc := tt

		t.Run(tc.name, func(t *testing.T) {
			defer log.Reset()

			params := tc.params
			params = append([]uint64{uint64(tc.tableOffset)}, params...)

			results, err := mod.ExportedFunction(tc.funcName).Call(testCtx, params...)
			require.NoError(t, err)
			l := len(tc.expectedResults)
			require.Equal(t, l, len(results))
			if l > 0 {
				require.Equal(t, tc.expectedResults, results)
			}

			// We expect to see the dynamic function call target
			require.Equal(t, tc.expectedLog, log.String())

			// We expect an unreachable function to err
			params[0]++
			_, err = mod.ExportedFunction(tc.funcName).Call(testCtx, params...)
			require.Error(t, err)
		})
	}
}
