package xpath

import (
	"bufio"
	"errors"
	"fmt"
	"math"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/plugins/inputs"
	"github.com/influxdata/telegraf/plugins/inputs/file"
	"github.com/influxdata/telegraf/plugins/parsers/influx"
	"github.com/influxdata/telegraf/plugins/parsers/temporary/xpath"
	"github.com/influxdata/telegraf/testutil"
	"github.com/influxdata/toml"

	"github.com/stretchr/testify/require"
)

const invalidXML = `
<?xml version="1.0"?>
	<Device_1>This one has to fail due to missing end-tag
`

const singleMetricValuesXML = `
<?xml version="1.0"?>
	<Device_1>
		<Name>Device TestDevice1</Name>
		<State>ok</State>
		<Timestamp_unix>1577923199</Timestamp_unix>
		<Timestamp_unix_ms>1577923199128</Timestamp_unix_ms>
		<Timestamp_unix_us>1577923199128256</Timestamp_unix_us>
		<Timestamp_unix_ns>1577923199128256512</Timestamp_unix_ns>
		<Timestamp_iso>2020-01-01T23:59:59Z</Timestamp_iso>
		<value_int>98247</value_int>
		<value_float>98695.81</value_float>
		<value_bool>true</value_bool>
		<value_string>this is a test</value_string>
		<value_position>42;23</value_position>
	</Device_1>
`
const singleMetricAttributesXML = `
<?xml version="1.0"?>
	<Device_1>
		<Name value="Device TestDevice1"/>
		<State _="ok"/>
		<Timestamp_unix value="1577923199"/>
		<Timestamp_iso value="2020-01-01T23:59:59Z"/>
		<attr_int _="12345"/>
		<attr_float _="12345.678"/>
		<attr_bool _="true"/>
		<attr_bool_numeric _="1"/>
		<attr_string _="this is a test"/>
	</Device_1>
`
const singleMetricMultiValuesXML = `
<?xml version="1.0"?>
	<Timestamp value="1577923199"/>
	<Device>
		<Value>1</Value>
		<Value>2</Value>
		<Value>3</Value>
		<Value>4</Value>
		<Value>5</Value>
		<Value>6</Value>
	</Device>
`
const multipleNodesXML = `
<?xml version="1.0"?>
	<Timestamp value="1577923199"/>
	<Device name="Device 1">
		<Value mode="0">42.0</Value>
		<Active>1</Active>
		<State>ok</State>
	</Device>
	<Device name="Device 2">
		<Value mode="1">42.1</Value>
		<Active>0</Active>
		<State>ok</State>
	</Device>
	<Device name="Device 3">
		<Value mode="2">42.2</Value>
		<Active>1</Active>
		<State>ok</State>
	</Device>
	<Device name="Device 4">
		<Value mode="3">42.3</Value>
		<Active>0</Active>
		<State>failed</State>
	</Device>
	<Device name="Device 5">
		<Value mode="4">42.4</Value>
		<Active>1</Active>
		<State>failed</State>
	</Device>
`

const metricNameQueryXML = `
<?xml version="1.0"?>
	<Device_1>
		<Timestamp_unix>1577923199</Timestamp_unix>
		<Metric state="ok"/>
	</Device_1>
`

func TestParseInvalidXML(t *testing.T) {
	var tests = []struct {
		name          string
		input         string
		configs       []xpath.Config
		defaultTags   map[string]string
		expectedError string
	}{
		{
			name:  "invalid XML (missing close tag)",
			input: invalidXML,
			configs: []xpath.Config{
				{
					MetricQuery: "test",
					Timestamp:   "/Device_1/Timestamp_unix",
				},
			},
			defaultTags:   map[string]string{},
			expectedError: "XML syntax error on line 4: unexpected EOF",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName: "xml",
				Configs:           tt.configs,
				DefaultTags:       tt.defaultTags,
				Log:               testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			_, err := parser.ParseLine(tt.input)
			require.Error(t, err)
			require.Equal(t, tt.expectedError, err.Error())
		})
	}
}

func TestInvalidTypeQueriesFail(t *testing.T) {
	var tests = []struct {
		name          string
		input         string
		configs       []xpath.Config
		defaultTags   map[string]string
		expectedError string
	}{
		{
			name:  "invalid field (int) type",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix",
					FieldsInt: map[string]string{
						"a": "/Device_1/value_string",
					},
				},
			},
			defaultTags:   map[string]string{},
			expectedError: "failed to parse field (int) 'a': strconv.ParseInt: parsing \"this is a test\": invalid syntax",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName: "xml",
				Configs:           tt.configs,
				DefaultTags:       tt.defaultTags,
				Log:               testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			_, err := parser.ParseLine(tt.input)
			require.Error(t, err)
			require.Equal(t, tt.expectedError, err.Error())
		})
	}
}

func TestInvalidTypeQueries(t *testing.T) {
	var tests = []struct {
		name        string
		input       string
		configs     []xpath.Config
		defaultTags map[string]string
		expected    telegraf.Metric
	}{
		{
			name:  "invalid field type (number)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix",
					Fields: map[string]string{
						"a": "number(/Device_1/value_string)",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"a": math.NaN(),
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "invalid field type (boolean)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix",
					Fields: map[string]string{
						"a": "boolean(/Device_1/value_string)",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"a": true,
				},
				time.Unix(1577923199, 0),
			),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName: "test",
				Configs:           tt.configs,
				DefaultTags:       tt.defaultTags,
				Log:               testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			actual, err := parser.ParseLine(tt.input)
			require.NoError(t, err)

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

func TestParseTimestamps(t *testing.T) {
	var tests = []struct {
		name        string
		input       string
		configs     []xpath.Config
		defaultTags map[string]string
		expected    telegraf.Metric
	}{
		{
			name:  "parse timestamp (no fmt)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix",
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse timestamp (unix)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp:    "/Device_1/Timestamp_unix",
					TimestampFmt: "unix",
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse timestamp (unix_ms)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp:    "/Device_1/Timestamp_unix_ms",
					TimestampFmt: "unix_ms",
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{},
				time.Unix(0, int64(1577923199128*1e6)),
			),
		},
		{
			name:  "parse timestamp (unix_us)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp:    "/Device_1/Timestamp_unix_us",
					TimestampFmt: "unix_us",
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{},
				time.Unix(0, int64(1577923199128256*1e3)),
			),
		},
		{
			name:  "parse timestamp (unix_us)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp:    "/Device_1/Timestamp_unix_ns",
					TimestampFmt: "unix_ns",
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{},
				time.Unix(0, int64(1577923199128256512)),
			),
		},
		{
			name:  "parse timestamp (RFC3339)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp:    "/Device_1/Timestamp_iso",
					TimestampFmt: "2006-01-02T15:04:05Z",
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{},
				time.Unix(1577923199, 0),
			),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName: "test",
				Configs:           tt.configs,
				DefaultTags:       tt.defaultTags,
				Log:               testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			actual, err := parser.ParseLine(tt.input)
			require.NoError(t, err)

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

func TestParseSingleValues(t *testing.T) {
	var tests = []struct {
		name        string
		input       string
		configs     []xpath.Config
		defaultTags map[string]string
		expected    telegraf.Metric
	}{
		{
			name:  "parse scalar values as string fields",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix",
					Fields: map[string]string{
						"a": "/Device_1/value_int",
						"b": "/Device_1/value_float",
						"c": "/Device_1/value_bool",
						"d": "/Device_1/value_string",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"a": "98247",
					"b": "98695.81",
					"c": "true",
					"d": "this is a test",
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse scalar values as typed fields (w/o int)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix",
					Fields: map[string]string{
						"a": "number(Device_1/value_int)",
						"b": "number(/Device_1/value_float)",
						"c": "boolean(/Device_1/value_bool)",
						"d": "/Device_1/value_string",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"a": 98247.0,
					"b": 98695.81,
					"c": true,
					"d": "this is a test",
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse values as typed fields (w/ int)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix",
					Fields: map[string]string{
						"b": "number(/Device_1/value_float)",
						"c": "boolean(/Device_1/value_bool)",
						"d": "/Device_1/value_string",
					},
					FieldsInt: map[string]string{
						"a": "/Device_1/value_int",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"a": 98247,
					"b": 98695.81,
					"c": true,
					"d": "this is a test",
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse substring values",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix",
					Fields: map[string]string{
						"x": "substring-before(/Device_1/value_position, ';')",
						"y": "substring-after(/Device_1/value_position, ';')",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"x": "42",
					"y": "23",
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse substring values (typed)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix",
					Fields: map[string]string{
						"x": "number(substring-before(/Device_1/value_position, ';'))",
						"y": "number(substring-after(/Device_1/value_position, ';'))",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"x": 42.0,
					"y": 23.0,
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse substring values (typed int)",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix",
					FieldsInt: map[string]string{
						"x": "substring-before(/Device_1/value_position, ';')",
						"y": "substring-after(/Device_1/value_position, ';')",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"x": 42,
					"y": 23,
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse tags",
			input: singleMetricValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix",
					Tags: map[string]string{
						"state": "/Device_1/State",
						"name":  "substring-after(/Device_1/Name, ' ')",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{
					"state": "ok",
					"name":  "TestDevice1",
				},
				map[string]interface{}{},
				time.Unix(1577923199, 0),
			),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName: "test",
				Configs:           tt.configs,
				DefaultTags:       tt.defaultTags,
				Log:               testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			actual, err := parser.ParseLine(tt.input)
			require.NoError(t, err)

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

func TestParseSingleAttributes(t *testing.T) {
	var tests = []struct {
		name        string
		input       string
		configs     []xpath.Config
		defaultTags map[string]string
		expected    telegraf.Metric
	}{
		{
			name:  "parse attr timestamp (unix)",
			input: singleMetricAttributesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix/@value",
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse attr timestamp (RFC3339)",
			input: singleMetricAttributesXML,
			configs: []xpath.Config{
				{
					Timestamp:    "/Device_1/Timestamp_iso/@value",
					TimestampFmt: "2006-01-02T15:04:05Z",
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse attr as string fields",
			input: singleMetricAttributesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix/@value",
					Fields: map[string]string{
						"a": "/Device_1/attr_int/@_",
						"b": "/Device_1/attr_float/@_",
						"c": "/Device_1/attr_bool/@_",
						"d": "/Device_1/attr_string/@_",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"a": "12345",
					"b": "12345.678",
					"c": "true",
					"d": "this is a test",
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse attr as typed fields (w/o int)",
			input: singleMetricAttributesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix/@value",
					Fields: map[string]string{
						"a": "number(/Device_1/attr_int/@_)",
						"b": "number(/Device_1/attr_float/@_)",
						"c": "boolean(/Device_1/attr_bool/@_)",
						"d": "/Device_1/attr_string/@_",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"a": 12345.0,
					"b": 12345.678,
					"c": true,
					"d": "this is a test",
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse attr as typed fields (w/ int)",
			input: singleMetricAttributesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix/@value",
					Fields: map[string]string{
						"b": "number(/Device_1/attr_float/@_)",
						"c": "boolean(/Device_1/attr_bool/@_)",
						"d": "/Device_1/attr_string/@_",
					},
					FieldsInt: map[string]string{
						"a": "/Device_1/attr_int/@_",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"a": 12345,
					"b": 12345.678,
					"c": true,
					"d": "this is a test",
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse attr substring",
			input: singleMetricAttributesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix/@value",
					Fields: map[string]string{
						"name": "substring-after(/Device_1/Name/@value, ' ')",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"name": "TestDevice1",
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse attr tags",
			input: singleMetricAttributesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix/@value",
					Tags: map[string]string{
						"state": "/Device_1/State/@_",
						"name":  "substring-after(/Device_1/Name/@value, ' ')",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{
					"state": "ok",
					"name":  "TestDevice1",
				},
				map[string]interface{}{},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse attr bool",
			input: singleMetricAttributesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Device_1/Timestamp_unix/@value",
					Fields: map[string]string{
						"a": "/Device_1/attr_bool_numeric/@_ = 1",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"a": true,
				},
				time.Unix(1577923199, 0),
			),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName: "test",
				Configs:           tt.configs,
				DefaultTags:       tt.defaultTags,
				Log:               testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			actual, err := parser.ParseLine(tt.input)
			require.NoError(t, err)

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

func TestParseMultiValues(t *testing.T) {
	var tests = []struct {
		name        string
		input       string
		configs     []xpath.Config
		defaultTags map[string]string
		expected    telegraf.Metric
	}{
		{
			name:  "select values (float)",
			input: singleMetricMultiValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Timestamp/@value",
					Fields: map[string]string{
						"a": "number(/Device/Value[1])",
						"b": "number(/Device/Value[2])",
						"c": "number(/Device/Value[3])",
						"d": "number(/Device/Value[4])",
						"e": "number(/Device/Value[5])",
						"f": "number(/Device/Value[6])",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"a": 1.0,
					"b": 2.0,
					"c": 3.0,
					"d": 4.0,
					"e": 5.0,
					"f": 6.0,
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "select values (int)",
			input: singleMetricMultiValuesXML,
			configs: []xpath.Config{
				{
					Timestamp: "/Timestamp/@value",
					FieldsInt: map[string]string{
						"a": "/Device/Value[1]",
						"b": "/Device/Value[2]",
						"c": "/Device/Value[3]",
						"d": "/Device/Value[4]",
						"e": "/Device/Value[5]",
						"f": "/Device/Value[6]",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"test",
				map[string]string{},
				map[string]interface{}{
					"a": 1,
					"b": 2,
					"c": 3,
					"d": 4,
					"e": 5,
					"f": 6,
				},
				time.Unix(1577923199, 0),
			),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName: "test",
				Configs:           tt.configs,
				DefaultTags:       tt.defaultTags,
				Log:               testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			actual, err := parser.ParseLine(tt.input)
			require.NoError(t, err)

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

func TestParseMultiNodes(t *testing.T) {
	var tests = []struct {
		name        string
		input       string
		configs     []xpath.Config
		defaultTags map[string]string
		expected    []telegraf.Metric
	}{
		{
			name:  "select all devices",
			input: multipleNodesXML,
			configs: []xpath.Config{
				{
					Selection: "/Device",
					Timestamp: "/Timestamp/@value",
					Fields: map[string]string{
						"value":  "number(Value)",
						"active": "Active = 1",
					},
					FieldsInt: map[string]string{
						"mode": "Value/@mode",
					},
					Tags: map[string]string{
						"name":  "@name",
						"state": "State",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: []telegraf.Metric{
				testutil.MustMetric(
					"test",
					map[string]string{
						"name":  "Device 1",
						"state": "ok",
					},
					map[string]interface{}{
						"value":  42.0,
						"active": true,
						"mode":   0,
					},
					time.Unix(1577923199, 0),
				),
				testutil.MustMetric(
					"test",
					map[string]string{
						"name":  "Device 2",
						"state": "ok",
					},
					map[string]interface{}{
						"value":  42.1,
						"active": false,
						"mode":   1,
					},
					time.Unix(1577923199, 0),
				),
				testutil.MustMetric(
					"test",
					map[string]string{
						"name":  "Device 3",
						"state": "ok",
					},
					map[string]interface{}{
						"value":  42.2,
						"active": true,
						"mode":   2,
					},
					time.Unix(1577923199, 0),
				),
				testutil.MustMetric(
					"test",
					map[string]string{
						"name":  "Device 4",
						"state": "failed",
					},
					map[string]interface{}{
						"value":  42.3,
						"active": false,
						"mode":   3,
					},
					time.Unix(1577923199, 0),
				),
				testutil.MustMetric(
					"test",
					map[string]string{
						"name":  "Device 5",
						"state": "failed",
					},
					map[string]interface{}{
						"value":  42.4,
						"active": true,
						"mode":   4,
					},
					time.Unix(1577923199, 0),
				),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName: "test",
				Configs:           tt.configs,
				DefaultTags:       tt.defaultTags,
				Log:               testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			actual, err := parser.Parse([]byte(tt.input))
			require.NoError(t, err)

			testutil.RequireMetricsEqual(t, tt.expected, actual)
		})
	}
}

func TestParseMetricQuery(t *testing.T) {
	var tests = []struct {
		name        string
		input       string
		configs     []xpath.Config
		defaultTags map[string]string
		expected    telegraf.Metric
	}{
		{
			name:  "parse metric name query",
			input: metricNameQueryXML,
			configs: []xpath.Config{
				{
					MetricQuery: "name(/Device_1/Metric/@*[1])",
					Timestamp:   "/Device_1/Timestamp_unix",
					Fields: map[string]string{
						"value": "/Device_1/Metric/@*[1]",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"state",
				map[string]string{},
				map[string]interface{}{
					"value": "ok",
				},
				time.Unix(1577923199, 0),
			),
		},
		{
			name:  "parse metric name constant",
			input: metricNameQueryXML,
			configs: []xpath.Config{
				{
					MetricQuery: "'the_metric'",
					Timestamp:   "/Device_1/Timestamp_unix",
					Fields: map[string]string{
						"value": "/Device_1/Metric/@*[1]",
					},
				},
			},
			defaultTags: map[string]string{},
			expected: testutil.MustMetric(
				"the_metric",
				map[string]string{},
				map[string]interface{}{
					"value": "ok",
				},
				time.Unix(1577923199, 0),
			),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName: "test",
				Configs:           tt.configs,
				DefaultTags:       tt.defaultTags,
				Log:               testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			actual, err := parser.ParseLine(tt.input)
			require.NoError(t, err)

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

func TestParseErrors(t *testing.T) {
	var tests = []struct {
		name     string
		input    string
		configs  []xpath.Config
		expected string
	}{
		{
			name:  "string metric name query",
			input: metricNameQueryXML,
			configs: []xpath.Config{
				{
					MetricQuery: "arbitrary",
					Timestamp:   "/Device_1/Timestamp_unix",
					Fields: map[string]string{
						"value": "/Device_1/Metric/@*[1]",
					},
				},
			},
			expected: "failed to query metric name: query result is of type <nil> not 'string'",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName: "test",
				Configs:           tt.configs,
				DefaultTags:       map[string]string{},
				Log:               testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			_, err := parser.ParseLine(tt.input)
			require.Error(t, err)
			require.Equal(t, tt.expected, err.Error())
		})
	}
}

func TestEmptySelection(t *testing.T) {
	var tests = []struct {
		name    string
		input   string
		configs []xpath.Config
	}{
		{
			name:  "empty path",
			input: multipleNodesXML,
			configs: []xpath.Config{
				{
					Selection: "/Device/NonExisting",
					Fields:    map[string]string{"value": "number(Value)"},
					FieldsInt: map[string]string{"mode": "Value/@mode"},
					Tags:      map[string]string{},
				},
			},
		},
		{
			name:  "empty pattern",
			input: multipleNodesXML,
			configs: []xpath.Config{
				{
					Selection: "//NonExisting",
					Fields:    map[string]string{"value": "number(Value)"},
					FieldsInt: map[string]string{"mode": "Value/@mode"},
					Tags:      map[string]string{},
				},
			},
		},
		{
			name:  "empty axis",
			input: multipleNodesXML,
			configs: []xpath.Config{
				{
					Selection: "/Device/child::NonExisting",
					Fields:    map[string]string{"value": "number(Value)"},
					FieldsInt: map[string]string{"mode": "Value/@mode"},
					Tags:      map[string]string{},
				},
			},
		},
		{
			name:  "empty predicate",
			input: multipleNodesXML,
			configs: []xpath.Config{
				{
					Selection: "/Device[@NonExisting=true]",
					Fields:    map[string]string{"value": "number(Value)"},
					FieldsInt: map[string]string{"mode": "Value/@mode"},
					Tags:      map[string]string{},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName: "test",
				Configs:           tt.configs,
				DefaultTags:       map[string]string{},
				Log:               testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			_, err := parser.Parse([]byte(tt.input))
			require.Error(t, err)
			require.Equal(t, err.Error(), "cannot parse with empty selection node")
		})
	}
}

func TestEmptySelectionAllowed(t *testing.T) {
	var tests = []struct {
		name    string
		input   string
		configs []xpath.Config
	}{
		{
			name:  "empty path",
			input: multipleNodesXML,
			configs: []xpath.Config{
				{
					Selection: "/Device/NonExisting",
					Fields:    map[string]string{"value": "number(Value)"},
					FieldsInt: map[string]string{"mode": "Value/@mode"},
					Tags:      map[string]string{},
				},
			},
		},
		{
			name:  "empty pattern",
			input: multipleNodesXML,
			configs: []xpath.Config{
				{
					Selection: "//NonExisting",
					Fields:    map[string]string{"value": "number(Value)"},
					FieldsInt: map[string]string{"mode": "Value/@mode"},
					Tags:      map[string]string{},
				},
			},
		},
		{
			name:  "empty axis",
			input: multipleNodesXML,
			configs: []xpath.Config{
				{
					Selection: "/Device/child::NonExisting",
					Fields:    map[string]string{"value": "number(Value)"},
					FieldsInt: map[string]string{"mode": "Value/@mode"},
					Tags:      map[string]string{},
				},
			},
		},
		{
			name:  "empty predicate",
			input: multipleNodesXML,
			configs: []xpath.Config{
				{
					Selection: "/Device[@NonExisting=true]",
					Fields:    map[string]string{"value": "number(Value)"},
					FieldsInt: map[string]string{"mode": "Value/@mode"},
					Tags:      map[string]string{},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			parser := &Parser{
				DefaultMetricName:   "xml",
				Configs:             tt.configs,
				AllowEmptySelection: true,
				DefaultTags:         map[string]string{},
				Log:                 testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())

			_, err := parser.Parse([]byte(tt.input))
			require.NoError(t, err)
		})
	}
}

func TestTestCases(t *testing.T) {
	var tests = []struct {
		name     string
		filename string
	}{
		{
			name:     "explicit basic",
			filename: "testcases/multisensor_explicit_basic.conf",
		},
		{
			name:     "explicit batch",
			filename: "testcases/multisensor_explicit_batch.conf",
		},
		{
			name:     "field selection batch",
			filename: "testcases/multisensor_selection_batch.conf",
		},
		{
			name:     "earthquakes quakeml",
			filename: "testcases/earthquakes.conf",
		},
		{
			name:     "openweathermap forecast (xml)",
			filename: "testcases/openweathermap_xml.conf",
		},
		{
			name:     "openweathermap forecast (json)",
			filename: "testcases/openweathermap_json.conf",
		},
		{
			name:     "addressbook tutorial (protobuf)",
			filename: "testcases/addressbook.conf",
		},
		{
			name:     "message-pack",
			filename: "testcases/tracker_msgpack.conf",
		},
		{
			name:     "field and tag batch (json)",
			filename: "testcases/field_tag_batch.conf",
		},
	}

	parser := &influx.Parser{}
	require.NoError(t, parser.Init())

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			filename := filepath.FromSlash(tt.filename)
			cfg, header, err := loadTestConfiguration(filename)
			require.NoError(t, err)

			// Load the xml-content
			input, err := testutil.ParseRawLinesFrom(header, "File:")
			require.NoError(t, err)
			require.Len(t, input, 1)

			filefields := strings.Fields(input[0])
			require.GreaterOrEqual(t, len(filefields), 1)
			datafile := filepath.FromSlash(filefields[0])
			fileformat := ""
			if len(filefields) > 1 {
				fileformat = filefields[1]
			}

			// Load the protocol buffer information if required
			var pbmsgdef, pbmsgtype string
			if fileformat == "xpath_protobuf" {
				input, err := testutil.ParseRawLinesFrom(header, "Protobuf:")
				require.NoError(t, err)
				require.Len(t, input, 1)

				protofields := strings.Fields(input[0])
				require.Len(t, protofields, 2)
				pbmsgdef = protofields[0]
				pbmsgtype = protofields[1]
			}

			content, err := os.ReadFile(datafile)
			require.NoError(t, err)

			// Get the expectations
			expectedOutputs, err := testutil.ParseMetricsFrom(header, "Expected Output:", parser)
			require.NoError(t, err)

			expectedErrors, _ := testutil.ParseRawLinesFrom(header, "Expected Error:")

			// Setup the parser and run it.
			metricName := "xml"
			if fileformat != "" {
				metricName = fileformat
			}
			parser := &Parser{
				DefaultMetricName:   metricName,
				Format:              fileformat,
				ProtobufMessageDef:  pbmsgdef,
				ProtobufMessageType: pbmsgtype,
				Configs:             []xpath.Config{*cfg},
				Log:                 testutil.Logger{Name: "parsers.xml"},
			}
			require.NoError(t, parser.Init())
			outputs, err := parser.Parse(content)
			if len(expectedErrors) == 0 {
				require.NoError(t, err)
			}
			// If no timestamp is given we cannot test it. So use the one of the output
			if cfg.Timestamp == "" {
				testutil.RequireMetricsEqual(t, expectedOutputs, outputs, testutil.IgnoreTime())
			} else {
				testutil.RequireMetricsEqual(t, expectedOutputs, outputs)
			}
		})
	}
}

func TestProtobufImporting(t *testing.T) {
	// Setup the parser and run it.
	parser := &Parser{
		DefaultMetricName:   "xpath_protobuf",
		Format:              "xpath_protobuf",
		ProtobufMessageDef:  "person.proto",
		ProtobufMessageType: "importtest.Person",
		ProtobufImportPaths: []string{"testcases/protos"},
		Configs:             []xpath.Config{},
		Log:                 testutil.Logger{Name: "parsers.protobuf"},
	}
	require.NoError(t, parser.Init())
}

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

	// Make sure the folder contains data
	require.NotEmpty(t, folders)

	for _, f := range folders {
		if !f.IsDir() {
			continue
		}
		t.Run(f.Name(), func(t *testing.T) {
			configFilename := filepath.Join("testcases", f.Name(), "telegraf.conf")
			expectedFilename := filepath.Join("testcases", f.Name(), "expected.out")

			// Process the telegraf config file for the test
			buf, err := os.ReadFile(configFilename)
			if err != nil && errors.Is(err, os.ErrNotExist) {
				return
			}
			require.NoError(t, err)
			inputs.Add("file", func() telegraf.Input {
				return &file.File{}
			})
			cfg := config.NewConfig()
			require.NoError(t, cfg.LoadConfigData(buf))

			// Gather the metrics from the input file configure
			acc := testutil.Accumulator{}
			for _, input := range cfg.Inputs {
				require.NoError(t, input.Init())
				require.NoError(t, input.Gather(&acc))
			}

			// Process expected metrics and compare with resulting metrics
			expected, err := readMetricFile(expectedFilename)
			require.NoError(t, err)
			actual := acc.GetTelegrafMetrics()
			testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
		})
	}
}

func readMetricFile(filename string) ([]telegraf.Metric, error) {
	var metrics []telegraf.Metric

	f, err := os.Open(filename)
	if err != nil {
		return metrics, err
	}
	defer f.Close()

	parser := &influx.Parser{}
	if err := parser.Init(); err != nil {
		return nil, err
	}

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		if line != "" {
			m, err := parser.ParseLine(line)
			if err != nil {
				return nil, fmt.Errorf("unable to parse metric in %q failed: %v", line, err)
			}
			// The timezone needs to be UTC to match the timestamp test results
			m.SetTime(m.Time().UTC())
			metrics = append(metrics, m)
		}
	}

	return metrics, nil
}

func loadTestConfiguration(filename string) (*xpath.Config, []string, error) {
	buf, err := os.ReadFile(filename)
	if err != nil {
		return nil, nil, err
	}

	header := make([]string, 0)
	for _, line := range strings.Split(string(buf), "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "#") {
			header = append(header, line)
		}
	}
	cfg := xpath.Config{}
	err = toml.Unmarshal(buf, &cfg)
	return &cfg, header, err
}
