package metricecharts

import (
	"regexp"
	"strings"
	"testing"
	"time"

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

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

	response := &metricstui.QueryRangeResponse{
		Series: []metricstui.QueryRangeSeries{
			{
				Label: "series-a",
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(100, 0).UTC(), Value: 1},
					{Timestamp: time.Unix(120, 0).UTC(), Value: 2},
				},
			},
			{
				Label: "series-b",
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(110, 0).UTC(), Value: 3},
					{Timestamp: time.Unix(120, 0).UTC(), Value: 4},
				},
			},
		},
	}

	xAxis, seriesData := buildAlignedSeriesData(response)
	if got, want := len(xAxis), 3; got != want {
		t.Fatalf("xAxis length = %d, want %d", got, want)
	}
	if got, want := xAxis[0], time.Unix(100, 0).In(time.Local).Format(axisTimeLabelFormat); got != want {
		t.Fatalf("xAxis[0] = %q, want %q", got, want)
	}
	if got, want := xAxis[1], time.Unix(110, 0).In(time.Local).Format(axisTimeLabelFormat); got != want {
		t.Fatalf("xAxis[1] = %q, want %q", got, want)
	}
	if got, want := xAxis[2], time.Unix(120, 0).In(time.Local).Format(axisTimeLabelFormat); got != want {
		t.Fatalf("xAxis[2] = %q, want %q", got, want)
	}

	if got, want := len(seriesData), 2; got != want {
		t.Fatalf("series length = %d, want %d", got, want)
	}
	if got, want := seriesData[0].Values[0].Value, float64(1); got != want {
		t.Fatalf("series-a[0] = %v, want %v", got, want)
	}
	if got := seriesData[0].Values[1].Value; got != nil {
		t.Fatalf("series-a[1] = %v, want nil", got)
	}
	if got, want := seriesData[0].Values[2].Value, float64(2); got != want {
		t.Fatalf("series-a[2] = %v, want %v", got, want)
	}

	if got := seriesData[1].Values[0].Value; got != nil {
		t.Fatalf("series-b[0] = %v, want nil", got)
	}
	if got, want := seriesData[1].Values[1].Value, float64(3); got != want {
		t.Fatalf("series-b[1] = %v, want %v", got, want)
	}
	if got, want := seriesData[1].Values[2].Value, float64(4); got != want {
		t.Fatalf("series-b[2] = %v, want %v", got, want)
	}
}

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

	samples := make([]metricstui.QueryRangeSample, 0, 12)
	for i := 0; i < 12; i++ {
		samples = append(samples, metricstui.QueryRangeSample{
			Timestamp: time.Unix(int64(1712123000+i*10), 0).UTC(),
			Value:     float64(i + 1),
		})
	}

	response := &metricstui.QueryRangeResponse{
		Series: []metricstui.QueryRangeSeries{
			{
				Label:   "series-a",
				Samples: samples,
			},
		},
	}

	xAxis, _ := buildAlignedSeriesData(response)
	if got, want := len(xAxis), 12; got != want {
		t.Fatalf("xAxis length = %d, want %d", got, want)
	}

	if got, want := xAxis[0], time.Unix(1712123000, 0).In(time.Local).Format(axisTimeLabelFormat); got != want {
		t.Fatalf("xAxis[0] = %q, want %q", got, want)
	}
	if got, want := xAxis[1], time.Unix(1712123010, 0).In(time.Local).Format(axisTimeLabelFormat); got != want {
		t.Fatalf("xAxis[1] = %q, want %q", got, want)
	}
	if got, want := xAxis[11], time.Unix(1712123110, 0).In(time.Local).Format(axisTimeLabelFormat); got != want {
		t.Fatalf("xAxis[11] = %q, want %q", got, want)
	}
}

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

	response := &metricstui.QueryRangeResponse{
		Series: []metricstui.QueryRangeSeries{
			{
				Label: "cpu_usage{cluster=\"c1\",instance=\"a\",job=\"api\"}",
				Metric: map[string]string{
					"__name__": "cpu_usage",
					"cluster":  "c1",
					"instance": "a",
					"job":      "api",
				},
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(1712123000, 0).UTC(), Value: 1},
				},
			},
			{
				Label: "cpu_usage{cluster=\"c1\",instance=\"b\",job=\"api\"}",
				Metric: map[string]string{
					"__name__": "cpu_usage",
					"cluster":  "c1",
					"instance": "b",
					"job":      "api",
				},
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(1712123000, 0).UTC(), Value: 2},
				},
			},
		},
	}

	_, seriesData := buildAlignedSeriesData(response)
	if got, want := len(seriesData), 2; got != want {
		t.Fatalf("series length = %d, want %d", got, want)
	}
	if got, want := seriesData[0].Label, "a"; got != want {
		t.Fatalf("seriesData[0].Label = %q, want %q", got, want)
	}
	if got, want := seriesData[1].Label, "b"; got != want {
		t.Fatalf("seriesData[1].Label = %q, want %q", got, want)
	}
}

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

	response := &metricstui.QueryRangeResponse{
		Series: []metricstui.QueryRangeSeries{
			{
				Label: "cpu_usage{cluster=\"c1\",job=\"api\"}",
				Metric: map[string]string{
					"__name__": "cpu_usage",
					"cluster":  "c1",
					"job":      "api",
				},
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(1712123000, 0).UTC(), Value: 1},
				},
			},
			{
				Label: "cpu_usage{cluster=\"c1\",job=\"api\"}",
				Metric: map[string]string{
					"__name__": "cpu_usage",
					"cluster":  "c1",
					"job":      "api",
				},
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(1712123000, 0).UTC(), Value: 2},
				},
			},
		},
	}

	_, seriesData := buildAlignedSeriesData(response)
	if got, want := len(seriesData), 2; got != want {
		t.Fatalf("series length = %d, want %d", got, want)
	}
	if got, want := seriesData[0].Label, ""; got != want {
		t.Fatalf("seriesData[0].Label = %q, want %q", got, want)
	}
	if got, want := seriesData[1].Label, ""; got != want {
		t.Fatalf("seriesData[1].Label = %q, want %q", got, want)
	}
}

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

	response := &metricstui.QueryRangeResponse{
		Series: []metricstui.QueryRangeSeries{
			{
				Label: `cpu_usage{instance="a",pod="pod-a",pod_name="pod-name-a"}`,
				Metric: map[string]string{
					"__name__": "cpu_usage",
					"instance": "a",
					"pod":      "pod-a",
					"pod_name": "pod-name-a",
				},
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(1712123000, 0).UTC(), Value: 1},
				},
			},
			{
				Label: `cpu_usage{instance="b",pod="pod-b",pod_name="pod-name-b"}`,
				Metric: map[string]string{
					"__name__": "cpu_usage",
					"instance": "b",
					"pod":      "pod-b",
					"pod_name": "pod-name-b",
				},
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(1712123000, 0).UTC(), Value: 2},
				},
			},
		},
	}

	_, seriesData := buildAlignedSeriesData(response)
	if got, want := len(seriesData), 2; got != want {
		t.Fatalf("series length = %d, want %d", got, want)
	}
	if got, want := seriesData[0].Label, "a"; got != want {
		t.Fatalf("seriesData[0].Label = %q, want %q", got, want)
	}
	if got, want := seriesData[1].Label, "b"; got != want {
		t.Fatalf("seriesData[1].Label = %q, want %q", got, want)
	}
}

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

	response := &metricstui.QueryRangeResponse{
		Series: []metricstui.QueryRangeSeries{
			{
				Label: `cpu_usage{instance="a",job="api"}`,
				Metric: map[string]string{
					"__name__": "cpu_usage",
					"instance": "a",
					"job":      "api",
				},
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(1712123000, 0).UTC(), Value: 1},
					{Timestamp: time.Unix(1712123060, 0).UTC(), Value: 2},
				},
			},
			{
				Label: `cpu_usage{instance="b",job="api"}`,
				Metric: map[string]string{
					"__name__": "cpu_usage",
					"instance": "b",
					"job":      "api",
				},
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(1712123000, 0).UTC(), Value: 3},
					{Timestamp: time.Unix(1712123060, 0).UTC(), Value: 4},
				},
			},
		},
	}

	loc := time.FixedZone("UTC+8", 8*60*60)
	html, err := renderRangeChartHTML(response, RangeReportContext{
		ClusterID:    "cluster-42",
		ClusterName:  "cluster-blue",
		DeployType:   "premium",
		Query:        "up",
		TimeLocation: loc,
	})
	if err != nil {
		t.Fatalf("renderRangeChartHTML() error = %v", err)
	}

	content := string(html)
	assertNotContains(t, content, "clinic metrics range")
	assertContains(t, content, "cluster id: cluster-42")
	assertContains(t, content, "cluster name: cluster-blue")
	assertContains(t, content, "deploy type: premium")
	assertContains(t, content, "query: up")
	assertContains(t, content, "range: 2024-04-03 13:43:20 UTC+8 -> 2024-04-03 13:44:20 UTC+8")
	assertContains(t, content, "style=\"width:96vw;height:68vh;\"")
	assertContains(t, content, "xAxis")
	assertContains(t, content, "yAxis")
	assertContains(t, content, `"name":"a"`)
	assertContains(t, content, `"name":"b"`)
	assertNotContains(t, content, "cpu_usage")
	assertNotContains(t, content, `job=\"api\"`)
	assertContains(t, content, "\"top\":\"90px\"")
	assertContains(t, content, "\"containLabel\":true")
	assertContains(t, content, "legend.pageIconSize = 0")
	assertContains(t, content, "legendselectchanged")
	assertContains(t, content, "getZr && chart.getZr()")
	assertContains(t, content, "zr.on(\"mousedown\"")
	assertContains(t, content, "lastPointerAdditiveSelection")
	assertContains(t, content, "focusedSeriesName")
	assertContains(t, content, "focusedSeriesName === params.name")
	assertContains(t, content, "pointerEvent.metaKey")
	assertContains(t, content, "pointerEvent.ctrlKey")
	assertContains(t, content, "metaKey")
	assertContains(t, content, "ctrlKey")
	assertNotContains(t, content, "go-echarts.github.io")
	assertNotContains(t, content, "<script src=\"")

	optionBlock := extractOptionBlock(t, content)
	assertContains(t, optionBlock, "\"left\":\"2%\"")
	assertContains(t, optionBlock, "\"right\":\"2%\"")
	assertContains(t, optionBlock, "\"right\":\"20%\"")
	assertContains(t, optionBlock, "\"orient\":\"vertical\"")
	assertContains(t, optionBlock, "\"right\":\"1%\"")
	assertContains(t, optionBlock, "\"selectedMode\":\"multiple\"")
	assertContains(t, optionBlock, "\"position\":function")
	assertContains(t, optionBlock, "\"animation\":false")
	assertContains(t, optionBlock, "\"formatter\":function")
	assertContains(t, optionBlock, "index % 1 === 0")
	assertContains(t, optionBlock, "\"showMinLabel\":true")
	assertContains(t, optionBlock, "\"showMaxLabel\":true")
	assertNotContains(t, optionBlock, "\"dataZoom\"")
}

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

	response := &metricstui.QueryRangeResponse{
		Series: []metricstui.QueryRangeSeries{
			{
				Label: `cpu_usage{instance="a",job="api"}`,
				Metric: map[string]string{
					"__name__": "cpu_usage",
					"instance": "a",
					"job":      "api",
				},
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(1712123000, 0).UTC(), Value: 1},
					{Timestamp: time.Unix(1712123060, 0).UTC(), Value: 2},
				},
			},
		},
	}

	loc := time.FixedZone("UTC+8", 8*60*60)
	html, err := renderRangeChartHTML(response, RangeReportContext{
		ClusterID:    "cluster-42",
		ClusterName:  "cluster-blue",
		DeployType:   "premium",
		Query:        "up",
		TimeLocation: loc,
	})
	if err != nil {
		t.Fatalf("renderRangeChartHTML() error = %v", err)
	}

	content := string(html)
	assertContains(t, content, "id=\"clinic-report-metadata\"")
	assertContains(t, content, "user-select: text")
	assertContains(t, content, "font-size: 22px")
	assertContains(t, content, "font-size: 16px")
	assertContains(t, content, "cluster id: cluster-42")
	assertContains(t, content, "query: up")
	assertContains(t, content, "range: 2024-04-03 13:43:20 UTC+8 -> 2024-04-03 13:44:20 UTC+8")
}

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

	samples := make([]metricstui.QueryRangeSample, 0, 30)
	for i := 0; i < 30; i++ {
		samples = append(samples, metricstui.QueryRangeSample{
			Timestamp: time.Unix(int64(1712123000+i*60), 0).UTC(),
			Value:     float64(i + 1),
		})
	}
	response := &metricstui.QueryRangeResponse{
		Series: []metricstui.QueryRangeSeries{
			{
				Label:   "series-a",
				Samples: samples,
			},
		},
	}

	html, err := renderRangeChartHTML(response, RangeReportContext{})
	if err != nil {
		t.Fatalf("renderRangeChartHTML() error = %v", err)
	}

	optionBlock := extractOptionBlock(t, string(html))
	assertContains(t, optionBlock, "\"formatter\":function")
	assertContains(t, optionBlock, "index % 2 === 0")
}

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

	response := &metricstui.QueryRangeResponse{
		Series: []metricstui.QueryRangeSeries{
			{
				Label: "series-a",
				Samples: []metricstui.QueryRangeSample{
					{Timestamp: time.Unix(1712121090, 0).UTC(), Value: 1},
					{Timestamp: time.Unix(1712122155, 0).UTC(), Value: 2},
					{Timestamp: time.Unix(1712122890, 0).UTC(), Value: 3},
				},
			},
		},
	}

	xAxis, _ := buildAlignedSeriesData(response)
	if got := len(xAxis); got < 7 {
		t.Fatalf("xAxis length = %d, want at least 7 labels for sparse long range", got)
	}

	startLabel := time.Unix(1712121090, 0).In(time.Local).Format(axisTimeLabelFormat)
	endLabel := time.Unix(1712122890, 0).In(time.Local).Format(axisTimeLabelFormat)
	if got := xAxis[0]; got != startLabel {
		t.Fatalf("xAxis[0] = %q, want %q", got, startLabel)
	}
	if got := xAxis[len(xAxis)-1]; got != endLabel {
		t.Fatalf("xAxis[last] = %q, want %q", got, endLabel)
	}

	html, err := renderRangeChartHTML(response, RangeReportContext{})
	if err != nil {
		t.Fatalf("renderRangeChartHTML() error = %v", err)
	}
	optionBlock := extractOptionBlock(t, string(html))
	assertContains(t, optionBlock, "\"formatter\":function")
	assertContains(t, optionBlock, "index % 1 === 0")
}

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

	got := buildInlineJSScriptTag(`console.log("</script>")`)
	assertContains(t, got, "<\\/script>")
	assertNotContains(t, got, `console.log("</script>")`)
}

func assertContains(t *testing.T, content, want string) {
	t.Helper()
	if !strings.Contains(content, want) {
		t.Fatalf("content missing %q", want)
	}
}

func assertNotContains(t *testing.T, content, needle string) {
	t.Helper()
	if strings.Contains(content, needle) {
		t.Fatalf("content unexpectedly contains %q", needle)
	}
}

func extractOptionBlock(t *testing.T, content string) string {
	t.Helper()
	re := regexp.MustCompile(`(?s)let option_[^=]+=\s*(\{.*?\})\s*goecharts_[^=]+\.setOption`)
	matches := re.FindStringSubmatch(content)
	if len(matches) < 2 {
		t.Fatal("unable to locate chart option block in rendered HTML")
	}
	return matches[1]
}
