package cmd

import (
	"bytes"
	"errors"
	"io"
	"net/http"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/tidbcloud/clinic-cli/internal/clinic"
	"github.com/tidbcloud/clinic-cli/internal/cmd/metricecharts"
	"github.com/tidbcloud/clinic-cli/internal/config"
)

func TestMetricsQueryRequiresClusterIDAndQuery(t *testing.T) {
	t.Parallel()

	ctx := &RuntimeContext{Client: &recordingClinicClient{}}
	testCases := []struct {
		name string
		args []string
		want string
	}{
		{
			name: "missing cluster id",
			args: []string{"query", "--query", "up"},
			want: "missing required --cluster-id",
		},
		{
			name: "missing query",
			args: []string{"query", "--cluster-id", "cluster-42"},
			want: "missing required --query",
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			err := metricsCommandHandler(ctx, tc.args, &strings.Builder{})
			if err == nil {
				t.Fatal("metricsCommandHandler() error = nil, want validation error")
			}
			if got := err.Error(); got != tc.want {
				t.Fatalf("error = %q, want %q", got, tc.want)
			}
		})
	}
}

func TestMetricsQueryUsesExpectedParams(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"status":"success"}`)}}}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := metricsCommandHandler(ctx, []string{
		"query",
		"--cluster-id", "cluster-42",
		"--query", "sum(rate(tidb_server_handle_query_duration_seconds_count[5m]))",
		"--time", "1712123456",
		"--timeout", "30s",
	}, &stdout); err != nil {
		t.Fatalf("metricsCommandHandler() error = %v", err)
	}

	if got, want := client.request.Method, http.MethodGet; got != want {
		t.Fatalf("method = %q, want %q", got, want)
	}
	if got, want := client.request.Path, "/data-proxy/metrics/cluster-42/api/v1/query"; got != want {
		t.Fatalf("path = %q, want %q", got, want)
	}
	assertQueryValue(t, client.request.Query, "query", "sum(rate(tidb_server_handle_query_duration_seconds_count[5m]))")
	assertQueryValue(t, client.request.Query, "time", "1712123456")
	assertQueryValue(t, client.request.Query, "timeout", "30s")
	if client.request.Body != nil {
		t.Fatalf("body = %#v, want nil", client.request.Body)
	}
	assertJSONEqual(t, stdout.String(), `{"status":"success"}`)
}

func TestMetricsRangeRequiresQueryStartAndEnd(t *testing.T) {
	t.Parallel()

	ctx := &RuntimeContext{Client: &recordingClinicClient{}}
	testCases := []struct {
		name string
		args []string
		want string
	}{
		{
			name: "missing cluster id",
			args: []string{"range", "--query", "up", "--start", "10", "--end", "20", "--step", "30s"},
			want: "missing required --cluster-id",
		},
		{
			name: "missing query",
			args: []string{"range", "--cluster-id", "cluster-42", "--start", "10", "--end", "20", "--step", "30s"},
			want: "missing required --query",
		},
		{
			name: "missing start",
			args: []string{"range", "--cluster-id", "cluster-42", "--query", "up", "--end", "20", "--step", "30s"},
			want: "missing required --start",
		},
		{
			name: "missing end",
			args: []string{"range", "--cluster-id", "cluster-42", "--query", "up", "--start", "10", "--step", "30s"},
			want: "missing required --end",
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			err := metricsCommandHandler(ctx, tc.args, &strings.Builder{})
			if err == nil {
				t.Fatal("metricsCommandHandler() error = nil, want validation error")
			}
			if got := err.Error(); got != tc.want {
				t.Fatalf("error = %q, want %q", got, tc.want)
			}
		})
	}
}

func TestMetricsRangeUsesLatestFiveMinutesWhenStartEndCannotBeResolved(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"status":"success","data":{"result":[]}}`)}}}
	ctx := &RuntimeContext{Client: client}

	before := time.Now().Unix()
	var stdout strings.Builder
	if err := metricsCommandHandler(ctx, []string{
		"range",
		"--cluster-id", "cluster-42",
		"--query", "up",
		"--output", "json",
	}, &stdout); err != nil {
		t.Fatalf("metricsCommandHandler() error = %v", err)
	}
	after := time.Now().Unix()

	startRaw := client.request.Query.Get("start")
	endRaw := client.request.Query.Get("end")
	startUnix, err := strconv.ParseInt(startRaw, 10, 64)
	if err != nil {
		t.Fatalf("start query value = %q, want unix seconds: %v", startRaw, err)
	}
	endUnix, err := strconv.ParseInt(endRaw, 10, 64)
	if err != nil {
		t.Fatalf("end query value = %q, want unix seconds: %v", endRaw, err)
	}
	if got, want := endUnix-startUnix, int64(300); got != want {
		t.Fatalf("range length = %d, want %d", got, want)
	}
	if endUnix < before || endUnix > after {
		t.Fatalf("end query value = %d, want between %d and %d", endUnix, before, after)
	}
}

func TestMetricsRangeUsesExpectedParams(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"status":"success","data":{"result":[]}}`)}}}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := metricsCommandHandler(ctx, []string{
		"range",
		"--cluster-id", "cluster-42",
		"--query", "up",
		"--start", "1712123000",
		"--end", "1712123600",
		"--step", "60s",
		"--timeout", "45s",
		"--output", "json",
	}, &stdout); err != nil {
		t.Fatalf("metricsCommandHandler() error = %v", err)
	}

	if got, want := client.request.Method, http.MethodGet; got != want {
		t.Fatalf("method = %q, want %q", got, want)
	}
	if got, want := client.request.Path, "/data-proxy/metrics/cluster-42/api/v1/query_range"; got != want {
		t.Fatalf("path = %q, want %q", got, want)
	}
	assertQueryValue(t, client.request.Query, "query", "up")
	assertQueryValue(t, client.request.Query, "start", "1712123000")
	assertQueryValue(t, client.request.Query, "end", "1712123600")
	assertQueryValue(t, client.request.Query, "step", "60s")
	assertQueryValue(t, client.request.Query, "timeout", "45s")
	assertJSONEqual(t, stdout.String(), `{"status":"success","data":{"result":[]}}`)
}

func TestMetricsRangeShortAliasesWithCobra(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"status":"success","data":{"result":[]}}`)}}}
	ctx := &RuntimeContext{
		Client: client,
		SelectedProfile: &config.ProfileRecord{
			ClusterID:           "tenant-42",
			OrgID:               "org-1",
			ParentID:            "pool-9",
			ClusterDeployTypeV2: "premium",
		},
	}

	var stdout strings.Builder
	if err := metricsCommandHandler(ctx, []string{
		"range",
		"--cluster-id", "tenant-42",
		"-p",
		"--query", "up",
		"--start", "1712123000",
		"--end", "1712123600",
		"-o", "json",
	}, &stdout); err != nil {
		t.Fatalf("metricsCommandHandler() error = %v", err)
	}

	if got, want := client.request.Path, "/data-proxy/metrics/pool-9/api/v1/query_range"; got != want {
		t.Fatalf("path = %q, want %q", got, want)
	}
	assertQueryValue(t, client.request.Query, "query", "up")
	assertJSONEqual(t, stdout.String(), `{"status":"success","data":{"result":[]}}`)
}

func TestMetricsRangeUsesDefaultStepWhenOmitted(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"status":"success","data":{"result":[]}}`)}}}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := metricsCommandHandler(ctx, []string{
		"range",
		"--cluster-id", "cluster-42",
		"--query", "up",
		"--start", "1712123000",
		"--end", "1712123600",
		"--output", "json",
	}, &stdout); err != nil {
		t.Fatalf("metricsCommandHandler() error = %v", err)
	}

	assertQueryValue(t, client.request.Query, "step", "15s")
	assertJSONEqual(t, stdout.String(), `{"status":"success","data":{"result":[]}}`)
}

func TestMetricsSeriesRepeatableMatchWithCobra(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"status":"success","data":[]}`)}}}
	ctx := &RuntimeContext{
		Client: client,
		SelectedProfile: &config.ProfileRecord{
			ClusterID:           "tenant-42",
			OrgID:               "org-1",
			ParentID:            "pool-9",
			ClusterDeployTypeV2: "premium",
		},
	}

	var stdout strings.Builder
	if err := metricsCommandHandler(ctx, []string{
		"series",
		"--cluster-id", "tenant-42",
		"-p",
		"--match", `{__name__="up"}`,
		"--match", `{job="tidb"}`,
		"--start", "1712123000",
		"--end", "1712123600",
	}, &stdout); err != nil {
		t.Fatalf("metricsCommandHandler() error = %v", err)
	}

	if got, want := client.request.Path, "/data-proxy/metrics/pool-9/api/v1/series"; got != want {
		t.Fatalf("path = %q, want %q", got, want)
	}
	gotMatches := client.request.Query["match[]"]
	wantMatches := []string{`{__name__="up"}`, `{job="tidb"}`}
	if strings.Join(gotMatches, "\n") != strings.Join(wantMatches, "\n") {
		t.Fatalf("match[] = %#v, want %#v", gotMatches, wantMatches)
	}
	assertJSONEqual(t, stdout.String(), `{"status":"success","data":[]}`)
}

func TestMetricsRangeDataServerUsesExpectedParamsAndHeaders(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name string
		flag string
	}{
		{
			name: "long flag",
			flag: "--data-server",
		},
		{
			name: "short flag alias",
			flag: "--ds",
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			client := &recordingClinicClient{
				responses: []clinicResponse{
					{body: []byte(`{"status":"success","data":{"result":[]}}`)},
				},
			}
			ctx := &RuntimeContext{
				Client: client,
				SelectedProfile: &config.ProfileRecord{
					ClusterID: "cluster-42",
					OrgID:     "org-from-profile",
				},
			}

			var stdout strings.Builder
			if err := metricsCommandHandler(ctx, []string{
				"range",
				"--cluster-id", "cluster-42",
				tc.flag,
				"--query", "up",
				"--start", "1712123000",
				"--end", "1712123600",
				"--step", "60s",
				"--timeout", "45s",
				"--output", "json",
			}, &stdout); err != nil {
				t.Fatalf("metricsCommandHandler() error = %v", err)
			}

			if got, want := len(client.requests), 1; got != want {
				t.Fatalf("request count = %d, want %d", got, want)
			}
			if got, want := client.request.Method, http.MethodGet; got != want {
				t.Fatalf("method = %q, want %q", got, want)
			}
			if got, want := client.request.Path, "/clinic/api/v1/data/metrics"; got != want {
				t.Fatalf("path = %q, want %q", got, want)
			}
			assertQueryValue(t, client.request.Query, "query", "up")
			assertQueryValue(t, client.request.Query, "start", "1712123000")
			assertQueryValue(t, client.request.Query, "end", "1712123600")
			assertQueryValue(t, client.request.Query, "step", "60s")
			assertQueryValue(t, client.request.Query, "timeout", "45s")
			assertHeaderValue(t, client.request.Headers, "X-OrgType", "cloud")
			assertHeaderValue(t, client.request.Headers, "X-OrgID", "org-from-profile")
			assertHeaderValue(t, client.request.Headers, "X-ClusterID", "cluster-42")
			if client.request.Body != nil {
				t.Fatalf("body = %#v, want nil", client.request.Body)
			}
			assertJSONEqual(t, stdout.String(), `{"status":"success","data":{"result":[]}}`)
		})
	}
}

func TestMetricsRangeDataServerFallsBackToRemoteWhenSelectedProfileMetadataUnavailableOrMismatched(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name            string
		selectedProfile *config.ProfileRecord
	}{
		{
			name: "selected profile missing org id",
			selectedProfile: &config.ProfileRecord{
				ClusterID: "cluster-42",
			},
		},
		{
			name: "selected profile cluster id mismatched",
			selectedProfile: &config.ProfileRecord{
				ClusterID: "cluster-other",
				OrgID:     "org-mismatched",
			},
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			client := &recordingClinicClient{
				responses: []clinicResponse{
					{body: []byte(`{"items":[{"clusterID":"cluster-42","orgID":"org-from-api"}]}`)},
					{body: []byte(`{"status":"success","data":{"result":[]}}`)},
				},
			}
			ctx := &RuntimeContext{
				Client:          client,
				SelectedProfile: tc.selectedProfile,
			}

			var stdout strings.Builder
			if err := metricsCommandHandler(ctx, []string{
				"range",
				"--cluster-id", "cluster-42",
				"--data-server",
				"--query", "up",
				"--start", "1712123000",
				"--end", "1712123600",
				"--output", "json",
			}, &stdout); err != nil {
				t.Fatalf("metricsCommandHandler() error = %v", err)
			}

			if got, want := len(client.requests), 2; got != want {
				t.Fatalf("request count = %d, want %d", got, want)
			}
			if got, want := client.requests[0].Path, dashboardClustersPath; got != want {
				t.Fatalf("lookup path = %q, want %q", got, want)
			}
			assertQueryValue(t, client.requests[0].Query, "cluster_id", "cluster-42")
			assertQueryValue(t, client.requests[0].Query, "show_deleted", "true")
			if got, want := client.requests[1].Path, "/clinic/api/v1/data/metrics"; got != want {
				t.Fatalf("metrics path = %q, want %q", got, want)
			}
			assertHeaderValue(t, client.requests[1].Headers, "X-OrgType", "cloud")
			assertHeaderValue(t, client.requests[1].Headers, "X-OrgID", "org-from-api")
			assertHeaderValue(t, client.requests[1].Headers, "X-ClusterID", "cluster-42")
			assertJSONEqual(t, stdout.String(), `{"status":"success","data":{"result":[]}}`)
		})
	}
}

func TestMetricsRangeValidatesAndRoutesOutputMode(t *testing.T) {
	t.Parallel()

	t.Run("rejects unsupported output value", func(t *testing.T) {
		t.Parallel()

		ctx := &RuntimeContext{Client: &recordingClinicClient{}}
		err := metricsCommandHandler(ctx, []string{
			"range",
			"--cluster-id", "cluster-42",
			"--query", "up",
			"--start", "1712123000",
			"--end", "1712123600",
			"--output", "yaml",
		}, &strings.Builder{})
		if err == nil {
			t.Fatal("metricsCommandHandler() error = nil, want validation error")
		}
		if got, want := err.Error(), "output must be one of: json, tui, echarts"; got != want {
			t.Fatalf("error = %q, want %q", got, want)
		}
	})

	t.Run("routes explicit json output through existing json path", func(t *testing.T) {
		t.Parallel()

		rawBody := `{"status":"success","data":{"result":[]}}`
		client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(rawBody)}}}
		ctx := &RuntimeContext{Client: client}

		var stdout strings.Builder
		runnerCalled := false
		echartsCalled := false
		if err := runMetricsRangeWithHooks(ctx, metricsRangeOptions{
			ClusterID: "cluster-42",
			Query:     "up",
			Start:     "1712123000",
			End:       "1712123600",
			Output:    "json",
		}, &stdout, func(io.Writer) bool {
			return true
		}, func(io.Writer, []byte) error {
			runnerCalled = true
			return nil
		}, func(io.Writer, []byte, metricecharts.RangeReportContext) error {
			echartsCalled = true
			return nil
		}); err != nil {
			t.Fatalf("metricsCommandHandler() error = %v", err)
		}

		if runnerCalled {
			t.Fatal("TUI runner was called for explicit json output")
		}
		if echartsCalled {
			t.Fatal("echarts runner was called for explicit json output")
		}
		assertJSONEqual(t, stdout.String(), rawBody)
	})

	t.Run("defaults to echarts when output is omitted", func(t *testing.T) {
		t.Parallel()

		client := &recordingClinicClient{responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"result":[]}}`)},
			{body: []byte(`{"items":[{"clusterID":"cluster-42","name":"cluster-blue","clusterDeployTypeV2":"premium"}]}`)},
		}}
		ctx := &RuntimeContext{Client: client}

		var (
			tuiRunnerCalled bool
			gotRunnerBody   []byte
			gotContext      metricecharts.RangeReportContext
		)
		wantStdout := &strings.Builder{}
		err := runMetricsRangeWithHooks(ctx, metricsRangeOptions{
			ClusterID: "cluster-42",
			Query:     "up",
			Start:     "1712123000",
			End:       "1712123600",
		}, wantStdout, func(io.Writer) bool {
			return false
		}, func(gotStdout io.Writer, rawResponse []byte) error {
			tuiRunnerCalled = true
			if gotStdout != wantStdout {
				t.Fatal("runner received unexpected stdout writer")
			}
			gotRunnerBody = append([]byte(nil), rawResponse...)
			return nil
		}, func(gotStdout io.Writer, rawResponse []byte, reportContext metricecharts.RangeReportContext) error {
			if gotStdout != wantStdout {
				t.Fatal("echarts runner received unexpected stdout writer")
			}
			gotContext = reportContext
			gotRunnerBody = append([]byte(nil), rawResponse...)
			return nil
		})
		if err != nil {
			t.Fatalf("runMetricsRangeWithHooks() error = %v", err)
		}
		if tuiRunnerCalled {
			t.Fatal("TUI runner was called for default output")
		}
		if got, want := string(gotRunnerBody), `{"status":"success","data":{"result":[]}}`; got != want {
			t.Fatalf("echarts runner body = %q, want %q", got, want)
		}
		if got, want := gotContext.ClusterID, "cluster-42"; got != want {
			t.Fatalf("echarts context cluster id = %q, want %q", got, want)
		}
		if got, want := gotContext.ClusterName, "cluster-blue"; got != want {
			t.Fatalf("echarts context cluster name = %q, want %q", got, want)
		}
		if got, want := gotContext.DeployType, "premium"; got != want {
			t.Fatalf("echarts context deploy type = %q, want %q", got, want)
		}
		if got, want := gotContext.Query, "up"; got != want {
			t.Fatalf("echarts context query = %q, want %q", got, want)
		}
		if got, want := len(client.requests), 2; got != want {
			t.Fatalf("request count = %d, want %d", got, want)
		}
		if got, want := client.requests[0].Path, "/data-proxy/metrics/cluster-42/api/v1/query_range"; got != want {
			t.Fatalf("query_range path = %q, want %q", got, want)
		}
		if got, want := client.requests[1].Path, dashboardClustersPath; got != want {
			t.Fatalf("metadata path = %q, want %q", got, want)
		}
		assertQueryValue(t, client.requests[1].Query, "cluster_id", "cluster-42")
		assertQueryValue(t, client.requests[1].Query, "show_deleted", "true")
	})

	t.Run("routes explicit echarts output through echarts runner", func(t *testing.T) {
		t.Parallel()

		rawBody := `{"status":"success","data":{"result":[]}}`
		client := &recordingClinicClient{responses: []clinicResponse{
			{body: []byte(rawBody)},
			{body: []byte(`{"items":[{"clusterID":"cluster-42","name":"cluster-blue","clusterDeployTypeV2":"premium"}]}`)},
		}}
		ctx := &RuntimeContext{Client: client}

		wantStdout := &strings.Builder{}
		var (
			tuiRunnerCalled   bool
			echartsRunnerBody []byte
			gotContext        metricecharts.RangeReportContext
		)
		err := runMetricsRangeWithHooks(ctx, metricsRangeOptions{
			ClusterID: "cluster-42",
			Query:     "up",
			Start:     "1712123000",
			End:       "1712123600",
			Output:    "echarts",
		}, wantStdout, func(io.Writer) bool {
			return false
		}, func(io.Writer, []byte) error {
			tuiRunnerCalled = true
			return nil
		}, func(gotStdout io.Writer, rawResponse []byte, reportContext metricecharts.RangeReportContext) error {
			if gotStdout != wantStdout {
				t.Fatal("echarts runner received unexpected stdout writer")
			}
			gotContext = reportContext
			echartsRunnerBody = append([]byte(nil), rawResponse...)
			return nil
		})
		if err != nil {
			t.Fatalf("runMetricsRangeWithHooks() error = %v", err)
		}
		if tuiRunnerCalled {
			t.Fatal("TUI runner was called for explicit echarts output")
		}
		if got, want := string(echartsRunnerBody), rawBody; got != want {
			t.Fatalf("echarts runner body = %q, want %q", got, want)
		}
		if got, want := gotContext.ClusterID, "cluster-42"; got != want {
			t.Fatalf("echarts context cluster id = %q, want %q", got, want)
		}
		if got, want := gotContext.ClusterName, "cluster-blue"; got != want {
			t.Fatalf("echarts context cluster name = %q, want %q", got, want)
		}
		if got, want := gotContext.DeployType, "premium"; got != want {
			t.Fatalf("echarts context deploy type = %q, want %q", got, want)
		}
		if got, want := gotContext.Query, "up"; got != want {
			t.Fatalf("echarts context query = %q, want %q", got, want)
		}
	})

	t.Run("rejects tui output on non-tty", func(t *testing.T) {
		t.Parallel()

		client := &recordingClinicClient{}
		ctx := &RuntimeContext{Client: client}

		var runnerCalled bool
		err := runMetricsRangeWithHooks(ctx, metricsRangeOptions{
			ClusterID: "cluster-42",
			Query:     "up",
			Start:     "1712123000",
			End:       "1712123600",
			Output:    "tui",
		}, &strings.Builder{}, func(io.Writer) bool {
			return false
		}, func(io.Writer, []byte) error {
			runnerCalled = true
			return nil
		}, func(io.Writer, []byte, metricecharts.RangeReportContext) error {
			t.Fatal("echarts runner was called for non-tty tui output")
			return nil
		})
		if err == nil {
			t.Fatal("runMetricsRangeWithHooks() error = nil, want tty validation error")
		}
		if got, want := err.Error(), "output tui requires a TTY"; got != want {
			t.Fatalf("error = %q, want %q", got, want)
		}
		if runnerCalled {
			t.Fatal("TUI runner was called for non-tty tui output")
		}
		if got, want := len(client.requests), 0; got != want {
			t.Fatalf("request count = %d, want %d", got, want)
		}
	})
}

func TestMetricsRangeDataServerValidatesAndRoutesOutputMode(t *testing.T) {
	t.Parallel()

	t.Run("rejects unsupported output value", func(t *testing.T) {
		t.Parallel()

		client := &recordingClinicClient{}
		ctx := &RuntimeContext{Client: client}

		err := metricsCommandHandler(ctx, []string{
			"range",
			"--cluster-id", "cluster-42",
			"--query", "up",
			"--start", "1712123000",
			"--end", "1712123600",
			"--data-server",
			"--output", "yaml",
		}, &strings.Builder{})
		if err == nil {
			t.Fatal("metricsCommandHandler() error = nil, want validation error")
		}
		if got, want := err.Error(), "output must be one of: json, tui, echarts"; got != want {
			t.Fatalf("error = %q, want %q", got, want)
		}
		if got, want := len(client.requests), 0; got != want {
			t.Fatalf("request count = %d, want %d", got, want)
		}
	})

	t.Run("rejects pool with deterministic error before any request", func(t *testing.T) {
		t.Parallel()

		client := &recordingClinicClient{}
		ctx := &RuntimeContext{Client: client}

		err := metricsCommandHandler(ctx, []string{
			"range",
			"--cluster-id", "cluster-42",
			"--query", "up",
			"--start", "1712123000",
			"--end", "1712123600",
			"--data-server",
			"--pool",
		}, &strings.Builder{})
		if err == nil {
			t.Fatal("metricsCommandHandler() error = nil, want validation error")
		}
		if got, want := err.Error(), "--data-server cannot be used with --pool"; got != want {
			t.Fatalf("error = %q, want %q", got, want)
		}
		if got, want := len(client.requests), 0; got != want {
			t.Fatalf("request count = %d, want %d", got, want)
		}
	})

	t.Run("routes explicit json output through existing json path", func(t *testing.T) {
		t.Parallel()

		rawBody := `{"status":"success","data":{"result":[]}}`
		client := &recordingClinicClient{
			responses: []clinicResponse{
				{body: []byte(rawBody)},
			},
		}
		ctx := &RuntimeContext{
			Client: client,
			SelectedProfile: &config.ProfileRecord{
				ClusterID:           "cluster-42",
				ClusterName:         "cluster-blue",
				OrgID:               "org-1",
				ClusterDeployTypeV2: "dedicated",
			},
		}

		var stdout strings.Builder
		runnerCalled := false
		echartsCalled := false
		if err := runMetricsRangeWithHooks(ctx, metricsRangeOptions{
			ClusterID:  "cluster-42",
			DataServer: true,
			Query:      "up",
			Start:      "1712123000",
			End:        "1712123600",
			Output:     "json",
		}, &stdout, func(io.Writer) bool {
			return true
		}, func(io.Writer, []byte) error {
			runnerCalled = true
			return nil
		}, func(io.Writer, []byte, metricecharts.RangeReportContext) error {
			echartsCalled = true
			return nil
		}); err != nil {
			t.Fatalf("runMetricsRangeWithHooks() error = %v", err)
		}

		if runnerCalled {
			t.Fatal("TUI runner was called for explicit json output")
		}
		if echartsCalled {
			t.Fatal("echarts runner was called for explicit json output")
		}
		if got, want := len(client.requests), 1; got != want {
			t.Fatalf("request count = %d, want %d", got, want)
		}
		if got, want := client.request.Path, "/clinic/api/v1/data/metrics"; got != want {
			t.Fatalf("path = %q, want %q", got, want)
		}
		assertJSONEqual(t, stdout.String(), rawBody)
	})

	t.Run("routes explicit echarts output through echarts runner", func(t *testing.T) {
		t.Parallel()

		rawBody := `{"status":"success","data":{"result":[]}}`
		client := &recordingClinicClient{
			responses: []clinicResponse{
				{body: []byte(rawBody)},
			},
		}
		ctx := &RuntimeContext{
			Client: client,
			SelectedProfile: &config.ProfileRecord{
				ClusterID:           "cluster-42",
				ClusterName:         "cluster-blue",
				OrgID:               "org-1",
				ClusterDeployTypeV2: "dedicated",
			},
		}

		wantStdout := &strings.Builder{}
		var (
			tuiRunnerCalled   bool
			echartsRunnerBody []byte
			gotContext        metricecharts.RangeReportContext
		)
		err := runMetricsRangeWithHooks(ctx, metricsRangeOptions{
			ClusterID:  "cluster-42",
			DataServer: true,
			Query:      "up",
			Start:      "1712123000",
			End:        "1712123600",
			Output:     "echarts",
		}, wantStdout, func(io.Writer) bool {
			return false
		}, func(io.Writer, []byte) error {
			tuiRunnerCalled = true
			return nil
		}, func(gotStdout io.Writer, rawResponse []byte, reportContext metricecharts.RangeReportContext) error {
			if gotStdout != wantStdout {
				t.Fatal("echarts runner received unexpected stdout writer")
			}
			gotContext = reportContext
			echartsRunnerBody = append([]byte(nil), rawResponse...)
			return nil
		})
		if err != nil {
			t.Fatalf("runMetricsRangeWithHooks() error = %v", err)
		}
		if tuiRunnerCalled {
			t.Fatal("TUI runner was called for explicit echarts output")
		}
		if got, want := len(client.requests), 1; got != want {
			t.Fatalf("request count = %d, want %d", got, want)
		}
		if got, want := client.requests[0].Path, "/clinic/api/v1/data/metrics"; got != want {
			t.Fatalf("path = %q, want %q", got, want)
		}
		if got, want := string(echartsRunnerBody), rawBody; got != want {
			t.Fatalf("echarts runner body = %q, want %q", got, want)
		}
		if got, want := gotContext.ClusterID, "cluster-42"; got != want {
			t.Fatalf("echarts context cluster id = %q, want %q", got, want)
		}
		if got, want := gotContext.ClusterName, "cluster-blue"; got != want {
			t.Fatalf("echarts context cluster name = %q, want %q", got, want)
		}
		if got, want := gotContext.DeployType, "dedicated"; got != want {
			t.Fatalf("echarts context deploy type = %q, want %q", got, want)
		}
		if got, want := gotContext.Query, "up"; got != want {
			t.Fatalf("echarts context query = %q, want %q", got, want)
		}
	})

	t.Run("defaults to echarts when output is omitted", func(t *testing.T) {
		t.Parallel()

		rawBody := `{"status":"success","data":{"result":[]}}`
		client := &recordingClinicClient{
			responses: []clinicResponse{
				{body: []byte(rawBody)},
			},
		}
		ctx := &RuntimeContext{
			Client: client,
			SelectedProfile: &config.ProfileRecord{
				ClusterID:           "cluster-42",
				ClusterName:         "cluster-blue",
				OrgID:               "org-1",
				ClusterDeployTypeV2: "dedicated",
			},
		}

		wantStdout := &strings.Builder{}
		var (
			tuiRunnerCalled bool
			gotRunnerBody   []byte
			gotContext      metricecharts.RangeReportContext
		)
		err := runMetricsRangeWithHooks(ctx, metricsRangeOptions{
			ClusterID:  "cluster-42",
			DataServer: true,
			Query:      "up",
			Start:      "1712123000",
			End:        "1712123600",
		}, wantStdout, func(io.Writer) bool {
			return false
		}, func(io.Writer, []byte) error {
			tuiRunnerCalled = true
			return nil
		}, func(gotStdout io.Writer, rawResponse []byte, reportContext metricecharts.RangeReportContext) error {
			if gotStdout != wantStdout {
				t.Fatal("echarts runner received unexpected stdout writer")
			}
			gotContext = reportContext
			gotRunnerBody = append([]byte(nil), rawResponse...)
			return nil
		})
		if err != nil {
			t.Fatalf("runMetricsRangeWithHooks() error = %v", err)
		}
		if tuiRunnerCalled {
			t.Fatal("TUI runner was called for default output")
		}
		if got, want := len(client.requests), 1; got != want {
			t.Fatalf("request count = %d, want %d", got, want)
		}
		if got, want := client.requests[0].Path, "/clinic/api/v1/data/metrics"; got != want {
			t.Fatalf("query_range path = %q, want %q", got, want)
		}
		if got, want := string(gotRunnerBody), rawBody; got != want {
			t.Fatalf("echarts runner body = %q, want %q", got, want)
		}
		if got, want := gotContext.ClusterID, "cluster-42"; got != want {
			t.Fatalf("echarts context cluster id = %q, want %q", got, want)
		}
		if got, want := gotContext.ClusterName, "cluster-blue"; got != want {
			t.Fatalf("echarts context cluster name = %q, want %q", got, want)
		}
		if got, want := gotContext.DeployType, "dedicated"; got != want {
			t.Fatalf("echarts context deploy type = %q, want %q", got, want)
		}
		if got, want := gotContext.Query, "up"; got != want {
			t.Fatalf("echarts context query = %q, want %q", got, want)
		}
	})

	t.Run("rejects tui output on non-tty without issuing requests", func(t *testing.T) {
		t.Parallel()

		client := &recordingClinicClient{}
		ctx := &RuntimeContext{
			Client: client,
			SelectedProfile: &config.ProfileRecord{
				ClusterID: "cluster-42",
				OrgID:     "org-1",
			},
		}

		var tuiRunnerCalled bool
		err := runMetricsRangeWithHooks(ctx, metricsRangeOptions{
			ClusterID:  "cluster-42",
			DataServer: true,
			Query:      "up",
			Start:      "1712123000",
			End:        "1712123600",
			Output:     "tui",
		}, &strings.Builder{}, func(io.Writer) bool {
			return false
		}, func(io.Writer, []byte) error {
			tuiRunnerCalled = true
			return nil
		}, func(io.Writer, []byte, metricecharts.RangeReportContext) error {
			t.Fatal("echarts runner was called for non-tty tui output")
			return nil
		})
		if err == nil {
			t.Fatal("runMetricsRangeWithHooks() error = nil, want tty validation error")
		}
		if got, want := err.Error(), "output tui requires a TTY"; got != want {
			t.Fatalf("error = %q, want %q", got, want)
		}
		if tuiRunnerCalled {
			t.Fatal("TUI runner was called for non-tty tui output")
		}
		if got, want := len(client.requests), 0; got != want {
			t.Fatalf("request count = %d, want %d", got, want)
		}
	})
}

func TestMetricsRangeParsesDatetimeWithoutTimezoneAsLocal(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"status":"success","data":{"result":[]}}`)}}}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := metricsCommandHandler(ctx, []string{
		"range",
		"--cluster-id", "cluster-42",
		"--query", "up",
		"--start", "2026-04-02 14:29:09",
		"--end", "2026-04-02 16:54:13",
		"--output", "json",
	}, &stdout); err != nil {
		t.Fatalf("metricsCommandHandler() error = %v", err)
	}

	expectedStart := strconv.FormatInt(time.Date(2026, time.April, 2, 14, 29, 9, 0, time.Local).Unix(), 10)
	expectedEnd := strconv.FormatInt(time.Date(2026, time.April, 2, 16, 54, 13, 0, time.Local).Unix(), 10)
	assertQueryValue(t, client.request.Query, "start", expectedStart)
	assertQueryValue(t, client.request.Query, "end", expectedEnd)
}

func TestMetricsRangeSupportsNowExpressions(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"status":"success","data":{"result":[]}}`)}}}
	ctx := &RuntimeContext{Client: client}

	before := time.Now().Unix()
	var stdout strings.Builder
	if err := metricsCommandHandler(ctx, []string{
		"range",
		"--cluster-id", "cluster-42",
		"--query", "up",
		"--start", "now-30m",
		"--end", "now",
		"--output", "json",
	}, &stdout); err != nil {
		t.Fatalf("metricsCommandHandler() error = %v", err)
	}
	after := time.Now().Unix()

	startRaw := client.request.Query.Get("start")
	endRaw := client.request.Query.Get("end")
	startUnix, err := strconv.ParseInt(startRaw, 10, 64)
	if err != nil {
		t.Fatalf("start query value = %q, want unix seconds: %v", startRaw, err)
	}
	endUnix, err := strconv.ParseInt(endRaw, 10, 64)
	if err != nil {
		t.Fatalf("end query value = %q, want unix seconds: %v", endRaw, err)
	}
	if got := endUnix - startUnix; got < 1800 || got > 1801 {
		t.Fatalf("range length = %d, want between 1800 and 1801 seconds", got)
	}
	if endUnix < before || endUnix > after {
		t.Fatalf("end query value = %d, want between %d and %d", endUnix, before, after)
	}
}

type metricsTestTTYWriter struct {
	bytes.Buffer
}

func (w *metricsTestTTYWriter) IsTerminal() bool {
	return true
}

func TestMetricsQueryParsesOptionalTimeWithoutTimezoneAsLocal(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"status":"success"}`)}}}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := metricsCommandHandler(ctx, []string{
		"query",
		"--cluster-id", "cluster-42",
		"--query", "up",
		"--time", "2026-04-03 10:04:43",
	}, &stdout); err != nil {
		t.Fatalf("metricsCommandHandler() error = %v", err)
	}

	expected := strconv.FormatInt(time.Date(2026, time.April, 3, 10, 4, 43, 0, time.Local).Unix(), 10)
	assertQueryValue(t, client.request.Query, "time", expected)
}

func TestMetricsSeriesParsesOptionalRangeWithoutTimezoneAsLocal(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"status":"success","data":[]}`)}}}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := metricsCommandHandler(ctx, []string{
		"series",
		"--cluster-id", "cluster-42",
		"--match", `{__name__="up"}`,
		"--start", "2026-04-02 14:29:09",
		"--end", "2026-04-02 16:54:13",
	}, &stdout); err != nil {
		t.Fatalf("metricsCommandHandler() error = %v", err)
	}

	expectedStart := strconv.FormatInt(time.Date(2026, time.April, 2, 14, 29, 9, 0, time.Local).Unix(), 10)
	expectedEnd := strconv.FormatInt(time.Date(2026, time.April, 2, 16, 54, 13, 0, time.Local).Unix(), 10)
	assertQueryValue(t, client.request.Query, "start", expectedStart)
	assertQueryValue(t, client.request.Query, "end", expectedEnd)
}

func TestMetricsSeriesRequiresMatchSelector(t *testing.T) {
	t.Parallel()

	ctx := &RuntimeContext{Client: &recordingClinicClient{}}
	testCases := []struct {
		name string
		args []string
		want string
	}{
		{
			name: "missing cluster id",
			args: []string{"series", "--match", `{__name__="up"}`},
			want: "missing required --cluster-id",
		},
		{
			name: "missing match",
			args: []string{"series", "--cluster-id", "cluster-42"},
			want: "missing required --match",
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			err := metricsCommandHandler(ctx, tc.args, &strings.Builder{})
			if err == nil {
				t.Fatal("metricsCommandHandler() error = nil, want validation error")
			}
			if got := err.Error(); got != tc.want {
				t.Fatalf("error = %q, want %q", got, tc.want)
			}
		})
	}
}

func TestMetricsSeriesUsesRepeatedMatchParams(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"status":"success","data":[]}`)}}}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := metricsCommandHandler(ctx, []string{
		"series",
		"--cluster-id", "cluster-42",
		"--match", `{__name__="up"}`,
		"--match", `{job="tidb"}`,
		"--start", "1712123000",
		"--end", "1712123600",
	}, &stdout); err != nil {
		t.Fatalf("metricsCommandHandler() error = %v", err)
	}

	if got, want := client.request.Method, http.MethodGet; got != want {
		t.Fatalf("method = %q, want %q", got, want)
	}
	if got, want := client.request.Path, "/data-proxy/metrics/cluster-42/api/v1/series"; got != want {
		t.Fatalf("path = %q, want %q", got, want)
	}
	matches := client.request.Query["match[]"]
	if got, want := len(matches), 2; got != want {
		t.Fatalf("match count = %d, want %d", got, want)
	}
	if got, want := matches[0], `{__name__="up"}`; got != want {
		t.Fatalf("first match = %q, want %q", got, want)
	}
	if got, want := matches[1], `{job="tidb"}`; got != want {
		t.Fatalf("second match = %q, want %q", got, want)
	}
	assertQueryValue(t, client.request.Query, "start", "1712123000")
	assertQueryValue(t, client.request.Query, "end", "1712123600")
	assertJSONEqual(t, stdout.String(), `{"status":"success","data":[]}`)
}

func TestMetricsPoolUsesPremiumParentID(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name     string
		args     []string
		wantPath string
		wantBody string
	}{
		{
			name: "query",
			args: []string{
				"query",
				"--cluster-id", "tenant-42",
				"--query", "up",
				"--pool",
			},
			wantPath: "/data-proxy/metrics/pool-9/api/v1/query",
			wantBody: `{"status":"success"}`,
		},
		{
			name: "range with short flag",
			args: []string{
				"range",
				"--cluster-id", "tenant-42",
				"--query", "up",
				"--start", "1712123000",
				"--end", "1712123600",
				"--output", "json",
				"-p",
			},
			wantPath: "/data-proxy/metrics/pool-9/api/v1/query_range",
			wantBody: `{"status":"success","data":{"result":[]}}`,
		},
		{
			name: "series",
			args: []string{
				"series",
				"--cluster-id", "tenant-42",
				"--match", `{__name__="up"}`,
				"--pool",
			},
			wantPath: "/data-proxy/metrics/pool-9/api/v1/series",
			wantBody: `{"status":"success","data":[]}`,
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			client := &recordingClinicClient{
				responses: []clinicResponse{
					{
						body: []byte(`{"items":[{"clusterID":"tenant-42","orgID":"org-1","clusterDeployTypeV2":"premium","parentID":"pool-9"}]}`),
					},
					{
						body: []byte(tc.wantBody),
					},
				},
			}
			ctx := &RuntimeContext{Client: client}

			var stdout strings.Builder
			if err := metricsCommandHandler(ctx, tc.args, &stdout); err != nil {
				t.Fatalf("metricsCommandHandler() error = %v", err)
			}

			if got, want := len(client.requests), 2; got != want {
				t.Fatalf("request count = %d, want %d", got, want)
			}
			if got, want := client.requests[0].Path, dashboardClustersPath; got != want {
				t.Fatalf("search path = %q, want %q", got, want)
			}
			assertQueryValue(t, client.requests[0].Query, "cluster_id", "tenant-42")
			assertQueryValue(t, client.requests[0].Query, "show_deleted", "true")
			if got, want := client.requests[1].Path, tc.wantPath; got != want {
				t.Fatalf("metrics path = %q, want %q", got, want)
			}
			assertJSONEqual(t, stdout.String(), tc.wantBody)
		})
	}
}

func TestMetricsPoolRejectsNonPremiumCluster(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{
				body: []byte(`{"items":[{"clusterID":"tenant-42","orgID":"org-1","clusterDeployTypeV2":"dedicated","parentID":"pool-9"}]}`),
			},
		},
	}
	ctx := &RuntimeContext{Client: client}

	err := metricsCommandHandler(ctx, []string{
		"query",
		"--cluster-id", "tenant-42",
		"--query", "up",
		"--pool",
	}, &strings.Builder{})
	if err == nil {
		t.Fatal("metricsCommandHandler() error = nil, want validation error")
	}
	if got, want := err.Error(), "--pool is only supported for premium clusters with parent_id: tenant-42 (deploy type: dedicated)"; got != want {
		t.Fatalf("error = %q, want %q", got, want)
	}
	if got, want := len(client.requests), 1; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
}

func TestMetricsPoolRequiresParentIDForPremiumCluster(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{
				body: []byte(`{"items":[{"clusterID":"tenant-42","orgID":"org-1","clusterDeployTypeV2":"premium"}]}`),
			},
		},
	}
	ctx := &RuntimeContext{Client: client}

	err := metricsCommandHandler(ctx, []string{
		"query",
		"--cluster-id", "tenant-42",
		"--query", "up",
		"--pool",
	}, &strings.Builder{})
	if err == nil {
		t.Fatal("metricsCommandHandler() error = nil, want validation error")
	}
	if got, want := err.Error(), "premium cluster has no parentID: tenant-42"; got != want {
		t.Fatalf("error = %q, want %q", got, want)
	}
	if got, want := len(client.requests), 1; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
}

func TestMetricsCommandsRetryGatewayTimeout(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name     string
		args     []string
		wantPath string
		wantBody string
	}{
		{
			name: "query",
			args: []string{
				"query",
				"--cluster-id", "cluster-42",
				"--query", "up",
			},
			wantPath: "/data-proxy/metrics/cluster-42/api/v1/query",
			wantBody: `{"status":"success"}`,
		},
		{
			name: "range",
			args: []string{
				"range",
				"--cluster-id", "cluster-42",
				"--query", "up",
				"--start", "1712123000",
				"--end", "1712123600",
				"--output", "json",
			},
			wantPath: "/data-proxy/metrics/cluster-42/api/v1/query_range",
			wantBody: `{"status":"success","data":{"result":[]}}`,
		},
		{
			name: "series",
			args: []string{
				"series",
				"--cluster-id", "cluster-42",
				"--match", `{__name__="up"}`,
			},
			wantPath: "/data-proxy/metrics/cluster-42/api/v1/series",
			wantBody: `{"status":"success","data":[]}`,
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			client := &recordingClinicClient{
				responses: []clinicResponse{
					{
						err: &clinic.HTTPError{
							StatusCode: http.StatusGatewayTimeout,
							Status:     "504 Gateway Timeout",
						},
					},
					{
						body: []byte(tc.wantBody),
					},
				},
			}
			ctx := &RuntimeContext{Client: client}

			var stdout strings.Builder
			err := metricsCommandHandler(ctx, tc.args, &stdout)
			if err != nil {
				t.Fatalf("metricsCommandHandler() error = %v", err)
			}
			if got, want := len(client.requests), 2; got != want {
				t.Fatalf("request count = %d, want %d", got, want)
			}
			if got, want := client.requests[0].Path, tc.wantPath; got != want {
				t.Fatalf("first request path = %q, want %q", got, want)
			}
			if got, want := client.requests[1].Path, tc.wantPath; got != want {
				t.Fatalf("second request path = %q, want %q", got, want)
			}
			assertJSONEqual(t, stdout.String(), tc.wantBody)
		})
	}
}

func TestMetricsQueryRetryGatewayTimeoutLimit(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{
				err: &clinic.HTTPError{
					StatusCode: http.StatusGatewayTimeout,
					Status:     "504 Gateway Timeout",
				},
			},
			{
				err: &clinic.HTTPError{
					StatusCode: http.StatusGatewayTimeout,
					Status:     "504 Gateway Timeout",
				},
			},
			{
				err: &clinic.HTTPError{
					StatusCode: http.StatusGatewayTimeout,
					Status:     "504 Gateway Timeout",
				},
			},
			{
				err: &clinic.HTTPError{
					StatusCode: http.StatusGatewayTimeout,
					Status:     "504 Gateway Timeout",
				},
			},
		},
	}
	ctx := &RuntimeContext{Client: client}

	err := metricsCommandHandler(ctx, []string{
		"query",
		"--cluster-id", "cluster-42",
		"--query", "up",
	}, &strings.Builder{})
	if err == nil {
		t.Fatal("metricsCommandHandler() error = nil, want gateway timeout error")
	}

	var httpErr *clinic.HTTPError
	if ok := errors.As(err, &httpErr); !ok {
		t.Fatalf("error type = %T, want *clinic.HTTPError", err)
	}
	if got, want := httpErr.StatusCode, http.StatusGatewayTimeout; got != want {
		t.Fatalf("status code = %d, want %d", got, want)
	}
	if got, want := len(client.requests), 4; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
}

func TestMetricsQueryDoesNotRetryNonGatewayTimeout(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{
				err: &clinic.HTTPError{
					StatusCode: http.StatusBadGateway,
					Status:     "502 Bad Gateway",
				},
			},
		},
	}
	ctx := &RuntimeContext{Client: client}

	err := metricsCommandHandler(ctx, []string{
		"query",
		"--cluster-id", "cluster-42",
		"--query", "up",
	}, &strings.Builder{})
	if err == nil {
		t.Fatal("metricsCommandHandler() error = nil, want non-504 error")
	}
	if got, want := len(client.requests), 1; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
}

func assertHeaderValue(t *testing.T, headers http.Header, key, want string) {
	t.Helper()
	values, ok := headers[key]
	if !ok {
		t.Fatalf("header[%q] missing", key)
	}
	if len(values) != 1 {
		t.Fatalf("header[%q] value count = %d, want 1", key, len(values))
	}
	if got := values[0]; got != want {
		t.Fatalf("header[%q] = %q, want %q", key, got, want)
	}
}
