package binary

import (
	"bytes"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/stretchr/testify/require"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/internal"
	"github.com/influxdata/telegraf/metric"
	"github.com/influxdata/telegraf/testutil"
	test "github.com/influxdata/telegraf/testutil/plugin_input"
)

var dummyEntry = Entry{
	Name:       "dummy",
	Type:       "uint8",
	Bits:       8,
	Assignment: "field",
}

func generateBinary(data []interface{}, order binary.ByteOrder) ([]byte, error) {
	var buf bytes.Buffer

	for _, x := range data {
		var err error
		switch v := x.(type) {
		case []byte:
			buf.Write(v)
		case string:
			buf.WriteString(v)
		default:
			err = binary.Write(&buf, order, x)
		}
		if err != nil {
			return nil, err
		}
	}
	return buf.Bytes(), nil
}

func determineEndianness(endianness string) binary.ByteOrder {
	switch endianness {
	case "le":
		return binary.LittleEndian
	case "be":
		return binary.BigEndian
	case "host":
		return internal.HostEndianness
	}
	panic(fmt.Errorf("unknown endianness %q", endianness))
}

func TestInitInvalid(t *testing.T) {
	var tests = []struct {
		name       string
		metric     string
		config     []Config
		endianness string
		expected   string
	}{
		{
			name:       "wrong endianness",
			metric:     "binary",
			endianness: "garbage",
			expected:   `unknown endianness "garbage"`,
		},
		{
			name:       "empty configuration",
			metric:     "binary",
			endianness: "host",
			expected:   `no configuration given`,
		},
		{
			name:       "no metric name",
			config:     []Config{{}},
			endianness: "host",
			expected:   `config 0 invalid: no metric name given`,
		},
		{
			name:     "no field",
			config:   []Config{{}},
			metric:   "binary",
			expected: `config 0 invalid: no field defined`,
		},
		{
			name: "invalid entry",
			config: []Config{{
				Entries: []Entry{
					{
						Bits: 8,
					},
				},
			}},
			metric:   "binary",
			expected: `config 0 invalid: entry "" (0): missing name`,
		},
		{
			name: "multiple measurements",
			config: []Config{{
				Entries: []Entry{
					{
						Bits:       8,
						Assignment: "measurement",
					},
					{
						Bits:       8,
						Assignment: "measurement",
					},
				},
			}},
			metric:   "binary",
			expected: `config 0 invalid: multiple definitions of "measurement"`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				Endianness: tt.endianness,
				Log:        testutil.Logger{Name: "parsers.binary"},
				metricName: tt.metric,
			}

			parser.Configs = tt.config
			require.EqualError(t, parser.Init(), tt.expected)
		})
	}
}

func TestFilterInvalid(t *testing.T) {
	var tests = []struct {
		name     string
		filter   *Filter
		expected string
	}{
		{
			name:     "both length and length-min",
			filter:   &Filter{Length: 35, LengthMin: 33},
			expected: `config 0 invalid: length and length_min cannot be used together`,
		},
		{
			name:     "filter too long length",
			filter:   &Filter{Length: 3, Selection: []BinaryPart{{Offset: 16, Bits: 16}}},
			expected: `config 0 invalid: filter length (4) larger than constraint (3)`,
		},
		{
			name:     "filter invalid match",
			filter:   &Filter{Selection: []BinaryPart{{Offset: 16, Bits: 16, Match: "XYZ"}}},
			expected: `config 0 invalid: decoding match 0 failed: encoding/hex: invalid byte: U+0058 'X'`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				Configs:    []Config{{Filter: tt.filter}},
				Log:        testutil.Logger{Name: "parsers.binary"},
				metricName: "binary",
			}
			require.EqualError(t, parser.Init(), tt.expected)
		})
	}
}

func TestFilterMatchInvalid(t *testing.T) {
	testdata := []byte{0x01, 0x02}

	var tests = []struct {
		name     string
		filter   *Filter
		expected string
	}{
		{
			name:     "filter length mismatch",
			filter:   &Filter{Selection: []BinaryPart{{Offset: 0, Bits: 8, Match: "0x0102"}}},
			expected: `no matching configuration`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				Configs:    []Config{{Filter: tt.filter, Entries: []Entry{{Name: "test", Type: "uint8"}}}},
				Log:        testutil.Logger{Name: "parsers.binary"},
				metricName: "binary",
			}
			require.NoError(t, parser.Init())
			_, err := parser.Parse(testdata)
			require.EqualError(t, err, tt.expected)
		})
	}
}

func TestFilterNoMatch(t *testing.T) {
	testdata := []interface{}{uint16(0x0102)}

	t.Run("no match error", func(t *testing.T) {
		parser := &Parser{
			Configs: []Config{
				{
					Filter:  &Filter{Length: 32},
					Entries: []Entry{dummyEntry},
				},
			},
			Log:        testutil.Logger{Name: "parsers.binary"},
			metricName: "binary",
		}
		require.NoError(t, parser.Init())

		data, err := generateBinary(testdata, internal.HostEndianness)
		require.NoError(t, err)

		_, err = parser.Parse(data)
		require.EqualError(t, err, "no matching configuration")
	})

	t.Run("no match allow", func(t *testing.T) {
		parser := &Parser{
			AllowNoMatch: true,
			Configs: []Config{
				{
					Filter:  &Filter{Length: 32},
					Entries: []Entry{dummyEntry},
				},
			},
			Log:        testutil.Logger{Name: "parsers.binary"},
			metricName: "binary",
		}
		require.NoError(t, parser.Init())

		data, err := generateBinary(testdata, internal.HostEndianness)
		require.NoError(t, err)

		metrics, err := parser.Parse(data)
		require.NoError(t, err)
		require.Empty(t, metrics)
	})
}

func TestFilterNone(t *testing.T) {
	testdata := []interface{}{
		uint64(0x01020304050607),
		uint64(0x08090A0B0C0D0E),
		uint64(0x0F101213141516),
		uint64(0x1718191A1B1C1D),
		uint64(0x1E1F2021222324),
	}

	var tests = []struct {
		name       string
		data       []interface{}
		filter     *Filter
		endianness string
	}{
		{
			name:       "no filter (BE)",
			data:       testdata,
			filter:     nil,
			endianness: "be",
		},
		{
			name:       "no filter (LE)",
			data:       testdata,
			filter:     nil,
			endianness: "le",
		},
		{
			name:       "no filter (host)",
			data:       testdata,
			filter:     nil,
			endianness: "host",
		},
		{
			name:       "empty filter (BE)",
			data:       testdata,
			filter:     &Filter{},
			endianness: "be",
		},
		{
			name:       "empty filter (LE)",
			data:       testdata,
			filter:     &Filter{},
			endianness: "le",
		},
		{
			name:       "empty filter (host)",
			data:       testdata,
			filter:     &Filter{},
			endianness: "host",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				Endianness: tt.endianness,
				Configs: []Config{
					{
						Filter:  tt.filter,
						Entries: []Entry{dummyEntry},
					},
				},
				Log:        testutil.Logger{Name: "parsers.binary"},
				metricName: "binary",
			}
			require.NoError(t, parser.Init())

			order := determineEndianness(tt.endianness)
			data, err := generateBinary(tt.data, order)
			require.NoError(t, err)

			metrics, err := parser.Parse(data)
			require.NoError(t, err)
			require.NotEmpty(t, metrics)
		})
	}
}

func TestFilterLength(t *testing.T) {
	testdata := []interface{}{
		uint64(0x01020304050607),
		uint64(0x08090A0B0C0D0E),
		uint64(0x0F101213141516),
		uint64(0x1718191A1B1C1D),
		uint64(0x1E1F2021222324),
	}

	var tests = []struct {
		name     string
		data     []interface{}
		filter   *Filter
		expected bool
	}{
		{
			name:     "length match",
			data:     testdata,
			filter:   &Filter{Length: 40},
			expected: true,
		},
		{
			name:     "length no match too short",
			data:     testdata,
			filter:   &Filter{Length: 41},
			expected: false,
		},
		{
			name:     "length no match too long",
			data:     testdata,
			filter:   &Filter{Length: 39},
			expected: false,
		},
		{
			name:     "length min match",
			data:     testdata,
			filter:   &Filter{LengthMin: 40},
			expected: true,
		},
		{
			name:     "length min no match too short",
			data:     testdata,
			filter:   &Filter{LengthMin: 41},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				AllowNoMatch: true,
				Configs: []Config{
					{
						Filter:  tt.filter,
						Entries: []Entry{dummyEntry},
					},
				},
				Log:        testutil.Logger{Name: "parsers.binary"},
				metricName: "binary",
			}
			require.NoError(t, parser.Init())

			data, err := generateBinary(tt.data, internal.HostEndianness)
			require.NoError(t, err)

			metrics, err := parser.Parse(data)
			require.NoError(t, err)
			if tt.expected {
				require.NotEmpty(t, metrics)
			} else {
				require.Empty(t, metrics)
			}
		})
	}
}

func TestFilterContent(t *testing.T) {
	testdata := [][]byte{
		{0x01, 0x02, 0x03, 0xA4, 0x05, 0x06, 0x07, 0x08},
		{0x01, 0xA2, 0x03, 0x04, 0x15, 0x01, 0x07, 0x08},
		{0xF1, 0xB1, 0x03, 0xA4, 0x25, 0x06, 0x07, 0x08},
		{0xF1, 0xC2, 0x03, 0x04, 0x35, 0x01, 0x07, 0x08},
		{0x42, 0xD1, 0x03, 0xA4, 0x25, 0x06, 0x42, 0x08},
		{0x42, 0xE2, 0x03, 0x04, 0x35, 0x01, 0x42, 0x08},
		{0x01, 0x00, 0x00, 0xA4},
	}
	var tests = []struct {
		name     string
		filter   *Filter
		expected int
	}{
		{
			name: "first byte",
			filter: &Filter{
				Selection: []BinaryPart{
					{
						Offset: 0,
						Bits:   8,
						Match:  "0xF1",
					},
				},
			},
			expected: 2,
		},
		{
			name: "last byte",
			filter: &Filter{
				Selection: []BinaryPart{
					{
						Offset: 7 * 8,
						Bits:   8,
						Match:  "0x08",
					},
				},
			},
			expected: 6,
		},
		{
			name: "none-byte boundary begin",
			filter: &Filter{
				Selection: []BinaryPart{
					{
						Offset: 12,
						Bits:   12,
						Match:  "0x0203",
					},
				},
			},
			expected: 4,
		},
		{
			name: "none-byte boundary end",
			filter: &Filter{
				Selection: []BinaryPart{
					{
						Offset: 16,
						Bits:   12,
						Match:  "0x003A",
					},
				},
			},
			expected: 3,
		},
		{
			name: "none-byte boundary end",
			filter: &Filter{
				Selection: []BinaryPart{
					{
						Offset: 36,
						Bits:   8,
						Match:  "0x50",
					},
				},
			},
			expected: 6,
		},
		{
			name: "multiple elements",
			filter: &Filter{
				Selection: []BinaryPart{
					{
						Offset: 4,
						Bits:   4,
						Match:  "0x01",
					},
					{
						Offset: 24,
						Bits:   8,
						Match:  "0xA4",
					},
				},
			},
			expected: 3,
		},
		{
			name: "multiple elements and length",
			filter: &Filter{
				Selection: []BinaryPart{
					{
						Offset: 4,
						Bits:   4,
						Match:  "0x01",
					},
					{
						Offset: 24,
						Bits:   8,
						Match:  "0xA4",
					},
				},
				Length: 4,
			},
			expected: 1,
		},
		{
			name: "multiple elements and length-min",
			filter: &Filter{
				Selection: []BinaryPart{
					{
						Offset: 4,
						Bits:   4,
						Match:  "0x01",
					},
					{
						Offset: 24,
						Bits:   8,
						Match:  "0xA4",
					},
				},
				LengthMin: 5,
			},
			expected: 2,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				AllowNoMatch: true,
				Configs: []Config{
					{
						Filter:  tt.filter,
						Entries: []Entry{dummyEntry},
					},
				},
				Log:        testutil.Logger{Name: "parsers.binary"},
				metricName: "binary",
			}
			require.NoError(t, parser.Init())

			var metrics []telegraf.Metric
			for _, data := range testdata {
				m, err := parser.Parse(data)
				require.NoError(t, err)
				metrics = append(metrics, m...)
			}
			require.Len(t, metrics, tt.expected)
		})
	}
}

func TestParseLineInvalid(t *testing.T) {
	var tests = []struct {
		name     string
		data     []interface{}
		configs  []Config
		expected string
	}{
		{
			name: "out-of-bounds",
			data: []interface{}{
				"2022-07-25T20:41:29+02:00", // time
				uint16(0x0102),              // address
				float64(42.123),             // value
			},
			configs: []Config{
				{
					Entries: []Entry{
						{
							Type:       "2006-01-02T15:04:05Z07:00",
							Assignment: "time",
							Timezone:   "UTC",
						},
						{
							Type: "uint32",
							Omit: true,
						},
						{
							Name: "value",
							Type: "float64",
						},
					},
				},
			},
			expected: `out-of-bounds @232 with 64 bits`,
		},
		{
			name: "multiple matches",
			data: []interface{}{
				"2022-07-25T20:41:29+02:00", // time
				uint16(0x0102),              // address
				float64(42.123),             // value
			},
			configs: []Config{
				{
					Entries: []Entry{
						{
							Type:       "2006-01-02T15:04:05Z07:00",
							Assignment: "time",
							Timezone:   "UTC",
						},
						{
							Type: "uint16",
							Omit: true,
						},
						{
							Name: "value",
							Type: "float64",
						},
					},
				},
				{
					Entries: []Entry{
						{
							Type:       "2006-01-02T15:04:05Z07:00",
							Assignment: "time",
							Timezone:   "UTC",
						},
						{
							Name:       "address",
							Type:       "uint16",
							Assignment: "tag",
						},
						{
							Name: "value",
							Type: "float64",
						},
					},
				},
			},
			expected: `cannot parse line with multiple (2) metrics`,
		},
	}

	for _, tt := range tests {
		for _, endianness := range []string{"be", "le", "host"} {
			name := fmt.Sprintf("%s (%s)", tt.name, endianness)
			t.Run(name, func(t *testing.T) {
				parser := &Parser{
					Endianness: endianness,
					Configs:    tt.configs,
					Log:        testutil.Logger{Name: "parsers.binary"},
					metricName: "binary",
				}
				require.NoError(t, parser.Init())

				order := determineEndianness(endianness)
				data, err := generateBinary(tt.data, order)
				require.NoError(t, err)

				_, err = parser.ParseLine(string(data))
				require.EqualError(t, err, tt.expected)
			})
		}
	}
}

func TestParseLine(t *testing.T) {
	var tests = []struct {
		name     string
		data     []interface{}
		filter   *Filter
		entries  []Entry
		expected telegraf.Metric
	}{
		{
			name: "no match",
			data: []interface{}{
				"2022-07-25T20:41:29+02:00", // time
				uint16(0x0102),              // address
				float64(42.123),             // value
			},
			filter: &Filter{Length: 4},
			entries: []Entry{
				{
					Type:       "2006-01-02T15:04:05Z07:00",
					Assignment: "time",
					Timezone:   "UTC",
				},
				{
					Type: "uint16",
					Omit: true,
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
		},
		{
			name: "single match",
			data: []interface{}{
				"2022-07-25T20:41:29+02:00", // time
				uint16(0x0102),              // address
				float64(42.123),             // value
			},
			entries: []Entry{
				{
					Type:       "2006-01-02T15:04:05Z07:00",
					Assignment: "time",
					Timezone:   "UTC",
				},
				{
					Type: "uint16",
					Omit: true,
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: metric.New(
				"binary",
				map[string]string{},
				map[string]interface{}{"value": float64(42.123)},
				time.Unix(1658774489, 0),
			),
		},
	}

	for _, tt := range tests {
		for _, endianness := range []string{"be", "le", "host"} {
			name := fmt.Sprintf("%s (%s)", tt.name, endianness)
			t.Run(name, func(t *testing.T) {
				parser := &Parser{
					AllowNoMatch: true,
					Endianness:   endianness,
					Configs: []Config{{
						Filter:  tt.filter,
						Entries: tt.entries,
					}},
					Log:        testutil.Logger{Name: "parsers.binary"},
					metricName: "binary",
				}
				require.NoError(t, parser.Init())

				order := determineEndianness(endianness)
				data, err := generateBinary(tt.data, order)
				require.NoError(t, err)

				m, err := parser.ParseLine(string(data))
				require.NoError(t, err)

				testutil.RequireMetricEqual(t, tt.expected, m)
			})
		}
	}
}

func TestParseInvalid(t *testing.T) {
	var tests = []struct {
		name     string
		data     []interface{}
		entries  []Entry
		expected string
	}{
		{
			name: "message too short",
			data: []interface{}{uint64(0x0102030405060708)},
			entries: []Entry{
				{
					Name:       "command",
					Type:       "uint32",
					Assignment: "tag",
				},
				{
					Name:       "version",
					Type:       "uint32",
					Assignment: "tag",
				},
				{
					Name:       "address",
					Type:       "uint32",
					Assignment: "tag",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: `out-of-bounds @64 with 32 bits`,
		},
		{
			name: "non-terminated string",
			data: []interface{}{
				uint16(0xAB42),       // address
				"testmetric",         // metric
				float64(42.23432243), // value
			},
			entries: []Entry{
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Type:       "string",
					Terminator: "null",
					Assignment: "measurement",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: `terminator not found for "measurement"`,
		},
		{
			name: "invalid time",
			data: []interface{}{
				"2022-07-25T18:41:XYZ", // time
				uint16(0x0102),         // address
				float64(42.123),        // value
			},
			entries: []Entry{
				{
					Type:       "2006-01-02T15:04:05Z",
					Assignment: "time",
				},
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: `time failed: parsing time "2022-07-25T18:41:XYZ" as "2006-01-02T15:04:05Z": cannot parse "XYZ" as "05"`,
		},
	}

	for _, tt := range tests {
		for _, endianness := range []string{"be", "le", "host"} {
			name := fmt.Sprintf("%s (%s)", tt.name, endianness)
			t.Run(name, func(t *testing.T) {
				parser := &Parser{
					Endianness: endianness,
					Configs:    []Config{{Entries: tt.entries}},
					Log:        testutil.Logger{Name: "parsers.binary"},
					metricName: "binary",
				}
				require.NoError(t, parser.Init())

				order := determineEndianness(endianness)
				data, err := generateBinary(tt.data, order)
				require.NoError(t, err)

				_, err = parser.Parse(data)
				require.EqualError(t, err, tt.expected)
			})
		}
	}
}

func TestParse(t *testing.T) {
	timeBerlin, err := time.Parse(time.RFC3339, "2022-07-25T20:41:29+02:00")
	require.NoError(t, err)
	timeBerlinMilli, err := time.Parse(time.RFC3339Nano, "2022-07-25T20:41:29.123+02:00")
	require.NoError(t, err)

	var tests = []struct {
		name       string
		data       []interface{}
		entries    []Entry
		ignoreTime bool
		expected   []telegraf.Metric
	}{
		{
			name: "fixed numbers",
			data: []interface{}{
				uint16(0xAB42),             // command
				uint8(0x02),                // version
				uint32(0x010000FF),         // address
				uint64(0x0102030405060708), // serial-number
				int8(-25),                  // countdown as int32
				int16(-42),                 // overdue
				int32(-65535),              // batchleft
				int64(12345678),            // counter
				float32(3.1415),            // x
				float32(99.471),            // y
				float64(0.23432243),        // z
				uint8(0xFF),                // status
				uint8(0x0F),                // on/off bit-field
			},
			entries: []Entry{
				{
					Name:       "command",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Name:       "version",
					Type:       "uint8",
					Assignment: "tag",
				},
				{
					Name:       "address",
					Type:       "uint32",
					Assignment: "tag",
				},
				{
					Name:       "serialnumber",
					Type:       "uint64",
					Assignment: "tag",
				},
				{
					Name: "countdown",
					Type: "int8",
				},
				{
					Name: "overdue",
					Type: "int16",
				},
				{
					Name: "batchleft",
					Type: "int32",
				},
				{
					Name: "counter",
					Type: "int64",
				},
				{
					Name: "x",
					Type: "float32",
				},
				{
					Name: "y",
					Type: "float32",
				},
				{
					Name: "z",
					Type: "float64",
				},
				{
					Name: "status",
					Type: "bool",
					Bits: 8,
				},
				{
					Name:       "error_part",
					Type:       "bool",
					Bits:       4,
					Assignment: "tag",
				},
				{
					Name:       "ok_part1",
					Type:       "bool",
					Bits:       1,
					Assignment: "tag",
				},
				{
					Name:       "ok_part2",
					Type:       "bool",
					Bits:       1,
					Assignment: "tag",
				},
				{
					Name:       "ok_part3",
					Type:       "bool",
					Bits:       1,
					Assignment: "tag",
				},
				{
					Name:       "ok_part4",
					Type:       "bool",
					Bits:       1,
					Assignment: "tag",
				},
			},
			ignoreTime: true,
			expected: []telegraf.Metric{
				metric.New(
					"binary",
					map[string]string{
						"command":      "43842",
						"version":      "2",
						"address":      "16777471",
						"serialnumber": "72623859790382856",
						"error_part":   "false",
						"ok_part1":     "true",
						"ok_part2":     "true",
						"ok_part3":     "true",
						"ok_part4":     "true",
					},
					map[string]interface{}{
						"x":         float32(3.1415),
						"y":         float32(99.471),
						"z":         float64(0.23432243),
						"countdown": int8(-25),
						"overdue":   int16(-42),
						"batchleft": int32(-65535),
						"counter":   int64(12345678),

						"status": true,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "fixed length string",
			data: []interface{}{
				uint16(0xAB42),      // address
				"test",              // metric
				float64(0.23432243), // value
			},
			entries: []Entry{
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Name:       "app",
					Type:       "string",
					Bits:       4 * 8,
					Assignment: "field",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			ignoreTime: true,
			expected: []telegraf.Metric{
				metric.New(
					"binary",
					map[string]string{"address": "43842"},
					map[string]interface{}{
						"app":   "test",
						"value": float64(0.23432243),
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "null-terminated string",
			data: []interface{}{
				uint16(0xAB42),                     // address
				append([]byte("testmetric"), 0x00), // metric
				float64(42.23432243),               // value
			},
			entries: []Entry{
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Type:       "string",
					Terminator: "null",
					Assignment: "measurement",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			ignoreTime: true,
			expected: []telegraf.Metric{
				metric.New(
					"testmetric",
					map[string]string{"address": "43842"},
					map[string]interface{}{"value": float64(42.23432243)},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "char-terminated string",
			data: []interface{}{
				uint16(0xAB42),                           // address
				append([]byte("testmetric"), 0x0A, 0x0B), // metric
				float64(42.23432243),                     // value
			},
			entries: []Entry{
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Type:       "string",
					Terminator: "0x0A0B",
					Assignment: "measurement",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			ignoreTime: true,
			expected: []telegraf.Metric{
				metric.New(
					"testmetric",
					map[string]string{"address": "43842"},
					map[string]interface{}{"value": float64(42.23432243)},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "time (unix/UTC)",
			data: []interface{}{
				uint64(1658774489), // time
				uint16(0x0102),     // address
				float64(42.123),    // value
			},
			entries: []Entry{
				{
					Type:       "unix",
					Assignment: "time",
				},
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: []telegraf.Metric{
				metric.New(
					"binary",
					map[string]string{"address": "258"},
					map[string]interface{}{"value": float64(42.123)},
					time.Unix(1658774489, 0),
				),
			},
		},
		{
			name: "time (unix/Berlin)",
			data: []interface{}{
				uint64(1658774489), // time
				uint16(0x0102),     // address
				float64(42.123),    // value
			},
			entries: []Entry{
				{
					Type:       "unix",
					Assignment: "time",
					Timezone:   "Europe/Berlin",
				},
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: []telegraf.Metric{
				metric.New(
					"binary",
					map[string]string{"address": "258"},
					map[string]interface{}{"value": float64(42.123)},
					timeBerlin,
				),
			},
		},
		{
			name: "time (unix_ms/UTC)",
			data: []interface{}{
				uint64(1658774489123), // time
				uint16(0x0102),        // address
				float64(42.123),       // value
			},
			entries: []Entry{
				{
					Type:       "unix_ms",
					Assignment: "time",
				},
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: []telegraf.Metric{
				metric.New(
					"binary",
					map[string]string{"address": "258"},
					map[string]interface{}{"value": float64(42.123)},
					time.Unix(0, 1658774489123*1_000_000),
				),
			},
		},
		{
			name: "time (unix_ms/Berlin)",
			data: []interface{}{
				uint64(1658774489123), // time
				uint16(0x0102),        // address
				float64(42.123),       // value
			},
			entries: []Entry{
				{
					Type:       "unix_ms",
					Assignment: "time",
					Timezone:   "Europe/Berlin",
				},
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: []telegraf.Metric{
				metric.New(
					"binary",
					map[string]string{"address": "258"},
					map[string]interface{}{"value": float64(42.123)},
					timeBerlinMilli,
				),
			},
		},
		{
			name: "time (RFC3339/UTC)",
			data: []interface{}{
				"2022-07-25T18:41:29Z", // time
				uint16(0x0102),         // address
				float64(42.123),        // value
			},
			entries: []Entry{
				{
					Type:       "2006-01-02T15:04:05Z",
					Assignment: "time",
				},
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: []telegraf.Metric{
				metric.New(
					"binary",
					map[string]string{"address": "258"},
					map[string]interface{}{"value": float64(42.123)},
					time.Unix(1658774489, 0),
				),
			},
		},
		{
			name: "time (RFC3339/Berlin)",
			data: []interface{}{
				"2022-07-25T20:41:29+02:00", // time
				uint16(0x0102),              // address
				float64(42.123),             // value
			},
			entries: []Entry{
				{
					Type:       "2006-01-02T15:04:05Z07:00",
					Assignment: "time",
					Timezone:   "Europe/Berlin",
				},
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: []telegraf.Metric{
				metric.New(
					"binary",
					map[string]string{"address": "258"},
					map[string]interface{}{"value": float64(42.123)},
					timeBerlin,
				),
			},
		},
		{
			name: "time (RFC3339/Berlin->UTC)",
			data: []interface{}{
				"2022-07-25T20:41:29+02:00", // time
				uint16(0x0102),              // address
				float64(42.123),             // value
			},
			entries: []Entry{
				{
					Type:       "2006-01-02T15:04:05Z07:00",
					Assignment: "time",
					Timezone:   "UTC",
				},
				{
					Name:       "address",
					Type:       "uint16",
					Assignment: "tag",
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: []telegraf.Metric{
				metric.New(
					"binary",
					map[string]string{"address": "258"},
					map[string]interface{}{"value": float64(42.123)},
					time.Unix(1658774489, 0),
				),
			},
		},
		{
			name: "omit",
			data: []interface{}{
				"2022-07-25T20:41:29+02:00", // time
				uint16(0x0102),              // address
				float64(42.123),             // value
			},
			entries: []Entry{
				{
					Type:       "2006-01-02T15:04:05Z07:00",
					Assignment: "time",
					Timezone:   "UTC",
				},
				{
					Type: "uint16",
					Omit: true,
				},
				{
					Name: "value",
					Type: "float64",
				},
			},
			expected: []telegraf.Metric{
				metric.New(
					"binary",
					map[string]string{},
					map[string]interface{}{"value": float64(42.123)},
					time.Unix(1658774489, 0),
				),
			},
		},
	}

	for _, tt := range tests {
		for _, endianness := range []string{"be", "le", "host"} {
			name := fmt.Sprintf("%s (%s)", tt.name, endianness)
			t.Run(name, func(t *testing.T) {
				parser := &Parser{
					Endianness: endianness,
					Configs:    []Config{{Entries: tt.entries}},
					Log:        testutil.Logger{Name: "parsers.binary"},
					metricName: "binary",
				}
				require.NoError(t, parser.Init())

				order := determineEndianness(endianness)
				data, err := generateBinary(tt.data, order)
				require.NoError(t, err)

				metrics, err := parser.Parse(data)
				require.NoError(t, err)

				var options []cmp.Option
				if tt.ignoreTime {
					options = append(options, testutil.IgnoreTime())
				}
				testutil.RequireMetricsEqual(t, tt.expected, metrics, options...)
			})
		}
	}
}

func TestCases(t *testing.T) {
	// Get all directories in testdata
	folders, err := os.ReadDir("testcases")
	require.NoError(t, err)
	require.NotEmpty(t, folders)

	for _, f := range folders {
		testcasePath := filepath.Join("testcases", f.Name())
		configFilename := filepath.Join(testcasePath, "telegraf.conf")

		t.Run(f.Name(), func(t *testing.T) {
			// Configure the plugin
			cfg := config.NewConfig()
			require.NoError(t, cfg.LoadConfig(configFilename))
			require.NoError(t, err)
			require.Len(t, cfg.Inputs, 1)

			// Tune the test-plugin
			plugin := cfg.Inputs[0].Input.(*test.Plugin)
			plugin.Path = testcasePath
			require.NoError(t, plugin.Init())

			// Gather the metrics and check for potential errors
			var acc testutil.Accumulator
			err := plugin.Gather(&acc)
			switch len(plugin.ExpectedErrors) {
			case 0:
				require.NoError(t, err)
			case 1:
				require.ErrorContains(t, err, plugin.ExpectedErrors[0])
			default:
				require.Contains(t, plugin.ExpectedErrors, err.Error())
			}

			// Determine checking options
			options := []cmp.Option{
				cmpopts.EquateApprox(0, 1e-6),
			}
			if plugin.ShouldIgnoreTimestamp {
				options = append(options, testutil.IgnoreTime())
			}

			// Process expected metrics and compare with resulting metrics
			actual := acc.GetTelegrafMetrics()
			testutil.RequireMetricsEqual(t, plugin.Expected, actual, options...)
		})
	}
}

func TestHexEncoding(t *testing.T) {
	testdata := []interface{}{
		uint64(0x01020304050607),
		uint64(0x08090A0B0C0D0E),
		uint64(0x0F101213141516),
		uint64(0x1718191A1B1C1D),
		uint64(0x1E1F2021222324),
	}

	parser := &Parser{
		Endianness: "be",
		Encoding:   "hex",
		Configs: []Config{
			{
				Entries: []Entry{dummyEntry},
			},
		},
		Log:        testutil.Logger{Name: "parsers.binary"},
		metricName: "binary",
	}
	require.NoError(t, parser.Init())

	// Generate the binary data and encode it to HEX
	data, err := generateBinary(testdata, binary.BigEndian)
	require.NoError(t, err)
	encoded := hex.EncodeToString(data)

	metrics, err := parser.Parse([]byte(encoded))
	require.NoError(t, err)
	require.NotEmpty(t, metrics)
}

var benchmarkData = [][]byte{
	{
		0x6d, 0x79, 0x68, 0x6f, 0x73, 0x74, 0x00, 0x33,
		0x2e, 0x31, 0x31, 0x2e, 0x35, 0x00, 0x70, 0x79,
		0x74, 0x68, 0x6f, 0x6e, 0x00, 0x40, 0x14, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00,
	},
	{
		0x6d, 0x79, 0x68, 0x6f, 0x73, 0x74, 0x00, 0x33,
		0x2e, 0x31, 0x31, 0x2e, 0x34, 0x00, 0x70, 0x79,
		0x74, 0x68, 0x6f, 0x6e, 0x00, 0x40, 0x10, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00,
	},
}

func TestBenchmarkData(t *testing.T) {
	plugin := &Parser{
		Endianness: "be",
		Configs: []Config{
			{
				MetricName: "benchmark",
				Entries: []Entry{
					{
						Name:       "source",
						Type:       "string",
						Assignment: "tag",
						Terminator: "null",
					},
					{
						Name:       "tags_sdkver",
						Type:       "string",
						Assignment: "tag",
						Terminator: "null",
					},
					{
						Name:       "tags_platform",
						Type:       "string",
						Assignment: "tag",
						Terminator: "null",
					},
					{
						Name:       "value",
						Type:       "float64",
						Assignment: "field",
					},
				},
			},
		},
	}
	require.NoError(t, plugin.Init())

	expected := []telegraf.Metric{
		metric.New(
			"benchmark",
			map[string]string{
				"source":        "myhost",
				"tags_platform": "python",
				"tags_sdkver":   "3.11.5",
			},
			map[string]interface{}{
				"value": 5.0,
			},
			time.Unix(0, 0),
		),
		metric.New(
			"benchmark",
			map[string]string{
				"source":        "myhost",
				"tags_platform": "python",
				"tags_sdkver":   "3.11.4",
			},
			map[string]interface{}{
				"value": 4.0,
			},
			time.Unix(0, 0),
		),
	}

	actual := make([]telegraf.Metric, 0, 2)
	for _, buf := range benchmarkData {
		m, err := plugin.Parse(buf)
		require.NoError(t, err)
		actual = append(actual, m...)
	}
	testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime(), testutil.SortMetrics())
}

func BenchmarkParsing(b *testing.B) {
	plugin := &Parser{
		Endianness: "be",
		Configs: []Config{
			{
				MetricName: "benchmark",
				Entries: []Entry{
					{
						Name:       "source",
						Type:       "string",
						Assignment: "tag",
						Terminator: "null",
					},
					{
						Name:       "tags_sdkver",
						Type:       "string",
						Assignment: "tag",
						Terminator: "null",
					},
					{
						Name:       "tags_platform",
						Type:       "string",
						Assignment: "tag",
						Terminator: "null",
					},
					{
						Name:       "value",
						Type:       "float64",
						Assignment: "field",
					},
				},
			},
		},
	}
	require.NoError(b, plugin.Init())

	for n := 0; n < b.N; n++ {
		//nolint:errcheck // Benchmarking so skip the error check to avoid the unnecessary operations
		plugin.Parse(benchmarkData[n%2])
	}
}
