package handlers

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"sync/atomic"
	"testing"

	"github.com/gin-gonic/gin"
	clinic_internal_api "github.com/pingcap-inc/clinic/clinic_data_service/pkg/clinic-internal-api"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/config"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/dto"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/service"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestMetricsQueryHandler_PostFormInstantAzure(t *testing.T) {
	gin.SetMode(gin.TestMode)

	var gotPath, gotQuery, gotTime string
	upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotPath = r.URL.Path
		gotQuery = r.URL.Query().Get("query")
		gotTime = r.URL.Query().Get("time")

		w.Header().Set("Content-Type", "application/json")
		require.NoError(t, json.NewEncoder(w).Encode(dto.PrometheusResponse{
			Status: "success",
			Data: map[string]any{
				"resultType": "vector",
				"result":     []any{},
			},
		}))
	}))
	t.Cleanup(upstreamServer.Close)

	cluster := clinic_internal_api.ClusterDataURLs{
		ClusterID:         "azure-cluster",
		Provider:          "azure",
		Region:            "southeastasia",
		DeployTypeV2:      "dedicated",
		ClusterMetricsURL: upstreamServer.URL + "/api/v1/",
	}
	clinicServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/clinic/api/v1/internal/clusters/azure-cluster/cluster-dataurls", r.URL.Path)
		require.Equal(t, http.MethodGet, r.Method)
		w.Header().Set("Content-Type", "application/json")
		require.NoError(t, json.NewEncoder(w).Encode(cluster))
	}))
	t.Cleanup(clinicServer.Close)

	cfg := config.DefaultConfig()
	cfg.ClinicAPIBase = clinicServer.URL
	svc := service.NewMetricsQueryService(cfg)

	router := gin.New()
	NewMetricsQueryHandler(svc).RegisterRoutes(router)

	form := url.Values{}
	form.Set("query", "sum(tidb_server_query_total)")
	form.Set("time", "2026-03-06T22:00:00Z")
	req := httptest.NewRequest(http.MethodPost, "/data-proxy/metrics/azure-cluster/api/v1/query", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)

	require.Equal(t, http.StatusOK, recorder.Code)
	assert.Equal(t, "/api/v1/query", gotPath)
	assert.Equal(t, "sum(tidb_server_query_total)", gotQuery)
	assert.Equal(t, "1772834400", gotTime)
	assert.NotContains(t, recorder.Body.String(), "provider")
	assert.NotContains(t, recorder.Body.String(), "region")
}

func TestMetricsQueryHandlerReturnsSafeUpstream422(t *testing.T) {
	upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusUnprocessableEntity)
		_, _ = w.Write([]byte(`{"status":"error","errorType":"422","error":"vmstorage 10.24.2.3 cannot select more than -search.maxSamplesPerQuery=1000000000 samples; see http://internal.example/path"}`))
	}))
	t.Cleanup(upstreamServer.Close)

	cluster := clinic_internal_api.ClusterDataURLs{
		ClusterID:         "cluster-a",
		Provider:          "gcp",
		Region:            "asia-northeast1",
		DeployTypeV2:      "dedicated",
		ClusterMetricsURL: upstreamServer.URL + "/api/v1/",
	}
	clinicServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		require.NoError(t, json.NewEncoder(w).Encode(cluster))
	}))
	t.Cleanup(clinicServer.Close)

	cfg := config.DefaultConfig()
	cfg.ClinicAPIBase = clinicServer.URL
	router := gin.New()
	NewMetricsQueryHandler(service.NewMetricsQueryService(cfg)).RegisterRoutes(router)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet,
		"/data-proxy/metrics/cluster-a/api/v1/query?query=up", nil))

	require.Equal(t, http.StatusUnprocessableEntity, recorder.Code)
	require.JSONEq(t, `{
		"status":"error",
		"errorType":"execution",
		"error":"cannot select more than -search.maxSamplesPerQuery=1000000000 samples; reduce the query range or add more specific label filters",
		"code":"MAX_SAMPLES_PER_QUERY",
		"details":{"upstreamStatus":422,"upstreamErrorType":"422","retryable":false}
	}`, recorder.Body.String())
	assert.NotContains(t, recorder.Body.String(), "vmstorage")
	assert.NotContains(t, recorder.Body.String(), "10.24.2.3")
	assert.NotContains(t, recorder.Body.String(), "internal.example")
}

func TestCanonicalMetricsPreservesSafeClassified422(t *testing.T) {
	resp := &dto.PrometheusResponse{
		Status:         "error",
		ErrorType:      "execution",
		Error:          "safe actionable error",
		Code:           "METRICS_UPSTREAM_UNPROCESSABLE",
		Details:        &dto.MetricsErrorDetails{UpstreamStatus: 422, UpstreamErrorType: "execution", Retryable: false},
		UpstreamStatus: http.StatusUnprocessableEntity,
	}

	sanitizeCanonicalMetricsResponse(resp)

	assert.Equal(t, "safe actionable error", resp.Error)
	assert.Equal(t, http.StatusUnprocessableEntity, metricsHTTPStatusCode(resp, true))
	assert.Equal(t, "PROMETHEUS_UPSTREAM_UNPROCESSABLE", errCodeFromPrometheusResponse(resp))
}

func TestMetricsQueryHandler_StarterNoMetricLabelSeriesReturnsHTTP200(t *testing.T) {
	gin.SetMode(gin.TestMode)

	var queryCalled atomic.Bool
	upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v1/series":
			require.Equal(t, `tidb_server_connections{cluster_id="starter-cluster"}`, r.URL.Query().Get("match[]"))
			w.Header().Set("Content-Type", "application/json")
			require.NoError(t, json.NewEncoder(w).Encode(dto.PrometheusResponse{
				Status: "success",
				Data:   []map[string]string{},
			}))
		case "/api/v1/query":
			queryCalled.Store(true)
			w.Header().Set("Content-Type", "application/json")
			require.NoError(t, json.NewEncoder(w).Encode(dto.PrometheusResponse{
				Status: "success",
				Data: map[string]any{
					"resultType": "vector",
					"result":     []any{},
				},
			}))
		default:
			require.Failf(t, "unexpected upstream path", "path=%s", r.URL.Path)
		}
	}))
	t.Cleanup(upstreamServer.Close)

	cluster := clinic_internal_api.ClusterDataURLs{
		ClusterID:         "starter-cluster",
		Provider:          "aws",
		Region:            "us-west-2",
		DeployTypeV2:      "starter",
		ClusterMetricsURL: upstreamServer.URL + "/api/v1/",
	}
	clinicServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/clinic/api/v1/internal/clusters/starter-cluster/cluster-dataurls", r.URL.Path)
		require.Equal(t, http.MethodGet, r.Method)
		w.Header().Set("Content-Type", "application/json")
		require.NoError(t, json.NewEncoder(w).Encode(cluster))
	}))
	t.Cleanup(clinicServer.Close)

	cfg := config.DefaultConfig()
	cfg.ClinicAPIBase = clinicServer.URL
	svc := service.NewMetricsQueryService(cfg)

	router := gin.New()
	NewMetricsQueryHandler(svc).RegisterRoutes(router)

	req := httptest.NewRequest(http.MethodGet, "/data-proxy/metrics/starter-cluster/api/v1/query?query=sum(tidb_server_query_total)", nil)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)

	require.Equal(t, http.StatusOK, recorder.Code)
	assert.False(t, queryCalled.Load())

	var resp dto.PrometheusResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
	require.Equal(t, "success", resp.Status)
	data, ok := resp.Data.(map[string]any)
	require.True(t, ok)
	assert.Equal(t, "vector", data["resultType"])
	assert.Equal(t, []any{}, data["result"])
	assert.NotContains(t, recorder.Body.String(), "provider")
	assert.NotContains(t, recorder.Body.String(), "region")
}
