package slowquery

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"

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

	"github.com/pingcap-inc/clinic/pkg/o11y"
)

type stubSlowQuery struct {
	availableFields []string
	list            []*SlowLog
}

func (s *stubSlowQuery) List(_ context.Context, _ *ListParams) ([]*SlowLog, error) {
	return s.list, nil
}

func (s *stubSlowQuery) Detail(_ context.Context, _ *DetailParams) (*SlowLog, error) {
	return nil, nil
}

func (s *stubSlowQuery) Analyze(_ context.Context, _ *AnalyzeParams) error {
	return nil
}

func (s *stubSlowQuery) AvailableFields(_ context.Context) ([]string, error) {
	return s.availableFields, nil
}

func (s *stubSlowQuery) Download(_ context.Context, _ *DownloadParams) (*DownloadData, error) {
	return nil, nil
}

func newTestO11yClient(t *testing.T) *o11y.Client {
	t.Helper()
	cli, err := o11y.New(
		"www.gs.us-west-2.aws.observability-staging.pingcap.cloud",
		"www.ds.%s.%s.observability-staging.pingcap.cloud",
		"foo",
		"bar",
		"dev",
	)
	require.NoError(t, err)
	return cli
}

func mockSlowQueryStorageTarget(t *testing.T) {
	t.Helper()
	httpmock.Activate()
	t.Cleanup(httpmock.DeactivateAndReset)

	httpmock.RegisterResponder(http.MethodPost, "https://tidb-soc2.us.auth0.com/oauth/token", func(r *http.Request) (*http.Response, error) {
		return httpmock.NewStringResponse(http.StatusOK, `{"access_token":"xxx","scope":"read:client_grants","expires_in":86400,"token_type":"Bearer"}`), nil
	})
	httpmock.RegisterResponder(http.MethodGet, "http://www.gs.us-west-2.aws.observability-staging.pingcap.cloud/api/v1/clusters/",
		func(r *http.Request) (*http.Response, error) {
			query := r.URL.Query()
			require.Equal(t, "dev-devtier", query.Get("bizType"))
			require.Equal(t, "aws", query.Get("vendor"))
			require.Equal(t, "us-west-2", query.Get("region"))
			require.Equal(t, "123", query.Get("tenantID"))
			require.Empty(t, query.Get("projectID"))
			require.Equal(t, "123", query.Get("applicationID"))

			content := `{"code":0,"data":{"clusters":[{"applicationID":"123","bizType":"dev-devtier","id":"serverless-o11y-id","projectID":"old-project","region":"us-west-2","tenantID":"123","vendor":"aws","version":"1","storageInfo":[{"bucketName":"bucket-from-o11y","category":"slowquery","objectPath":"path/from/o11y"}]}],"continueToken":"","remainingItemCount":0},"message":""}`
			return httpmock.NewStringResponse(http.StatusOK, content), nil
		})
}

func TestService_handleList(t *testing.T) {
	gin.SetMode(gin.TestMode)
	mockSlowQueryStorageTarget(t)
	svc := &Service{
		o11yCli: newTestO11yClient(t),
		SlowQuery: &slowQuery{
			stores: newMockStorageFactory(&mockStorage{data: testS3Data}),
		},
	}
	params := url.Values{}
	params.Set("begin_time", "0")
	params.Set("end_time", "1662452176")
	params.Set("desc", "true")
	params.Set("fields", "query,timestamp,query_time,memory_max,digest")
	resp := httptest.NewRecorder()
	var err error
	ctx, _ := gin.CreateTestContext(resp)
	ctx.Request, err = http.NewRequest("", "/list?"+params.Encode(), nil)
	ctx.Request.Header.Set("x-provider", "aws")
	ctx.Request.Header.Set("x-region", "us-west-2")
	ctx.Request.Header.Set("x-org-id", "123")
	ctx.Request.Header.Set("x-project-id", "123")
	ctx.Request.Header.Set("x-cluster-id", "123")
	ctx.Request.Header.Set("x-deploy-type", "shared")
	require.NoError(t, err)
	svc.handleList(ctx)
	require.Equal(t, http.StatusOK, resp.Code)
	var rs []slowLogRespItem
	err = json.Unmarshal(resp.Body.Bytes(), &rs)
	require.NoError(t, err)
	require.Len(t, rs, 5)
}

func TestService_handleListFiltersUnavailableFields(t *testing.T) {
	gin.SetMode(gin.TestMode)
	mockSlowQueryStorageTarget(t)
	svc := &Service{
		o11yCli: newTestO11yClient(t),
		SlowQuery: &stubSlowQuery{
			availableFields: []string{"query", "timestamp", "query_time", "memory_max"},
			list: []*SlowLog{
				{
					Time:      time.Unix(1662452176, 0),
					Query:     "select 1",
					QueryTime: 1.2,
					Host:      "192.0.2.1",
				},
			},
		},
	}
	params := url.Values{}
	params.Set("begin_time", "0")
	params.Set("end_time", "1662452176")
	params.Set("fields", "query,host,query_time")
	resp := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(resp)
	var err error
	ctx.Request, err = http.NewRequest("", "/list?"+params.Encode(), nil)
	require.NoError(t, err)
	ctx.Request.Header.Set("x-provider", "aws")
	ctx.Request.Header.Set("x-region", "us-west-2")
	ctx.Request.Header.Set("x-org-id", "123")
	ctx.Request.Header.Set("x-project-id", "123")
	ctx.Request.Header.Set("x-cluster-id", "123")
	ctx.Request.Header.Set("x-deploy-type", "shared")

	svc.handleList(ctx)

	require.Equal(t, http.StatusOK, resp.Code)
	require.Equal(t, "query,query_time", ctx.Request.URL.Query().Get("fields"))
	var rs []slowLogRespItem
	err = json.Unmarshal(resp.Body.Bytes(), &rs)
	require.NoError(t, err)
	require.Len(t, rs, 1)
	require.Equal(t, "select 1", rs[0].Query)
	require.Equal(t, 1.2, rs[0].QueryTime)
	require.Empty(t, rs[0].Host)
}

func TestFilterListFields(t *testing.T) {
	tests := []struct {
		name            string
		fields          string
		availableFields []string
		want            string
	}{
		{
			name:            "drops unavailable fields",
			fields:          "query,host,timestamp",
			availableFields: []string{"query", "timestamp"},
			want:            "query,timestamp",
		},
		{
			name:            "falls back to default fields when all fields are unavailable",
			fields:          "host,db",
			availableFields: []string{"query", "timestamp", "query_time", "memory_max"},
			want:            defaultListFields,
		},
		{
			name:            "keeps selected field order",
			fields:          "memory_max,query,timestamp",
			availableFields: []string{"query", "timestamp", "query_time", "memory_max"},
			want:            "memory_max,query,timestamp",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			require.Equal(t, tt.want, filterListFields(tt.fields, tt.availableFields))
		})
	}
}

func TestService_sanitizeListRequestFieldsUsesO11YAvailableFieldsBeforeProxy(t *testing.T) {
	gin.SetMode(gin.TestMode)
	originalFetch := fetchO11ySlowQueryAvailableFields
	defer func() {
		fetchO11ySlowQueryAvailableFields = originalFetch
	}()

	fetchO11ySlowQueryAvailableFields = func(s *Service, _ context.Context, _ *gin.Context, header clusterHeader) ([]string, error) {
		require.Equal(t, "prod", s.env)
		require.Equal(t, "dedicated", header.DeployType)
		return []string{"query", "query_time"}, nil
	}

	resp := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(resp)
	var err error
	ctx.Request, err = http.NewRequest("", "/list?fields=host%2Cquery_time%2Cquery", nil)
	require.NoError(t, err)

	svc := &Service{env: "prod"}
	req := &listRequest{Fields: "host,query_time,query"}
	err = svc.sanitizeListRequestFields(context.Background(), ctx, clusterHeader{DeployType: "dedicated"}, req)
	require.NoError(t, err)
	require.Equal(t, "query_time,query", req.Fields)
	require.Equal(t, "query_time,query", ctx.Request.URL.Query().Get("fields"))
}

func TestService_shouldProxySlowQueryToO11Y(t *testing.T) {
	tests := []struct {
		name       string
		env        string
		deployType string
		want       bool
	}{
		{name: "dedicated prod", env: "prod", deployType: o11y.DedicatedType, want: true},
		{name: "dedicated staging", env: "staging", deployType: o11y.DedicatedType, want: true},
		{name: "premium staging", env: "staging", deployType: o11y.PremiumType, want: true},
		{name: "byoc staging", env: "staging", deployType: o11y.ByocType, want: true},
		{name: "shared staging", env: "staging", deployType: o11y.SharedType, want: false},
		{name: "serverless staging", env: "staging", deployType: o11y.ServerlessType, want: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			svc := &Service{env: tt.env}
			require.Equal(t, tt.want, svc.shouldProxySlowQueryToO11y(tt.deployType))
		})
	}
}

func TestService_handleDetail(t *testing.T) {
	gin.SetMode(gin.TestMode)
	mockSlowQueryStorageTarget(t)
	svc := &Service{
		o11yCli: newTestO11yClient(t),
		SlowQuery: &slowQuery{
			stores: newMockStorageFactory(&mockStorage{data: testS3Data}),
		},
	}
	params := url.Values{}
	params.Set("digest", "926597c5c848f662ee6caba21f181a5aac72d8b2aee8ebbcc8ee361b46f7d9cb")
	params.Set("timestamp", "1661930034.022015")
	params.Set("connect_id", "6204412174144681481")
	resp := httptest.NewRecorder()
	var err error
	ctx, _ := gin.CreateTestContext(resp)
	ctx.Request, err = http.NewRequest("", "/detail?"+params.Encode(), nil)
	require.NoError(t, err)
	ctx.Request.Header.Set("x-provider", "aws")
	ctx.Request.Header.Set("x-region", "us-west-2")
	ctx.Request.Header.Set("x-org-id", "123")
	ctx.Request.Header.Set("x-project-id", "123")
	ctx.Request.Header.Set("x-cluster-id", "123")
	ctx.Request.Header.Set("x-deploy-type", "shared")
	svc.handleDetail(ctx)
	require.Equal(t, http.StatusOK, resp.Code)
	var item slowLogRespItem
	err = json.Unmarshal(resp.Body.Bytes(), &item)
	require.NoError(t, err)
	require.Equal(t, 1.000614937, item.QueryTime)
}

func TestSlowLogToRespItem_IAFields(t *testing.T) {
	sl := &SlowLog{
		IARemoteReadSegmentCount:    8,
		IARemoteReadSegmentSize:     31629224,
		IARemoteReadSegmentWaitTime: 0.4,
	}

	item := slowLogToRespItem(context.Background(), sl)
	require.Equal(t, uint64(8), item.IARemoteReadSegmentCount)
	require.Equal(t, uint64(31629224), item.IARemoteReadSegmentSize)
	require.Equal(t, 0.4, item.IARemoteReadSegmentWaitTime)
}

func TestSlowLogToRespItemByFields_IAFields(t *testing.T) {
	sl := &SlowLog{
		IARemoteReadSegmentCount:    8,
		IARemoteReadSegmentSize:     31629224,
		IARemoteReadSegmentWaitTime: 0.4,
	}

	item := slowLogToRespItemByFields(context.Background(), sl, []string{
		"ia_remote_read_segment_count",
		"ia_remote_read_segment_size",
		"ia_remote_read_segment_wait_time",
	})
	require.Equal(t, uint64(8), item.IARemoteReadSegmentCount)
	require.Equal(t, uint64(31629224), item.IARemoteReadSegmentSize)
	require.Equal(t, 0.4, item.IARemoteReadSegmentWaitTime)
}
