package datadog

import (
	"encoding/json"
	"fmt"
	"math"
	"net/http"
	"net/http/httptest"
	"reflect"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

var (
	fakeURL    = "http://test.datadog.com"
	fakeAPIKey = "123456"
)

func NewDatadog(url string) *Datadog {
	return &Datadog{
		URL: url,
		Log: testutil.Logger{},
	}
}

func fakeDatadog() *Datadog {
	d := NewDatadog(fakeURL)
	d.Apikey = fakeAPIKey
	return d
}

func TestUriOverride(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		json.NewEncoder(w).Encode(`{"status":"ok"}`) //nolint:errcheck // Ignore the returned error as the test will fail anyway
	}))
	defer ts.Close()

	d := NewDatadog(ts.URL)
	d.Apikey = "123456"
	err := d.Connect()
	require.NoError(t, err)
	err = d.Write(testutil.MockMetrics())
	require.NoError(t, err)
}

func TestCompressionOverride(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		json.NewEncoder(w).Encode(`{"status":"ok"}`) //nolint:errcheck // Ignore the returned error as the test will fail anyway
	}))
	defer ts.Close()

	d := NewDatadog(ts.URL)
	d.Apikey = "123456"
	d.Compression = "zlib"
	err := d.Connect()
	require.NoError(t, err)
	err = d.Write(testutil.MockMetrics())
	require.NoError(t, err)
}

func TestBadStatusCode(t *testing.T) {
	errorString := `{"errors": ["Something bad happened to the server.", "Your query made the server very sad."]}`
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprint(w, errorString)
	}))
	defer ts.Close()

	d := NewDatadog(ts.URL)
	d.Apikey = "123456"
	err := d.Connect()
	require.NoError(t, err)
	err = d.Write(testutil.MockMetrics())
	if err == nil {
		t.Errorf("error expected but none returned")
	} else {
		require.EqualError(t, err, fmt.Sprintf("received bad status code, %v: %s", http.StatusInternalServerError, errorString))
	}
}

func TestAuthenticatedUrl(t *testing.T) {
	d := fakeDatadog()

	authURL := d.authenticatedURL()
	require.EqualValues(t, fmt.Sprintf("%s?api_key=%s", fakeURL, fakeAPIKey), authURL)
}

func TestBuildTags(t *testing.T) {
	var tagtests = []struct {
		ptIn    []*telegraf.Tag
		outTags []string
	}{
		{
			ptIn: []*telegraf.Tag{
				{
					Key:   "one",
					Value: "two",
				},
				{
					Key:   "three",
					Value: "four",
				},
			},
			outTags: []string{"one:two", "three:four"},
		},
		{
			ptIn: []*telegraf.Tag{
				{
					Key:   "aaa",
					Value: "bbb",
				},
			},
			outTags: []string{"aaa:bbb"},
		},
		{
			ptIn:    make([]*telegraf.Tag, 0),
			outTags: make([]string, 0),
		},
	}
	for _, tt := range tagtests {
		tags := buildTags(tt.ptIn)
		if !reflect.DeepEqual(tags, tt.outTags) {
			t.Errorf("\nexpected %+v\ngot %+v\n", tt.outTags, tags)
		}
	}
}

func TestBuildPoint(t *testing.T) {
	var tagtests = []struct {
		ptIn  telegraf.Metric
		outPt Point
		err   error
	}{
		{
			testutil.TestMetric(0.0, "test1"),
			Point{
				float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
				0.0,
			},
			nil,
		},
		{
			testutil.TestMetric(1.0, "test2"),
			Point{
				float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
				1.0,
			},
			nil,
		},
		{
			testutil.TestMetric(10, "test3"),
			Point{
				float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
				10.0,
			},
			nil,
		},
		{
			testutil.TestMetric(int32(112345), "test4"),
			Point{
				float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
				112345.0,
			},
			nil,
		},
		{
			testutil.TestMetric(int64(112345), "test5"),
			Point{
				float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
				112345.0,
			},
			nil,
		},
		{
			testutil.TestMetric(float32(11234.5), "test6"),
			Point{
				float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
				11234.5,
			},
			nil,
		},
		{
			testutil.TestMetric(true, "test7"),
			Point{
				float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
				1.0,
			},
			nil,
		},
		{
			testutil.TestMetric(false, "test8"),
			Point{
				float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
				0.0,
			},
			nil,
		},
		{
			testutil.TestMetric(int64(0), "test int64"),
			Point{
				float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
				0.0,
			},
			nil,
		},
		{
			testutil.TestMetric(uint64(0), "test uint64"),
			Point{
				float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
				0.0,
			},
			nil,
		},
		{
			testutil.TestMetric(true, "test bool"),
			Point{
				float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
				1.0,
			},
			nil,
		},
	}
	for _, tt := range tagtests {
		pt, err := buildMetrics(tt.ptIn)
		if err != nil && tt.err == nil {
			t.Errorf("%s: unexpected error, %+v\n", tt.ptIn.Name(), err)
		}
		if tt.err != nil && err == nil {
			t.Errorf("%s: expected an error (%s) but none returned", tt.ptIn.Name(), tt.err.Error())
		}
		if !reflect.DeepEqual(pt["value"], tt.outPt) && tt.err == nil {
			t.Errorf("%s: \nexpected %+v\ngot %+v\n",
				tt.ptIn.Name(), tt.outPt, pt["value"])
		}
	}
}

func TestVerifyValue(t *testing.T) {
	var tagtests = []struct {
		ptIn        telegraf.Metric
		validMetric bool
	}{
		{
			testutil.TestMetric(float32(11234.5), "test1"),
			true,
		},
		{
			testutil.TestMetric("11234.5", "test2"),
			false,
		},
	}
	for _, tt := range tagtests {
		ok := verifyValue(tt.ptIn.Fields()["value"])
		if tt.validMetric != ok {
			t.Errorf("%s: verification failed\n", tt.ptIn.Name())
		}
	}
}

func TestNaNIsSkipped(t *testing.T) {
	plugin := &Datadog{
		Apikey: "testing",
		URL:    "", // No request will be sent because all fields are skipped
	}

	err := plugin.Connect()
	require.NoError(t, err)

	err = plugin.Write([]telegraf.Metric{
		metric.New(
			"cpu",
			map[string]string{},
			map[string]interface{}{
				"time_idle": math.NaN(),
			},
			time.Now()),
	})
	require.NoError(t, err)
}

func TestInfIsSkipped(t *testing.T) {
	plugin := &Datadog{
		Apikey: "testing",
		URL:    "", // No request will be sent because all fields are skipped
	}

	err := plugin.Connect()
	require.NoError(t, err)

	err = plugin.Write([]telegraf.Metric{
		metric.New(
			"cpu",
			map[string]string{},
			map[string]interface{}{
				"time_idle": math.Inf(0),
			},
			time.Now()),
	})
	require.NoError(t, err)
}

func TestNonZeroRateIntervalConvertsRatesToCount(t *testing.T) {
	d := &Datadog{
		Apikey:       "123456",
		RateInterval: config.Duration(10 * time.Second),
	}

	var tests = []struct {
		name       string
		metricsIn  []telegraf.Metric
		metricsOut []*Metric
	}{
		{
			"convert counter metrics to rate",
			[]telegraf.Metric{
				metric.New(
					"count_metric",
					map[string]string{
						"metric_type": "counter",
					},
					map[string]interface{}{
						"value": 100,
					},
					time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
					telegraf.Counter,
				),
			},
			[]*Metric{
				{
					Metric: "count_metric",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							10,
						},
					},
					Type: "rate",
					Tags: []string{
						"metric_type:counter",
					},
					Interval: 10,
				},
			},
		},
		{
			"convert count value in timing metrics to rate",
			[]telegraf.Metric{
				metric.New(
					"timing_metric",
					map[string]string{
						"metric_type": "timing",
					},
					map[string]interface{}{
						"count":  1,
						"lower":  float64(10),
						"mean":   float64(10),
						"median": float64(10),
						"stddev": float64(0),
						"sum":    float64(10),
						"upper":  float64(10),
					},
					time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
					telegraf.Untyped,
				),
			},
			[]*Metric{
				{
					Metric: "timing_metric.count",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							0.1,
						},
					},
					Type: "rate",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 10,
				},
				{
					Metric: "timing_metric.lower",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
				{
					Metric: "timing_metric.mean",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
				{
					Metric: "timing_metric.median",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
				{
					Metric: "timing_metric.stddev",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(0),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
				{
					Metric: "timing_metric.sum",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
				{
					Metric: "timing_metric.upper",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
			},
		},
		{
			"convert count value in histogram metrics to rate",
			[]telegraf.Metric{
				metric.New(
					"histogram_metric",
					map[string]string{
						"metric_type": "histogram",
					},
					map[string]interface{}{
						"count":  1,
						"lower":  float64(10),
						"mean":   float64(10),
						"median": float64(10),
						"stddev": float64(0),
						"sum":    float64(10),
						"upper":  float64(10),
					},
					time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
					telegraf.Untyped,
				),
			},
			[]*Metric{
				{
					Metric: "histogram_metric.count",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							0.1,
						},
					},
					Type: "rate",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 10,
				},
				{
					Metric: "histogram_metric.lower",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
				{
					Metric: "histogram_metric.mean",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
				{
					Metric: "histogram_metric.median",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
				{
					Metric: "histogram_metric.stddev",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(0),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
				{
					Metric: "histogram_metric.sum",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
				{
					Metric: "histogram_metric.upper",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actualMetricsOut := d.convertToDatadogMetric(tt.metricsIn)
			require.ElementsMatch(t, tt.metricsOut, actualMetricsOut)
		})
	}
}

func TestZeroRateIntervalConvertsRatesToCount(t *testing.T) {
	d := &Datadog{
		Apikey: "123456",
	}

	var tests = []struct {
		name       string
		metricsIn  []telegraf.Metric
		metricsOut []*Metric
	}{
		{
			"does not convert counter metrics to rate",
			[]telegraf.Metric{
				metric.New(
					"count_metric",
					map[string]string{
						"metric_type": "counter",
					},
					map[string]interface{}{
						"value": 100,
					},
					time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
					telegraf.Counter,
				),
			},
			[]*Metric{
				{
					Metric: "count_metric",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							100,
						},
					},
					Type: "count",
					Tags: []string{
						"metric_type:counter",
					},
					Interval: 1,
				},
			},
		},
		{
			"does not convert count value in timing metrics to rate",
			[]telegraf.Metric{
				metric.New(
					"timing_metric",
					map[string]string{
						"metric_type": "timing",
					},
					map[string]interface{}{
						"count":  1,
						"lower":  float64(10),
						"mean":   float64(10),
						"median": float64(10),
						"stddev": float64(0),
						"sum":    float64(10),
						"upper":  float64(10),
					},
					time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
					telegraf.Untyped,
				),
			},
			[]*Metric{
				{
					Metric: "timing_metric.count",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							1,
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
				{
					Metric: "timing_metric.lower",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
				{
					Metric: "timing_metric.mean",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
				{
					Metric: "timing_metric.median",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
				{
					Metric: "timing_metric.stddev",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(0),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
				{
					Metric: "timing_metric.sum",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
				{
					Metric: "timing_metric.upper",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:timing",
					},
					Interval: 1,
				},
			},
		},
		{
			"does not convert count value in histogram metrics to rate",
			[]telegraf.Metric{
				metric.New(
					"histogram_metric",
					map[string]string{
						"metric_type": "histogram",
					},
					map[string]interface{}{
						"count":  1,
						"lower":  float64(10),
						"mean":   float64(10),
						"median": float64(10),
						"stddev": float64(0),
						"sum":    float64(10),
						"upper":  float64(10),
					},
					time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
					telegraf.Untyped,
				),
			},
			[]*Metric{
				{
					Metric: "histogram_metric.count",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							1,
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
				{
					Metric: "histogram_metric.lower",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
				{
					Metric: "histogram_metric.mean",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
				{
					Metric: "histogram_metric.median",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
				{
					Metric: "histogram_metric.stddev",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(0),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
				{
					Metric: "histogram_metric.sum",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
				{
					Metric: "histogram_metric.upper",
					Points: [1]Point{
						{
							float64(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()),
							float64(10),
						},
					},
					Type: "",
					Tags: []string{
						"metric_type:histogram",
					},
					Interval: 1,
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actualMetricsOut := d.convertToDatadogMetric(tt.metricsIn)
			require.ElementsMatch(t, tt.metricsOut, actualMetricsOut)
		})
	}
}
