package prometheus_client

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"regexp"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/metric"
	inputs "github.com/influxdata/telegraf/plugins/inputs/prometheus"
	"github.com/influxdata/telegraf/plugins/serializers/prometheus"
	"github.com/influxdata/telegraf/testutil"
)

func TestMetricVersion2(t *testing.T) {
	logger := testutil.Logger{Name: "outputs.prometheus_client"}
	tests := []struct {
		name     string
		output   *PrometheusClient
		metrics  []telegraf.Metric
		accept   string
		expected []byte
	}{
		{
			name: "untyped telegraf metric",
			output: &PrometheusClient{
				Listen:            ":0",
				MetricVersion:     2,
				CollectorsExclude: []string{"gocollector", "process"},
				Path:              "/metrics",
				Log:               logger,
			},
			metrics: []telegraf.Metric{
				metric.New(
					"cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="example.org"} 42
`),
		},
		{
			name: "summary no quantiles",
			output: &PrometheusClient{
				Listen:            ":0",
				MetricVersion:     2,
				CollectorsExclude: []string{"gocollector", "process"},
				Path:              "/metrics",
				Log:               logger,
			},
			metrics: []telegraf.Metric{
				metric.New(
					"prometheus",
					map[string]string{},
					map[string]interface{}{
						"rpc_duration_seconds_sum":   1.7560473e+07,
						"rpc_duration_seconds_count": 2693,
					},
					time.Unix(0, 0),
					telegraf.Summary,
				),
			},
			expected: []byte(`
# HELP rpc_duration_seconds Telegraf collected metric
# TYPE rpc_duration_seconds summary
rpc_duration_seconds_sum 1.7560473e+07
rpc_duration_seconds_count 2693
`),
		},
		{
			name: "when export timestamp is true timestamp is present in the metric",
			output: &PrometheusClient{
				Listen:            ":0",
				MetricVersion:     2,
				CollectorsExclude: []string{"gocollector", "process"},
				Path:              "/metrics",
				ExportTimestamp:   true,
				Log:               logger,
			},
			metrics: []telegraf.Metric{
				metric.New(
					"cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="example.org"} 42 0
`),
		},
		{
			name: "strings as labels",
			output: &PrometheusClient{
				Listen:            ":0",
				MetricVersion:     2,
				CollectorsExclude: []string{"gocollector", "process"},
				Path:              "/metrics",
				StringAsLabel:     true,
				Log:               logger,
			},
			metrics: []telegraf.Metric{
				metric.New(
					"cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
						"host":      "example.org",
					},
					time.Unix(0, 0),
				),
			},
			expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="example.org"} 42
`),
		},
		{
			name: "when strings as labels is false string fields are discarded",
			output: &PrometheusClient{
				Listen:            ":0",
				MetricVersion:     2,
				CollectorsExclude: []string{"gocollector", "process"},
				Path:              "/metrics",
				StringAsLabel:     false,
				Log:               logger,
			},
			metrics: []telegraf.Metric{
				metric.New(
					"cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
						"host":      "example.org",
					},
					time.Unix(0, 0),
				),
			},
			expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle 42
`),
		},
		{
			name: "utf8 name sanitization supports utf8 metric and label names",
			output: &PrometheusClient{
				Listen:            ":0",
				MetricVersion:     2,
				CollectorsExclude: []string{"gocollector", "process"},
				Path:              "/metrics",
				NameSanitization:  "utf8",
				Log:               logger,
			},
			metrics: []telegraf.Metric{
				metric.New(
					"温度-指标",
					map[string]string{
						"主机-名": "example.org",
					},
					map[string]interface{}{
						"数值-值": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			accept: "text/plain; version=0.0.4; escaping=allow-utf-8",
			expected: []byte(`
# HELP "温度-指标_数值-值" Telegraf collected metric
# TYPE "温度-指标_数值-值" untyped
{"温度-指标_数值-值","主机-名"="example.org"} 42
`),
		},
		{
			name: "untype prometheus metric",
			output: &PrometheusClient{
				Listen:            ":0",
				MetricVersion:     2,
				CollectorsExclude: []string{"gocollector", "process"},
				Path:              "/metrics",
				Log:               logger,
			},
			metrics: []telegraf.Metric{
				metric.New(
					"prometheus",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{
						"cpu_time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="example.org"} 42
`),
		},
		{
			name: "telegraf histogram",
			output: &PrometheusClient{
				Listen:            ":0",
				MetricVersion:     2,
				CollectorsExclude: []string{"gocollector", "process"},
				Path:              "/metrics",
				Log:               logger,
			},
			metrics: []telegraf.Metric{
				metric.New(
					"cpu",
					map[string]string{
						"cpu": "cpu1",
					},
					map[string]interface{}{
						"usage_idle_sum":   2000.0,
						"usage_idle_count": 20.0,
					},
					time.Unix(0, 0),
					telegraf.Histogram,
				),
				metric.New(
					"cpu",
					map[string]string{
						"cpu": "cpu1",
						"le":  "0.0",
					},
					map[string]interface{}{
						"usage_idle_bucket": 0.0,
					},
					time.Unix(0, 0),
					telegraf.Histogram,
				),
				metric.New(
					"cpu",
					map[string]string{
						"cpu": "cpu1",
						"le":  "50.0",
					},
					map[string]interface{}{
						"usage_idle_bucket": 7.0,
					},
					time.Unix(0, 0),
					telegraf.Histogram,
				),
				metric.New(
					"cpu",
					map[string]string{
						"cpu": "cpu1",
						"le":  "100.0",
					},
					map[string]interface{}{
						"usage_idle_bucket": 20.0,
					},
					time.Unix(0, 0),
					telegraf.Histogram,
				),
				metric.New(
					"cpu",
					map[string]string{
						"cpu": "cpu1",
						"le":  "+Inf",
					},
					map[string]interface{}{
						"usage_idle_bucket": 20.0,
					},
					time.Unix(0, 0),
					telegraf.Histogram,
				),
			},
			expected: []byte(`
# HELP cpu_usage_idle Telegraf collected metric
# TYPE cpu_usage_idle histogram
cpu_usage_idle_bucket{cpu="cpu1",le="0"} 0
cpu_usage_idle_bucket{cpu="cpu1",le="50"} 7
cpu_usage_idle_bucket{cpu="cpu1",le="100"} 20
cpu_usage_idle_bucket{cpu="cpu1",le="+Inf"} 20
cpu_usage_idle_sum{cpu="cpu1"} 2000
cpu_usage_idle_count{cpu="cpu1"} 20
`),
		},
		{
			name: "histogram no buckets",
			output: &PrometheusClient{
				Listen:            ":0",
				MetricVersion:     2,
				CollectorsExclude: []string{"gocollector", "process"},
				Path:              "/metrics",
				Log:               logger,
			},
			metrics: []telegraf.Metric{
				metric.New(
					"cpu",
					map[string]string{
						"cpu": "cpu1",
					},
					map[string]interface{}{
						"usage_idle_sum":   2000.0,
						"usage_idle_count": 20.0,
					},
					time.Unix(0, 0),
					telegraf.Histogram,
				),
			},
			expected: []byte(`
# HELP cpu_usage_idle Telegraf collected metric
# TYPE cpu_usage_idle histogram
cpu_usage_idle_bucket{cpu="cpu1",le="+Inf"} 20
cpu_usage_idle_sum{cpu="cpu1"} 2000
cpu_usage_idle_count{cpu="cpu1"} 20
`),
		},
		{
			name: "untyped forced to counter",
			output: &PrometheusClient{
				Listen:            ":0",
				MetricVersion:     2,
				CollectorsExclude: []string{"gocollector", "process"},
				Path:              "/metrics",
				TypeMappings:      prometheus.MetricTypes{Counter: []string{"cpu_time_idle"}},
				Log:               logger,
			},
			metrics: []telegraf.Metric{
				metric.New(
					"prometheus",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{
						"cpu_time_idle": 42,
					},
					time.Unix(0, 0),
				),
			},
			expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle counter
cpu_time_idle{host="example.org"} 42
`),
		},
		{
			name: "untyped forced to gauge",
			output: &PrometheusClient{
				Listen:            ":0",
				MetricVersion:     2,
				CollectorsExclude: []string{"gocollector", "process"},
				Path:              "/metrics",
				TypeMappings:      prometheus.MetricTypes{Gauge: []string{"cpu_time_idle"}},
				Log:               logger,
			},
			metrics: []telegraf.Metric{
				metric.New(
					"prometheus",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{
						"cpu_time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle gauge
cpu_time_idle{host="example.org"} 42
`),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			require.NoError(t, tt.output.Init())
			require.NoError(t, tt.output.Connect())

			defer func() {
				require.NoError(t, tt.output.Close())
			}()

			require.NoError(t, tt.output.Write(tt.metrics))

			req, err := http.NewRequest(http.MethodGet, tt.output.URL(), nil)
			require.NoError(t, err)
			if tt.accept != "" {
				req.Header.Set("Accept", tt.accept)
			}

			resp, err := http.DefaultClient.Do(req)
			require.NoError(t, err)
			require.Equal(t, http.StatusOK, resp.StatusCode)
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)

			require.Equal(t,
				strings.TrimSpace(string(tt.expected)),
				strings.TrimSpace(string(body)))
		})
	}
}

func TestRoundTripMetricVersion2(t *testing.T) {
	logger := testutil.Logger{Name: "outputs.prometheus_client"}
	regxPattern := regexp.MustCompile(`.*prometheus_request_.*`)
	tests := []struct {
		name string
		data []byte
	}{
		{
			name: "untyped",
			data: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle untyped
cpu_time_idle{host="example.org"} 42
`),
		},
		{
			name: "counter",
			data: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle counter
cpu_time_idle{host="example.org"} 42
`),
		},
		{
			name: "gauge",
			data: []byte(`
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle gauge
cpu_time_idle{host="example.org"} 42
`),
		},
		{
			name: "multi",
			data: []byte(`
# HELP cpu_time_guest Telegraf collected metric
# TYPE cpu_time_guest gauge
cpu_time_guest{host="one.example.org"} 42
cpu_time_guest{host="two.example.org"} 42
# HELP cpu_time_idle Telegraf collected metric
# TYPE cpu_time_idle gauge
cpu_time_idle{host="one.example.org"} 42
cpu_time_idle{host="two.example.org"} 42
`),
		},
		{
			name: "histogram",
			data: []byte(`
# HELP http_request_duration_seconds Telegraf collected metric
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 24054
http_request_duration_seconds_bucket{le="0.1"} 33444
http_request_duration_seconds_bucket{le="0.2"} 100392
http_request_duration_seconds_bucket{le="0.5"} 129389
http_request_duration_seconds_bucket{le="1"} 133988
http_request_duration_seconds_bucket{le="+Inf"} 144320
http_request_duration_seconds_sum 53423
http_request_duration_seconds_count 144320
`),
		},
		{
			name: "summary",
			data: []byte(`
# HELP rpc_duration_seconds Telegraf collected metric
# TYPE rpc_duration_seconds summary
rpc_duration_seconds{quantile="0.01"} 3102
rpc_duration_seconds{quantile="0.05"} 3272
rpc_duration_seconds{quantile="0.5"} 4773
rpc_duration_seconds{quantile="0.9"} 9001
rpc_duration_seconds{quantile="0.99"} 76656
rpc_duration_seconds_sum 1.7560473e+07
rpc_duration_seconds_count 2693
`),
		},
	}

	ts := httptest.NewServer(http.NotFoundHandler())
	defer ts.Close()

	url := fmt.Sprintf("http://%s", ts.Listener.Addr())

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.WriteHeader(http.StatusOK)
				if _, err := w.Write(tt.data); err != nil {
					w.WriteHeader(http.StatusInternalServerError)
					t.Error(err)
					return
				}
			})

			input := &inputs.Prometheus{
				Log:           logger,
				URLs:          []string{url},
				URLTag:        "",
				MetricVersion: 2,
			}
			err := input.Init()
			require.NoError(t, err)

			var acc testutil.Accumulator
			err = input.Start(&acc)
			require.NoError(t, err)
			err = input.Gather(&acc)
			require.NoError(t, err)
			input.Stop()

			metrics := acc.GetTelegrafMetrics()

			output := &PrometheusClient{
				Listen:            "127.0.0.1:0",
				Path:              defaultPath,
				MetricVersion:     2,
				Log:               logger,
				CollectorsExclude: []string{"gocollector", "process"},
			}
			err = output.Init()
			require.NoError(t, err)
			err = output.Connect()
			require.NoError(t, err)
			defer func() {
				err = output.Close()
				require.NoError(t, err)
			}()
			err = output.Write(metrics)
			require.NoError(t, err)

			resp, err := http.Get(output.URL())
			require.NoError(t, err)
			defer resp.Body.Close()

			actual, err := io.ReadAll(resp.Body)
			require.NoError(t, err)

			current := regxPattern.ReplaceAllLiteralString(string(actual), "")
			require.Equal(t,
				strings.TrimSpace(string(tt.data)),
				strings.TrimSpace(current))
		})
	}
}
