package modbus

import (
	"fmt"
	"strconv"
	"strings"
	"testing"
	"time"

	mb "github.com/grid-x/modbus"
	"github.com/stretchr/testify/require"
	"github.com/tbrandon/mbserver"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/metric"
	"github.com/influxdata/telegraf/testutil"
)

func TestRequest(t *testing.T) {
	modbus := Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		ConfigurationType: "request",
		Log:               testutil.Logger{},
	}
	modbus.Requests = []requestDefinition{
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "coil",
			Fields: []requestFieldDefinition{
				{
					Name:    "coil-0",
					Address: uint16(0),
				},
				{
					Name:    "coil-1",
					Address: uint16(1),
					Omit:    true,
				},
				{
					Name:        "coil-2",
					Address:     uint16(2),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "UINT16",
					Measurement: "modbus",
				},
				{
					Name:        "coil-3",
					Address:     uint16(3),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "BOOL",
					Measurement: "modbus",
				},
			},
		},
		{
			SlaveID:      1,
			RegisterType: "coil",
			Fields: []requestFieldDefinition{
				{
					Name:    "coil-4",
					Address: uint16(6),
				},
				{
					Name:    "coil-5",
					Address: uint16(7),
					Omit:    true,
				},
				{
					Name:        "coil-6",
					Address:     uint16(8),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "UINT16",
					Measurement: "modbus",
				},
				{
					Name:        "coil-7",
					Address:     uint16(9),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "BOOL",
					Measurement: "modbus",
				},
			},
		},
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "discrete",
			Fields: []requestFieldDefinition{
				{
					Name:    "discrete-0",
					Address: uint16(0),
				},
				{
					Name:    "discrete-1",
					Address: uint16(1),
					Omit:    true,
				},
				{
					Name:        "discrete-2",
					Address:     uint16(2),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "UINT16",
					Measurement: "modbus",
				},
				{
					Name:        "discrete-3",
					Address:     uint16(3),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "BOOL",
					Measurement: "modbus",
				},
			},
		},
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "holding",
			Fields: []requestFieldDefinition{
				{
					Name:      "holding-0",
					Address:   uint16(0),
					InputType: "INT16",
				},
				{
					Name:      "holding-1",
					Address:   uint16(1),
					InputType: "UINT16",
					Omit:      true,
				},
				{
					Name:        "holding-2",
					Address:     uint16(2),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "FLOAT64",
					Measurement: "modbus",
				},
			},
		},
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "input",
			Fields: []requestFieldDefinition{
				{
					Name:      "input-0",
					Address:   uint16(0),
					InputType: "INT16",
				},
				{
					Name:      "input-1",
					Address:   uint16(1),
					InputType: "UINT16",
					Omit:      true,
				},
				{
					Name:        "input-2",
					Address:     uint16(2),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "FLOAT64",
					Measurement: "modbus",
				},
			},
		},
	}

	require.NoError(t, modbus.Init())
	require.NotEmpty(t, modbus.requests)
	require.NotNil(t, modbus.requests[1])
	require.Len(t, modbus.requests[1].coil, 2)
	require.Len(t, modbus.requests[1].discrete, 1)
	require.Len(t, modbus.requests[1].holding, 1)
	require.Len(t, modbus.requests[1].input, 1)
}

func TestRequestWithTags(t *testing.T) {
	modbus := Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		ConfigurationType: "request",
		Log:               testutil.Logger{},
	}
	modbus.Requests = []requestDefinition{
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "coil",
			Fields: []requestFieldDefinition{
				{
					Name:    "coil-0",
					Address: uint16(0),
				},
				{
					Name:    "coil-1",
					Address: uint16(1),
					Omit:    true,
				},
				{
					Name:        "coil-2",
					Address:     uint16(2),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "UINT16",
					Measurement: "modbus",
				},
			},
			Tags: map[string]string{
				"first":  "a",
				"second": "bb",
				"third":  "ccc",
			},
		},
		{
			SlaveID:      1,
			RegisterType: "coil",
			Fields: []requestFieldDefinition{
				{
					Name:    "coil-3",
					Address: uint16(6),
				},
				{
					Name:    "coil-4",
					Address: uint16(7),
					Omit:    true,
				},
				{
					Name:        "coil-5",
					Address:     uint16(8),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "UINT16",
					Measurement: "modbus",
				},
			},
			Tags: map[string]string{
				"first":  "a",
				"second": "bb",
				"third":  "ccc",
			},
		},
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "discrete",
			Fields: []requestFieldDefinition{
				{
					Name:    "discrete-0",
					Address: uint16(0),
				},
				{
					Name:    "discrete-1",
					Address: uint16(1),
					Omit:    true,
				},
				{
					Name:        "discrete-2",
					Address:     uint16(2),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "UINT16",
					Measurement: "modbus",
				},
			},
			Tags: map[string]string{
				"first":  "a",
				"second": "bb",
				"third":  "ccc",
			},
		},
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "holding",
			Fields: []requestFieldDefinition{
				{
					Name:      "holding-0",
					Address:   uint16(0),
					InputType: "INT16",
				},
				{
					Name:      "holding-1",
					Address:   uint16(1),
					InputType: "UINT16",
					Omit:      true,
				},
				{
					Name:        "holding-2",
					Address:     uint16(2),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "FLOAT64",
					Measurement: "modbus",
				},
			},
			Tags: map[string]string{
				"first":  "a",
				"second": "bb",
				"third":  "ccc",
			},
		},
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "input",
			Fields: []requestFieldDefinition{
				{
					Name:      "input-0",
					Address:   uint16(0),
					InputType: "INT16",
				},
				{
					Name:      "input-1",
					Address:   uint16(1),
					InputType: "UINT16",
					Omit:      true,
				},
				{
					Name:        "input-2",
					Address:     uint16(2),
					InputType:   "INT64",
					Scale:       1.2,
					OutputType:  "FLOAT64",
					Measurement: "modbus",
				},
			},
			Tags: map[string]string{
				"first":  "a",
				"second": "bb",
				"third":  "ccc",
			},
		},
	}

	require.NoError(t, modbus.Init())
	require.NotEmpty(t, modbus.requests)
	require.NotNil(t, modbus.requests[1])
	require.Len(t, modbus.requests[1].coil, 2)
	require.Len(t, modbus.requests[1].discrete, 1)
	require.Len(t, modbus.requests[1].holding, 1)
	require.Len(t, modbus.requests[1].input, 1)

	expectedTags := map[string]string{
		"first":  "a",
		"second": "bb",
		"third":  "ccc",
	}
	require.Equal(t, expectedTags, modbus.requests[1].coil[0].fields[0].tags)
	require.Equal(t, expectedTags, modbus.requests[1].coil[1].fields[0].tags)
	require.Equal(t, expectedTags, modbus.requests[1].discrete[0].fields[0].tags)
	require.Equal(t, expectedTags, modbus.requests[1].holding[0].fields[0].tags)
	require.Equal(t, expectedTags, modbus.requests[1].input[0].fields[0].tags)
}

func TestRequestTypesCoil(t *testing.T) {
	tests := []struct {
		name        string
		address     uint16
		dataTypeOut string
		write       uint16
		read        interface{}
	}{
		{
			name:    "coil-1-off",
			address: 1,
			write:   0,
			read:    uint16(0),
		},
		{
			name:    "coil-2-on",
			address: 2,
			write:   0xFF00,
			read:    uint16(1),
		},
		{
			name:        "coil-3-false",
			address:     3,
			dataTypeOut: "BOOL",
			write:       0,
			read:        false,
		},
		{
			name:        "coil-4-true",
			address:     4,
			dataTypeOut: "BOOL",
			write:       0xFF00,
			read:        true,
		},
	}

	serv := mbserver.NewServer()
	require.NoError(t, serv.ListenTCP("localhost:1502"))
	defer serv.Close()

	handler := mb.NewTCPClientHandler("localhost:1502")
	require.NoError(t, handler.Connect())
	defer handler.Close()
	client := mb.NewClient(handler)

	for _, hrt := range tests {
		t.Run(hrt.name, func(t *testing.T) {
			_, err := client.WriteSingleCoil(hrt.address, hrt.write)
			require.NoError(t, err)

			modbus := Modbus{
				Name:              "TestRequestTypesCoil",
				Controller:        "tcp://localhost:1502",
				ConfigurationType: "request",
				Log:               testutil.Logger{},
			}
			modbus.Requests = []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "coil",
					Fields: []requestFieldDefinition{
						{
							Name:       hrt.name,
							OutputType: hrt.dataTypeOut,
							Address:    hrt.address,
						},
					},
				},
			}

			expected := []telegraf.Metric{
				metric.New(
					"modbus",
					map[string]string{
						"type":     cCoils,
						"slave_id": "1",
						"name":     modbus.Name,
					},
					map[string]interface{}{hrt.name: hrt.read},
					time.Unix(0, 0),
				),
			}

			var acc testutil.Accumulator
			require.NoError(t, modbus.Init())
			require.NotEmpty(t, modbus.requests)
			require.NoError(t, modbus.Gather(&acc))
			acc.Wait(len(expected))

			testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
		})
	}
}

func TestRequestTypesHoldingABCD(t *testing.T) {
	byteOrder := "ABCD"
	tests := []struct {
		name        string
		address     uint16
		bit         uint8
		length      uint16
		byteOrder   string
		dataTypeIn  string
		dataTypeOut string
		scale       float64
		write       []byte
		read        interface{}
	}{
		{
			name:       "register5_bit3",
			address:    5,
			dataTypeIn: "BIT",
			bit:        3,
			write:      []byte{0x18, 0x0d},
			read:       uint8(1),
		},
		{
			name:       "register5_bit14",
			address:    5,
			dataTypeIn: "BIT",
			bit:        14,
			write:      []byte{0x18, 0x0d},
			read:       uint8(0),
		},
		{
			name:       "register10_uint8L",
			address:    10,
			dataTypeIn: "UINT8L",
			write:      []byte{0x18, 0x0d},
			read:       uint8(13),
		},
		{
			name:       "register10_uint8L-scale_.1",
			address:    10,
			dataTypeIn: "UINT8L",
			scale:      .1,
			write:      []byte{0x18, 0x0d},
			read:       float64(1.3),
		},
		{
			name:       "register10_uint8L_scale_10",
			address:    10,
			dataTypeIn: "UINT8L",
			scale:      10,
			write:      []byte{0x18, 0x0d},
			read:       float64(130),
		},
		{
			name:        "register10_uint8L_uint64",
			address:     10,
			dataTypeIn:  "UINT8L",
			dataTypeOut: "UINT64",
			write:       []byte{0x18, 0x0d},
			read:        uint64(13),
		},
		{
			name:        "register10_uint8L_int64",
			address:     10,
			dataTypeIn:  "UINT8L",
			dataTypeOut: "INT64",
			write:       []byte{0x18, 0x0d},
			read:        int64(13),
		},
		{
			name:        "register10_uint8L_float64",
			address:     10,
			dataTypeIn:  "UINT8L",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x18, 0x0d},
			read:        float64(13),
		},
		{
			name:       "register10_uint8L_float64_scale",
			address:    10,
			dataTypeIn: "UINT8L",
			scale:      1.0,
			write:      []byte{0x18, 0x0d},
			read:       float64(13),
		},
		{
			name:       "register15_int8L",
			address:    15,
			dataTypeIn: "UINT8L",
			write:      []byte{0x18, 0x0d},
			read:       uint8(13),
		},
		{
			name:       "register15_int8L-scale_.1",
			address:    15,
			dataTypeIn: "INT8L",
			scale:      .1,
			write:      []byte{0x18, 0x0d},
			read:       float64(1.3),
		},
		{
			name:       "register15_int8L_scale_10",
			address:    15,
			dataTypeIn: "INT8L",
			scale:      10,
			write:      []byte{0x18, 0x0d},
			read:       float64(130),
		},
		{
			name:        "register15_int8L_uint64",
			address:     15,
			dataTypeIn:  "INT8L",
			dataTypeOut: "UINT64",
			write:       []byte{0x18, 0x0d},
			read:        uint64(13),
		},
		{
			name:        "register15_int8L_int64",
			address:     15,
			dataTypeIn:  "INT8L",
			dataTypeOut: "INT64",
			write:       []byte{0x18, 0x0d},
			read:        int64(13),
		},
		{
			name:        "register15_int8L_float64",
			address:     15,
			dataTypeIn:  "INT8L",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x18, 0x0d},
			read:        float64(13),
		},
		{
			name:       "register15_int8L_float64_scale",
			address:    15,
			dataTypeIn: "INT8L",
			scale:      1.0,
			write:      []byte{0x18, 0x0d},
			read:       float64(13),
		},
		{
			name:       "register20_uint16",
			address:    20,
			dataTypeIn: "UINT16",
			write:      []byte{0x08, 0x98},
			read:       uint16(2200),
		},
		{
			name:       "register20_uint16-scale_.1",
			address:    20,
			dataTypeIn: "UINT16",
			scale:      .1,
			write:      []byte{0x08, 0x98},
			read:       float64(220),
		},
		{
			name:       "register20_uint16_scale_10",
			address:    20,
			dataTypeIn: "UINT16",
			scale:      10,
			write:      []byte{0x08, 0x98},
			read:       float64(22000),
		},
		{
			name:        "register20_uint16_uint64",
			address:     20,
			dataTypeIn:  "UINT16",
			dataTypeOut: "UINT64",
			write:       []byte{0x08, 0x98},
			read:        uint64(2200),
		},
		{
			name:        "register20_uint16_int64",
			address:     20,
			dataTypeIn:  "UINT16",
			dataTypeOut: "INT64",
			write:       []byte{0x08, 0x98},
			read:        int64(2200),
		},
		{
			name:        "register20_uint16_float64",
			address:     20,
			dataTypeIn:  "UINT16",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x08, 0x98},
			read:        float64(2200),
		},
		{
			name:       "register20_uint16_float64_scale",
			address:    20,
			dataTypeIn: "UINT16",
			scale:      1.0,
			write:      []byte{0x08, 0x98},
			read:       float64(2200),
		},
		{
			name:       "register30_int16",
			address:    30,
			dataTypeIn: "INT16",
			write:      []byte{0xf8, 0x98},
			read:       int16(-1896),
		},
		{
			name:       "register30_int16-scale_.1",
			address:    30,
			dataTypeIn: "INT16",
			scale:      .1,
			write:      []byte{0xf8, 0x98},
			read:       float64(-189.60000000000002),
		},
		{
			name:       "register30_int16_scale_10",
			address:    30,
			dataTypeIn: "INT16",
			scale:      10,
			write:      []byte{0xf8, 0x98},
			read:       float64(-18960),
		},
		{
			name:        "register30_int16_uint64",
			address:     30,
			dataTypeIn:  "INT16",
			dataTypeOut: "UINT64",
			write:       []byte{0xf8, 0x98},
			read:        uint64(18446744073709549720),
		},
		{
			name:        "register30_int16_int64",
			address:     30,
			dataTypeIn:  "INT16",
			dataTypeOut: "INT64",
			write:       []byte{0xf8, 0x98},
			read:        int64(-1896),
		},
		{
			name:        "register30_int16_float64",
			address:     30,
			dataTypeIn:  "INT16",
			dataTypeOut: "FLOAT64",
			write:       []byte{0xf8, 0x98},
			read:        float64(-1896),
		},
		{
			name:       "register30_int16_float64_scale",
			address:    30,
			dataTypeIn: "INT16",
			scale:      1.0,
			write:      []byte{0xf8, 0x98},
			read:       float64(-1896),
		},
		{
			name:       "register40_uint32",
			address:    40,
			dataTypeIn: "UINT32",
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:       uint32(168496141),
		},
		{
			name:       "register40_uint32-scale_.1",
			address:    40,
			dataTypeIn: "UINT32",
			scale:      .1,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:       float64(16849614.1),
		},
		{
			name:       "register40_uint32_scale_10",
			address:    40,
			dataTypeIn: "UINT32",
			scale:      10,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:       float64(1684961410),
		},
		{
			name:        "register40_uint32_uint64",
			address:     40,
			dataTypeIn:  "UINT32",
			dataTypeOut: "UINT64",
			write:       []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:        uint64(168496141),
		},
		{
			name:        "register40_uint32_int64",
			address:     40,
			dataTypeIn:  "UINT32",
			dataTypeOut: "INT64",
			write:       []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:        int64(168496141),
		},
		{
			name:        "register40_uint32_float64",
			address:     40,
			dataTypeIn:  "UINT32",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:        float64(168496141),
		},
		{
			name:       "register40_uint32_float64_scale",
			address:    40,
			dataTypeIn: "UINT32",
			scale:      1.0,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:       float64(168496141),
		},
		{
			name:       "register50_int32",
			address:    50,
			dataTypeIn: "INT32",
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:       int32(-99939315),
		},
		{
			name:       "register50_int32-scale_.1",
			address:    50,
			dataTypeIn: "INT32",
			scale:      .1,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:       float64(-9993931.5),
		},
		{
			name:       "register50_int32_scale_10",
			address:    50,
			dataTypeIn: "INT32",
			scale:      10,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:       float64(-999393150),
		},
		{
			name:        "register50_int32_uint64",
			address:     50,
			dataTypeIn:  "INT32",
			dataTypeOut: "UINT64",
			write:       []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:        uint64(18446744073609612301),
		},
		{
			name:        "register50_int32_int64",
			address:     50,
			dataTypeIn:  "INT32",
			dataTypeOut: "INT64",
			write:       []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:        int64(-99939315),
		},
		{
			name:        "register50_int32_float64",
			address:     50,
			dataTypeIn:  "INT32",
			dataTypeOut: "FLOAT64",
			write:       []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:        float64(-99939315),
		},
		{
			name:       "register50_int32_float64_scale",
			address:    50,
			dataTypeIn: "INT32",
			scale:      1.0,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:       float64(-99939315),
		},
		{
			name:       "register60_uint64",
			address:    60,
			dataTypeIn: "UINT64",
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       uint64(723685415333069058),
		},
		{
			name:       "register60_uint64-scale_.1",
			address:    60,
			dataTypeIn: "UINT64",
			scale:      .1,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(72368541533306905.8),
		},
		{
			name:       "register60_uint64_scale_10",
			address:    60,
			dataTypeIn: "UINT64",
			scale:      10,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(7236854153330690000), // quantization error
		},
		{
			name:        "register60_uint64_int64",
			address:     60,
			dataTypeIn:  "UINT64",
			dataTypeOut: "INT64",
			write:       []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:        int64(723685415333069058),
		},
		{
			name:        "register60_uint64_float64",
			address:     60,
			dataTypeIn:  "UINT64",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:        float64(723685415333069058),
		},
		{
			name:       "register60_uint64_float64_scale",
			address:    60,
			dataTypeIn: "UINT64",
			scale:      1.0,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(723685415333069058),
		},
		{
			name:       "register70_int64",
			address:    70,
			dataTypeIn: "INT64",
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       int64(-429236089273777918),
		},
		{
			name:       "register70_int64-scale_.1",
			address:    70,
			dataTypeIn: "INT64",
			scale:      .1,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(-42923608927377791.8),
		},
		{
			name:       "register70_int64_scale_10",
			address:    70,
			dataTypeIn: "INT64",
			scale:      10,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(-4292360892737779180),
		},
		{
			name:        "register70_int64_uint64",
			address:     70,
			dataTypeIn:  "INT64",
			dataTypeOut: "UINT64",
			write:       []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:        uint64(18017507984435773698),
		},
		{
			name:        "register70_int64_float64",
			address:     70,
			dataTypeIn:  "INT64",
			dataTypeOut: "FLOAT64",
			write:       []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:        float64(-429236089273777918),
		},
		{
			name:       "register70_int64_float64_scale",
			address:    70,
			dataTypeIn: "INT64",
			scale:      1.0,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(-429236089273777918),
		},
		{
			name:       "register80_float32",
			address:    80,
			dataTypeIn: "FLOAT32",
			write:      []byte{0x40, 0x49, 0x0f, 0xdb},
			read:       float32(3.1415927410125732421875),
		},
		{
			name:       "register80_float32-scale_.1",
			address:    80,
			dataTypeIn: "FLOAT32",
			scale:      .1,
			write:      []byte{0x40, 0x49, 0x0f, 0xdb},
			read:       float64(0.31415927410125732421875),
		},
		{
			name:       "register80_float32_scale_10",
			address:    80,
			dataTypeIn: "FLOAT32",
			scale:      10,
			write:      []byte{0x40, 0x49, 0x0f, 0xdb},
			read:       float64(31.415927410125732421875),
		},
		{
			name:        "register80_float32_float64",
			address:     80,
			dataTypeIn:  "FLOAT32",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x40, 0x49, 0x0f, 0xdb},
			read:        float64(3.1415927410125732421875),
		},
		{
			name:       "register80_float32_float64_scale",
			address:    80,
			dataTypeIn: "FLOAT32",
			scale:      1.0,
			write:      []byte{0x40, 0x49, 0x0f, 0xdb},
			read:       float64(3.1415927410125732421875),
		},
		{
			name:       "register90_float64",
			address:    90,
			dataTypeIn: "FLOAT64",
			write:      []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
			read:       float64(3.14159265359000006156975359772),
		},
		{
			name:       "register90_float64-scale_.1",
			address:    90,
			dataTypeIn: "FLOAT64",
			scale:      .1,
			write:      []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
			read:       float64(0.314159265359000006156975359772),
		},
		{
			name:       "register90_float64_scale_10",
			address:    90,
			dataTypeIn: "FLOAT64",
			scale:      10,
			write:      []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
			read:       float64(31.4159265359000006156975359772),
		},
		{
			name:       "register90_float64_float64_scale",
			address:    90,
			dataTypeIn: "FLOAT64",
			scale:      1.0,
			write:      []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
			read:       float64(3.14159265359000006156975359772),
		},
		{
			name:       "register100_float16",
			address:    100,
			dataTypeIn: "FLOAT16",
			write:      []byte{0xb8, 0x14},
			read:       float64(-0.509765625),
		},
		{
			name:       "register100_float16-scale_.1",
			address:    100,
			dataTypeIn: "FLOAT16",
			scale:      .1,
			write:      []byte{0xb8, 0x14},
			read:       float64(-0.0509765625),
		},
		{
			name:       "register100_float16_scale_10",
			address:    100,
			dataTypeIn: "FLOAT16",
			scale:      10,
			write:      []byte{0xb8, 0x14},
			read:       float64(-5.09765625),
		},
		{
			name:       "register100_float16_float64_scale",
			address:    100,
			dataTypeIn: "FLOAT16",
			scale:      1.0,
			write:      []byte{0xb8, 0x14},
			read:       float64(-0.509765625),
		},
		{
			name:       "register110_string",
			address:    110,
			dataTypeIn: "STRING",
			length:     7,
			write:      []byte{0x4d, 0x6f, 0x64, 0x62, 0x75, 0x73, 0x20, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x00},
			read:       "Modbus String",
		},
	}

	serv := mbserver.NewServer()
	require.NoError(t, serv.ListenTCP("localhost:1502"))
	defer serv.Close()

	handler := mb.NewTCPClientHandler("localhost:1502")
	require.NoError(t, handler.Connect())
	defer handler.Close()
	client := mb.NewClient(handler)

	for _, hrt := range tests {
		t.Run(hrt.name, func(t *testing.T) {
			quantity := uint16(len(hrt.write) / 2)
			_, err := client.WriteMultipleRegisters(hrt.address, quantity, hrt.write)
			require.NoError(t, err)

			modbus := Modbus{
				Name:              "TestRequestTypesHoldingABCD",
				Controller:        "tcp://localhost:1502",
				ConfigurationType: "request",
				Log:               testutil.Logger{},
			}
			modbus.Requests = []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    byteOrder,
					RegisterType: "holding",
					Fields: []requestFieldDefinition{
						{
							Name:       hrt.name,
							InputType:  hrt.dataTypeIn,
							OutputType: hrt.dataTypeOut,
							Scale:      hrt.scale,
							Address:    hrt.address,
							Length:     hrt.length,
							Bit:        hrt.bit,
						},
					},
				},
			}

			expected := []telegraf.Metric{
				metric.New(
					"modbus",
					map[string]string{
						"type":     cHoldingRegisters,
						"slave_id": "1",
						"name":     modbus.Name,
					},
					map[string]interface{}{hrt.name: hrt.read},
					time.Unix(0, 0),
				),
			}

			var acc testutil.Accumulator
			require.NoError(t, modbus.Init())
			require.NotEmpty(t, modbus.requests)
			require.NoError(t, modbus.Gather(&acc))
			acc.Wait(len(expected))

			testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
		})
	}
}

func TestRequestTypesHoldingDCBA(t *testing.T) {
	byteOrder := "DCBA"
	tests := []struct {
		name        string
		address     uint16
		length      uint16
		byteOrder   string
		dataTypeIn  string
		dataTypeOut string
		scale       float64
		write       []byte
		read        interface{}
	}{
		{
			name:       "register10_uint8L",
			address:    10,
			dataTypeIn: "UINT8L",
			write:      []byte{0x18, 0x0d},
			read:       uint8(13),
		},
		{
			name:       "register10_uint8L-scale_.1",
			address:    10,
			dataTypeIn: "UINT8L",
			scale:      .1,
			write:      []byte{0x18, 0x0d},
			read:       float64(1.3),
		},
		{
			name:       "register10_uint8L_scale_10",
			address:    10,
			dataTypeIn: "UINT8L",
			scale:      10,
			write:      []byte{0x18, 0x0d},
			read:       float64(130),
		},
		{
			name:        "register10_uint8L_uint64",
			address:     10,
			dataTypeIn:  "UINT8L",
			dataTypeOut: "UINT64",
			write:       []byte{0x18, 0x0d},
			read:        uint64(13),
		},
		{
			name:        "register10_uint8L_int64",
			address:     10,
			dataTypeIn:  "UINT8L",
			dataTypeOut: "INT64",
			write:       []byte{0x18, 0x0d},
			read:        int64(13),
		},
		{
			name:        "register10_uint8L_float64",
			address:     10,
			dataTypeIn:  "UINT8L",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x18, 0x0d},
			read:        float64(13),
		},
		{
			name:       "register10_uint8L_float64_scale",
			address:    10,
			dataTypeIn: "UINT8L",
			scale:      1.0,
			write:      []byte{0x18, 0x0d},
			read:       float64(13),
		},
		{
			name:       "register15_int8L",
			address:    15,
			dataTypeIn: "UINT8L",
			write:      []byte{0x18, 0x0d},
			read:       uint8(13),
		},
		{
			name:       "register15_int8L-scale_.1",
			address:    15,
			dataTypeIn: "INT8L",
			scale:      .1,
			write:      []byte{0x18, 0x0d},
			read:       float64(1.3),
		},
		{
			name:       "register15_int8L_scale_10",
			address:    15,
			dataTypeIn: "INT8L",
			scale:      10,
			write:      []byte{0x18, 0x0d},
			read:       float64(130),
		},
		{
			name:        "register15_int8L_uint64",
			address:     15,
			dataTypeIn:  "INT8L",
			dataTypeOut: "UINT64",
			write:       []byte{0x18, 0x0d},
			read:        uint64(13),
		},
		{
			name:        "register15_int8L_int64",
			address:     15,
			dataTypeIn:  "INT8L",
			dataTypeOut: "INT64",
			write:       []byte{0x18, 0x0d},
			read:        int64(13),
		},
		{
			name:        "register15_int8L_float64",
			address:     15,
			dataTypeIn:  "INT8L",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x18, 0x0d},
			read:        float64(13),
		},
		{
			name:       "register15_int8L_float64_scale",
			address:    15,
			dataTypeIn: "INT8L",
			scale:      1.0,
			write:      []byte{0x18, 0x0d},
			read:       float64(13),
		},
		{
			name:       "register20_uint16",
			address:    20,
			dataTypeIn: "UINT16",
			write:      []byte{0x08, 0x98},
			read:       uint16(2200),
		},
		{
			name:       "register20_uint16-scale_.1",
			address:    20,
			dataTypeIn: "UINT16",
			scale:      .1,
			write:      []byte{0x08, 0x98},
			read:       float64(220),
		},
		{
			name:       "register20_uint16_scale_10",
			address:    20,
			dataTypeIn: "UINT16",
			scale:      10,
			write:      []byte{0x08, 0x98},
			read:       float64(22000),
		},
		{
			name:        "register20_uint16_uint64",
			address:     20,
			dataTypeIn:  "UINT16",
			dataTypeOut: "UINT64",
			write:       []byte{0x08, 0x98},
			read:        uint64(2200),
		},
		{
			name:        "register20_uint16_int64",
			address:     20,
			dataTypeIn:  "UINT16",
			dataTypeOut: "INT64",
			write:       []byte{0x08, 0x98},
			read:        int64(2200),
		},
		{
			name:        "register20_uint16_float64",
			address:     20,
			dataTypeIn:  "UINT16",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x08, 0x98},
			read:        float64(2200),
		},
		{
			name:       "register20_uint16_float64_scale",
			address:    20,
			dataTypeIn: "UINT16",
			scale:      1.0,
			write:      []byte{0x08, 0x98},
			read:       float64(2200),
		},
		{
			name:       "register30_int16",
			address:    30,
			dataTypeIn: "INT16",
			write:      []byte{0xf8, 0x98},
			read:       int16(-1896),
		},
		{
			name:       "register30_int16-scale_.1",
			address:    30,
			dataTypeIn: "INT16",
			scale:      .1,
			write:      []byte{0xf8, 0x98},
			read:       float64(-189.60000000000002),
		},
		{
			name:       "register30_int16_scale_10",
			address:    30,
			dataTypeIn: "INT16",
			scale:      10,
			write:      []byte{0xf8, 0x98},
			read:       float64(-18960),
		},
		{
			name:        "register30_int16_uint64",
			address:     30,
			dataTypeIn:  "INT16",
			dataTypeOut: "UINT64",
			write:       []byte{0xf8, 0x98},
			read:        uint64(18446744073709549720),
		},
		{
			name:        "register30_int16_int64",
			address:     30,
			dataTypeIn:  "INT16",
			dataTypeOut: "INT64",
			write:       []byte{0xf8, 0x98},
			read:        int64(-1896),
		},
		{
			name:        "register30_int16_float64",
			address:     30,
			dataTypeIn:  "INT16",
			dataTypeOut: "FLOAT64",
			write:       []byte{0xf8, 0x98},
			read:        float64(-1896),
		},
		{
			name:       "register30_int16_float64_scale",
			address:    30,
			dataTypeIn: "INT16",
			scale:      1.0,
			write:      []byte{0xf8, 0x98},
			read:       float64(-1896),
		},
		{
			name:       "register40_uint32",
			address:    40,
			dataTypeIn: "UINT32",
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:       uint32(168496141),
		},
		{
			name:       "register40_uint32-scale_.1",
			address:    40,
			dataTypeIn: "UINT32",
			scale:      .1,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:       float64(16849614.1),
		},
		{
			name:       "register40_uint32_scale_10",
			address:    40,
			dataTypeIn: "UINT32",
			scale:      10,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:       float64(1684961410),
		},
		{
			name:        "register40_uint32_uint64",
			address:     40,
			dataTypeIn:  "UINT32",
			dataTypeOut: "UINT64",
			write:       []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:        uint64(168496141),
		},
		{
			name:        "register40_uint32_int64",
			address:     40,
			dataTypeIn:  "UINT32",
			dataTypeOut: "INT64",
			write:       []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:        int64(168496141),
		},
		{
			name:        "register40_uint32_float64",
			address:     40,
			dataTypeIn:  "UINT32",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:        float64(168496141),
		},
		{
			name:       "register40_uint32_float64_scale",
			address:    40,
			dataTypeIn: "UINT32",
			scale:      1.0,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d},
			read:       float64(168496141),
		},
		{
			name:       "register50_int32",
			address:    50,
			dataTypeIn: "INT32",
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:       int32(-99939315),
		},
		{
			name:       "register50_int32-scale_.1",
			address:    50,
			dataTypeIn: "INT32",
			scale:      .1,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:       float64(-9993931.5),
		},
		{
			name:       "register50_int32_scale_10",
			address:    50,
			dataTypeIn: "INT32",
			scale:      10,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:       float64(-999393150),
		},
		{
			name:        "register50_int32_uint64",
			address:     50,
			dataTypeIn:  "INT32",
			dataTypeOut: "UINT64",
			write:       []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:        uint64(18446744073609612301),
		},
		{
			name:        "register50_int32_int64",
			address:     50,
			dataTypeIn:  "INT32",
			dataTypeOut: "INT64",
			write:       []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:        int64(-99939315),
		},
		{
			name:        "register50_int32_float64",
			address:     50,
			dataTypeIn:  "INT32",
			dataTypeOut: "FLOAT64",
			write:       []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:        float64(-99939315),
		},
		{
			name:       "register50_int32_float64_scale",
			address:    50,
			dataTypeIn: "INT32",
			scale:      1.0,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d},
			read:       float64(-99939315),
		},
		{
			name:       "register60_uint64",
			address:    60,
			dataTypeIn: "UINT64",
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       uint64(723685415333069058),
		},
		{
			name:       "register60_uint64-scale_.1",
			address:    60,
			dataTypeIn: "UINT64",
			scale:      .1,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(72368541533306905.8),
		},
		{
			name:       "register60_uint64_scale_10",
			address:    60,
			dataTypeIn: "UINT64",
			scale:      10,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(7236854153330690000), // quantization error
		},
		{
			name:        "register60_uint64_int64",
			address:     60,
			dataTypeIn:  "UINT64",
			dataTypeOut: "INT64",
			write:       []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:        int64(723685415333069058),
		},
		{
			name:        "register60_uint64_float64",
			address:     60,
			dataTypeIn:  "UINT64",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:        float64(723685415333069058),
		},
		{
			name:       "register60_uint64_float64_scale",
			address:    60,
			dataTypeIn: "UINT64",
			scale:      1.0,
			write:      []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(723685415333069058),
		},
		{
			name:       "register70_int64",
			address:    70,
			dataTypeIn: "INT64",
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       int64(-429236089273777918),
		},
		{
			name:       "register70_int64-scale_.1",
			address:    70,
			dataTypeIn: "INT64",
			scale:      .1,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(-42923608927377791.8),
		},
		{
			name:       "register70_int64_scale_10",
			address:    70,
			dataTypeIn: "INT64",
			scale:      10,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(-4292360892737779180),
		},
		{
			name:        "register70_int64_uint64",
			address:     70,
			dataTypeIn:  "INT64",
			dataTypeOut: "UINT64",
			write:       []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:        uint64(18017507984435773698),
		},
		{
			name:        "register70_int64_float64",
			address:     70,
			dataTypeIn:  "INT64",
			dataTypeOut: "FLOAT64",
			write:       []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:        float64(-429236089273777918),
		},
		{
			name:       "register70_int64_float64_scale",
			address:    70,
			dataTypeIn: "INT64",
			scale:      1.0,
			write:      []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
			read:       float64(-429236089273777918),
		},
		{
			name:       "register80_float32",
			address:    80,
			dataTypeIn: "FLOAT32",
			write:      []byte{0x40, 0x49, 0x0f, 0xdb},
			read:       float32(3.1415927410125732421875),
		},
		{
			name:       "register80_float32-scale_.1",
			address:    80,
			dataTypeIn: "FLOAT32",
			scale:      .1,
			write:      []byte{0x40, 0x49, 0x0f, 0xdb},
			read:       float64(0.31415927410125732421875),
		},
		{
			name:       "register80_float32_scale_10",
			address:    80,
			dataTypeIn: "FLOAT32",
			scale:      10,
			write:      []byte{0x40, 0x49, 0x0f, 0xdb},
			read:       float64(31.415927410125732421875),
		},
		{
			name:        "register80_float32_float64",
			address:     80,
			dataTypeIn:  "FLOAT32",
			dataTypeOut: "FLOAT64",
			write:       []byte{0x40, 0x49, 0x0f, 0xdb},
			read:        float64(3.1415927410125732421875),
		},
		{
			name:       "register80_float32_float64_scale",
			address:    80,
			dataTypeIn: "FLOAT32",
			scale:      1.0,
			write:      []byte{0x40, 0x49, 0x0f, 0xdb},
			read:       float64(3.1415927410125732421875),
		},
		{
			name:       "register90_float64",
			address:    90,
			dataTypeIn: "FLOAT64",
			write:      []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
			read:       float64(3.14159265359000006156975359772),
		},
		{
			name:       "register90_float64-scale_.1",
			address:    90,
			dataTypeIn: "FLOAT64",
			scale:      .1,
			write:      []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
			read:       float64(0.314159265359000006156975359772),
		},
		{
			name:       "register90_float64_scale_10",
			address:    90,
			dataTypeIn: "FLOAT64",
			scale:      10,
			write:      []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
			read:       float64(31.4159265359000006156975359772),
		},
		{
			name:       "register90_float64_float64_scale",
			address:    90,
			dataTypeIn: "FLOAT64",
			scale:      1.0,
			write:      []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
			read:       float64(3.14159265359000006156975359772),
		},
		{
			name:       "register100_float16",
			address:    100,
			dataTypeIn: "FLOAT16",
			write:      []byte{0xb8, 0x14},
			read:       float64(-0.509765625),
		},
		{
			name:       "register100_float16-scale_.1",
			address:    100,
			dataTypeIn: "FLOAT16",
			scale:      .1,
			write:      []byte{0xb8, 0x14},
			read:       float64(-0.0509765625),
		},
		{
			name:       "register100_float16_scale_10",
			address:    100,
			dataTypeIn: "FLOAT16",
			scale:      10,
			write:      []byte{0xb8, 0x14},
			read:       float64(-5.09765625),
		},
		{
			name:       "register100_float16_float64_scale",
			address:    100,
			dataTypeIn: "FLOAT16",
			scale:      1.0,
			write:      []byte{0xb8, 0x14},
			read:       float64(-0.509765625),
		},
		{
			name:       "register110_string",
			address:    110,
			dataTypeIn: "STRING",
			length:     7,
			write:      []byte{0x6f, 0x4d, 0x62, 0x64, 0x73, 0x75, 0x53, 0x20, 0x72, 0x74, 0x6e, 0x69, 0x00, 0x67},
			read:       "Modbus String",
		},
	}

	serv := mbserver.NewServer()
	require.NoError(t, serv.ListenTCP("localhost:1502"))
	defer serv.Close()

	handler := mb.NewTCPClientHandler("localhost:1502")
	require.NoError(t, handler.Connect())
	defer handler.Close()
	client := mb.NewClient(handler)

	for _, hrt := range tests {
		t.Run(hrt.name, func(t *testing.T) {
			quantity := uint16(len(hrt.write) / 2)
			invert := make([]byte, 0, len(hrt.write))
			if hrt.dataTypeIn != "STRING" {
				for i := len(hrt.write) - 1; i >= 0; i-- {
					invert = append(invert, hrt.write[i])
				}
			} else {
				// Put in raw data for strings
				invert = append(invert, hrt.write...)
			}
			_, err := client.WriteMultipleRegisters(hrt.address, quantity, invert)
			require.NoError(t, err)

			modbus := Modbus{
				Name:              "TestRequestTypesHoldingDCBA",
				Controller:        "tcp://localhost:1502",
				ConfigurationType: "request",
				Log:               testutil.Logger{},
			}
			modbus.Requests = []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    byteOrder,
					RegisterType: "holding",
					Fields: []requestFieldDefinition{
						{
							Name:       hrt.name,
							InputType:  hrt.dataTypeIn,
							OutputType: hrt.dataTypeOut,
							Scale:      hrt.scale,
							Address:    hrt.address,
							Length:     hrt.length,
						},
					},
				},
			}

			expected := []telegraf.Metric{
				metric.New(
					"modbus",
					map[string]string{
						"type":     cHoldingRegisters,
						"slave_id": "1",
						"name":     modbus.Name,
					},
					map[string]interface{}{hrt.name: hrt.read},
					time.Unix(0, 0),
				),
			}

			var acc testutil.Accumulator
			require.NoError(t, modbus.Init())
			require.NotEmpty(t, modbus.requests)
			require.NoError(t, modbus.Gather(&acc))
			acc.Wait(len(expected))

			testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
		})
	}
}

func TestRequestFail(t *testing.T) {
	tests := []struct {
		name     string
		requests []requestDefinition
		errormsg string
	}{
		{
			name: "empty field name (coil)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "coil",
					Fields: []requestFieldDefinition{
						{
							Address: uint16(15),
						},
					},
				},
			},
			errormsg: "empty field name in request for slave 1",
		},
		{
			name: "invalid byte-order (coil)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "AB",
					RegisterType: "coil",
				},
			},
			errormsg: "unknown byte-order \"AB\"",
		},
		{
			name: "duplicate fields (coil)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "coil",
					Fields: []requestFieldDefinition{
						{
							Name:    "coil-0",
							Address: uint16(0),
						},
						{
							Name:    "coil-0",
							Address: uint16(1),
						},
					},
				},
			},
			errormsg: "field \"coil-0\" duplicated in measurement \"modbus\" (slave 1/\"coil\")",
		},
		{
			name: "duplicate fields multiple requests (coil)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "coil",
					Fields: []requestFieldDefinition{
						{
							Name:        "coil-0",
							Address:     uint16(0),
							Measurement: "foo",
						},
					},
				},
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "coil",
					Fields: []requestFieldDefinition{
						{
							Name:        "coil-0",
							Address:     uint16(0),
							Measurement: "foo",
						},
					},
				},
			},
			errormsg: "field \"coil-0\" duplicated in measurement \"foo\" (slave 1/\"coil\")",
		},
		{
			name: "invalid byte-order (discrete)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "AB",
					RegisterType: "discrete",
				},
			},
			errormsg: "unknown byte-order \"AB\"",
		},
		{
			name: "duplicate fields (discrete)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "discrete",
					Fields: []requestFieldDefinition{
						{
							Name:    "discrete-0",
							Address: uint16(0),
						},
						{
							Name:    "discrete-0",
							Address: uint16(1),
						},
					},
				},
			},
			errormsg: "field \"discrete-0\" duplicated in measurement \"modbus\" (slave 1/\"discrete\")",
		},
		{
			name: "duplicate fields multiple requests (discrete)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "discrete",
					Fields: []requestFieldDefinition{
						{
							Name:        "discrete-0",
							Address:     uint16(0),
							Measurement: "foo",
						},
					},
				},
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "discrete",
					Fields: []requestFieldDefinition{
						{
							Name:        "discrete-0",
							Address:     uint16(0),
							Measurement: "foo",
						},
					},
				},
			},
			errormsg: "field \"discrete-0\" duplicated in measurement \"foo\" (slave 1/\"discrete\")",
		},
		{
			name: "invalid byte-order (holding)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "AB",
					RegisterType: "holding",
				},
			},
			errormsg: "unknown byte-order \"AB\"",
		},
		{
			name: "invalid field name (holding)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					RegisterType: "holding",
					Fields: []requestFieldDefinition{
						{
							Address: uint16(0),
						},
					},
				},
			},
			errormsg: "empty field name in request for slave 1",
		},
		{
			name: "invalid field input type (holding)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					RegisterType: "holding",
					Fields: []requestFieldDefinition{
						{
							Name:    "holding-0",
							Address: uint16(0),
						},
					},
				},
			},
			errormsg: "initializing field \"holding-0\" failed: invalid input datatype \"\" for determining field length",
		},
		{
			name: "invalid field output type (holding)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					RegisterType: "holding",
					Fields: []requestFieldDefinition{
						{
							Name:       "holding-0",
							Address:    uint16(0),
							InputType:  "UINT16",
							OutputType: "UINT8",
						},
					},
				},
			},
			errormsg: `unknown output data-type "UINT8" for field "holding-0"`,
		},
		{
			name: "duplicate fields (holding)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "holding",
					Fields: []requestFieldDefinition{
						{
							Name:    "holding-0",
							Address: uint16(0),
						},
						{
							Name:    "holding-0",
							Address: uint16(1),
						},
					},
				},
			},
			errormsg: "field \"holding-0\" duplicated in measurement \"modbus\" (slave 1/\"holding\")",
		},
		{
			name: "duplicate fields multiple requests (holding)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "holding",
					Fields: []requestFieldDefinition{
						{
							Name:        "holding-0",
							Address:     uint16(0),
							Measurement: "foo",
						},
					},
				},
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "holding",
					Fields: []requestFieldDefinition{
						{
							Name:        "holding-0",
							Address:     uint16(0),
							Measurement: "foo",
						},
					},
				},
			},
			errormsg: "field \"holding-0\" duplicated in measurement \"foo\" (slave 1/\"holding\")",
		},
		{
			name: "invalid byte-order (input)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "AB",
					RegisterType: "input",
				},
			},
			errormsg: "unknown byte-order \"AB\"",
		},
		{
			name: "invalid field name (input)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					RegisterType: "input",
					Fields: []requestFieldDefinition{
						{
							Address: uint16(0),
						},
					},
				},
			},
			errormsg: "empty field name in request for slave 1",
		},
		{
			name: "invalid field input type (input)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					RegisterType: "input",
					Fields: []requestFieldDefinition{
						{
							Name:    "input-0",
							Address: uint16(0),
						},
					},
				},
			},
			errormsg: "initializing field \"input-0\" failed: invalid input datatype \"\" for determining field length",
		},
		{
			name: "invalid field output type (input)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					RegisterType: "input",
					Fields: []requestFieldDefinition{
						{
							Name:       "input-0",
							Address:    uint16(0),
							InputType:  "UINT16",
							OutputType: "UINT8",
						},
					},
				},
			},
			errormsg: `unknown output data-type "UINT8" for field "input-0"`,
		},
		{
			name: "duplicate fields (input)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "input",
					Fields: []requestFieldDefinition{
						{
							Name:    "input-0",
							Address: uint16(0),
						},
						{
							Name:    "input-0",
							Address: uint16(1),
						},
					},
				},
			},
			errormsg: "field \"input-0\" duplicated in measurement \"modbus\" (slave 1/\"input\")",
		},
		{
			name: "duplicate fields multiple requests (input)",
			requests: []requestDefinition{
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "input",
					Fields: []requestFieldDefinition{
						{
							Name:        "input-0",
							Address:     uint16(0),
							Measurement: "foo",
						},
					},
				},
				{
					SlaveID:      1,
					ByteOrder:    "ABCD",
					RegisterType: "input",
					Fields: []requestFieldDefinition{
						{
							Name:        "input-0",
							Address:     uint16(0),
							Measurement: "foo",
						},
					},
				},
			},
			errormsg: "field \"input-0\" duplicated in measurement \"foo\" (slave 1/\"input\")",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			plugin := Modbus{
				Name:              "Test",
				Controller:        "tcp://localhost:1502",
				ConfigurationType: "request",
				Log:               testutil.Logger{},
			}
			plugin.Requests = tt.requests

			require.ErrorContains(t, plugin.Init(), tt.errormsg)
			require.Empty(t, plugin.requests)
		})
	}
}

func TestRequestStartingWithOmits(t *testing.T) {
	modbus := Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		ConfigurationType: "request",
		Log:               testutil.Logger{},
	}
	modbus.Requests = []requestDefinition{
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "holding",
			Fields: []requestFieldDefinition{
				{
					Name:      "holding-0",
					Address:   uint16(0),
					InputType: "INT16",
					Omit:      true,
				},
				{
					Name:      "holding-1",
					Address:   uint16(1),
					InputType: "UINT16",
					Omit:      true,
				},
				{
					Name:      "holding-2",
					Address:   uint16(2),
					InputType: "INT16",
				},
			},
		},
	}
	require.NoError(t, modbus.Init())
	require.NotEmpty(t, modbus.requests)
	require.NotNil(t, modbus.requests[1])
	require.Equal(t, uint16(0), modbus.requests[1].holding[0].address)

	serv := mbserver.NewServer()
	require.NoError(t, serv.ListenTCP("localhost:1502"))
	defer serv.Close()

	handler := mb.NewTCPClientHandler("localhost:1502")
	require.NoError(t, handler.Connect())
	defer handler.Close()
	client := mb.NewClient(handler)
	_, err := client.WriteMultipleRegisters(uint16(0), 3, []byte{0x00, 0x01, 0x00, 0x02, 0x00, 0x03})
	require.NoError(t, err)

	expected := []telegraf.Metric{
		metric.New(
			"modbus",
			map[string]string{
				"type":     cHoldingRegisters,
				"slave_id": strconv.Itoa(int(modbus.Requests[0].SlaveID)),
				"name":     modbus.Name,
			},
			map[string]interface{}{"holding-2": int16(3)},
			time.Unix(0, 0),
		),
	}

	var acc testutil.Accumulator
	require.NoError(t, modbus.Gather(&acc))
	acc.Wait(len(expected))
	testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
}

func TestRequestWithOmittedFieldsOnly(t *testing.T) {
	modbus := Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		ConfigurationType: "request",
		Log:               testutil.Logger{},
	}
	modbus.Requests = []requestDefinition{
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "holding",
			Fields: []requestFieldDefinition{
				{
					Name:      "holding-0",
					Address:   uint16(0),
					InputType: "INT16",
					Omit:      true,
				},
				{
					Name:      "holding-1",
					Address:   uint16(1),
					InputType: "UINT16",
					Omit:      true,
				},
				{
					Name:      "holding-2",
					Address:   uint16(2),
					InputType: "INT16",
					Omit:      true,
				},
			},
		},
	}
	require.NoError(t, modbus.Init())
	require.Empty(t, modbus.requests)
}

func TestRequestGroupWithOmittedFieldsOnly(t *testing.T) {
	modbus := Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		ConfigurationType: "request",
		Log:               testutil.Logger{},
	}
	modbus.Requests = []requestDefinition{
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "holding",
			Fields: []requestFieldDefinition{
				{
					Name:      "holding-0",
					Address:   uint16(0),
					InputType: "INT16",
					Omit:      true,
				},
				{
					Name:      "holding-1",
					Address:   uint16(1),
					InputType: "UINT16",
					Omit:      true,
				},
				{
					Name:      "holding-2",
					Address:   uint16(2),
					InputType: "INT16",
					Omit:      true,
				},
				{
					Name:      "holding-8",
					Address:   uint16(8),
					InputType: "INT16",
				},
			},
		},
	}
	require.NoError(t, modbus.Init())
	require.Len(t, modbus.requests, 1)
	require.NotNil(t, modbus.requests[1])
	require.Len(t, modbus.requests[1].holding, 1)
	require.Equal(t, uint16(8), modbus.requests[1].holding[0].address)
	require.Equal(t, uint16(1), modbus.requests[1].holding[0].length)
}

func TestRequestEmptyFields(t *testing.T) {
	modbus := Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		ConfigurationType: "request",
		Log:               testutil.Logger{},
	}
	modbus.Requests = []requestDefinition{
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "holding",
		},
	}
	err := modbus.Init()
	require.ErrorContains(t, err, `found request section without fields`)
}

func TestRequestMultipleSlavesOneFail(t *testing.T) {
	modbus := Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		Retries:           1,
		ConfigurationType: "request",
		Log:               testutil.Logger{},
	}
	modbus.Requests = []requestDefinition{
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "holding",
			Fields: []requestFieldDefinition{
				{
					Name:      "holding-0",
					Address:   uint16(0),
					InputType: "INT16",
				},
			},
		},
		{
			SlaveID:      2,
			ByteOrder:    "ABCD",
			RegisterType: "holding",
			Fields: []requestFieldDefinition{
				{
					Name:      "holding-0",
					Address:   uint16(0),
					InputType: "INT16",
				},
			},
		},
		{
			SlaveID:      3,
			ByteOrder:    "ABCD",
			RegisterType: "holding",
			Fields: []requestFieldDefinition{
				{
					Name:      "holding-0",
					Address:   uint16(0),
					InputType: "INT16",
				},
			},
		},
	}
	require.NoError(t, modbus.Init())

	serv := mbserver.NewServer()
	require.NoError(t, serv.ListenTCP("localhost:1502"))
	defer serv.Close()

	serv.RegisterFunctionHandler(3,
		func(_ *mbserver.Server, frame mbserver.Framer) ([]byte, *mbserver.Exception) {
			tcpframe, ok := frame.(*mbserver.TCPFrame)
			if !ok {
				return nil, &mbserver.IllegalFunction
			}

			if tcpframe.Device == 2 {
				// Simulate device 2 being unavailable
				return nil, &mbserver.GatewayTargetDeviceFailedtoRespond
			}
			return []byte{0x02, 0x00, 0x42}, &mbserver.Success
		},
	)

	expected := []telegraf.Metric{
		metric.New(
			"modbus",
			map[string]string{
				"type":     cHoldingRegisters,
				"slave_id": "1",
				"name":     modbus.Name,
			},
			map[string]interface{}{"holding-0": int16(0x42)},
			time.Unix(0, 0),
		),
		metric.New(
			"modbus",
			map[string]string{
				"type":     cHoldingRegisters,
				"slave_id": "3",
				"name":     modbus.Name,
			},
			map[string]interface{}{"holding-0": int16(0x42)},
			time.Unix(0, 0),
		),
	}

	var acc testutil.Accumulator
	require.NoError(t, modbus.Gather(&acc))
	acc.Wait(len(expected))
	actual := acc.GetTelegrafMetrics()
	testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime(), testutil.SortMetrics())
	require.Len(t, acc.Errors, 1)
	require.ErrorContains(t, acc.FirstError(), `slave 2 on controller "tcp://localhost:1502": modbus: exception '11' (gateway target device failed to respond)`)
}

func TestRequestOptimizationShrink(t *testing.T) {
	maxsize := maxQuantityHoldingRegisters
	tests := []struct {
		name     string
		inputs   []rangeDefinition
		expected []requestExpectation
	}{
		{
			name: "no omit",
			inputs: []rangeDefinition{
				{0, 2 * maxQuantityHoldingRegisters, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{{start: 0, count: maxsize, length: 1}},
					req:    request{address: 0, length: maxsize},
				},
				{
					fields: []rangeDefinition{{start: maxsize, count: maxsize, length: 1}},
					req:    request{address: maxsize, length: maxsize},
				},
			},
		},
		{
			name: "borders",
			inputs: []rangeDefinition{
				{0, 1, 1, 1, "INT16", false},
				{1, maxsize - 2, 1, 1, "INT16", true},
				{maxsize - 1, 2, 1, 1, "INT16", false},
				{maxsize + 1, maxsize - 2, 1, 1, "INT16", true},
				{2*maxsize - 1, 1, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{
						{start: 0, count: 1, length: 1},
						{start: maxsize - 1, count: 1, length: 1},
					},
					req: request{address: 0, length: maxsize},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize, count: 1, length: 1},
						{start: 2*maxsize - 1, count: 1, length: 1},
					},
					req: request{address: maxsize, length: maxsize},
				},
			},
		},
		{
			name: "borders with gap",
			inputs: []rangeDefinition{
				{0, 1, 1, 1, "INT16", false},
				{1, maxsize - 2, 1, 1, "INT16", true},
				{maxsize - 1, 2, 1, 1, "INT16", false},
				{maxsize + 1, 4, 1, 1, "INT16", true},
				{2*maxsize - 1, 1, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{
						{start: 0, count: 1, length: 1},
						{start: maxsize - 1, count: 1, length: 1},
					},
					req: request{address: 0, length: maxsize},
				},
				{
					fields: []rangeDefinition{{start: maxsize, count: 1, length: 1}},
					req:    request{address: maxsize, length: 1},
				},
				{
					fields: []rangeDefinition{{start: 2*maxsize - 1, count: 1, length: 1}},
					req:    request{address: 2*maxsize - 1, length: 1},
				},
			},
		},
		{
			name: "large gaps",
			inputs: []rangeDefinition{
				{18, 3, 1, 1, "INT16", false},
				{maxsize - 2, 5, 1, 1, "INT16", false},
				{maxsize + 42, 2, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
					req:    request{address: 18, length: 3},
				},
				{
					fields: []rangeDefinition{{start: maxsize - 2, count: 5, length: 1}},
					req:    request{address: maxsize - 2, length: 5},
				},
				{
					fields: []rangeDefinition{{start: maxsize + 42, count: 2, length: 1}},
					req:    request{address: maxsize + 42, length: 2},
				},
			},
		},
		{
			name: "large gaps filled",
			inputs: []rangeDefinition{
				{0, 1, 1, 1, "INT16", false},
				{1, 17, 1, 1, "INT16", true},
				{18, 3, 1, 1, "INT16", false},
				{21, maxsize - 23, 1, 1, "INT16", true},
				{maxsize - 2, 5, 1, 1, "INT16", false},
				{maxsize + 3, 39, 1, 1, "INT16", true},
				{maxsize + 42, 2, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{
						{start: 0, count: 1, length: 1},
						{start: 18, count: 3, length: 1},
						{start: maxsize - 2, count: 2, length: 1},
					},
					req: request{address: 0, length: maxsize},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize, count: 3, length: 1},
						{start: maxsize + 42, count: 2, length: 1},
					},
					req: request{address: maxsize, length: 44},
				},
			},
		},
		{
			name: "large gaps filled with offset",
			inputs: []rangeDefinition{
				{18, 3, 1, 1, "INT16", false},
				{21, maxsize - 23, 1, 1, "INT16", true},
				{maxsize - 2, 5, 1, 1, "INT16", false},
				{maxsize + 3, 39, 1, 1, "INT16", true},
				{maxsize + 42, 2, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{
						{start: 18, count: 3, length: 1},
						{start: maxsize - 2, count: 5, length: 1},
					},
					req: request{address: 18, length: 110},
				},
				{
					fields: []rangeDefinition{{start: maxsize + 42, count: 2, length: 1}},
					req:    request{address: maxsize + 42, length: 2},
				},
			},
		},
		{
			name: "worst case",
			inputs: []rangeDefinition{
				{0, maxsize, 2, 1, "INT16", false},
				{1, maxsize, 2, 1, "INT16", true},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{{start: 0, count: maxsize/2 + 1, increment: 2, length: 1}},
					req:    request{address: 0, length: maxsize},
				},
				{
					fields: []rangeDefinition{{start: maxsize + 1, count: maxsize / 2, increment: 2, length: 1}},
					req:    request{address: maxsize + 1, length: maxsize - 2},
				},
			},
		},
		{
			name: "from PR #11106",
			inputs: []rangeDefinition{
				{0, 2, 1, 1, "INT16", true},
				{2, 1, 1, 1, "INT16", false},
				{3, 2*maxsize + 1, 1, 1, "INT16", true},
				{2*maxsize + 1, 1, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{{start: 2, count: 1, length: 1}},
					req:    request{address: 2, length: 1},
				},
				{
					fields: []rangeDefinition{{start: 2*maxsize + 1, count: 1, length: 1}},
					req:    request{address: 2*maxsize + 1, length: 1},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Generate the input structure and the expectation
			requestFields := generateRequestDefinitions(tt.inputs)
			expected := generateExpectation(tt.expected)

			// Setup the plugin
			slaveID := byte(1)
			plugin := Modbus{
				Name:              "Test",
				Controller:        "tcp://localhost:1502",
				ConfigurationType: "request",
				Log:               testutil.Logger{},
			}
			plugin.Requests = []requestDefinition{
				{
					SlaveID:      slaveID,
					ByteOrder:    "ABCD",
					RegisterType: "holding",
					Optimization: "shrink",
					Fields:       requestFields,
				},
			}
			require.NoError(t, plugin.Init())
			require.NotEmpty(t, plugin.requests)
			require.Contains(t, plugin.requests, slaveID)
			requireEqualRequests(t, expected, plugin.requests[slaveID].holding)
		})
	}
}

func TestRequestOptimizationRearrange(t *testing.T) {
	maxsize := maxQuantityHoldingRegisters
	tests := []struct {
		name     string
		inputs   []rangeDefinition
		expected []requestExpectation
	}{
		{
			name: "no omit",
			inputs: []rangeDefinition{
				{0, 2 * maxQuantityHoldingRegisters, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{{start: 0, count: maxsize, length: 1}},
					req:    request{address: 0, length: maxsize},
				},
				{
					fields: []rangeDefinition{{start: maxsize, count: maxsize, length: 1}},
					req:    request{address: maxsize, length: maxsize},
				},
			},
		},
		{
			name: "borders",
			inputs: []rangeDefinition{
				{0, 1, 1, 1, "INT16", false},
				{1, maxsize - 2, 1, 1, "INT16", true},
				{maxsize - 1, 2, 1, 1, "INT16", false},
				{maxsize + 1, maxsize - 2, 1, 1, "INT16", true},
				{2*maxsize - 1, 1, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{
						{start: 0, count: 1, length: 1},
						{start: maxsize - 1, count: 1, length: 1},
					},
					req: request{address: 0, length: maxsize},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize, count: 1, length: 1},
						{start: 2*maxsize - 1, count: 1, length: 1},
					},
					req: request{address: maxsize, length: maxsize},
				},
			},
		},
		{
			name: "borders with gap",
			inputs: []rangeDefinition{
				{0, 1, 1, 1, "INT16", false},
				{1, maxsize - 2, 1, 1, "INT16", true},
				{maxsize - 1, 2, 1, 1, "INT16", false},
				{maxsize + 1, 4, 1, 1, "INT16", true},
				{2*maxsize - 1, 1, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{{start: 0, count: 1, length: 1}},
					req:    request{address: 0, length: 1},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize - 1, count: 1, length: 1},
						{start: maxsize, count: 1, length: 1},
					},
					req: request{address: maxsize - 1, length: 2},
				},
				{
					fields: []rangeDefinition{{start: 2*maxsize - 1, count: 1, length: 1}},
					req:    request{address: 2*maxsize - 1, length: 1},
				},
			},
		},
		{
			name: "large gaps",
			inputs: []rangeDefinition{
				{18, 3, 1, 1, "INT16", false},
				{maxsize - 2, 5, 1, 1, "INT16", false},
				{maxsize + 42, 2, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
					req:    request{address: 18, length: 3},
				},
				{
					fields: []rangeDefinition{{start: maxsize - 2, count: 5, length: 1}},
					req:    request{address: maxsize - 2, length: 5},
				},
				{
					fields: []rangeDefinition{{start: maxsize + 42, count: 2, length: 1}},
					req:    request{address: maxsize + 42, length: 2},
				},
			},
		},
		{
			name: "large gaps filled",
			inputs: []rangeDefinition{
				{0, 1, 1, 1, "INT16", false},
				{1, 17, 1, 1, "INT16", true},
				{18, 3, 1, 1, "INT16", false},
				{21, maxsize - 23, 1, 1, "INT16", true},
				{maxsize - 2, 5, 1, 1, "INT16", false},
				{maxsize + 3, 39, 1, 1, "INT16", true},
				{maxsize + 42, 2, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{
						{start: 0, count: 1, length: 1},
						{start: 18, count: 3, length: 1},
					},
					req: request{address: 0, length: 21},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize - 2, count: 5, length: 1},
						{start: maxsize + 42, count: 2, length: 1},
					},
					req: request{address: maxsize - 2, length: 46},
				},
			},
		},
		{
			name: "large gaps filled with offset",
			inputs: []rangeDefinition{
				{18, 3, 1, 1, "INT16", false},
				{21, maxsize - 23, 1, 1, "INT16", true},
				{maxsize - 2, 5, 1, 1, "INT16", false},
				{maxsize + 3, 39, 1, 1, "INT16", true},
				{maxsize + 42, 2, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
					req:    request{address: 18, length: 3},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize - 2, count: 5, length: 1},
						{start: maxsize + 42, count: 2, length: 1},
					},
					req: request{address: maxsize - 2, length: 46},
				},
			},
		},
		{
			name: "from PR #11106",
			inputs: []rangeDefinition{
				{0, 2, 1, 1, "INT16", true},
				{2, 1, 1, 1, "INT16", false},
				{3, 2*maxsize + 1, 1, 1, "INT16", true},
				{2*maxsize + 1, 1, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{{start: 2, count: 1, length: 1}},
					req:    request{address: 2, length: 1},
				},
				{
					fields: []rangeDefinition{{start: 2*maxsize + 1, count: 1, length: 1}},
					req:    request{address: 2*maxsize + 1, length: 1},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Generate the input structure and the expectation
			requestFields := generateRequestDefinitions(tt.inputs)
			expected := generateExpectation(tt.expected)

			// Setup the plugin
			slaveID := byte(1)
			plugin := Modbus{
				Name:              "Test",
				Controller:        "tcp://localhost:1502",
				ConfigurationType: "request",
				Log:               testutil.Logger{},
			}
			plugin.Requests = []requestDefinition{
				{
					SlaveID:      slaveID,
					ByteOrder:    "ABCD",
					RegisterType: "holding",
					Optimization: "rearrange",
					Fields:       requestFields,
				},
			}
			require.NoError(t, plugin.Init())
			require.NotEmpty(t, plugin.requests)
			require.Contains(t, plugin.requests, slaveID)
			requireEqualRequests(t, expected, plugin.requests[slaveID].holding)
		})
	}
}

func TestRequestOptimizationMaxExtraRegisterFail(t *testing.T) {
	tests := []struct {
		name     string
		requests []requestDefinition
		errormsg string
	}{{
		name: "MaxExtraRegister too large",
		requests: []requestDefinition{
			{
				SlaveID:           1,
				ByteOrder:         "ABCD",
				RegisterType:      "input",
				Optimization:      "max_insert",
				MaxExtraRegisters: 5000,
				Fields: []requestFieldDefinition{
					{
						Name:        "input-0",
						Address:     uint16(0),
						Measurement: "foo",
					},
				},
			},
		},
		errormsg: "optimization_max_register_fill has to be between 1 and 125",
	},
		{
			name: "MaxExtraRegister too small",
			requests: []requestDefinition{
				{
					SlaveID:           1,
					ByteOrder:         "ABCD",
					RegisterType:      "input",
					Optimization:      "max_insert",
					MaxExtraRegisters: 0,
					Fields: []requestFieldDefinition{
						{
							Name:        "input-0",
							Address:     uint16(0),
							Measurement: "foo",
						},
					},
				},
			},
			errormsg: "optimization_max_register_fill has to be between 1 and 125",
		}}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			plugin := Modbus{
				Name:              "Test",
				Controller:        "tcp://localhost:1502",
				ConfigurationType: "request",
				Log:               testutil.Logger{},
			}
			plugin.Requests = tt.requests

			require.ErrorContains(t, plugin.Init(), tt.errormsg)
			require.Empty(t, plugin.requests)
		})
	}
}

func TestRequestOptimizationMaxInsertSmall(t *testing.T) {
	maxsize := maxQuantityHoldingRegisters
	maxExtraRegisters := uint16(5)
	tests := []struct {
		name     string
		inputs   []rangeDefinition
		expected []requestExpectation
	}{
		{
			name: "large gaps",
			inputs: []rangeDefinition{
				{18, 3, 1, 1, "INT16", false},
				{maxsize - 2, 5, 1, 1, "INT16", false},
				{maxsize + 42, 2, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
					req:    request{address: 18, length: 3},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize - 2, count: 5, length: 1},
					},
					req: request{address: maxsize - 2, length: 5},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize + 42, count: 2, length: 1},
					},
					req: request{address: maxsize + 42, length: 2},
				},
			},
		},
		{
			name: "large gaps filled",
			inputs: []rangeDefinition{
				{0, 1, 1, 1, "INT16", false},
				{1, 17, 1, 1, "INT16", true},
				{18, 3, 1, 1, "INT16", false},
				{21, maxsize - 23, 1, 1, "INT16", true},
				{maxsize - 2, 5, 1, 1, "INT16", false},
				{maxsize + 3, 39, 1, 1, "INT16", true},
				{maxsize + 42, 2, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{
						{start: 0, count: 1, length: 1},
					},
					req: request{address: 0, length: 1},
				},
				{
					fields: []rangeDefinition{
						{start: 18, count: 3, length: 1},
					},
					req: request{address: 18, length: 3},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize - 2, count: 5, length: 1},
					},
					req: request{address: maxsize - 2, length: 5},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize + 42, count: 2, length: 1},
					},
					req: request{address: maxsize + 42, length: 2},
				},
			},
		},
		{
			name: "large gaps filled with offset",
			inputs: []rangeDefinition{
				{18, 3, 1, 1, "INT16", false},
				{21, maxsize - 23, 1, 1, "INT16", true},
				{maxsize - 2, 5, 1, 1, "INT16", false},
				{maxsize + 3, 39, 1, 1, "INT16", true},
				{maxsize + 42, 2, 1, 1, "INT16", false},
			},
			expected: []requestExpectation{
				{
					fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
					req:    request{address: 18, length: 3},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize - 2, count: 5, length: 1},
					},
					req: request{address: maxsize - 2, length: 5},
				},
				{
					fields: []rangeDefinition{
						{start: maxsize + 42, count: 2, length: 1},
					},
					req: request{address: maxsize + 42, length: 2},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Generate the input structure and the expectation
			requestFields := generateRequestDefinitions(tt.inputs)
			expected := generateExpectation(tt.expected)

			// Setup the plugin
			slaveID := byte(1)
			plugin := Modbus{
				Name:              "Test",
				Controller:        "tcp://localhost:1502",
				ConfigurationType: "request",
				Log:               testutil.Logger{},
			}
			plugin.Requests = []requestDefinition{
				{
					SlaveID:           slaveID,
					ByteOrder:         "ABCD",
					RegisterType:      "holding",
					Optimization:      "max_insert",
					MaxExtraRegisters: maxExtraRegisters,
					Fields:            requestFields,
				},
			}
			require.NoError(t, plugin.Init())
			require.NotEmpty(t, plugin.requests)
			require.Contains(t, plugin.requests, slaveID)
			requireEqualRequests(t, expected, plugin.requests[slaveID].holding)
		})
	}
}
func TestRequestWorkaroundsOneRequestPerField(t *testing.T) {
	plugin := Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		ConfigurationType: "request",
		Log:               testutil.Logger{},
		Workarounds:       workarounds{OnRequestPerField: true},
	}
	plugin.Requests = []requestDefinition{
		{
			SlaveID:      1,
			ByteOrder:    "ABCD",
			RegisterType: "holding",
			Fields: []requestFieldDefinition{
				{
					Name:      "holding-1",
					Address:   uint16(1),
					InputType: "INT16",
				},
				{
					Name:      "holding-2",
					Address:   uint16(2),
					InputType: "INT16",
				},
				{
					Name:      "holding-3",
					Address:   uint16(3),
					InputType: "INT16",
				},
				{
					Name:      "holding-4",
					Address:   uint16(4),
					InputType: "INT16",
				},
				{
					Name:      "holding-5",
					Address:   uint16(5),
					InputType: "INT16",
				},
			},
		},
	}
	require.NoError(t, plugin.Init())
	require.Len(t, plugin.requests[1].holding, len(plugin.Requests[0].Fields))
}

func TestRequestWorkaroundsReadCoilsStartingAtZeroRequest(t *testing.T) {
	plugin := Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		ConfigurationType: "request",
		Log:               testutil.Logger{},
		Workarounds:       workarounds{ReadCoilsStartingAtZero: true},
	}
	plugin.SlaveID = 1
	plugin.Requests = []requestDefinition{
		{
			SlaveID:      1,
			RegisterType: "coil",
			Fields: []requestFieldDefinition{
				{
					Name:    "coil-8",
					Address: uint16(8),
				},
				{
					Name:    "coil-new-group",
					Address: maxQuantityCoils,
				},
			},
		},
	}
	require.NoError(t, plugin.Init())
	require.Len(t, plugin.requests[1].coil, 2)

	// First group should now start at zero and have the cumulated length
	require.Equal(t, uint16(0), plugin.requests[1].coil[0].address)
	require.Equal(t, uint16(9), plugin.requests[1].coil[0].length)

	// The second field should form a new group as the previous request
	// is now too large (beyond max-coils-per-read) after zero enforcement.
	require.Equal(t, maxQuantityCoils, plugin.requests[1].coil[1].address)
	require.Equal(t, uint16(1), plugin.requests[1].coil[1].length)
}

func TestRequestOverlap(t *testing.T) {
	logger := &testutil.CaptureLogger{}
	plugin := Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		ConfigurationType: "request",
		Log:               logger,
		Workarounds:       workarounds{ReadCoilsStartingAtZero: true},
	}
	plugin.Requests = []requestDefinition{
		{
			SlaveID:           1,
			RegisterType:      "holding",
			Optimization:      "max_insert",
			MaxExtraRegisters: 16,
			Fields: []requestFieldDefinition{
				{
					Name:      "field-1",
					InputType: "UINT32",
					Address:   uint16(1),
				},
				{
					Name:      "field-2",
					InputType: "UINT64",
					Address:   uint16(3),
				},
				{
					Name:      "field-3",
					InputType: "UINT32",
					Address:   uint16(5),
				},
				{
					Name:      "field-4",
					InputType: "UINT32",
					Address:   uint16(7),
				},
			},
		},
	}
	require.NoError(t, plugin.Init())

	require.Eventually(t, func() bool {
		return len(logger.Warnings()) > 0
	}, 3*time.Second, 100*time.Millisecond)

	var found bool
	for _, w := range logger.Warnings() {
		if strings.Contains(w, "Request at 3 with length 4 overlaps with next request at 5") {
			found = true
			break
		}
	}
	require.True(t, found, "Overlap warning not found!")

	require.Len(t, plugin.requests, 1)
	require.Len(t, plugin.requests[1].holding, 1)
}

func TestRequestAddressOverflow(t *testing.T) {
	logger := &testutil.CaptureLogger{}
	plugin := Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		ConfigurationType: "request",
		Log:               logger,
		Workarounds:       workarounds{ReadCoilsStartingAtZero: true},
	}
	plugin.Requests = []requestDefinition{
		{
			SlaveID:      1,
			RegisterType: "holding",
			Fields: []requestFieldDefinition{
				{
					Name:      "field",
					InputType: "UINT64",
					Address:   uint16(65534),
				},
			},
		},
	}
	require.ErrorIs(t, plugin.Init(), errAddressOverflow)
}

func TestRequestMaxRegistersWorkaround(t *testing.T) {
	plugin := &Modbus{
		Name:              "Test",
		Controller:        "tcp://localhost:1502",
		ConfigurationType: "request",
		Log:               &testutil.Logger{},
		Workarounds: workarounds{
			MaxBitRegistersPerRequest:  6,
			MaxWordRegistersPerRequest: 8,
		},
	}

	fields := make([]requestFieldDefinition, 0, 10)
	for i := range 10 {
		fields = append(fields,
			requestFieldDefinition{
				Name:    fmt.Sprintf("field-coil-%d", i),
				Address: uint16(i),
			},
		)
	}
	plugin.Requests = append(plugin.Requests, requestDefinition{
		SlaveID:      1,
		RegisterType: "coil",
		Fields:       fields,
	})

	fields = make([]requestFieldDefinition, 0, 10)
	for i := range 10 {
		fields = append(fields,
			requestFieldDefinition{
				Name:    fmt.Sprintf("field-discrete-%d", i),
				Address: uint16(i),
			},
		)
	}
	plugin.Requests = append(plugin.Requests, requestDefinition{
		SlaveID:      1,
		RegisterType: "discrete",
		Fields:       fields,
	})

	fields = make([]requestFieldDefinition, 0, 10)
	for i := range 10 {
		fields = append(fields,
			requestFieldDefinition{
				Name:      fmt.Sprintf("field-holding-%d", i),
				Address:   uint16(4 * i),
				InputType: "UINT64",
			},
		)
	}
	plugin.Requests = append(plugin.Requests, requestDefinition{
		SlaveID:      1,
		RegisterType: "holding",
		Fields:       fields,
	})

	fields = make([]requestFieldDefinition, 0, 10)
	for i := range 10 {
		fields = append(fields,
			requestFieldDefinition{
				Name:      fmt.Sprintf("field-input-%d", i),
				Address:   uint16(4 * i),
				InputType: "UINT64",
			},
		)
	}
	plugin.Requests = append(plugin.Requests, requestDefinition{
		SlaveID:      1,
		RegisterType: "input",
		Fields:       fields,
	})
	require.NoError(t, plugin.Init())

	require.Len(t, plugin.requests, 1)
	require.Contains(t, plugin.requests, byte(1))
	require.Len(t, plugin.requests[1].coil, 2, "coil")
	require.Len(t, plugin.requests[1].discrete, 2, "discrete")
	require.Len(t, plugin.requests[1].holding, 5, "holding")
	require.Len(t, plugin.requests[1].input, 5, "input")
}
