package logfmt

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

func TestParse(t *testing.T) {
	tests := []struct {
		name        string
		measurement string
		bytes       []byte
		want        []telegraf.Metric
		wantErr     bool
	}{
		{
			name: "no bytes returns no metrics",
		},
		{
			name:        "test without trailing end",
			bytes:       []byte("foo=\"bar\""),
			measurement: "testlog",
			want: []telegraf.Metric{
				metric.New(
					"testlog",
					map[string]string{},
					map[string]interface{}{
						"foo": "bar",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name:        "test with trailing end",
			bytes:       []byte("foo=\"bar\"\n"),
			measurement: "testlog",
			want: []telegraf.Metric{
				metric.New(
					"testlog",
					map[string]string{},
					map[string]interface{}{
						"foo": "bar",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name:        "logfmt parser returns all the fields",
			bytes:       []byte(`ts=2018-07-24T19:43:40.275Z lvl=info msg="http request" method=POST`),
			measurement: "testlog",
			want: []telegraf.Metric{
				metric.New(
					"testlog",
					map[string]string{},
					map[string]interface{}{
						"lvl":    "info",
						"msg":    "http request",
						"method": "POST",
						"ts":     "2018-07-24T19:43:40.275Z",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "logfmt parser parses every line",
			bytes: []byte(
				"ts=2018-07-24T19:43:40.275Z lvl=info msg=\"http request\" method=POST\nparent_id=088876RL000 duration=7.45 log_id=09R4e4Rl000",
			),
			measurement: "testlog",
			want: []telegraf.Metric{
				metric.New(
					"testlog",
					map[string]string{},
					map[string]interface{}{
						"lvl":    "info",
						"msg":    "http request",
						"method": "POST",
						"ts":     "2018-07-24T19:43:40.275Z",
					},
					time.Unix(0, 0),
				),
				metric.New(
					"testlog",
					map[string]string{},
					map[string]interface{}{
						"parent_id": "088876RL000",
						"duration":  7.45,
						"log_id":    "09R4e4Rl000",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name:    "keys without = or values are ignored",
			bytes:   []byte(`i am no data.`),
			wantErr: false,
		},
		{
			name:    "keys without values are ignored",
			bytes:   []byte(`foo="" bar=`),
			wantErr: false,
		},
		{
			name:        "unterminated quote produces error",
			measurement: "testlog",
			bytes:       []byte(`bar=baz foo="bar`),
			wantErr:     true,
		},
		{
			name:        "malformed key",
			measurement: "testlog",
			bytes:       []byte(`"foo=" bar=baz`),
			wantErr:     true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			plugin := Parser{
				metricName: tt.measurement,
			}
			require.NoError(t, plugin.Init())

			actual, err := plugin.Parse(tt.bytes)
			if tt.wantErr {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
				testutil.RequireMetricsEqual(t, tt.want, actual, testutil.IgnoreTime())
			}
		})
	}
}

func TestParseLine(t *testing.T) {
	tests := []struct {
		name        string
		s           string
		measurement string
		want        telegraf.Metric
		wantErr     bool
	}{
		{
			name:    "No Metric In line",
			wantErr: true,
		},
		{
			name:        "Log parser fmt returns all fields",
			measurement: "testlog",
			s:           `ts=2018-07-24T19:43:35.207268Z lvl=5 msg="Write failed" log_id=09R4e4Rl000`,
			want: metric.New(
				"testlog",
				map[string]string{},
				map[string]interface{}{
					"ts":     "2018-07-24T19:43:35.207268Z",
					"lvl":    int64(5),
					"msg":    "Write failed",
					"log_id": "09R4e4Rl000",
				},
				time.Unix(0, 0),
			),
		},
		{
			name:        "ParseLine only returns metrics from first string",
			measurement: "testlog",
			s: "ts=2018-07-24T19:43:35.207268Z lvl=5 msg=\"Write failed\" log_id=09R4e4Rl000\nmethod=POST " +
				"parent_id=088876RL000 duration=7.45 log_id=09R4e4Rl000",
			want: metric.New(
				"testlog",
				map[string]string{},
				map[string]interface{}{
					"ts":     "2018-07-24T19:43:35.207268Z",
					"lvl":    int64(5),
					"msg":    "Write failed",
					"log_id": "09R4e4Rl000",
				},
				time.Unix(0, 0),
			),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			plugin := Parser{
				metricName: tt.measurement,
			}
			require.NoError(t, plugin.Init())

			actual, err := plugin.ParseLine(tt.s)
			if tt.wantErr {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
				testutil.RequireMetricEqual(t, tt.want, actual, testutil.IgnoreTime())
			}
		})
	}
}

func TestTags(t *testing.T) {
	tests := []struct {
		name        string
		measurement string
		tagKeys     []string
		s           string
		want        telegraf.Metric
		wantErr     bool
	}{
		{
			name:        "logfmt parser returns tags and fields",
			measurement: "testlog",
			tagKeys:     []string{"lvl"},
			s:           "ts=2018-07-24T19:43:40.275Z lvl=info msg=\"http request\" method=POST",
			want: metric.New(
				"testlog",
				map[string]string{
					"lvl": "info",
				},
				map[string]interface{}{
					"msg":    "http request",
					"method": "POST",
					"ts":     "2018-07-24T19:43:40.275Z",
				},
				time.Unix(0, 0),
			),
		},
		{
			name:        "logfmt parser returns no empty metrics",
			measurement: "testlog",
			tagKeys:     []string{"lvl"},
			s:           "lvl=info",
			want: metric.New(
				"testlog",
				map[string]string{
					"lvl": "info",
				},
				map[string]interface{}{},
				time.Unix(0, 0),
			),
		},
		{
			name:        "logfmt parser returns all keys as tag",
			measurement: "testlog",
			tagKeys:     []string{"*"},
			s:           "ts=2018-07-24T19:43:40.275Z lvl=info msg=\"http request\" method=POST",
			want: metric.New(
				"testlog",
				map[string]string{
					"lvl":    "info",
					"msg":    "http request",
					"method": "POST",
					"ts":     "2018-07-24T19:43:40.275Z",
				},
				map[string]interface{}{},
				time.Unix(0, 0),
			),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			plugin := &Parser{
				metricName:  tt.measurement,
				DefaultTags: map[string]string{},
				TagKeys:     tt.tagKeys,
			}
			require.NoError(t, plugin.Init())

			actual, err := plugin.ParseLine(tt.s)
			if tt.wantErr {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
			}
			testutil.RequireMetricEqual(t, tt.want, actual, testutil.IgnoreTime())
		})
	}
}

const benchmarkData = `tags_host=myhost tags_platform=python tags_sdkver=3.11.5 value=5
tags_host=myhost tags_platform=python tags_sdkver=3.11.4 value=4
`

func TestBenchmarkData(t *testing.T) {
	plugin := &Parser{
		TagKeys: []string{"tags_host", "tags_platform", "tags_sdkver"},
	}
	require.NoError(t, plugin.Init())

	expected := []telegraf.Metric{
		metric.New(
			"",
			map[string]string{
				"tags_host":     "myhost",
				"tags_platform": "python",
				"tags_sdkver":   "3.11.5",
			},
			map[string]interface{}{
				"value": 5,
			},
			time.Unix(0, 0),
		),
		metric.New(
			"",
			map[string]string{
				"tags_host":     "myhost",
				"tags_platform": "python",
				"tags_sdkver":   "3.11.4",
			},
			map[string]interface{}{
				"value": 4,
			},
			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{
		TagKeys: []string{"tags_host", "tags_platform", "tags_sdkver"},
	}
	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))
	}
}
