package handlers

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

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"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 fakeBYOCSlowQueryQuerier struct {
	result    *service.BYOCSlowQueryResult
	err       error
	calls     int
	orgID     uint64
	clusterID string
	operation service.BYOCSlowQueryOperation
	query     url.Values
}

func (f *fakeBYOCSlowQueryQuerier) Query(
	_ context.Context,
	orgID uint64,
	clusterID string,
	operation service.BYOCSlowQueryOperation,
	query url.Values,
) (*service.BYOCSlowQueryResult, error) {
	f.calls++
	f.orgID = orgID
	f.clusterID = clusterID
	f.operation = operation
	f.query = cloneURLValues(query)
	return f.result, f.err
}

func TestRegisterBYOCSlowQueryRoutes(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	NewBYOCSlowQueryHandler(&fakeBYOCSlowQueryQuerier{}).RegisterRoutes(router)

	want := map[string]bool{
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/slow-queries":                  false,
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/slow-queries/available-fields": false,
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/slow-queries/detail":           false,
	}
	for _, route := range router.Routes() {
		key := route.Method + " " + route.Path
		if _, ok := want[key]; ok {
			want[key] = true
		}
	}
	for route, found := range want {
		assert.Truef(t, found, "route %s was not registered", route)
	}
}

func TestBYOCSlowQueryAvailableFieldsPreservesResponse(t *testing.T) {
	fake := &fakeBYOCSlowQueryQuerier{result: &service.BYOCSlowQueryResult{
		Body:         []byte(`["query","timestamp"]`),
		Provider:     "aws",
		Region:       "us-east-1",
		DeployTypeV2: "byoc",
	}}
	response := performBYOCSlowQueryRequest(t, fake, http.MethodGet,
		"/data-proxy/api/v1/orgs/123/clusters/logical/slow-queries/available-fields", "", nil)

	require.Equal(t, http.StatusOK, response.Code)
	assert.JSONEq(t, `["query","timestamp"]`, response.Body.String())
	assert.Equal(t, 1, fake.calls)
	assert.Equal(t, uint64(123), fake.orgID)
	assert.Equal(t, "logical", fake.clusterID)
	assert.Equal(t, service.BYOCSlowQueryOperationAvailableFields, fake.operation)
	assert.Empty(t, fake.query)
}

func TestBYOCSlowQueryListValidatesAndNormalizesQuery(t *testing.T) {
	fake := successfulBYOCSlowQueryQuerier(`[{"digest":"abc"}]`)
	query := url.Values{}
	query.Set("begin_time", "100")
	query.Set("end_time", "200")
	query.Set("fields", " query ,query_time ")
	query.Add("db", "app")
	query.Add("db", "analytics")
	query.Set("limit", "20")
	query.Set("text", "select")
	query.Set("orderBy", "QUERY_TIME")
	query.Set("desc", "true")
	query.Add("plans", "plan-a")
	query.Add("plans", "plan-b")
	query.Set("digest", "digest-a")
	query.Set("show_internal", "false")

	response := performBYOCSlowQueryRequest(t, fake, http.MethodGet,
		"/data-proxy/api/v1/orgs/123/clusters/physical/slow-queries?"+query.Encode(), "", nil)

	require.Equal(t, http.StatusOK, response.Code, response.Body.String())
	assert.JSONEq(t, `[{"digest":"abc"}]`, response.Body.String())
	assert.Equal(t, service.BYOCSlowQueryOperationList, fake.operation)
	assert.Equal(t, "physical", fake.clusterID)
	assert.Equal(t, "query,query_time", fake.query.Get("fields"))
	assert.Equal(t, "20", fake.query.Get("limit"))
	assert.Equal(t, "query_time", fake.query.Get("orderBy"))
	assert.Equal(t, []string{"app", "analytics"}, fake.query["db"])
	assert.Equal(t, []string{"plan-a", "plan-b"}, fake.query["plans"])
}

func TestBYOCSlowQueryListAppliesSafeDefaults(t *testing.T) {
	fake := successfulBYOCSlowQueryQuerier(`[]`)
	response := performBYOCSlowQueryRequest(t, fake, http.MethodGet,
		"/data-proxy/api/v1/orgs/123/clusters/logical/slow-queries?begin_time=100&end_time=200", "", nil)

	require.Equal(t, http.StatusOK, response.Code, response.Body.String())
	assert.Equal(t, defaultBYOCSlowQueryFields, fake.query.Get("fields"))
	assert.Equal(t, "100", fake.query.Get("limit"))
	assert.Equal(t, "timestamp", fake.query.Get("orderBy"))
}

func TestBYOCSlowQueryDetailForwardsOnlyCompositeKey(t *testing.T) {
	fake := successfulBYOCSlowQueryQuerier(`{"digest":"abc","query_time":1.25}`)
	query := url.Values{}
	query.Set("digest", "abc")
	query.Set("timestamp", "1661930034.022015")
	query.Set("connect_id", "18446744073709551615")
	query.Set("show_internal", "true")

	response := performBYOCSlowQueryRequest(t, fake, http.MethodGet,
		"/data-proxy/api/v1/orgs/123/clusters/physical/slow-queries/detail?"+query.Encode(), "", nil)

	require.Equal(t, http.StatusOK, response.Code, response.Body.String())
	assert.JSONEq(t, `{"digest":"abc","query_time":1.25}`, response.Body.String())
	assert.Equal(t, service.BYOCSlowQueryOperationDetail, fake.operation)
	assert.Equal(t, "physical", fake.clusterID)
	assert.Equal(t, query, fake.query)
}

func TestBYOCSlowQueryRejectsInvalidRequestsBeforeService(t *testing.T) {
	tooManyDBs := url.Values{"begin_time": {"1"}, "end_time": {"2"}}
	for i := 0; i <= maxBYOCSlowQueryCollectionValues; i++ {
		tooManyDBs.Add("db", fmt.Sprintf("db-%d", i))
	}
	longQuery := "begin_time=1&end_time=2&text=" + strings.Repeat("a", maxBYOCSlowQueryQueryBytes)

	tests := []struct {
		name       string
		method     string
		path       string
		body       string
		headers    http.Header
		wantStatus int
	}{
		{name: "available fields query", method: http.MethodGet, path: slowQueryPath("/available-fields?limit=1"), wantStatus: http.StatusBadRequest},
		{name: "unknown list parameter", method: http.MethodGet, path: slowQueryPath("?begin_time=1&end_time=2&unknown=x"), wantStatus: http.StatusBadRequest},
		{name: "duplicate scalar", method: http.MethodGet, path: slowQueryPath("?begin_time=1&begin_time=2&end_time=3"), wantStatus: http.StatusBadRequest},
		{name: "missing begin", method: http.MethodGet, path: slowQueryPath("?end_time=2"), wantStatus: http.StatusBadRequest},
		{name: "negative begin", method: http.MethodGet, path: slowQueryPath("?begin_time=-1&end_time=2"), wantStatus: http.StatusBadRequest},
		{name: "reverse window", method: http.MethodGet, path: slowQueryPath("?begin_time=2&end_time=1"), wantStatus: http.StatusBadRequest},
		{name: "window too wide", method: http.MethodGet, path: slowQueryPath(fmt.Sprintf("?begin_time=0&end_time=%d", maxBYOCSlowQueryTimeRangeSeconds+1)), wantStatus: http.StatusBadRequest},
		{name: "zero limit", method: http.MethodGet, path: slowQueryPath("?begin_time=1&end_time=2&limit=0"), wantStatus: http.StatusBadRequest},
		{name: "limit too large", method: http.MethodGet, path: slowQueryPath("?begin_time=1&end_time=2&limit=1001"), wantStatus: http.StatusBadRequest},
		{name: "bad order", method: http.MethodGet, path: slowQueryPath("?begin_time=1&end_time=2&orderBy=query"), wantStatus: http.StatusBadRequest},
		{name: "non strict bool", method: http.MethodGet, path: slowQueryPath("?begin_time=1&end_time=2&desc=1"), wantStatus: http.StatusBadRequest},
		{name: "invalid field", method: http.MethodGet, path: slowQueryPath("?begin_time=1&end_time=2&fields=query%2Cbad-name"), wantStatus: http.StatusBadRequest},
		{name: "duplicate field", method: http.MethodGet, path: slowQueryPath("?begin_time=1&end_time=2&fields=query%2CQUERY"), wantStatus: http.StatusBadRequest},
		{name: "too many db filters", method: http.MethodGet, path: slowQueryPath("?" + tooManyDBs.Encode()), wantStatus: http.StatusBadRequest},
		{name: "bad escaping", method: http.MethodGet, path: slowQueryPath("?begin_time=1&end_time=%ZZ"), wantStatus: http.StatusBadRequest},
		{name: "query too large", method: http.MethodGet, path: slowQueryPath("?" + longQuery), wantStatus: http.StatusRequestEntityTooLarge},
		{name: "get body", method: http.MethodGet, path: slowQueryPath("?begin_time=1&end_time=2"), body: "x", wantStatus: http.StatusBadRequest},
		{name: "caller placement query", method: http.MethodGet, path: slowQueryPath("?begin_time=1&end_time=2&provider=aws"), wantStatus: http.StatusBadRequest},
		{name: "caller placement header", method: http.MethodGet, path: slowQueryPath("?begin_time=1&end_time=2"), headers: http.Header{"X-Region": {"us-east-1"}}, wantStatus: http.StatusBadRequest},
		{name: "detail missing digest", method: http.MethodGet, path: slowQueryPath("/detail?timestamp=1&connect_id=1"), wantStatus: http.StatusBadRequest},
		{name: "detail nan timestamp", method: http.MethodGet, path: slowQueryPath("/detail?digest=d&timestamp=NaN&connect_id=1"), wantStatus: http.StatusBadRequest},
		{name: "detail negative connection", method: http.MethodGet, path: slowQueryPath("/detail?digest=d&timestamp=1&connect_id=-1"), wantStatus: http.StatusBadRequest},
		{name: "detail connection overflow", method: http.MethodGet, path: slowQueryPath("/detail?digest=d&timestamp=1&connect_id=18446744073709551616"), wantStatus: http.StatusBadRequest},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fake := successfulBYOCSlowQueryQuerier(`[]`)
			response := performBYOCSlowQueryRequest(t, fake, tt.method, tt.path, tt.body, tt.headers)
			assert.Equal(t, tt.wantStatus, response.Code, response.Body.String())
			assert.Zero(t, fake.calls)
			assert.Contains(t, response.Header().Get("Content-Type"), "application/json")
		})
	}
}

func TestBYOCSlowQueryMapsServiceErrorsWithoutLeakingBodies(t *testing.T) {
	tests := []struct {
		name       string
		err        error
		operation  string
		wantStatus int
	}{
		{name: "canceled", err: context.Canceled, wantStatus: clientClosedStatus},
		{name: "deadline", err: context.DeadlineExceeded, wantStatus: http.StatusGatewayTimeout},
		{name: "unavailable", err: service.ErrBYOCSlowQueryUnavailable, wantStatus: http.StatusServiceUnavailable},
		{name: "invalid target", err: service.ErrInvalidBYOCSlowQueryTarget, wantStatus: http.StatusBadGateway},
		{name: "invalid response", err: service.ErrInvalidBYOCSlowQueryResponse, wantStatus: http.StatusBadGateway},
		{name: "unsupported target", err: &clinic_internal_api.UpstreamError{StatusCode: http.StatusBadRequest}, wantStatus: http.StatusBadRequest},
		{name: "missing target", err: &clinic_internal_api.UpstreamError{StatusCode: http.StatusNotFound}, wantStatus: http.StatusNotFound},
		{name: "target rate limited", err: &clinic_internal_api.UpstreamError{StatusCode: http.StatusTooManyRequests}, wantStatus: http.StatusTooManyRequests},
		{name: "target unavailable", err: &clinic_internal_api.UpstreamError{StatusCode: http.StatusServiceUnavailable}, wantStatus: http.StatusServiceUnavailable},
		{name: "invalid upstream request", err: &service.BYOCSlowQueryUpstreamError{StatusCode: http.StatusBadRequest}, wantStatus: http.StatusBadRequest},
		{name: "missing list data", err: &service.BYOCSlowQueryUpstreamError{StatusCode: http.StatusNotFound}, wantStatus: http.StatusNotFound},
		{name: "missing detail", err: &service.BYOCSlowQueryUpstreamError{StatusCode: http.StatusNotFound}, operation: "/detail", wantStatus: http.StatusNotFound},
		{name: "rate limited", err: &service.BYOCSlowQueryUpstreamError{StatusCode: http.StatusTooManyRequests}, wantStatus: http.StatusTooManyRequests},
		{name: "upstream unavailable", err: &service.BYOCSlowQueryUpstreamError{StatusCode: http.StatusServiceUnavailable}, wantStatus: http.StatusServiceUnavailable},
		{name: "upstream timeout", err: &service.BYOCSlowQueryUpstreamError{StatusCode: http.StatusGatewayTimeout}, wantStatus: http.StatusGatewayTimeout},
		{name: "upstream internal", err: &service.BYOCSlowQueryUpstreamError{StatusCode: http.StatusInternalServerError}, wantStatus: http.StatusBadGateway},
		{name: "unknown", err: errors.New("secret upstream body"), wantStatus: http.StatusBadGateway},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fake := &fakeBYOCSlowQueryQuerier{err: tt.err}
			path := slowQueryPath("?begin_time=1&end_time=2")
			if tt.operation == "/detail" {
				path = slowQueryPath("/detail?digest=d&timestamp=1&connect_id=1")
			}
			response := performBYOCSlowQueryRequest(t, fake, http.MethodGet, path, "", nil)
			assert.Equal(t, tt.wantStatus, response.Code, response.Body.String())
			assert.NotContains(t, response.Body.String(), "secret upstream body")
		})
	}
}

func TestBYOCSlowQueryRejectsNilServiceResult(t *testing.T) {
	fake := &fakeBYOCSlowQueryQuerier{}
	response := performBYOCSlowQueryRequest(t, fake, http.MethodGet,
		slowQueryPath("?begin_time=1&end_time=2"), "", nil)

	assert.Equal(t, http.StatusBadGateway, response.Code)
	assert.Equal(t, 1, fake.calls)
}

func successfulBYOCSlowQueryQuerier(body string) *fakeBYOCSlowQueryQuerier {
	return &fakeBYOCSlowQueryQuerier{result: &service.BYOCSlowQueryResult{
		Body:         []byte(body),
		Provider:     "aws",
		Region:       "us-east-1",
		DeployTypeV2: "byoc",
	}}
}

func slowQueryPath(suffix string) string {
	return "/data-proxy/api/v1/orgs/123/clusters/logical/slow-queries" + suffix
}

func performBYOCSlowQueryRequest(
	t *testing.T,
	fake *fakeBYOCSlowQueryQuerier,
	method string,
	path string,
	body string,
	headers http.Header,
) *httptest.ResponseRecorder {
	t.Helper()
	gin.SetMode(gin.TestMode)
	router := gin.New()
	NewBYOCSlowQueryHandler(fake).RegisterRoutes(router)
	request := httptest.NewRequest(method, path, strings.NewReader(body))
	for key, values := range headers {
		for _, value := range values {
			request.Header.Add(key, value)
		}
	}
	response := httptest.NewRecorder()
	router.ServeHTTP(response, request)
	return response
}
