package handlers

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	clinic_internal_api "github.com/pingcap-inc/clinic/clinic_data_service/pkg/clinic-internal-api"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/service"
)

type fakeTopRUService struct {
	result    *service.TopRUResult
	err       error
	orgID     uint64
	clusterID string
	operation service.TopRUOperation
	query     url.Values
}

func (f *fakeTopRUService) Query(
	_ context.Context,
	orgID uint64,
	clusterID string,
	operation service.TopRUOperation,
	query url.Values,
) (*service.TopRUResult, error) {
	f.orgID = orgID
	f.clusterID = clusterID
	f.operation = operation
	f.query = query
	return f.result, f.err
}

func topRUTestRouter(topRUService topRUQuerier) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	NewTopRUHandler(topRUService).RegisterRoutes(router)
	return router
}

func TestTopRUHandlerSummaryPreservesAllowlistedQueryAndRawJSON(t *testing.T) {
	querier := &fakeTopRUService{result: &service.TopRUResult{
		Body:         []byte(`{"status":"success","data":{"data":[],"step":60},"future":true}`),
		Provider:     "aws",
		Region:       "us-west-2",
		DeployTypeV2: "premium",
	}}
	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet,
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/summary?start=1&end=2&top=10&top=20&order_by=ru&user=&step=60",
		nil,
	)
	request.Header.Set("Authorization", "must-not-be-forwarded")
	request.Header.Set("Cookie", "must-not-be-forwarded")
	request.Header.Set("X-Provider", "forged")
	topRUTestRouter(querier).ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code, response.Body.String())
	require.JSONEq(t, `{"status":"success","data":{"data":[],"step":60},"future":true}`, response.Body.String())
	require.Equal(t, uint64(123), querier.orgID)
	require.Equal(t, "cluster-a", querier.clusterID)
	require.Equal(t, service.TopRUOperationSummary, querier.operation)
	require.Equal(t, []string{"10", "20"}, querier.query["top"])
	user, exists := querier.query["user"]
	require.True(t, exists)
	require.Equal(t, []string{""}, user)
}

func TestTopRUHandlerSummaryPlanPreservesAllowlistedQueryAndRawJSON(t *testing.T) {
	wantBody := []byte(`{"status":"success","data":{"data":[{"sql_digest":"sql-digest","plan_digest":"plan-a"},{"sql_digest":"sql-digest","plan_digest":"plan-b"}],"step":30},"future":true}`)
	querier := &fakeTopRUService{result: &service.TopRUResult{Body: wantBody}}
	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet,
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/summary-plan?start=1&end=2&top=10&digest=sql-digest&order_by=ru&user=&data_source=deltalake&step=30",
		nil,
	)
	topRUTestRouter(querier).ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code, response.Body.String())
	require.Equal(t, wantBody, response.Body.Bytes())
	require.Equal(t, uint64(123), querier.orgID)
	require.Equal(t, "cluster-a", querier.clusterID)
	require.Equal(t, service.TopRUOperationSummaryPlan, querier.operation)
	require.Equal(t, url.Values{
		"start":       {"1"},
		"end":         {"2"},
		"top":         {"10"},
		"digest":      {"sql-digest"},
		"order_by":    {"ru"},
		"user":        {""},
		"data_source": {"deltalake"},
		"step":        {"30"},
	}, querier.query)
}

func TestTopRUHandlerDistinguishesMissingEmptyAndNamedUser(t *testing.T) {
	for _, endpoint := range []string{"summary", "summary-plan"} {
		t.Run(endpoint, func(t *testing.T) {
			for _, tt := range []struct {
				name       string
				query      string
				wantExists bool
				wantValues []string
			}{
				{name: "missing", query: "start=1&end=2&top=10", wantExists: false},
				{name: "empty", query: "start=1&end=2&top=10&user=", wantExists: true, wantValues: []string{""}},
				{name: "named", query: "start=1&end=2&top=10&user=alice", wantExists: true, wantValues: []string{"alice"}},
			} {
				t.Run(tt.name, func(t *testing.T) {
					querier := &fakeTopRUService{result: &service.TopRUResult{Body: []byte(`{"status":"success","data":[]}`)}}
					response := httptest.NewRecorder()
					request := httptest.NewRequest(http.MethodGet,
						"/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/"+endpoint+"?"+tt.query, nil)
					topRUTestRouter(querier).ServeHTTP(response, request)
					require.Equal(t, http.StatusOK, response.Code)
					values, exists := querier.query["user"]
					require.Equal(t, tt.wantExists, exists)
					require.Equal(t, tt.wantValues, values)
				})
			}
		})
	}
}

func TestTopRUHandlerUserStatsUsesCanonicalOperation(t *testing.T) {
	querier := &fakeTopRUService{result: &service.TopRUResult{Body: []byte(`{"status":"success","data":[]}`)}}
	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet,
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/user-stats?start=1&end=2&top=20", nil)
	topRUTestRouter(querier).ServeHTTP(response, request)
	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, service.TopRUOperationUserStats, querier.operation)
	require.Equal(t, "20", querier.query.Get("top"))
}

func TestTopRUHandlerRejectsCallerRoutingAndUnknownQueryParameters(t *testing.T) {
	for _, tt := range []struct {
		name      string
		path      string
		parameter string
	}{
		{
			name:      "summary rejects caller routing",
			path:      "/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/summary?start=1&end=2&top=10&provider=forged",
			parameter: "provider",
		},
		{
			name:      "summary rejects plan digest",
			path:      "/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/summary?start=1&end=2&top=10&digest=sql-digest",
			parameter: "digest",
		},
		{
			name:      "summary plan rejects caller routing",
			path:      "/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/summary-plan?start=1&end=2&top=10&provider=forged",
			parameter: "provider",
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			querier := &fakeTopRUService{result: &service.TopRUResult{Body: []byte(`{}`)}}
			response := httptest.NewRecorder()
			request := httptest.NewRequest(http.MethodGet, tt.path, nil)
			topRUTestRouter(querier).ServeHTTP(response, request)
			require.Equal(t, http.StatusBadRequest, response.Code)
			require.JSONEq(t,
				`{"code":"InvalidArgument","message":"unsupported Top RU query parameter \"`+tt.parameter+`\""}`,
				response.Body.String(),
			)
			require.Empty(t, querier.clusterID)
		})
	}
}

func TestTopRUHandlerRejectsMalformedQueryEscaping(t *testing.T) {
	querier := &fakeTopRUService{result: &service.TopRUResult{Body: []byte(`{}`)}}
	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet,
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/summary?start=%ZZ&end=2&top=10", nil)
	topRUTestRouter(querier).ServeHTTP(response, request)
	require.Equal(t, http.StatusBadRequest, response.Code)
	require.Contains(t, response.Body.String(), "query contains invalid escaping")
	require.Empty(t, querier.clusterID)
}

func TestTopRUHandlerMapsTargetAndUpstreamErrors(t *testing.T) {
	tests := []struct {
		name       string
		err        error
		wantStatus int
		wantCode   string
	}{
		{name: "wrong org", err: &clinic_internal_api.UpstreamError{StatusCode: http.StatusNotFound}, wantStatus: http.StatusNotFound, wantCode: "NotFound"},
		{name: "target unavailable", err: &clinic_internal_api.UpstreamError{StatusCode: http.StatusServiceUnavailable}, wantStatus: http.StatusServiceUnavailable, wantCode: "Unavailable"},
		{name: "invalid upstream query", err: &service.TopRUUpstreamError{StatusCode: http.StatusBadRequest}, wantStatus: http.StatusBadRequest, wantCode: "InvalidArgument"},
		{name: "rate limited", err: &service.TopRUUpstreamError{StatusCode: http.StatusTooManyRequests}, wantStatus: http.StatusTooManyRequests, wantCode: "ResourceExhausted"},
		{name: "upstream failure", err: &service.TopRUUpstreamError{StatusCode: http.StatusInternalServerError}, wantStatus: http.StatusBadGateway, wantCode: "BadGateway"},
		{name: "invalid target", err: service.ErrInvalidTopRUTarget, wantStatus: http.StatusBadGateway, wantCode: "BadGateway"},
		{name: "canceled", err: context.Canceled, wantStatus: clientClosedStatus, wantCode: "Canceled"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			querier := &fakeTopRUService{err: tt.err}
			response := httptest.NewRecorder()
			request := httptest.NewRequest(http.MethodGet,
				"/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/summary?start=1&end=2&top=10", nil)
			topRUTestRouter(querier).ServeHTTP(response, request)
			require.Equal(t, tt.wantStatus, response.Code, response.Body.String())
			require.Contains(t, response.Body.String(), `"code":"`+tt.wantCode+`"`)
			require.NotContains(t, response.Body.String(), tt.err.Error())
		})
	}
}

func TestTopRUHandlerRejectsInvalidPathIDs(t *testing.T) {
	for _, path := range []string{
		"/data-proxy/api/v1/orgs/not-a-number/clusters/cluster-a/top-ru/summary",
		"/data-proxy/api/v1/orgs/123/clusters/%0A/top-ru/summary",
	} {
		response := httptest.NewRecorder()
		request := httptest.NewRequest(http.MethodGet, path+"?start=1&end=2&top=10", nil)
		topRUTestRouter(&fakeTopRUService{err: errors.New("must not be called")}).ServeHTTP(response, request)
		require.Equal(t, http.StatusBadRequest, response.Code, path)
	}
}
