package binary

import (
	"bytes"
	"fmt"
	"testing"

	"github.com/tetratelabs/wazero/api"
	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
	"github.com/tetratelabs/wazero/internal/testing/require"
	"github.com/tetratelabs/wazero/internal/wasm"
)

func TestFunctionType(t *testing.T) {
	i32, i64, funcRef, externRef := wasm.ValueTypeI32, wasm.ValueTypeI64, wasm.ValueTypeFuncref, wasm.ValueTypeExternref
	tests := []struct {
		name     string
		input    wasm.FunctionType
		expected []byte
	}{
		{
			name:     "empty",
			input:    wasm.FunctionType{},
			expected: []byte{0x60, 0, 0},
		},
		{
			name:     "one param no result",
			input:    wasm.FunctionType{Params: []wasm.ValueType{i32}},
			expected: []byte{0x60, 1, i32.Kind(), 0},
		},
		{
			name:     "no param one result",
			input:    wasm.FunctionType{Results: []wasm.ValueType{i32}},
			expected: []byte{0x60, 0, 1, i32.Kind()},
		},
		{
			name:     "one param one result",
			input:    wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32}},
			expected: []byte{0x60, 1, i64.Kind(), 1, i32.Kind()},
		},
		{
			name:     "two params no result",
			input:    wasm.FunctionType{Params: []wasm.ValueType{i32, i64}},
			expected: []byte{0x60, 2, i32.Kind(), i64.Kind(), 0},
		},
		{
			name:     "two param one result",
			input:    wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32}},
			expected: []byte{0x60, 2, i32.Kind(), i64.Kind(), 1, i32.Kind()},
		},
		{
			name:     "no param two results",
			input:    wasm.FunctionType{Results: []wasm.ValueType{i32, i64}},
			expected: []byte{0x60, 0, 2, i32.Kind(), i64.Kind()},
		},
		{
			name:     "one param two results",
			input:    wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32, i64}},
			expected: []byte{0x60, 1, i64.Kind(), 2, i32.Kind(), i64.Kind()},
		},
		{
			name:     "two param two results",
			input:    wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32, i64}},
			expected: []byte{0x60, 2, i32.Kind(), i64.Kind(), 2, i32.Kind(), i64.Kind()},
		},
		{
			name:     "two param two results with funcrefs",
			input:    wasm.FunctionType{Params: []wasm.ValueType{i32, funcRef}, Results: []wasm.ValueType{funcRef, i64}},
			expected: []byte{0x60, 2, i32.Kind(), funcRef.Kind(), 2, funcRef.Kind(), i64.Kind()},
		},
		{
			name:     "two param two results with externrefs",
			input:    wasm.FunctionType{Params: []wasm.ValueType{i32, externRef}, Results: []wasm.ValueType{externRef, i64}},
			expected: []byte{0x60, 2, i32.Kind(), externRef.Kind(), 2, externRef.Kind(), i64.Kind()},
		},
	}

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

		b := binaryencoding.EncodeFunctionType(&tc.input)
		t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) {
			require.Equal(t, tc.expected, b)
		})

		t.Run(fmt.Sprintf("decode - %s", tc.name), func(t *testing.T) {
			var actual wasm.FunctionType
			err := decodeFunctionType(api.CoreFeaturesV2, bytes.NewReader(b), &actual)
			require.NoError(t, err)
			// Set the FunctionType key on the input.
			_ = tc.input.String()
			require.Equal(t, actual, tc.input)
		})
	}
}

func TestDecodeFunctionType_Errors(t *testing.T) {
	i32, i64 := wasm.ValueTypeI32.Kind(), wasm.ValueTypeI64.Kind()
	tests := []struct {
		name            string
		input           []byte
		enabledFeatures api.CoreFeatures
		expectedErr     string
	}{
		{
			name:        "undefined param no result",
			input:       []byte{0x60, 1, 0x6e, 0},
			expectedErr: "could not read parameter types: invalid value type: 110",
		},
		{
			name:        "no param undefined result",
			input:       []byte{0x60, 0, 1, 0x6e},
			expectedErr: "could not read result types: invalid value type: 110",
		},
		{
			name:        "undefined param undefined result",
			input:       []byte{0x60, 1, 0x6e, 1, 0x6e},
			expectedErr: "could not read parameter types: invalid value type: 110",
		},
		{
			name:        "no param two results - multi-value not enabled",
			input:       []byte{0x60, 0, 2, i32, i64},
			expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled",
		},
		{
			name:        "one param two results - multi-value not enabled",
			input:       []byte{0x60, 1, i64, 2, i32, i64},
			expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled",
		},
		{
			name:        "two param two results - multi-value not enabled",
			input:       []byte{0x60, 2, i32, i64, 2, i32, i64},
			expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled",
		},
	}

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

		t.Run(tc.name, func(t *testing.T) {
			var actual wasm.FunctionType
			err := decodeFunctionType(api.CoreFeaturesV1, bytes.NewReader(tc.input), &actual)
			require.EqualError(t, err, tc.expectedErr)
		})
	}
}
