package value

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

func TestParseValidValues(t *testing.T) {
	tests := []struct {
		name     string
		dtype    string
		input    []byte
		expected interface{}
	}{
		{
			name:     "integer",
			dtype:    "integer",
			input:    []byte("55"),
			expected: int64(55),
		},
		{
			name:     "float",
			dtype:    "float",
			input:    []byte("64"),
			expected: float64(64),
		},
		{
			name:     "string",
			dtype:    "string",
			input:    []byte("foobar"),
			expected: "foobar",
		},
		{
			name:     "base64",
			dtype:    "base64",
			input:    []byte("foobar"),
			expected: "Zm9vYmFy",
		},
		{
			name:     "boolean",
			dtype:    "boolean",
			input:    []byte("true"),
			expected: true,
		},
		{
			name:     "multiple integers",
			dtype:    "integer",
			input:    []byte(`55 45 223 12 999`),
			expected: int64(999),
		},
		{
			name:     "auto integer",
			dtype:    "auto_integer",
			input:    []byte("55"),
			expected: int64(55),
		},
		{
			name:     "auto integer with string",
			dtype:    "auto_integer",
			input:    []byte("foobar"),
			expected: "foobar",
		},
		{
			name:     "auto integer with float",
			dtype:    "auto_integer",
			input:    []byte("55.0"),
			expected: "55.0",
		},
		{
			name:     "auto float",
			dtype:    "auto_float",
			input:    []byte("64.2"),
			expected: float64(64.2),
		},
		{
			name:     "auto float with string",
			dtype:    "auto_float",
			input:    []byte("foobar"),
			expected: "foobar",
		},
		{
			name:     "auto float with integer",
			dtype:    "auto_float",
			input:    []byte("64"),
			expected: float64(64),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			expected := metric.New(
				"value_test",
				map[string]string{},
				map[string]interface{}{"value": tt.expected},
				time.Unix(0, 0),
			)

			plugin := Parser{
				MetricName: "value_test",
				DataType:   tt.dtype,
			}
			require.NoError(t, plugin.Init())
			actual, err := plugin.Parse(tt.input)
			require.NoError(t, err)
			require.Len(t, actual, 1)
			testutil.RequireMetricEqual(t, expected, actual[0], testutil.IgnoreTime())
		})
	}
}

func TestParseLineValidValues(t *testing.T) {
	tests := []struct {
		name     string
		dtype    string
		input    string
		expected interface{}
	}{
		{
			name:     "integer",
			dtype:    "integer",
			input:    "55",
			expected: int64(55),
		},
		{
			name:     "float",
			dtype:    "float",
			input:    "64",
			expected: float64(64),
		},
		{
			name:     "string",
			dtype:    "string",
			input:    "foobar",
			expected: "foobar",
		},
		{
			name:     "base64",
			dtype:    "base64",
			input:    "foobar",
			expected: "Zm9vYmFy",
		},
		{
			name:     "boolean",
			dtype:    "boolean",
			input:    "true",
			expected: true,
		},
		{
			name:     "multiple integers",
			dtype:    "integer",
			input:    `55 45 223 12 999`,
			expected: int64(999),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			expected := metric.New(
				"value_test",
				map[string]string{},
				map[string]interface{}{"value": tt.expected},
				time.Unix(0, 0),
			)

			plugin := Parser{
				MetricName: "value_test",
				DataType:   tt.dtype,
			}
			require.NoError(t, plugin.Init())
			actual, err := plugin.ParseLine(tt.input)
			require.NoError(t, err)
			testutil.RequireMetricEqual(t, expected, actual, testutil.IgnoreTime())
		})
	}
}

func TestParseCustomFieldName(t *testing.T) {
	parser := Parser{
		MetricName: "value_test",
		DataType:   "integer",
		FieldName:  "penguin",
	}
	require.NoError(t, parser.Init())

	metrics, err := parser.Parse([]byte(`55`))
	require.NoError(t, err)
	require.Equal(t, map[string]interface{}{"penguin": int64(55)}, metrics[0].Fields())
}

func TestParseInvalidValues(t *testing.T) {
	tests := []struct {
		name  string
		dtype string
		input []byte
	}{
		{
			name:  "integer",
			dtype: "integer",
			input: []byte("55.0"),
		},
		{
			name:  "float",
			dtype: "float",
			input: []byte("foobar"),
		},
		{
			name:  "boolean",
			dtype: "boolean",
			input: []byte("213"),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			plugin := Parser{
				MetricName: "value_test",
				DataType:   tt.dtype,
			}
			require.NoError(t, plugin.Init())
			actual, err := plugin.Parse(tt.input)
			require.ErrorContains(t, err, "invalid syntax")
			require.Empty(t, actual)
		})
	}
}

func TestParseLineInvalidValues(t *testing.T) {
	tests := []struct {
		name  string
		dtype string
		input string
	}{
		{
			name:  "integer",
			dtype: "integer",
			input: "55.0",
		},
		{
			name:  "float",
			dtype: "float",
			input: "foobar",
		},
		{
			name:  "boolean",
			dtype: "boolean",
			input: "213",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			plugin := Parser{
				MetricName: "value_test",
				DataType:   tt.dtype,
			}
			require.NoError(t, plugin.Init())
			actual, err := plugin.ParseLine(tt.input)
			require.ErrorContains(t, err, "invalid syntax")
			require.Empty(t, actual)
		})
	}
}

func TestParseValidValuesDefaultTags(t *testing.T) {
	expected := metric.New(
		"value_test",
		map[string]string{"test": "tag"},
		map[string]interface{}{"value": int64(55)},
		time.Unix(0, 0),
	)

	plugin := Parser{
		MetricName: "value_test",
		DataType:   "integer",
	}
	require.NoError(t, plugin.Init())
	plugin.SetDefaultTags(map[string]string{"test": "tag"})

	actual, err := plugin.Parse([]byte("55"))
	require.NoError(t, err)
	require.Len(t, actual, 1)

	testutil.RequireMetricEqual(t, expected, actual[0], testutil.IgnoreTime())
}

func TestParseValuesWithNullCharacter(t *testing.T) {
	parser := Parser{
		MetricName: "value_test",
		DataType:   "integer",
	}
	require.NoError(t, parser.Init())
	metrics, err := parser.Parse([]byte("55\x00"))
	require.NoError(t, err)
	require.Len(t, metrics, 1)
	require.Equal(t, "value_test", metrics[0].Name())
	require.Equal(t, map[string]interface{}{
		"value": int64(55),
	}, metrics[0].Fields())
	require.Equal(t, map[string]string{}, metrics[0].Tags())
}

func TestInvalidDatatype(t *testing.T) {
	parser := Parser{
		MetricName: "value_test",
		DataType:   "foo",
	}
	require.ErrorContains(t, parser.Init(), "unknown datatype")
}

const benchmarkData = `5`

func TestBenchmarkData(t *testing.T) {
	plugin := &Parser{}
	require.NoError(t, plugin.Init())

	expected := []telegraf.Metric{
		metric.New(
			"",
			map[string]string{},
			map[string]interface{}{
				"value": 5,
			},
			time.Unix(0, 0),
		),
	}

	actual, err := plugin.Parse([]byte(benchmarkData))
	require.NoError(t, err)
	testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime(), testutil.SortMetrics())
}

func BenchmarkParsing(b *testing.B) {
	plugin := &Parser{}
	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([]byte(benchmarkData))
	}
}
