package wazero

import (
	"context"
	_ "embed"
	"errors"
	"sync"
	"testing"
	"time"

	"github.com/tetratelabs/wazero/api"
	"github.com/tetratelabs/wazero/experimental"
	"github.com/tetratelabs/wazero/internal/filecache"
	"github.com/tetratelabs/wazero/internal/platform"
	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
	"github.com/tetratelabs/wazero/internal/testing/require"
	"github.com/tetratelabs/wazero/internal/wasm"
	"github.com/tetratelabs/wazero/sys"
)

type arbitrary struct{}

var (
	binaryNamedZero = binaryencoding.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "0"}})
	// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
	testCtx = context.WithValue(context.Background(), arbitrary{}, "arbitrary")
)

var _ context.Context = &HostContext{}

// HostContext contain the content will be used in host function call
type HostContext struct {
	Content string
}

func (h *HostContext) Deadline() (deadline time.Time, ok bool) { return }

func (h *HostContext) Done() <-chan struct{} { return nil }

func (h *HostContext) Err() error { return nil }

func (h *HostContext) Value(key interface{}) interface{} { return nil }

func TestRuntime_CompileModule(t *testing.T) {
	tests := []struct {
		name          string
		runtime       Runtime
		wasm          *wasm.Module
		moduleBuilder HostModuleBuilder
		expected      func(CompiledModule)
	}{
		{
			name: "no name section",
			wasm: &wasm.Module{},
		},
		{
			name: "empty NameSection.ModuleName",
			wasm: &wasm.Module{NameSection: &wasm.NameSection{}},
		},
		{
			name: "NameSection.ModuleName",
			wasm: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}},
			expected: func(compiled CompiledModule) {
				require.Equal(t, "test", compiled.Name())
			},
		},
		{
			name: "FunctionSection, but not exported",
			wasm: &wasm.Module{
				TypeSection:     []wasm.FunctionType{{Params: []wasm.ValueType{wasm.ValueTypeI32}}},
				FunctionSection: []wasm.Index{0},
				CodeSection:     []wasm.Code{{Body: []byte{wasm.OpcodeEnd}}},
			},
			expected: func(compiled CompiledModule) {
				require.Nil(t, compiled.ImportedFunctions())
				require.Zero(t, len(compiled.ExportedFunctions()))
			},
		},
		{
			name: "FunctionSection exported",
			wasm: &wasm.Module{
				TypeSection:     []wasm.FunctionType{{Params: []wasm.ValueType{wasm.ValueTypeI32}}},
				FunctionSection: []wasm.Index{0},
				CodeSection:     []wasm.Code{{Body: []byte{wasm.OpcodeEnd}}},
				ExportSection: []wasm.Export{{
					Type:  wasm.ExternTypeFunc,
					Name:  "function",
					Index: 0,
				}},
			},
			expected: func(compiled CompiledModule) {
				require.Nil(t, compiled.ImportedFunctions())
				f := compiled.ExportedFunctions()["function"]
				require.Equal(t, []api.ValueType{api.ValueTypeI32}, f.ParamTypes())
			},
		},
		{
			name: "MemorySection, but not exported",
			wasm: &wasm.Module{
				MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true},
			},
			expected: func(compiled CompiledModule) {
				require.Nil(t, compiled.ImportedMemories())
				require.Zero(t, len(compiled.ExportedMemories()))
			},
		},
		{
			name: "MemorySection exported",
			wasm: &wasm.Module{
				MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true},
				ExportSection: []wasm.Export{{
					Type:  wasm.ExternTypeMemory,
					Name:  "memory",
					Index: 0,
				}},
			},
			expected: func(compiled CompiledModule) {
				require.Nil(t, compiled.ImportedMemories())
				mem := compiled.ExportedMemories()["memory"]
				require.Equal(t, uint32(2), mem.Min())
				max, ok := mem.Max()
				require.Equal(t, uint32(3), max)
				require.True(t, ok)
			},
		},
	}

	_r := NewRuntime(testCtx)
	defer _r.Close(testCtx)

	r := _r.(*runtime)

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

		t.Run(tc.name, func(t *testing.T) {
			bin := binaryencoding.EncodeModule(tc.wasm)

			m, err := r.CompileModule(testCtx, bin)
			require.NoError(t, err)
			if tc.expected == nil {
				tc.expected = func(CompiledModule) {}
			}
			tc.expected(m)
			require.Equal(t, r.store.Engine, m.(*compiledModule).compiledEngine)

			// TypeIDs must be assigned to compiledModule.
			expTypeIDs, err := r.store.GetFunctionTypeIDs(tc.wasm.TypeSection)
			require.NoError(t, err)
			require.Equal(t, expTypeIDs, m.(*compiledModule).typeIDs)
		})
	}
}

func TestRuntime_CompileModule_Errors(t *testing.T) {
	tests := []struct {
		name        string
		wasm        []byte
		expectedErr string
	}{
		{
			name:        "nil",
			expectedErr: "invalid magic number",
		},
		{
			name:        "invalid binary",
			wasm:        append(binaryencoding.Magic, []byte("yolo")...),
			expectedErr: "invalid version header",
		},
		{
			name:        "memory has too many pages",
			wasm:        binaryencoding.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: 70000, IsMaxEncoded: true}}),
			expectedErr: "section memory: max 70000 pages (4 Gi) over limit of 65536 pages (4 Gi)",
		},
	}

	r := NewRuntime(testCtx)
	defer r.Close(testCtx)

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

		t.Run(tc.name, func(t *testing.T) {
			_, err := r.CompileModule(testCtx, tc.wasm)
			require.EqualError(t, err, tc.expectedErr)
		})
	}
}

// TestModule_Memory only covers a couple cases to avoid duplication of internal/wasm/runtime_test.go
func TestModule_Memory(t *testing.T) {
	tests := []struct {
		name        string
		wasm        []byte
		expected    bool
		expectedLen uint32
	}{
		{
			name: "no memory",
			wasm: binaryencoding.EncodeModule(&wasm.Module{}),
		},
		{
			name: "memory exported, one page",
			wasm: binaryencoding.EncodeModule(&wasm.Module{
				MemorySection: &wasm.Memory{Min: 1},
				ExportSection: []wasm.Export{{Name: "memory", Type: api.ExternTypeMemory}},
			}),
			expected:    true,
			expectedLen: 65536,
		},
	}

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

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

			// Instantiate the module and get the export of the above memory
			module, err := r.Instantiate(testCtx, tc.wasm)
			require.NoError(t, err)

			mem := module.ExportedMemory("memory")
			if tc.expected {
				require.Equal(t, tc.expectedLen, mem.Size())
				defs := module.ExportedMemoryDefinitions()
				require.Equal(t, 1, len(defs))
				def := defs["memory"]
				require.Equal(t, tc.expectedLen>>16, def.Min())
			} else {
				require.Nil(t, mem)
				require.Zero(t, len(module.ExportedMemoryDefinitions()))
			}
		})
	}
}

// TestModule_Global only covers a couple cases to avoid duplication of internal/wasm/global_test.go
func TestModule_Global(t *testing.T) {
	globalVal := int64(100) // intentionally a value that differs in signed vs unsigned encoding

	tests := []struct {
		name                      string
		module                    *wasm.Module // module as wat doesn't yet support globals
		expected, expectedMutable bool
	}{
		{
			name:   "no global",
			module: &wasm.Module{},
		},
		{
			name: "global not exported",
			module: &wasm.Module{
				GlobalSection: []wasm.Global{
					{
						Type: wasm.GlobalType{ValType: wasm.ValueTypeI64, Mutable: true},
						Init: wasm.NewConstantExpressionFromI64(globalVal),
					},
				},
			},
		},
		{
			name: "global exported",
			module: &wasm.Module{
				GlobalSection: []wasm.Global{
					{
						Type: wasm.GlobalType{ValType: wasm.ValueTypeI64},
						Init: wasm.NewConstantExpressionFromI64(globalVal),
					},
				},
				Exports: map[string]*wasm.Export{
					"global": {Type: wasm.ExternTypeGlobal, Name: "global"},
				},
			},
			expected: true,
		},
		{
			name: "global exported and mutable",
			module: &wasm.Module{
				GlobalSection: []wasm.Global{
					{
						Type: wasm.GlobalType{ValType: wasm.ValueTypeI64, Mutable: true},
						Init: wasm.NewConstantExpressionFromI64(globalVal),
					},
				},
				Exports: map[string]*wasm.Export{
					"global": {Type: wasm.ExternTypeGlobal, Name: "global"},
				},
			},
			expected:        true,
			expectedMutable: true,
		},
	}

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

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

			code := &compiledModule{module: tc.module}

			err := r.store.Engine.CompileModule(testCtx, code.module, nil, false)
			require.NoError(t, err)

			// Instantiate the module and get the export of the above global
			module, err := r.InstantiateModule(testCtx, code, NewModuleConfig())
			require.NoError(t, err)

			global := module.ExportedGlobal("global")
			if !tc.expected {
				require.Nil(t, global)
				return
			}
			require.Equal(t, uint64(globalVal), global.Get())

			mutable, ok := global.(api.MutableGlobal)
			require.Equal(t, tc.expectedMutable, ok)
			if ok {
				mutable.Set(2)
				require.Equal(t, uint64(2), global.Get())
			}
		})
	}
}

func TestRuntime_InstantiateModule_UsesContext(t *testing.T) {
	r := NewRuntime(testCtx)
	defer r.Close(testCtx)

	// Define a function that will be set as the start function
	var calledStart bool
	start := func(ctx context.Context) {
		calledStart = true
		require.Equal(t, testCtx, ctx)
	}

	_, err := r.NewHostModuleBuilder("env").
		NewFunctionBuilder().WithFunc(start).Export("start").
		Instantiate(testCtx)
	require.NoError(t, err)

	one := uint32(1)
	binary := binaryencoding.EncodeModule(&wasm.Module{
		TypeSection:     []wasm.FunctionType{{}},
		ImportSection:   []wasm.Import{{Module: "env", Name: "start", Type: wasm.ExternTypeFunc, DescFunc: 0}},
		FunctionSection: []wasm.Index{0},
		CodeSection: []wasm.Code{
			{Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.start.
		},
		StartSection: &one,
	})

	code, err := r.CompileModule(testCtx, binary)
	require.NoError(t, err)

	// Instantiate the module, which calls the start function. This will fail if the context wasn't as intended.
	mod, err := r.InstantiateModule(testCtx, code, NewModuleConfig())
	require.NoError(t, err)

	require.True(t, calledStart)

	// Closing the module shouldn't remove the compiler cache
	require.NoError(t, mod.Close(testCtx))
	require.Equal(t, uint32(2), r.(*runtime).store.Engine.CompiledModuleCount())
}

// TestRuntime_Instantiate_DoesntEnforce_Start ensures wapc-go work when modules import WASI, but don't
// export "_start".
func TestRuntime_Instantiate_DoesntEnforce_Start(t *testing.T) {
	r := NewRuntime(testCtx)
	defer r.Close(testCtx)

	binary := binaryencoding.EncodeModule(&wasm.Module{
		MemorySection: &wasm.Memory{Min: 1},
		ExportSection: []wasm.Export{{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0}},
	})

	mod, err := r.Instantiate(testCtx, binary)
	require.NoError(t, err)
	require.NoError(t, mod.Close(testCtx))
}

func TestRuntime_Instantiate_ErrorOnStart(t *testing.T) {
	var indexZero wasm.Index
	tests := []struct {
		name string
		wasm *wasm.Module
	}{
		{
			name: "_start function",
			wasm: &wasm.Module{
				TypeSection:   []wasm.FunctionType{{}},
				ImportSection: []wasm.Import{{Module: "host", Name: "start", Type: wasm.ExternTypeFunc, DescFunc: 0}},
				ExportSection: []wasm.Export{{Name: "_start", Type: wasm.ExternTypeFunc, Index: 0}},
			},
		},
		{
			name: ".start function",
			wasm: &wasm.Module{
				TypeSection:   []wasm.FunctionType{{}},
				ImportSection: []wasm.Import{{Module: "host", Name: "start", Type: wasm.ExternTypeFunc, DescFunc: 0}},
				StartSection:  &indexZero,
			},
		},
	}

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

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

			start := func() {
				panic(errors.New("ice cream"))
			}

			host, err := r.NewHostModuleBuilder("host").
				NewFunctionBuilder().WithFunc(start).Export("start").
				Instantiate(testCtx)
			require.NoError(t, err)

			// Start the module as a WASI command. We expect it to fail with the error from the
			// start function.
			_, err = r.Instantiate(testCtx, binaryencoding.EncodeModule(tc.wasm))
			require.Error(t, err)
			require.Contains(t, err.Error(), "ice cream")

			// Close the imported module, which should remove its compiler cache.
			require.NoError(t, host.Close(testCtx))

			// The compiler cache of the importing module should be removed on error.
			require.Zero(t, r.(*runtime).store.Engine.CompiledModuleCount())
		})
	}
}

// TestRuntime_InstantiateModule_WithName tests that we can pre-validate (cache) a module and instantiate it under
// different names. This pattern is used in wapc-go.
func TestRuntime_InstantiateModule_WithName(t *testing.T) {
	r := NewRuntime(testCtx)
	defer r.Close(testCtx)

	base, err := r.CompileModule(testCtx, binaryNamedZero)
	require.NoError(t, err)

	require.Equal(t, "0", base.(*compiledModule).module.NameSection.ModuleName)

	// Use the same runtime to instantiate multiple modules
	internal := r.(*runtime)
	m1, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("1"))
	require.NoError(t, err)
	require.Equal(t, "1", m1.Name())

	require.Nil(t, internal.Module("0"))
	require.Equal(t, internal.Module("1"), m1)

	m2, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("2"))
	require.NoError(t, err)
	require.Equal(t, "2", m2.Name())

	require.Nil(t, internal.Module("0"))
	require.Equal(t, internal.Module("2"), m2)

	// Empty name module shouldn't be returned via Module() for future optimization.
	m3, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName(""))
	require.NoError(t, err)
	require.Equal(t, "", m3.Name())

	ret := internal.Module("")
	require.Nil(t, ret)
}

func TestRuntime_InstantiateModule_ExitError(t *testing.T) {
	r := NewRuntime(testCtx)
	defer r.Close(testCtx)

	tests := []struct {
		name        string
		exitCode    uint32
		export      bool
		expectedErr error
	}{
		{
			name:        "start: exit code 0",
			exitCode:    0,
			expectedErr: sys.NewExitError(0),
		},
		{
			name:        "start: exit code 2",
			exitCode:    2,
			expectedErr: sys.NewExitError(2),
		},
		{
			name:     "_start: exit code 0",
			exitCode: 0,
			export:   true,
		},
		{
			name:        "_start: exit code 2",
			exitCode:    2,
			export:      true,
			expectedErr: sys.NewExitError(2),
		},
	}

	for _, tt := range tests {
		tc := tt
		t.Run(tc.name, func(t *testing.T) {
			start := func(ctx context.Context, m api.Module) {
				require.NoError(t, m.CloseWithExitCode(ctx, tc.exitCode))
			}

			env, err := r.NewHostModuleBuilder("env").
				NewFunctionBuilder().WithFunc(start).Export("exit").
				Instantiate(testCtx)
			require.NoError(t, err)
			defer env.Close(testCtx)

			mod := &wasm.Module{
				TypeSection:     []wasm.FunctionType{{}},
				ImportSection:   []wasm.Import{{Module: "env", Name: "exit", Type: wasm.ExternTypeFunc, DescFunc: 0}},
				FunctionSection: []wasm.Index{0},
				CodeSection: []wasm.Code{
					{Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.start.
				},
			}
			if tc.export {
				mod.ExportSection = []wasm.Export{
					{Name: "_start", Type: wasm.ExternTypeFunc, Index: 1},
				}
			} else {
				one := uint32(1)
				mod.StartSection = &one
			}
			binary := binaryencoding.EncodeModule(mod)

			// Instantiate the module, which calls the start function.
			m, err := r.InstantiateWithConfig(testCtx, binary,
				NewModuleConfig().WithName("call-exit"))

			// Ensure the exit error propagated and didn't wrap.
			require.Equal(t, tc.expectedErr, err)

			// Ensure calling close again doesn't break
			if err == nil {
				require.NoError(t, m.Close(testCtx))
			}
		})
	}
}

func TestRuntime_CloseWithExitCode(t *testing.T) {
	bin := binaryencoding.EncodeModule(&wasm.Module{
		TypeSection:     []wasm.FunctionType{{}},
		FunctionSection: []wasm.Index{0},
		CodeSection:     []wasm.Code{{Body: []byte{wasm.OpcodeEnd}}},
		ExportSection:   []wasm.Export{{Type: wasm.ExternTypeFunc, Index: 0, Name: "func"}},
	})

	tests := []struct {
		name     string
		exitCode uint32
	}{
		{
			name:     "exit code 0",
			exitCode: uint32(0),
		},
		{
			name:     "exit code 2",
			exitCode: uint32(2),
		},
	}

	for _, tt := range tests {
		tc := tt
		t.Run(tc.name, func(t *testing.T) {
			r := NewRuntime(testCtx)

			code, err := r.CompileModule(testCtx, bin)
			require.NoError(t, err)

			// Instantiate two modules.
			m1, err := r.InstantiateModule(testCtx, code, NewModuleConfig().WithName("mod1"))
			require.NoError(t, err)
			m2, err := r.InstantiateModule(testCtx, code, NewModuleConfig().WithName("mod2"))
			require.NoError(t, err)

			func1 := m1.ExportedFunction("func")
			require.Equal(t, map[string]api.FunctionDefinition{"func": func1.Definition()},
				m1.ExportedFunctionDefinitions())
			func2 := m2.ExportedFunction("func")
			require.Equal(t, map[string]api.FunctionDefinition{"func": func2.Definition()},
				m2.ExportedFunctionDefinitions())

			// Modules not closed so calls succeed

			_, err = func1.Call(testCtx)
			require.NoError(t, err)

			_, err = func2.Call(testCtx)
			require.NoError(t, err)

			if tc.exitCode == 0 {
				err = r.Close(testCtx)
			} else {
				err = r.CloseWithExitCode(testCtx, tc.exitCode)
			}
			require.NoError(t, err)

			// Modules closed so calls fail
			_, err = func1.Call(testCtx)
			require.ErrorIs(t, err, sys.NewExitError(tc.exitCode))

			_, err = func2.Call(testCtx)
			require.ErrorIs(t, err, sys.NewExitError(tc.exitCode))
		})
	}
}

func TestHostFunctionWithCustomContext(t *testing.T) {
	for _, tc := range []struct {
		name   string
		config RuntimeConfig
	}{
		{name: "compiler", config: NewRuntimeConfigCompiler()},
		{name: "interpreter", config: NewRuntimeConfigInterpreter()},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if tc.name == "compiler" && !platform.CompilerSupported() {
				t.Skip("Compiler is not supported on this host")
			}
			const fistString = "hello"
			const secondString = "hello call"
			hostCtx := &HostContext{fistString}
			r := NewRuntimeWithConfig(hostCtx, tc.config)
			defer r.Close(hostCtx)

			// Define a function that will be set as the start function
			var calledStart bool
			var calledCall bool
			start := func(ctx context.Context, module api.Module) {
				hts, ok := ctx.(*HostContext)
				if !ok {
					t.Fatal("decorate call context could effect host ctx cast failed, please consider it.")
				}
				calledStart = true
				require.NotNil(t, hts)
				require.Equal(t, fistString, hts.Content)
			}

			callFunc := func(ctx context.Context, module api.Module) {
				hts, ok := ctx.(*HostContext)
				if !ok {
					t.Fatal("decorate call context could effect host ctx cast failed, please consider it.")
				}
				calledCall = true
				require.NotNil(t, hts)
				require.Equal(t, secondString, hts.Content)
			}

			_, err := r.NewHostModuleBuilder("env").
				NewFunctionBuilder().WithFunc(start).Export("host").
				NewFunctionBuilder().WithFunc(callFunc).Export("host2").
				Instantiate(hostCtx)
			require.NoError(t, err)

			startFnIndex := uint32(2)
			binary := binaryencoding.EncodeModule(&wasm.Module{
				TypeSection: []wasm.FunctionType{{}},
				ImportSection: []wasm.Import{
					{Module: "env", Name: "host", Type: wasm.ExternTypeFunc, DescFunc: 0},
					{Module: "env", Name: "host2", Type: wasm.ExternTypeFunc, DescFunc: 0},
				},
				FunctionSection: []wasm.Index{0, 0},
				CodeSection: []wasm.Code{
					{Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.host.
					{Body: []byte{wasm.OpcodeCall, 1, wasm.OpcodeEnd}}, // Call the imported env.host.
				},
				ExportSection: []wasm.Export{
					{Type: api.ExternTypeFunc, Name: "callHost", Index: uint32(3)},
				},
				StartSection: &startFnIndex,
			})

			// Instantiate the module, which calls the start function. This will fail if the context wasn't as intended.
			ins, err := r.Instantiate(hostCtx, binary)
			require.NoError(t, err)
			require.True(t, calledStart)

			// add the new context content for call with used in host function
			hostCtx.Content = secondString
			_, err = ins.ExportedFunction("callHost").Call(hostCtx)
			require.NoError(t, err)
			require.True(t, calledCall)
		})
	}
}

func TestRuntime_Close_ClosesCompiledModules(t *testing.T) {
	for _, tc := range []struct {
		name                 string
		withCompilationCache bool
	}{
		{name: "with cache", withCompilationCache: true},
		{name: "without cache", withCompilationCache: false},
	} {
		t.Run(tc.name, func(t *testing.T) {
			engine := &mockEngine{name: "mock", cachedModules: map[*wasm.Module]struct{}{}}
			conf := *engineLessConfig
			conf.newEngine = func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine { return engine }
			if tc.withCompilationCache {
				conf.cache = NewCompilationCache()
			}
			r := NewRuntimeWithConfig(testCtx, &conf)
			defer r.Close(testCtx)

			// Normally compiled modules are closed when instantiated but this is never instantiated.
			_, err := r.CompileModule(testCtx, binaryNamedZero)
			require.NoError(t, err)
			require.Equal(t, uint32(1), engine.CompiledModuleCount())

			err = r.Close(testCtx)
			require.NoError(t, err)

			// Closing the runtime should remove the compiler cache if cache is not configured.
			require.Equal(t, !tc.withCompilationCache, engine.closed)
		})
	}
}

// TestRuntime_Closed ensures invocation of closed Runtime's methods is safe.
func TestRuntime_Closed(t *testing.T) {
	for _, tc := range []struct {
		name    string
		errFunc func(r Runtime, mod CompiledModule) error
	}{
		{
			name: "InstantiateModule",
			errFunc: func(r Runtime, mod CompiledModule) error {
				_, err := r.InstantiateModule(testCtx, mod, NewModuleConfig())
				return err
			},
		},
		{
			name: "Instantiate",
			errFunc: func(r Runtime, mod CompiledModule) error {
				_, err := r.Instantiate(testCtx, binaryNamedZero)
				return err
			},
		},
		{
			name: "CompileModule",
			errFunc: func(r Runtime, mod CompiledModule) error {
				_, err := r.CompileModule(testCtx, binaryNamedZero)
				return err
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			engine := &mockEngine{name: "mock", cachedModules: map[*wasm.Module]struct{}{}}
			conf := *engineLessConfig
			conf.newEngine = func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine { return engine }
			r := NewRuntimeWithConfig(testCtx, &conf)
			defer r.Close(testCtx)

			// Normally compiled modules are closed when instantiated but this is never instantiated.
			mod, err := r.CompileModule(testCtx, binaryNamedZero)
			require.NoError(t, err)
			require.Equal(t, uint32(1), engine.CompiledModuleCount())

			err = r.Close(testCtx)
			require.NoError(t, err)

			// Closing the runtime should remove the compiler cache if cache is not configured.
			require.True(t, engine.closed)

			require.EqualError(t, tc.errFunc(r, mod), "runtime closed with exit_code(0)")
		})
	}
}

type mockEngine struct {
	name          string
	cachedModules map[*wasm.Module]struct{}
	closed        bool
}

// CompileModule implements the same method as documented on wasm.Engine.
func (e *mockEngine) CompileModule(_ context.Context, module *wasm.Module, _ []experimental.FunctionListener, _ bool) error {
	e.cachedModules[module] = struct{}{}
	return nil
}

// CompiledModuleCount implements the same method as documented on wasm.Engine.
func (e *mockEngine) CompiledModuleCount() uint32 {
	return uint32(len(e.cachedModules))
}

// DeleteCompiledModule implements the same method as documented on wasm.Engine.
func (e *mockEngine) DeleteCompiledModule(module *wasm.Module) {
	delete(e.cachedModules, module)
}

// NewModuleEngine implements the same method as documented on wasm.Engine.
func (e *mockEngine) NewModuleEngine(_ *wasm.Module, _ *wasm.ModuleInstance) (wasm.ModuleEngine, error) {
	return nil, nil
}

// NewModuleEngine implements the same method as documented on wasm.Close.
func (e *mockEngine) Close() (err error) {
	e.closed = true
	return
}

// TestNewRuntime_concurrent ensures that concurrent execution of NewRuntime is race-free.
// This depends on -race flag.
func TestNewRuntime_concurrent(t *testing.T) {
	const num = 100
	var wg sync.WaitGroup
	c := NewCompilationCache()
	// If available, uses two engine configurations for the single compilation cache.
	configs := [2]RuntimeConfig{NewRuntimeConfigInterpreter().WithCompilationCache(c)}
	if platform.CompilerSupported() {
		configs[1] = NewRuntimeConfigCompiler().WithCompilationCache(c)
	} else {
		configs[1] = NewRuntimeConfigInterpreter().WithCompilationCache(c)
	}
	wg.Add(num)
	for i := 0; i < num; i++ {
		i := i
		go func() {
			defer wg.Done()
			r := NewRuntimeWithConfig(testCtx, configs[i%2])
			err := r.Close(testCtx)
			require.NoError(t, err)
		}()
	}
	wg.Wait()
}
