// Copyright 2017 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package v1

import (
	"context"
	"errors"
	"io"
	"math"
	"net/http"
	"net/http/httptest"
	"net/url"
	"reflect"
	"strconv"
	"strings"
	"testing"
	"time"

	json "github.com/json-iterator/go"

	"github.com/prometheus/common/model"
)

type apiTest struct {
	do           func() (interface{}, Warnings, error)
	inWarnings   []string
	inErr        error
	inStatusCode int
	inRes        interface{}

	reqPath   string
	reqMethod string
	res       interface{}
	err       error
}

type apiTestClient struct {
	*testing.T
	curTest apiTest
}

func (c *apiTestClient) URL(ep string, args map[string]string) *url.URL {
	path := ep
	for k, v := range args {
		path = strings.ReplaceAll(path, ":"+k, v)
	}
	u := &url.URL{
		Host: "test:9090",
		Path: path,
	}
	return u
}

func (c *apiTestClient) Do(_ context.Context, req *http.Request) (*http.Response, []byte, Warnings, error) {
	test := c.curTest

	if req.URL.Path != test.reqPath {
		c.Errorf("unexpected request path: want %s, got %s", test.reqPath, req.URL.Path)
	}
	if req.Method != test.reqMethod {
		c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method)
	}

	b, err := json.Marshal(test.inRes)
	if err != nil {
		c.Fatal(err)
	}

	resp := &http.Response{}
	if test.inStatusCode != 0 {
		resp.StatusCode = test.inStatusCode
	} else if test.inErr != nil {
		resp.StatusCode = http.StatusUnprocessableEntity
	} else {
		resp.StatusCode = http.StatusOK
	}

	return resp, b, test.inWarnings, test.inErr
}

func (c *apiTestClient) DoGetFallback(ctx context.Context, u *url.URL, args url.Values) (*http.Response, []byte, Warnings, error) {
	req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(args.Encode()))
	if err != nil {
		return nil, nil, nil, err
	}
	return c.Do(ctx, req)
}

func TestAPIs(t *testing.T) {
	testTime := time.Now()

	tc := &apiTestClient{
		T: t,
	}
	promAPI := &httpAPI{
		client: tc,
	}

	doAlertManagers := func() func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.AlertManagers(context.Background())
			return v, nil, err
		}
	}

	doCleanTombstones := func() func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			return nil, nil, promAPI.CleanTombstones(context.Background())
		}
	}

	doConfig := func() func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.Config(context.Background())
			return v, nil, err
		}
	}

	doDeleteSeries := func(matcher string, startTime, endTime time.Time) func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			return nil, nil, promAPI.DeleteSeries(context.Background(), []string{matcher}, startTime, endTime)
		}
	}

	doFlags := func() func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.Flags(context.Background())
			return v, nil, err
		}
	}

	doBuildinfo := func() func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.Buildinfo(context.Background())
			return v, nil, err
		}
	}

	doRuntimeinfo := func() func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.Runtimeinfo(context.Background())
			return v, nil, err
		}
	}

	doLabelNames := func(matches []string, startTime, endTime time.Time, opts ...Option) func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			return promAPI.LabelNames(context.Background(), matches, startTime, endTime, opts...)
		}
	}

	doLabelValues := func(matches []string, label string, startTime, endTime time.Time, opts ...Option) func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			return promAPI.LabelValues(context.Background(), label, matches, startTime, endTime, opts...)
		}
	}

	doQuery := func(q string, ts time.Time, opts ...Option) func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			return promAPI.Query(context.Background(), q, ts, opts...)
		}
	}

	doQueryRange := func(q string, rng Range, opts ...Option) func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			return promAPI.QueryRange(context.Background(), q, rng, opts...)
		}
	}

	doSeries := func(matcher string, startTime, endTime time.Time, opts ...Option) func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			return promAPI.Series(context.Background(), []string{matcher}, startTime, endTime, opts...)
		}
	}

	doSnapshot := func(skipHead bool) func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.Snapshot(context.Background(), skipHead)
			return v, nil, err
		}
	}

	doRules := func() func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.Rules(context.Background())
			return v, nil, err
		}
	}

	doTargets := func() func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.Targets(context.Background())
			return v, nil, err
		}
	}

	doTargetsMetadata := func(matchTarget, metric, limit string) func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.TargetsMetadata(context.Background(), matchTarget, metric, limit)
			return v, nil, err
		}
	}

	doMetadata := func(metric, limit string) func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.Metadata(context.Background(), metric, limit)
			return v, nil, err
		}
	}

	doTSDB := func(opts ...Option) func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.TSDB(context.Background(), opts...)
			return v, nil, err
		}
	}

	doWalReply := func() func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.WalReplay(context.Background())
			return v, nil, err
		}
	}

	doQueryExemplars := func(query string, startTime, endTime time.Time) func() (interface{}, Warnings, error) {
		return func() (interface{}, Warnings, error) {
			v, err := promAPI.QueryExemplars(context.Background(), query, startTime, endTime)
			return v, nil, err
		}
	}

	queryTests := []apiTest{
		{
			do: doQuery("2", testTime, WithTimeout(5*time.Second)),
			inRes: &queryResult{
				Type: model.ValScalar,
				Result: &model.Scalar{
					Value:     2,
					Timestamp: model.TimeFromUnix(testTime.Unix()),
				},
			},

			reqMethod: "POST",
			reqPath:   "/api/v1/query",
			res: &model.Scalar{
				Value:     2,
				Timestamp: model.TimeFromUnix(testTime.Unix()),
			},
		},
		{
			do:    doQuery("2", testTime),
			inErr: errors.New("some error"),

			reqMethod: "POST",
			reqPath:   "/api/v1/query",
			err:       errors.New("some error"),
		},
		{
			do:           doQuery("2", testTime),
			inRes:        "some body",
			inStatusCode: 500,
			inErr: &Error{
				Type:   ErrServer,
				Msg:    "server error: 500",
				Detail: "some body",
			},

			reqMethod: "POST",
			reqPath:   "/api/v1/query",
			err:       errors.New("server_error: server error: 500"),
		},
		{
			do:           doQuery("2", testTime),
			inRes:        "some body",
			inStatusCode: 404,
			inErr: &Error{
				Type:   ErrClient,
				Msg:    "client error: 404",
				Detail: "some body",
			},

			reqMethod: "POST",
			reqPath:   "/api/v1/query",
			err:       errors.New("client_error: client error: 404"),
		},
		// Warning only.
		{
			do:         doQuery("2", testTime),
			inWarnings: []string{"warning"},
			inRes: &queryResult{
				Type: model.ValScalar,
				Result: &model.Scalar{
					Value:     2,
					Timestamp: model.TimeFromUnix(testTime.Unix()),
				},
			},

			reqMethod: "POST",
			reqPath:   "/api/v1/query",
			res: &model.Scalar{
				Value:     2,
				Timestamp: model.TimeFromUnix(testTime.Unix()),
			},
		},
		// Warning + error.
		{
			do:           doQuery("2", testTime),
			inWarnings:   []string{"warning"},
			inRes:        "some body",
			inStatusCode: 404,
			inErr: &Error{
				Type:   ErrClient,
				Msg:    "client error: 404",
				Detail: "some body",
			},

			reqMethod: "POST",
			reqPath:   "/api/v1/query",
			err:       errors.New("client_error: client error: 404"),
		},

		{
			do: doQueryRange("2", Range{
				Start: testTime.Add(-time.Minute),
				End:   testTime,
				Step:  1 * time.Minute,
			}, WithTimeout(5*time.Second)),
			inErr: errors.New("some error"),

			reqMethod: "POST",
			reqPath:   "/api/v1/query_range",
			err:       errors.New("some error"),
		},

		{
			do:        doLabelNames(nil, testTime.Add(-100*time.Hour), testTime),
			inRes:     []string{"val1", "val2"},
			reqMethod: "POST",
			reqPath:   "/api/v1/labels",
			res:       []string{"val1", "val2"},
		},
		{
			do:         doLabelNames(nil, testTime.Add(-100*time.Hour), testTime),
			inRes:      []string{"val1", "val2"},
			inWarnings: []string{"a"},
			reqMethod:  "POST",
			reqPath:    "/api/v1/labels",
			res:        []string{"val1", "val2"},
		},

		{
			do:        doLabelNames(nil, testTime.Add(-100*time.Hour), testTime),
			inErr:     errors.New("some error"),
			reqMethod: "POST",
			reqPath:   "/api/v1/labels",
			err:       errors.New("some error"),
		},
		{
			do:         doLabelNames(nil, testTime.Add(-100*time.Hour), testTime),
			inErr:      errors.New("some error"),
			inWarnings: []string{"a"},
			reqMethod:  "POST",
			reqPath:    "/api/v1/labels",
			err:        errors.New("some error"),
		},
		{
			do:        doLabelNames([]string{"up"}, testTime.Add(-100*time.Hour), testTime),
			inRes:     []string{"val1", "val2"},
			reqMethod: "POST",
			reqPath:   "/api/v1/labels",
			res:       []string{"val1", "val2"},
		},

		{
			do:        doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime),
			inRes:     []string{"val1", "val2"},
			reqMethod: "GET",
			reqPath:   "/api/v1/label/mylabel/values",
			res:       model.LabelValues{"val1", "val2"},
		},
		{
			do:         doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime),
			inRes:      []string{"val1", "val2"},
			inWarnings: []string{"a"},
			reqMethod:  "GET",
			reqPath:    "/api/v1/label/mylabel/values",
			res:        model.LabelValues{"val1", "val2"},
		},

		{
			do:        doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime),
			inErr:     errors.New("some error"),
			reqMethod: "GET",
			reqPath:   "/api/v1/label/mylabel/values",
			err:       errors.New("some error"),
		},
		{
			do:         doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime),
			inErr:      errors.New("some error"),
			inWarnings: []string{"a"},
			reqMethod:  "GET",
			reqPath:    "/api/v1/label/mylabel/values",
			err:        errors.New("some error"),
		},
		{
			do:        doLabelValues([]string{"up"}, "mylabel", testTime.Add(-100*time.Hour), testTime),
			inRes:     []string{"val1", "val2"},
			reqMethod: "GET",
			reqPath:   "/api/v1/label/mylabel/values",
			res:       model.LabelValues{"val1", "val2"},
		},

		{
			do: doSeries("up", testTime.Add(-time.Minute), testTime),
			inRes: []map[string]string{
				{
					"__name__": "up",
					"job":      "prometheus",
					"instance": "localhost:9090",
				},
			},
			reqMethod: "POST",
			reqPath:   "/api/v1/series",
			res: []model.LabelSet{
				{
					"__name__": "up",
					"job":      "prometheus",
					"instance": "localhost:9090",
				},
			},
		},
		// Series with data + warning.
		{
			do: doSeries("up", testTime.Add(-time.Minute), testTime),
			inRes: []map[string]string{
				{
					"__name__": "up",
					"job":      "prometheus",
					"instance": "localhost:9090",
				},
			},
			inWarnings: []string{"a"},
			reqMethod:  "POST",
			reqPath:    "/api/v1/series",
			res: []model.LabelSet{
				{
					"__name__": "up",
					"job":      "prometheus",
					"instance": "localhost:9090",
				},
			},
		},

		{
			do:        doSeries("up", testTime.Add(-time.Minute), testTime),
			inErr:     errors.New("some error"),
			reqMethod: "POST",
			reqPath:   "/api/v1/series",
			err:       errors.New("some error"),
		},
		// Series with error and warning.
		{
			do:         doSeries("up", testTime.Add(-time.Minute), testTime),
			inErr:      errors.New("some error"),
			inWarnings: []string{"a"},
			reqMethod:  "POST",
			reqPath:    "/api/v1/series",
			err:        errors.New("some error"),
		},

		{
			do: doSnapshot(true),
			inRes: map[string]string{
				"name": "20171210T211224Z-2be650b6d019eb54",
			},
			reqMethod: "POST",
			reqPath:   "/api/v1/admin/tsdb/snapshot",
			res: SnapshotResult{
				Name: "20171210T211224Z-2be650b6d019eb54",
			},
		},

		{
			do:        doSnapshot(true),
			inErr:     errors.New("some error"),
			reqMethod: "POST",
			reqPath:   "/api/v1/admin/tsdb/snapshot",
			err:       errors.New("some error"),
		},

		{
			do:        doCleanTombstones(),
			reqMethod: "POST",
			reqPath:   "/api/v1/admin/tsdb/clean_tombstones",
		},

		{
			do:        doCleanTombstones(),
			inErr:     errors.New("some error"),
			reqMethod: "POST",
			reqPath:   "/api/v1/admin/tsdb/clean_tombstones",
			err:       errors.New("some error"),
		},

		{
			do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime),
			inRes: []map[string]string{
				{
					"__name__": "up",
					"job":      "prometheus",
					"instance": "localhost:9090",
				},
			},
			reqMethod: "POST",
			reqPath:   "/api/v1/admin/tsdb/delete_series",
		},

		{
			do:        doDeleteSeries("up", testTime.Add(-time.Minute), testTime),
			inErr:     errors.New("some error"),
			reqMethod: "POST",
			reqPath:   "/api/v1/admin/tsdb/delete_series",
			err:       errors.New("some error"),
		},

		{
			do:        doConfig(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/config",
			inRes: map[string]string{
				"yaml": "<content of the loaded config file in YAML>",
			},
			res: ConfigResult{
				YAML: "<content of the loaded config file in YAML>",
			},
		},

		{
			do:        doConfig(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/config",
			inErr:     errors.New("some error"),
			err:       errors.New("some error"),
		},

		{
			do:        doFlags(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/flags",
			inRes: map[string]string{
				"alertmanager.notification-queue-capacity": "10000",
				"alertmanager.timeout":                     "10s",
				"log.level":                                "info",
				"query.lookback-delta":                     "5m",
				"query.max-concurrency":                    "20",
			},
			res: FlagsResult{
				"alertmanager.notification-queue-capacity": "10000",
				"alertmanager.timeout":                     "10s",
				"log.level":                                "info",
				"query.lookback-delta":                     "5m",
				"query.max-concurrency":                    "20",
			},
		},

		{
			do:        doFlags(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/flags",
			inErr:     errors.New("some error"),
			err:       errors.New("some error"),
		},

		{
			do:        doBuildinfo(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/buildinfo",
			inErr:     errors.New("some error"),
			err:       errors.New("some error"),
		},

		{
			do:        doBuildinfo(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/buildinfo",
			inRes: map[string]interface{}{
				"version":   "2.23.0",
				"revision":  "26d89b4b0776fe4cd5a3656dfa520f119a375273",
				"branch":    "HEAD",
				"buildUser": "root@37609b3a0a21",
				"buildDate": "20201126-10:56:17",
				"goVersion": "go1.15.5",
			},
			res: BuildinfoResult{
				Version:   "2.23.0",
				Revision:  "26d89b4b0776fe4cd5a3656dfa520f119a375273",
				Branch:    "HEAD",
				BuildUser: "root@37609b3a0a21",
				BuildDate: "20201126-10:56:17",
				GoVersion: "go1.15.5",
			},
		},

		{
			do:        doRuntimeinfo(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/runtimeinfo",
			inErr:     errors.New("some error"),
			err:       errors.New("some error"),
		},

		{
			do:        doRuntimeinfo(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/runtimeinfo",
			inRes: map[string]interface{}{
				"startTime":           "2020-05-18T15:52:53.4503113Z",
				"CWD":                 "/prometheus",
				"reloadConfigSuccess": true,
				"lastConfigTime":      "2020-05-18T15:52:56Z",
				"corruptionCount":     0,
				"goroutineCount":      217,
				"GOMAXPROCS":          2,
				"GOGC":                "100",
				"GODEBUG":             "allocfreetrace",
				"storageRetention":    "1d",
			},
			res: RuntimeinfoResult{
				StartTime:           time.Date(2020, 5, 18, 15, 52, 53, 450311300, time.UTC),
				CWD:                 "/prometheus",
				ReloadConfigSuccess: true,
				LastConfigTime:      time.Date(2020, 5, 18, 15, 52, 56, 0, time.UTC),
				CorruptionCount:     0,
				GoroutineCount:      217,
				GOMAXPROCS:          2,
				GOGC:                "100",
				GODEBUG:             "allocfreetrace",
				StorageRetention:    "1d",
			},
		},

		{
			do:        doAlertManagers(),
			reqMethod: "GET",
			reqPath:   "/api/v1/alertmanagers",
			inRes: map[string]interface{}{
				"activeAlertManagers": []map[string]string{
					{
						"url": "http://127.0.0.1:9091/api/v1/alerts",
					},
				},
				"droppedAlertManagers": []map[string]string{
					{
						"url": "http://127.0.0.1:9092/api/v1/alerts",
					},
				},
			},
			res: AlertManagersResult{
				Active: []AlertManager{
					{
						URL: "http://127.0.0.1:9091/api/v1/alerts",
					},
				},
				Dropped: []AlertManager{
					{
						URL: "http://127.0.0.1:9092/api/v1/alerts",
					},
				},
			},
		},

		{
			do:        doAlertManagers(),
			reqMethod: "GET",
			reqPath:   "/api/v1/alertmanagers",
			inErr:     errors.New("some error"),
			err:       errors.New("some error"),
		},

		{
			do:        doRules(),
			reqMethod: "GET",
			reqPath:   "/api/v1/rules",
			inRes: map[string]interface{}{
				"groups": []map[string]interface{}{
					{
						"file":     "/rules.yaml",
						"interval": 60,
						"name":     "example",
						"rules": []map[string]interface{}{
							{
								"alerts": []map[string]interface{}{
									{
										"activeAt": testTime.UTC().Format(time.RFC3339Nano),
										"annotations": map[string]interface{}{
											"summary": "High request latency",
										},
										"labels": map[string]interface{}{
											"alertname": "HighRequestLatency",
											"severity":  "page",
										},
										"state": "firing",
										"value": "1e+00",
									},
								},
								"annotations": map[string]interface{}{
									"summary": "High request latency",
								},
								"duration": 600,
								"health":   "ok",
								"labels": map[string]interface{}{
									"severity": "page",
								},
								"name":  "HighRequestLatency",
								"query": "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5",
								"type":  "alerting",
							},
							{
								"health": "ok",
								"name":   "job:http_inprogress_requests:sum",
								"query":  "sum(http_inprogress_requests) by (job)",
								"type":   "recording",
							},
						},
					},
				},
			},
			res: RulesResult{
				Groups: []RuleGroup{
					{
						Name:     "example",
						File:     "/rules.yaml",
						Interval: 60,
						Rules: []interface{}{
							AlertingRule{
								Alerts: []*Alert{
									{
										ActiveAt: testTime.UTC(),
										Annotations: model.LabelSet{
											"summary": "High request latency",
										},
										Labels: model.LabelSet{
											"alertname": "HighRequestLatency",
											"severity":  "page",
										},
										State: AlertStateFiring,
										Value: "1e+00",
									},
								},
								Annotations: model.LabelSet{
									"summary": "High request latency",
								},
								Labels: model.LabelSet{
									"severity": "page",
								},
								Duration:  600,
								Health:    RuleHealthGood,
								Name:      "HighRequestLatency",
								Query:     "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5",
								LastError: "",
							},
							RecordingRule{
								Health:    RuleHealthGood,
								Name:      "job:http_inprogress_requests:sum",
								Query:     "sum(http_inprogress_requests) by (job)",
								LastError: "",
							},
						},
					},
				},
			},
		},

		// This has the newer API elements like lastEvaluation, evaluationTime, etc.
		{
			do:        doRules(),
			reqMethod: "GET",
			reqPath:   "/api/v1/rules",
			inRes: map[string]interface{}{
				"groups": []map[string]interface{}{
					{
						"file":     "/rules.yaml",
						"interval": 60,
						"name":     "example",
						"rules": []map[string]interface{}{
							{
								"alerts": []map[string]interface{}{
									{
										"activeAt": testTime.UTC().Format(time.RFC3339Nano),
										"annotations": map[string]interface{}{
											"summary": "High request latency",
										},
										"labels": map[string]interface{}{
											"alertname": "HighRequestLatency",
											"severity":  "page",
										},
										"state": "firing",
										"value": "1e+00",
									},
								},
								"annotations": map[string]interface{}{
									"summary": "High request latency",
								},
								"duration": 600,
								"health":   "ok",
								"labels": map[string]interface{}{
									"severity": "page",
								},
								"name":           "HighRequestLatency",
								"query":          "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5",
								"type":           "alerting",
								"evaluationTime": 0.5,
								"lastEvaluation": "2020-05-18T15:52:53.4503113Z",
								"state":          "firing",
							},
							{
								"health":         "ok",
								"name":           "job:http_inprogress_requests:sum",
								"query":          "sum(http_inprogress_requests) by (job)",
								"type":           "recording",
								"evaluationTime": 0.3,
								"lastEvaluation": "2020-05-18T15:52:53.4503113Z",
							},
						},
					},
				},
			},
			res: RulesResult{
				Groups: []RuleGroup{
					{
						Name:     "example",
						File:     "/rules.yaml",
						Interval: 60,
						Rules: []interface{}{
							AlertingRule{
								Alerts: []*Alert{
									{
										ActiveAt: testTime.UTC(),
										Annotations: model.LabelSet{
											"summary": "High request latency",
										},
										Labels: model.LabelSet{
											"alertname": "HighRequestLatency",
											"severity":  "page",
										},
										State: AlertStateFiring,
										Value: "1e+00",
									},
								},
								Annotations: model.LabelSet{
									"summary": "High request latency",
								},
								Labels: model.LabelSet{
									"severity": "page",
								},
								Duration:       600,
								Health:         RuleHealthGood,
								Name:           "HighRequestLatency",
								Query:          "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5",
								LastError:      "",
								EvaluationTime: 0.5,
								LastEvaluation: time.Date(2020, 5, 18, 15, 52, 53, 450311300, time.UTC),
								State:          "firing",
							},
							RecordingRule{
								Health:         RuleHealthGood,
								Name:           "job:http_inprogress_requests:sum",
								Query:          "sum(http_inprogress_requests) by (job)",
								LastError:      "",
								EvaluationTime: 0.3,
								LastEvaluation: time.Date(2020, 5, 18, 15, 52, 53, 450311300, time.UTC),
							},
						},
					},
				},
			},
		},

		{
			do:        doRules(),
			reqMethod: "GET",
			reqPath:   "/api/v1/rules",
			inErr:     errors.New("some error"),
			err:       errors.New("some error"),
		},

		{
			do:        doTargets(),
			reqMethod: "GET",
			reqPath:   "/api/v1/targets",
			inRes: map[string]interface{}{
				"activeTargets": []map[string]interface{}{
					{
						"discoveredLabels": map[string]string{
							"__address__":      "127.0.0.1:9090",
							"__metrics_path__": "/metrics",
							"__scheme__":       "http",
							"job":              "prometheus",
						},
						"labels": map[string]string{
							"instance": "127.0.0.1:9090",
							"job":      "prometheus",
						},
						"scrapePool":         "prometheus",
						"scrapeUrl":          "http://127.0.0.1:9090",
						"globalUrl":          "http://127.0.0.1:9090",
						"lastError":          "error while scraping target",
						"lastScrape":         testTime.UTC().Format(time.RFC3339Nano),
						"lastScrapeDuration": 0.001146115,
						"health":             "up",
					},
				},
				"droppedTargets": []map[string]interface{}{
					{
						"discoveredLabels": map[string]string{
							"__address__":      "127.0.0.1:9100",
							"__metrics_path__": "/metrics",
							"__scheme__":       "http",
							"job":              "node",
						},
					},
				},
			},
			res: TargetsResult{
				Active: []ActiveTarget{
					{
						DiscoveredLabels: map[string]string{
							"__address__":      "127.0.0.1:9090",
							"__metrics_path__": "/metrics",
							"__scheme__":       "http",
							"job":              "prometheus",
						},
						Labels: model.LabelSet{
							"instance": "127.0.0.1:9090",
							"job":      "prometheus",
						},
						ScrapePool:         "prometheus",
						ScrapeURL:          "http://127.0.0.1:9090",
						GlobalURL:          "http://127.0.0.1:9090",
						LastError:          "error while scraping target",
						LastScrape:         testTime.UTC(),
						LastScrapeDuration: 0.001146115,
						Health:             HealthGood,
					},
				},
				Dropped: []DroppedTarget{
					{
						DiscoveredLabels: map[string]string{
							"__address__":      "127.0.0.1:9100",
							"__metrics_path__": "/metrics",
							"__scheme__":       "http",
							"job":              "node",
						},
					},
				},
			},
		},

		{
			do:        doTargets(),
			reqMethod: "GET",
			reqPath:   "/api/v1/targets",
			inErr:     errors.New("some error"),
			err:       errors.New("some error"),
		},

		{
			do: doTargetsMetadata("{job=\"prometheus\"}", "go_goroutines", "1"),
			inRes: []map[string]interface{}{
				{
					"target": map[string]interface{}{
						"instance": "127.0.0.1:9090",
						"job":      "prometheus",
					},
					"type": "gauge",
					"help": "Number of goroutines that currently exist.",
					"unit": "",
				},
			},
			reqMethod: "GET",
			reqPath:   "/api/v1/targets/metadata",
			res: []MetricMetadata{
				{
					Target: map[string]string{
						"instance": "127.0.0.1:9090",
						"job":      "prometheus",
					},
					Type: "gauge",
					Help: "Number of goroutines that currently exist.",
					Unit: "",
				},
			},
		},

		{
			do:        doTargetsMetadata("{job=\"prometheus\"}", "go_goroutines", "1"),
			inErr:     errors.New("some error"),
			reqMethod: "GET",
			reqPath:   "/api/v1/targets/metadata",
			err:       errors.New("some error"),
		},

		{
			do: doMetadata("go_goroutines", "1"),
			inRes: map[string]interface{}{
				"go_goroutines": []map[string]interface{}{
					{
						"type": "gauge",
						"help": "Number of goroutines that currently exist.",
						"unit": "",
					},
				},
			},
			reqMethod: "GET",
			reqPath:   "/api/v1/metadata",
			res: map[string][]Metadata{
				"go_goroutines": {
					{
						Type: "gauge",
						Help: "Number of goroutines that currently exist.",
						Unit: "",
					},
				},
			},
		},

		{
			do:        doMetadata("", "1"),
			inErr:     errors.New("some error"),
			reqMethod: "GET",
			reqPath:   "/api/v1/metadata",
			err:       errors.New("some error"),
		},

		{
			do:        doTSDB(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/tsdb",
			inErr:     errors.New("some error"),
			err:       errors.New("some error"),
		},

		{
			do:        doTSDB(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/tsdb",
			inRes: map[string]interface{}{
				"headStats": map[string]interface{}{
					"numSeries":     18476,
					"numLabelPairs": 4301,
					"chunkCount":    72692,
					"minTime":       1634644800304,
					"maxTime":       1634650590304,
				},
				"seriesCountByMetricName": []interface{}{
					map[string]interface{}{
						"name":  "kubelet_http_requests_duration_seconds_bucket",
						"value": 1000,
					},
				},
				"labelValueCountByLabelName": []interface{}{
					map[string]interface{}{
						"name":  "__name__",
						"value": 200,
					},
				},
				"memoryInBytesByLabelName": []interface{}{
					map[string]interface{}{
						"name":  "id",
						"value": 4096,
					},
				},
				"seriesCountByLabelValuePair": []interface{}{
					map[string]interface{}{
						"name":  "job=kubelet",
						"value": 30000,
					},
				},
			},
			res: TSDBResult{
				HeadStats: TSDBHeadStats{
					NumSeries:     18476,
					NumLabelPairs: 4301,
					ChunkCount:    72692,
					MinTime:       1634644800304,
					MaxTime:       1634650590304,
				},
				SeriesCountByMetricName: []Stat{
					{
						Name:  "kubelet_http_requests_duration_seconds_bucket",
						Value: 1000,
					},
				},
				LabelValueCountByLabelName: []Stat{
					{
						Name:  "__name__",
						Value: 200,
					},
				},
				MemoryInBytesByLabelName: []Stat{
					{
						Name:  "id",
						Value: 4096,
					},
				},
				SeriesCountByLabelValuePair: []Stat{
					{
						Name:  "job=kubelet",
						Value: 30000,
					},
				},
			},
		},

		{
			do:        doWalReply(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/walreplay",
			inErr:     errors.New("some error"),
			err:       errors.New("some error"),
		},

		{
			do:        doWalReply(),
			reqMethod: "GET",
			reqPath:   "/api/v1/status/walreplay",
			inRes: map[string]interface{}{
				"min":     2,
				"max":     5,
				"current": 40,
			},
			res: WalReplayStatus{
				Min:     2,
				Max:     5,
				Current: 40,
			},
		},

		{
			do:        doQueryExemplars("tns_request_duration_seconds_bucket", testTime.Add(-1*time.Minute), testTime),
			reqMethod: "POST",
			reqPath:   "/api/v1/query_exemplars",
			inErr:     errors.New("some error"),
			err:       errors.New("some error"),
		},

		{
			do:        doQueryExemplars("tns_request_duration_seconds_bucket", testTime.Add(-1*time.Minute), testTime),
			reqMethod: "POST",
			reqPath:   "/api/v1/query_exemplars",
			inRes: []interface{}{
				map[string]interface{}{
					"seriesLabels": map[string]interface{}{
						"__name__": "tns_request_duration_seconds_bucket",
						"instance": "app:80",
						"job":      "tns/app",
					},
					"exemplars": []interface{}{
						map[string]interface{}{
							"labels": map[string]interface{}{
								"traceID": "19fd8c8a33975a23",
							},
							"value":     "0.003863295",
							"timestamp": model.TimeFromUnixNano(testTime.UnixNano()),
						},
						map[string]interface{}{
							"labels": map[string]interface{}{
								"traceID": "67f743f07cc786b0",
							},
							"value":     "0.001535405",
							"timestamp": model.TimeFromUnixNano(testTime.UnixNano()),
						},
					},
				},
			},
			res: []ExemplarQueryResult{
				{
					SeriesLabels: model.LabelSet{
						"__name__": "tns_request_duration_seconds_bucket",
						"instance": "app:80",
						"job":      "tns/app",
					},
					Exemplars: []Exemplar{
						{
							Labels:    model.LabelSet{"traceID": "19fd8c8a33975a23"},
							Value:     0.003863295,
							Timestamp: model.TimeFromUnixNano(testTime.UnixNano()),
						},
						{
							Labels:    model.LabelSet{"traceID": "67f743f07cc786b0"},
							Value:     0.001535405,
							Timestamp: model.TimeFromUnixNano(testTime.UnixNano()),
						},
					},
				},
			},
		},
	}

	var tests []apiTest
	tests = append(tests, queryTests...)

	for i, test := range tests {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			tc.curTest = test

			res, warnings, err := test.do()

			if (test.inWarnings == nil) != (warnings == nil) && !reflect.DeepEqual(test.inWarnings, warnings) {
				t.Fatalf("mismatch in warnings expected=%v actual=%v", test.inWarnings, warnings)
			}

			if test.err != nil {
				if err == nil {
					t.Fatalf("expected error %q but got none", test.err)
				}
				if err.Error() != test.err.Error() {
					t.Errorf("unexpected error: want %s, got %s", test.err, err)
				}

				apiErr := &Error{}
				if ok := errors.As(err, &apiErr); ok {
					if apiErr.Detail != test.inRes {
						t.Errorf("%q should be %q", apiErr.Detail, test.inRes)
					}
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !reflect.DeepEqual(res, test.res) {
				t.Errorf("unexpected result: want %v, got %v", test.res, res)
			}
		})
	}
}

type testClient struct {
	*testing.T

	ch  chan apiClientTest
	req *http.Request
}

type apiClientTest struct {
	code             int
	response         interface{}
	expectedBody     string
	expectedErr      *Error
	expectedWarnings Warnings
}

func (c *testClient) URL(ep string, args map[string]string) *url.URL {
	return nil
}

func (c *testClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
	if ctx == nil {
		c.Fatalf("context was not passed down")
	}
	if req != c.req {
		c.Fatalf("request was not passed down")
	}

	test := <-c.ch

	var b []byte
	var err error

	switch v := test.response.(type) {
	case string:
		b = []byte(v)
	default:
		b, err = json.Marshal(v)
		if err != nil {
			c.Fatal(err)
		}
	}

	resp := &http.Response{
		StatusCode: test.code,
	}

	return resp, b, nil
}

func TestAPIClientDo(t *testing.T) {
	tests := []apiClientTest{
		{
			code: http.StatusUnprocessableEntity,
			response: &apiResponse{
				Status:    "error",
				Data:      json.RawMessage(`null`),
				ErrorType: ErrBadData,
				Error:     "failed",
			},
			expectedErr: &Error{
				Type: ErrBadData,
				Msg:  "failed",
			},
			expectedBody: `null`,
		},
		{
			code: http.StatusUnprocessableEntity,
			response: &apiResponse{
				Status:    "error",
				Data:      json.RawMessage(`"test"`),
				ErrorType: ErrTimeout,
				Error:     "timed out",
			},
			expectedErr: &Error{
				Type: ErrTimeout,
				Msg:  "timed out",
			},
			expectedBody: `test`,
		},
		{
			code:     http.StatusInternalServerError,
			response: "500 error details",
			expectedErr: &Error{
				Type:   ErrServer,
				Msg:    "server error: 500",
				Detail: "500 error details",
			},
		},
		{
			code:     http.StatusNotFound,
			response: "404 error details",
			expectedErr: &Error{
				Type:   ErrClient,
				Msg:    "client error: 404",
				Detail: "404 error details",
			},
		},
		{
			code: http.StatusBadRequest,
			response: &apiResponse{
				Status:    "error",
				Data:      json.RawMessage(`null`),
				ErrorType: ErrBadData,
				Error:     "end timestamp must not be before start time",
			},
			expectedErr: &Error{
				Type: ErrBadData,
				Msg:  "end timestamp must not be before start time",
			},
		},
		{
			code:     http.StatusUnprocessableEntity,
			response: "bad json",
			expectedErr: &Error{
				Type: ErrBadResponse,
				Msg:  "readObjectStart: expect { or n, but found b, error found in #1 byte of ...|bad json|..., bigger context ...|bad json|...",
			},
		},
		{
			code: http.StatusUnprocessableEntity,
			response: &apiResponse{
				Status: "success",
				Data:   json.RawMessage(`"test"`),
			},
			expectedErr: &Error{
				Type: ErrBadResponse,
				Msg:  "inconsistent body for response code",
			},
		},
		{
			code: http.StatusUnprocessableEntity,
			response: &apiResponse{
				Status:    "success",
				Data:      json.RawMessage(`"test"`),
				ErrorType: ErrTimeout,
				Error:     "timed out",
			},
			expectedErr: &Error{
				Type: ErrBadResponse,
				Msg:  "inconsistent body for response code",
			},
		},
		{
			code: http.StatusOK,
			response: &apiResponse{
				Status:    "error",
				Data:      json.RawMessage(`"test"`),
				ErrorType: ErrTimeout,
				Error:     "timed out",
			},
			expectedErr: &Error{
				Type: ErrTimeout,
				Msg:  "timed out",
			},
		},
		{
			code: http.StatusOK,
			response: &apiResponse{
				Status:    "error",
				Data:      json.RawMessage(`"test"`),
				ErrorType: ErrTimeout,
				Error:     "timed out",
				Warnings:  []string{"a"},
			},
			expectedErr: &Error{
				Type: ErrTimeout,
				Msg:  "timed out",
			},
			expectedWarnings: []string{"a"},
		},
	}

	tc := &testClient{
		T:   t,
		ch:  make(chan apiClientTest, 1),
		req: &http.Request{},
	}
	client := &apiClientImpl{
		client: tc,
	}

	for i, test := range tests {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			tc.ch <- test

			_, body, warnings, err := client.Do(context.Background(), tc.req)

			if test.expectedWarnings != nil {
				if !reflect.DeepEqual(test.expectedWarnings, warnings) {
					t.Fatalf("mismatch in warnings expected=%v actual=%v", test.expectedWarnings, warnings)
				}
			} else {
				if warnings != nil {
					t.Fatalf("unexpected warnings: %v", warnings)
				}
			}

			if test.expectedErr != nil {
				if err == nil {
					t.Fatal("expected error, but got none")
				}

				if test.expectedErr.Error() != err.Error() {
					t.Fatalf("expected error:%v, but got:%v", test.expectedErr.Error(), err.Error())
				}

				if test.expectedErr.Detail != "" {
					apiErr := &Error{}
					if errors.As(err, &apiErr) {
						if apiErr.Detail != test.expectedErr.Detail {
							t.Fatalf("expected error detail :%v, but got:%v", apiErr.Detail, test.expectedErr.Detail)
						}
					} else {
						t.Fatalf("expected v1.Error instance, but got:%T", err)
					}
				}

				return
			}

			if err != nil {
				t.Fatalf("unexpected error:%v", err)
			}
			if test.expectedBody != string(body) {
				t.Fatalf("expected body :%v, but got:%v", test.expectedBody, string(body))
			}
		})
	}
}

func TestSamplesJSONSerialization(t *testing.T) {
	tests := []struct {
		point    model.SamplePair
		expected string
	}{
		{
			point:    model.SamplePair{0, 0},
			expected: `[0,"0"]`,
		},
		{
			point:    model.SamplePair{1, 20},
			expected: `[0.001,"20"]`,
		},
		{
			point:    model.SamplePair{10, 20},
			expected: `[0.010,"20"]`,
		},
		{
			point:    model.SamplePair{100, 20},
			expected: `[0.100,"20"]`,
		},
		{
			point:    model.SamplePair{1001, 20},
			expected: `[1.001,"20"]`,
		},
		{
			point:    model.SamplePair{1010, 20},
			expected: `[1.010,"20"]`,
		},
		{
			point:    model.SamplePair{1100, 20},
			expected: `[1.100,"20"]`,
		},
		{
			point:    model.SamplePair{12345678123456555, 20},
			expected: `[12345678123456.555,"20"]`,
		},
		{
			point:    model.SamplePair{-1, 20},
			expected: `[-0.001,"20"]`,
		},
		{
			point:    model.SamplePair{0, model.SampleValue(math.NaN())},
			expected: `[0,"NaN"]`,
		},
		{
			point:    model.SamplePair{0, model.SampleValue(math.Inf(1))},
			expected: `[0,"+Inf"]`,
		},
		{
			point:    model.SamplePair{0, model.SampleValue(math.Inf(-1))},
			expected: `[0,"-Inf"]`,
		},
		{
			point:    model.SamplePair{0, model.SampleValue(1.2345678e6)},
			expected: `[0,"1234567.8"]`,
		},
		{
			point:    model.SamplePair{0, 1.2345678e-6},
			expected: `[0,"0.0000012345678"]`,
		},
		{
			point:    model.SamplePair{0, 1.2345678e-67},
			expected: `[0,"1.2345678e-67"]`,
		},
	}

	for _, test := range tests {
		t.Run(test.expected, func(t *testing.T) {
			b, err := json.Marshal(test.point)
			if err != nil {
				t.Fatal(err)
			}
			if string(b) != test.expected {
				t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b))
			}

			// To test Unmarshal we will Unmarshal then re-Marshal this way we
			// can do a string compare, otherwise Nan values don't show equivalence
			// properly.
			var sp model.SamplePair
			if err = json.Unmarshal(b, &sp); err != nil {
				t.Fatal(err)
			}

			b, err = json.Marshal(sp)
			if err != nil {
				t.Fatal(err)
			}
			if string(b) != test.expected {
				t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b))
			}
		})
	}
}

func TestHistogramJSONSerialization(t *testing.T) {
	tests := []struct {
		name     string
		point    model.SampleHistogramPair
		expected string
	}{
		{
			name: "empty histogram",
			point: model.SampleHistogramPair{
				Timestamp: 0,
				Histogram: &model.SampleHistogram{},
			},
			expected: `[0,{"count":"0","sum":"0"}]`,
		},
		{
			name: "histogram with NaN/Inf and no buckets",
			point: model.SampleHistogramPair{
				Timestamp: 0,
				Histogram: &model.SampleHistogram{
					Count: model.FloatString(math.NaN()),
					Sum:   model.FloatString(math.Inf(1)),
				},
			},
			expected: `[0,{"count":"NaN","sum":"+Inf"}]`,
		},
		{
			name: "six-bucket histogram",
			point: model.SampleHistogramPair{
				Timestamp: 1,
				Histogram: &model.SampleHistogram{
					Count: 13.5,
					Sum:   3897.1,
					Buckets: model.HistogramBuckets{
						{
							Boundaries: 1,
							Lower:      -4870.992343051145,
							Upper:      -4466.7196729968955,
							Count:      1,
						},
						{
							Boundaries: 1,
							Lower:      -861.0779292198035,
							Upper:      -789.6119426088657,
							Count:      2,
						},
						{
							Boundaries: 1,
							Lower:      -558.3399591246119,
							Upper:      -512,
							Count:      3,
						},
						{
							Boundaries: 0,
							Lower:      2048,
							Upper:      2233.3598364984477,
							Count:      1.5,
						},
						{
							Boundaries: 0,
							Lower:      2896.3093757400984,
							Upper:      3158.4477704354626,
							Count:      2.5,
						},
						{
							Boundaries: 0,
							Lower:      4466.7196729968955,
							Upper:      4870.992343051145,
							Count:      3.5,
						},
					},
				},
			},
			expected: `[0.001,{"count":"13.5","sum":"3897.1","buckets":[[1,"-4870.992343051145","-4466.7196729968955","1"],[1,"-861.0779292198035","-789.6119426088657","2"],[1,"-558.3399591246119","-512","3"],[0,"2048","2233.3598364984477","1.5"],[0,"2896.3093757400984","3158.4477704354626","2.5"],[0,"4466.7196729968955","4870.992343051145","3.5"]]}]`,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			b, err := json.Marshal(test.point)
			if err != nil {
				t.Fatal(err)
			}
			if string(b) != test.expected {
				t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b))
			}

			// To test Unmarshal we will Unmarshal then re-Marshal. This way we
			// can do a string compare, otherwise NaN values don't show equivalence
			// properly.
			var sp model.SampleHistogramPair
			if err = json.Unmarshal(b, &sp); err != nil {
				t.Fatal(err)
			}

			b, err = json.Marshal(sp)
			if err != nil {
				t.Fatal(err)
			}
			if string(b) != test.expected {
				t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b))
			}
		})
	}
}

func TestSampleStreamJSONSerialization(t *testing.T) {
	floats, histograms := generateData(1, 5)

	tests := []struct {
		name         string
		stream       model.SampleStream
		expectedJSON string
	}{
		{
			"floats",
			*floats[0],
			`{"metric":{"__name__":"timeseries_0","foo":"bar"},"values":[[1677587259.055,"1"],[1677587244.055,"2"],[1677587229.055,"3"],[1677587214.055,"4"],[1677587199.055,"5"]]}`,
		},
		{
			"histograms",
			*histograms[0],
			`{"metric":{"__name__":"timeseries_0","foo":"bar"},"histograms":[[1677587259.055,{"count":"13.5","sum":"0.1","buckets":[[1,"-4870.992343051145","-4466.7196729968955","1"],[1,"-861.0779292198035","-789.6119426088657","2"],[1,"-558.3399591246119","-512","3"],[0,"2048","2233.3598364984477","1.5"],[0,"2896.3093757400984","3158.4477704354626","2.5"],[0,"4466.7196729968955","4870.992343051145","3.5"]]}],[1677587244.055,{"count":"27","sum":"0.2","buckets":[[1,"-4870.992343051145","-4466.7196729968955","2"],[1,"-861.0779292198035","-789.6119426088657","4"],[1,"-558.3399591246119","-512","6"],[0,"2048","2233.3598364984477","3"],[0,"2896.3093757400984","3158.4477704354626","5"],[0,"4466.7196729968955","4870.992343051145","7"]]}],[1677587229.055,{"count":"40.5","sum":"0.30000000000000004","buckets":[[1,"-4870.992343051145","-4466.7196729968955","3"],[1,"-861.0779292198035","-789.6119426088657","6"],[1,"-558.3399591246119","-512","9"],[0,"2048","2233.3598364984477","4.5"],[0,"2896.3093757400984","3158.4477704354626","7.5"],[0,"4466.7196729968955","4870.992343051145","10.5"]]}],[1677587214.055,{"count":"54","sum":"0.4","buckets":[[1,"-4870.992343051145","-4466.7196729968955","4"],[1,"-861.0779292198035","-789.6119426088657","8"],[1,"-558.3399591246119","-512","12"],[0,"2048","2233.3598364984477","6"],[0,"2896.3093757400984","3158.4477704354626","10"],[0,"4466.7196729968955","4870.992343051145","14"]]}],[1677587199.055,{"count":"67.5","sum":"0.5","buckets":[[1,"-4870.992343051145","-4466.7196729968955","5"],[1,"-861.0779292198035","-789.6119426088657","10"],[1,"-558.3399591246119","-512","15"],[0,"2048","2233.3598364984477","7.5"],[0,"2896.3093757400984","3158.4477704354626","12.5"],[0,"4466.7196729968955","4870.992343051145","17.5"]]}]]}`,
		},
		{
			"both",
			model.SampleStream{
				Metric:     floats[0].Metric,
				Values:     floats[0].Values,
				Histograms: histograms[0].Histograms,
			},
			`{"metric":{"__name__":"timeseries_0","foo":"bar"},"values":[[1677587259.055,"1"],[1677587244.055,"2"],[1677587229.055,"3"],[1677587214.055,"4"],[1677587199.055,"5"]],"histograms":[[1677587259.055,{"count":"13.5","sum":"0.1","buckets":[[1,"-4870.992343051145","-4466.7196729968955","1"],[1,"-861.0779292198035","-789.6119426088657","2"],[1,"-558.3399591246119","-512","3"],[0,"2048","2233.3598364984477","1.5"],[0,"2896.3093757400984","3158.4477704354626","2.5"],[0,"4466.7196729968955","4870.992343051145","3.5"]]}],[1677587244.055,{"count":"27","sum":"0.2","buckets":[[1,"-4870.992343051145","-4466.7196729968955","2"],[1,"-861.0779292198035","-789.6119426088657","4"],[1,"-558.3399591246119","-512","6"],[0,"2048","2233.3598364984477","3"],[0,"2896.3093757400984","3158.4477704354626","5"],[0,"4466.7196729968955","4870.992343051145","7"]]}],[1677587229.055,{"count":"40.5","sum":"0.30000000000000004","buckets":[[1,"-4870.992343051145","-4466.7196729968955","3"],[1,"-861.0779292198035","-789.6119426088657","6"],[1,"-558.3399591246119","-512","9"],[0,"2048","2233.3598364984477","4.5"],[0,"2896.3093757400984","3158.4477704354626","7.5"],[0,"4466.7196729968955","4870.992343051145","10.5"]]}],[1677587214.055,{"count":"54","sum":"0.4","buckets":[[1,"-4870.992343051145","-4466.7196729968955","4"],[1,"-861.0779292198035","-789.6119426088657","8"],[1,"-558.3399591246119","-512","12"],[0,"2048","2233.3598364984477","6"],[0,"2896.3093757400984","3158.4477704354626","10"],[0,"4466.7196729968955","4870.992343051145","14"]]}],[1677587199.055,{"count":"67.5","sum":"0.5","buckets":[[1,"-4870.992343051145","-4466.7196729968955","5"],[1,"-861.0779292198035","-789.6119426088657","10"],[1,"-558.3399591246119","-512","15"],[0,"2048","2233.3598364984477","7.5"],[0,"2896.3093757400984","3158.4477704354626","12.5"],[0,"4466.7196729968955","4870.992343051145","17.5"]]}]]}`,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			b, err := json.Marshal(test.stream)
			if err != nil {
				t.Fatal(err)
			}
			if string(b) != test.expectedJSON {
				t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expectedJSON, string(b))
			}

			var stream model.SampleStream
			if err = json.Unmarshal(b, &stream); err != nil {
				t.Fatal(err)
			}

			if !reflect.DeepEqual(test.stream, stream) {
				t.Fatalf("Mismatch after unmarshal expected=%#v actual=%#v", test.stream, stream)
			}
		})
	}
}

type httpTestClient struct {
	client http.Client
}

func (c *httpTestClient) URL(ep string, args map[string]string) *url.URL {
	return nil
}

func (c *httpTestClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
	resp, err := c.client.Do(req)
	if err != nil {
		return nil, nil, err
	}

	var body []byte
	done := make(chan struct{})
	go func() {
		body, err = io.ReadAll(resp.Body)
		close(done)
	}()

	select {
	case <-ctx.Done():
		<-done
		err = resp.Body.Close()
		if err == nil {
			err = ctx.Err()
		}
	case <-done:
	}

	return resp, body, err
}

func TestDoGetFallback(t *testing.T) {
	v := url.Values{"a": []string{"1", "2"}}

	type testResponse struct {
		Values string
		Method string
	}

	// Start a local HTTP server.
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		req.ParseForm()
		testResp, _ := json.Marshal(&testResponse{
			Values: req.Form.Encode(),
			Method: req.Method,
		})

		apiResp := &apiResponse{
			Data: testResp,
		}

		body, _ := json.Marshal(apiResp)

		if req.Method == http.MethodPost {
			if req.URL.Path == "/blockPost405" {
				http.Error(w, string(body), http.StatusMethodNotAllowed)
				return
			}
		}

		if req.Method == http.MethodPost {
			if req.URL.Path == "/blockPost501" {
				http.Error(w, string(body), http.StatusNotImplemented)
				return
			}
		}

		w.Write(body)
	}))
	// Close the server when test finishes.
	defer server.Close()

	u, err := url.Parse(server.URL)
	if err != nil {
		t.Fatal(err)
	}
	client := &httpTestClient{client: *(server.Client())}
	api := &apiClientImpl{
		client: client,
	}

	// Do a post, and ensure that the post succeeds.
	_, b, _, err := api.DoGetFallback(context.TODO(), u, v)
	if err != nil {
		t.Fatalf("Error doing local request: %v", err)
	}
	resp := &testResponse{}
	if err := json.Unmarshal(b, resp); err != nil {
		t.Fatal(err)
	}
	if resp.Method != http.MethodPost {
		t.Fatalf("Mismatch method")
	}
	if resp.Values != v.Encode() {
		t.Fatalf("Mismatch in values")
	}

	// Do a fallback to a get on 405.
	u.Path = "/blockPost405"
	_, b, _, err = api.DoGetFallback(context.TODO(), u, v)
	if err != nil {
		t.Fatalf("Error doing local request: %v", err)
	}
	if err := json.Unmarshal(b, resp); err != nil {
		t.Fatal(err)
	}
	if resp.Method != http.MethodGet {
		t.Fatalf("Mismatch method")
	}
	if resp.Values != v.Encode() {
		t.Fatalf("Mismatch in values")
	}

	// Do a fallback to a get on 501.
	u.Path = "/blockPost501"
	_, b, _, err = api.DoGetFallback(context.TODO(), u, v)
	if err != nil {
		t.Fatalf("Error doing local request: %v", err)
	}
	if err := json.Unmarshal(b, resp); err != nil {
		t.Fatal(err)
	}
	if resp.Method != http.MethodGet {
		t.Fatalf("Mismatch method")
	}
	if resp.Values != v.Encode() {
		t.Fatalf("Mismatch in values")
	}
}
