package influxdb

import (
	"encoding/json"
	"io"
	"io/ioutil"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/stretchr/testify/require"
	"github.com/xorcare/pointer"
)

func prepare(text string) io.ReadCloser {
	return ioutil.NopCloser(strings.NewReader(text))
}

func TestInfluxdbResponseParser(t *testing.T) {
	t.Run("Influxdb response parser should handle invalid JSON", func(t *testing.T) {
		parser := &ResponseParser{}

		response := `{ invalid }`

		query := &Query{}

		result := parser.Parse(prepare(response), query)

		require.Nil(t, result.Responses["A"].Frames)
		require.Error(t, result.Responses["A"].Error)
	})

	t.Run("Influxdb response parser should parse everything normally", func(t *testing.T) {
		parser := &ResponseParser{}

		response := `
		{
			"results": [
				{
					"series": [
						{
							"name": "cpu",
							"columns": ["time","mean","sum"],
							"tags": {"datacenter": "America"},
							"values": [
								[111,222,333],
								[111,222,333],
								[111,null,333]
							]
						}
					]
				}
			]
		}
		`

		query := &Query{}
		labels, err := data.LabelsFromString("datacenter=America")
		require.Nil(t, err)
		newField := data.NewField("value", labels, []*float64{
			pointer.Float64(222), pointer.Float64(222), nil,
		})
		newField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.mean { datacenter: America }"}
		testFrame := data.NewFrame("cpu.mean { datacenter: America }",
			data.NewField("time", nil,
				[]time.Time{
					time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC),
					time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC),
					time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC),
				}),
			newField,
		)

		result := parser.Parse(prepare(response), query)

		frame := result.Responses["A"]
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}
	})

	t.Run("Influxdb response parser with invalid value-format", func(t *testing.T) {
		parser := &ResponseParser{}

		response := `
		{
			"results": [
				{
					"series": [
						{
							"name": "cpu",
							"columns": ["time","mean"],
							"values": [
								[100,50],
								[101,"hello"],
								[102,52]
							]
						}
					]
				}
			]
		}
		`

		query := &Query{}

		newField := data.NewField("value", nil, []*float64{
			pointer.Float64(50), nil, pointer.Float64(52),
		})
		newField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.mean"}
		testFrame := data.NewFrame("cpu.mean",
			data.NewField("time", nil,
				[]time.Time{
					time.Date(1970, 1, 1, 0, 1, 40, 0, time.UTC),
					time.Date(1970, 1, 1, 0, 1, 41, 0, time.UTC),
					time.Date(1970, 1, 1, 0, 1, 42, 0, time.UTC),
				}),
			newField,
		)

		result := parser.Parse(prepare(response), query)

		frame := result.Responses["A"]
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}
	})

	t.Run("Influxdb response parser with invalid timestamp-format", func(t *testing.T) {
		parser := &ResponseParser{}

		response := `
		{
			"results": [
				{
					"series": [
						{
							"name": "cpu",
							"columns": ["time","mean"],
							"values": [
								[100,50],
								["hello",51],
								["hello","hello"],
								[102,52]
							]
						}
					]
				}
			]
		}
		`

		query := &Query{}

		newField := data.NewField("value", nil, []*float64{
			pointer.Float64(50), pointer.Float64(52),
		})
		newField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.mean"}
		testFrame := data.NewFrame("cpu.mean",
			data.NewField("time", nil,
				[]time.Time{
					time.Date(1970, 1, 1, 0, 1, 40, 0, time.UTC),
					time.Date(1970, 1, 1, 0, 1, 42, 0, time.UTC),
				}),
			newField,
		)

		result := parser.Parse(prepare(response), query)

		frame := result.Responses["A"]
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}
	})

	t.Run("Influxdb response parser with alias", func(t *testing.T) {
		parser := &ResponseParser{}

		response := `
		{
			"results": [
				{
					"series": [
						{
							"name": "cpu.upc",
							"columns": ["time","mean","sum"],
							"tags": {
								"datacenter": "America",
								"dc.region.name": "Northeast",
								"cluster-name":   "Cluster",
								"/cluster/name/": "Cluster/",
								"@cluster@name@": "Cluster@"
							},
							"values": [
								[111,222,333]
							]
						}
					]
				}
			]
		}
		`

		query := &Query{Alias: "series alias"}
		labels, err := data.LabelsFromString("/cluster/name/=Cluster/, @cluster@name@=Cluster@, cluster-name=Cluster, datacenter=America, dc.region.name=Northeast")
		require.Nil(t, err)
		newField := data.NewField("value", labels, []*float64{
			pointer.Float64(222),
		})
		newField.Config = &data.FieldConfig{DisplayNameFromDS: "series alias"}
		testFrame := data.NewFrame("series alias",
			data.NewField("time", nil,
				[]time.Time{
					time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC),
				}),
			newField,
		)
		result := parser.Parse(prepare(response), query)

		frame := result.Responses["A"]
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias $m $measurement", Measurement: "10m"}
		result = parser.Parse(prepare(response), query)

		frame = result.Responses["A"]
		name := "alias 10m 10m"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias $col", Measurement: "10m"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias mean"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}
		name = "alias sum"
		testFrame.Name = name
		newField = data.NewField("value", labels, []*float64{
			pointer.Float64(333),
		})
		testFrame.Fields[1] = newField
		testFrame.Fields[1].Config = &data.FieldConfig{DisplayNameFromDS: name}
		if diff := cmp.Diff(testFrame, frame.Frames[1], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias $tag_datacenter"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias America"
		testFrame.Name = name
		newField = data.NewField("value", labels, []*float64{
			pointer.Float64(222),
		})
		testFrame.Fields[1] = newField
		testFrame.Fields[1].Config = &data.FieldConfig{DisplayNameFromDS: name}
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias $tag_datacenter/$tag_datacenter"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias America/America"
		testFrame.Name = name
		newField = data.NewField("value", labels, []*float64{
			pointer.Float64(222),
		})
		testFrame.Fields[1] = newField
		testFrame.Fields[1].Config = &data.FieldConfig{DisplayNameFromDS: name}
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias [[col]]", Measurement: "10m"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias mean"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias $1"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias upc"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias $5"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias $5"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "series alias"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "series alias"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias [[m]] [[measurement]]", Measurement: "10m"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias 10m 10m"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias [[tag_datacenter]]"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias America"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias [[tag_dc.region.name]]"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias Northeast"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias [[tag_cluster-name]]"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias Cluster"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias [[tag_/cluster/name/]]"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias Cluster/"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		query = &Query{Alias: "alias [[tag_@cluster@name@]]"}
		result = parser.Parse(prepare(response), query)
		frame = result.Responses["A"]
		name = "alias Cluster@"
		testFrame.Name = name
		testFrame.Fields[1].Config.DisplayNameFromDS = name
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}
	})

	t.Run("Influxdb response parser with errors", func(t *testing.T) {
		parser := &ResponseParser{}

		response := `
		{
			"results": [
				{
					"series": [
						{
							"name": "cpu",
							"columns": ["time","mean","sum"],
							"tags": {"datacenter": "America"},
							"values": [
								[111,222,333],
								[111,222,333],
								[111,null,333]
							]
						}
					]
				},
				{
					"error": "query-timeout limit exceeded"
				}
			]
		}
		`

		query := &Query{}
		labels, err := data.LabelsFromString("datacenter=America")
		require.Nil(t, err)
		newField := data.NewField("value", labels, []*float64{
			pointer.Float64(222), pointer.Float64(222), nil,
		})
		newField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.mean { datacenter: America }"}
		testFrame := data.NewFrame("cpu.mean { datacenter: America }",
			data.NewField("time", nil,
				[]time.Time{
					time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC),
					time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC),
					time.Date(1970, 1, 1, 0, 1, 51, 0, time.UTC),
				}),
			newField,
		)
		result := parser.Parse(prepare(response), query)

		frame := result.Responses["A"]
		if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
			t.Errorf("Result mismatch (-want +got):\n%s", diff)
		}

		require.EqualError(t, result.Responses["A"].Error, "query-timeout limit exceeded")
	})

	t.Run("Influxdb response parser with top-level error", func(t *testing.T) {
		parser := &ResponseParser{}

		response := `
		{
			"error": "error parsing query: found THING"
		}
		`

		query := &Query{}

		result := parser.Parse(prepare(response), query)

		require.Nil(t, result.Responses["A"].Frames)

		require.EqualError(t, result.Responses["A"].Error, "error parsing query: found THING")
	})

	t.Run("Influxdb response parser parseValue nil", func(t *testing.T) {
		value := parseValue(nil)
		require.Nil(t, value)
	})

	t.Run("Influxdb response parser parseValue valid JSON.number", func(t *testing.T) {
		value := parseValue(json.Number("95.4"))
		require.Equal(t, *value, 95.4)
	})

	t.Run("Influxdb response parser parseValue invalid type", func(t *testing.T) {
		value := parseValue("95.4")
		require.Nil(t, value)
	})

	t.Run("Influxdb response parser parseTimestamp valid JSON.number", func(t *testing.T) {
		// currently we use seconds-precision with influxdb, so the test works with that.
		// if we change this to for example milliseconds-precision, the tests will have to change.
		timestamp, err := parseTimestamp(json.Number("1609556645"))
		require.NoError(t, err)
		require.Equal(t, timestamp.Format(time.RFC3339), "2021-01-02T03:04:05Z")
	})

	t.Run("Influxdb response parser parseValue invalid type", func(t *testing.T) {
		_, err := parseTimestamp("hello")
		require.Error(t, err)
	})
}
