package conprof

import (
	"archive/zip"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"strings"
	"testing"

	gcpstorage "cloud.google.com/go/storage"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/observability/o11ydiagnosis/pkg/utils"
)

type mockConProf struct {
	mock.Mock
}

func (m *mockConProf) List(ctx context.Context, params *ListParams) ([]ProfileMeta, error) {
	args := m.Called(ctx, params)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]ProfileMeta), args.Error(1)
}

func (m *mockConProf) Download(ctx context.Context, params *DownloadParams) ([]byte, error) {
	args := m.Called(ctx, params)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]byte), args.Error(1)
}

func mustNewDownloadToken(t *testing.T, params downloadTokenParams) string {
	t.Helper()
	b, err := json.Marshal(params)
	require.NoError(t, err)
	token, err := utils.NewJWTString("conprof", string(b))
	require.NoError(t, err)
	return token
}

func Test_convertProfileMetasToListResponseItems(t *testing.T) {
	resp := convertProfileMetasToListResponseItems([]ProfileMeta{
		{Ts: 1, Component: "c-1", Address: "a-1", ProfileType: "p-1"},
		{Ts: 1, Component: "c-1", Address: "a-1", ProfileType: "p-2"},
		{Ts: 1, Component: "c-1", Address: "a-1", ProfileType: "p-3"},
		{Ts: 1, Component: "c-1", Address: "a-1", ProfileType: "p-4"},
		{Ts: 1, Component: "c-1", Address: "a-2", ProfileType: "p-1"},
		{Ts: 1, Component: "c-1", Address: "a-2", ProfileType: "p-2"},
		{Ts: 1, Component: "c-1", Address: "a-2", ProfileType: "p-3"},
		{Ts: 1, Component: "c-1", Address: "a-2", ProfileType: "p-4"},
		{Ts: 1, Component: "c-2", Address: "a-1", ProfileType: "p-1"},
		{Ts: 1, Component: "c-2", Address: "a-1", ProfileType: "p-2"},
		{Ts: 1, Component: "c-2", Address: "a-1", ProfileType: "p-3"},
		{Ts: 1, Component: "c-2", Address: "a-1", ProfileType: "p-4"},
		{Ts: 2, Component: "c-1", Address: "a-1", ProfileType: "p-1"},
		{Ts: 2, Component: "c-1", Address: "a-1", ProfileType: "p-2"},
		{Ts: 2, Component: "c-1", Address: "a-1", ProfileType: "p-3"},
		{Ts: 2, Component: "c-1", Address: "a-1", ProfileType: "p-4"},
		{Ts: 2, Component: "c-1", Address: "a-2", ProfileType: "p-1"},
		{Ts: 2, Component: "c-1", Address: "a-2", ProfileType: "p-2"},
		{Ts: 2, Component: "c-1", Address: "a-2", ProfileType: "p-3"},
		{Ts: 2, Component: "c-1", Address: "a-2", ProfileType: "p-4"},
	})
	require.Len(t, resp, 2)
	require.Equal(t, int64(1), resp[0].Ts)
	require.Len(t, resp[0].ComponentNum, 5+2)
	require.Equal(t, resp[0].ComponentNum["c-1"], 2)
	require.Equal(t, resp[0].ComponentNum["c-2"], 1)
	require.Equal(t, int64(2), resp[1].Ts)
	require.Len(t, resp[1].ComponentNum, 5+1)
	require.Equal(t, resp[1].ComponentNum["c-1"], 2)
}

func Test_convertProfileMetasToDetailResponse(t *testing.T) {
	resp := convertProfileMetasToDetailResponse([]ProfileMeta{
		{Ts: 1, Component: "c-1", Address: "a-1", ProfileType: "p-1"},
		{Ts: 1, Component: "c-1", Address: "a-1", ProfileType: "p-2"},
		{Ts: 1, Component: "c-1", Address: "a-1", ProfileType: "p-3"},
		{Ts: 1, Component: "c-1", Address: "a-1", ProfileType: "p-4"},
		{Ts: 1, Component: "c-1", Address: "a-2", ProfileType: "p-1"},
		{Ts: 1, Component: "c-1", Address: "a-2", ProfileType: "p-2"},
		{Ts: 1, Component: "c-1", Address: "a-2", ProfileType: "p-3"},
		{Ts: 1, Component: "c-1", Address: "a-2", ProfileType: "p-4"},
		{Ts: 1, Component: "c-2", Address: "a-1", ProfileType: "p-1"},
		{Ts: 1, Component: "c-2", Address: "a-1", ProfileType: "p-2"},
		{Ts: 1, Component: "c-2", Address: "a-1", ProfileType: "p-3"},
		{Ts: 1, Component: "c-2", Address: "a-1", ProfileType: "p-4"},
	})
	require.Equal(t, int64(1), resp.Ts)
	require.Len(t, resp.TargetProfiles, 12)
	require.Equal(t, "p-1", resp.TargetProfiles[0].ProfileType)
	require.Equal(t, "a-1", resp.TargetProfiles[0].Target.Address)
	require.Equal(t, "c-1", resp.TargetProfiles[0].Target.Component)
	require.Equal(t, "p-4", resp.TargetProfiles[11].ProfileType)
	require.Equal(t, "a-1", resp.TargetProfiles[11].Target.Address)
	require.Equal(t, "c-2", resp.TargetProfiles[11].Target.Component)
}

func Test_convertProfileMetasToListResponseItems_Empty(t *testing.T) {
	resp := convertProfileMetasToListResponseItems([]ProfileMeta{})
	require.Len(t, resp, 0)
}

func Test_convertProfileMetasToDetailResponse_Empty(t *testing.T) {
	resp := convertProfileMetasToDetailResponse([]ProfileMeta{})
	require.Nil(t, resp)
}

func Test_replaceNonAlphanumericWithDash(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		expected string
	}{
		{"alphanumeric only", "abc123", "abc123"},
		{"with spaces", "abc 123", "abc-123"},
		{"with special chars", "abc@123#def", "abc-123-def"},
		{"with dots", "127.0.0.1:10080", "127-0-0-1-10080"},
		{"empty string", "", ""},
		{"only special chars", "@#$", "-"},
		{"mixed", "a-b_c.d:e", "a-b-c-d-e"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := replaceNonAlphanumericWithDash(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestNewConProfService(t *testing.T) {
	store := &mockStorage{}
	service := NewConProfService("test", store, "/tmp/cache")
	assert.NotNil(t, service)
	assert.NotNil(t, service.ConProf)
	assert.Equal(t, "/tmp/cache", service.cacheDir)
}

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

	tests := []struct {
		name           string
		setupMock      func(*mockConProf)
		headers        map[string]string
		query          string
		expectedStatus int
		validateResp   func(*testing.T, *httptest.ResponseRecorder)
	}{
		{
			name: "success",
			setupMock: func(m *mockConProf) {
				m.On("List", mock.Anything, mock.AnythingOfType("*conprof.ListParams")).
					Return([]ProfileMeta{
						{
							Ts:          1500,
							Address:     "127.0.0.1:10080",
							Component:   "tidb",
							ProfileType: "cpu",
						},
					}, nil)
			},
			headers: map[string]string{
				"x-provider":   "aws",
				"x-region":     "us-west-2",
				"x-org-id":     "t1",
				"x-project-id": "p1",
				"x-cluster-id": "c1",
			},
			query:          "begin_time=1000&end_time=2000",
			expectedStatus: http.StatusOK,
		},
		{
			name: "invalid header",
			headers: map[string]string{
				"x-provider": "aws",
			},
			query:          "begin_time=1000&end_time=2000",
			expectedStatus: http.StatusBadRequest,
		},
		{
			name: "invalid query",
			headers: map[string]string{
				"x-provider":   "aws",
				"x-region":     "us-west-2",
				"x-cluster-id": "c1",
			},
			query:          "",
			expectedStatus: http.StatusBadRequest,
		},
		{
			name: "default deploy type",
			setupMock: func(m *mockConProf) {
				m.On("List", mock.Anything, mock.AnythingOfType("*conprof.ListParams")).
					Return([]ProfileMeta{}, nil)
			},
			headers: map[string]string{
				"x-provider":   "aws",
				"x-region":     "us-west-2",
				"x-org-id":     "t1",
				"x-project-id": "p1",
				"x-cluster-id": "c1",
			},
			query:          "begin_time=1000&end_time=2000",
			expectedStatus: http.StatusOK,
		},
		{
			name: "aws no such bucket error",
			setupMock: func(m *mockConProf) {
				m.On("List", mock.Anything, mock.AnythingOfType("*conprof.ListParams")).
					Return(nil, awserr.New(s3.ErrCodeNoSuchBucket, "not deployed", nil))
			},
			headers: map[string]string{
				"x-provider":   "aws",
				"x-region":     "us-west-2",
				"x-org-id":     "t1",
				"x-project-id": "p1",
				"x-cluster-id": "c1",
			},
			query:          "begin_time=1000&end_time=2000",
			expectedStatus: http.StatusBadRequest,
		},
		{
			name: "gcp bucket not exist error",
			setupMock: func(m *mockConProf) {
				m.On("List", mock.Anything, mock.AnythingOfType("*conprof.ListParams")).
					Return(nil, gcpstorage.ErrBucketNotExist)
			},
			headers: map[string]string{
				"x-provider":   "gcp",
				"x-region":     "us-central1",
				"x-org-id":     "t1",
				"x-project-id": "p1",
				"x-cluster-id": "c1",
			},
			query:          "begin_time=1000&end_time=2000",
			expectedStatus: http.StatusBadRequest,
		},
		{
			name: "generic error",
			setupMock: func(m *mockConProf) {
				m.On("List", mock.Anything, mock.AnythingOfType("*conprof.ListParams")).
					Return(nil, errors.New("boom"))
			},
			headers: map[string]string{
				"x-provider":   "aws",
				"x-region":     "us-west-2",
				"x-org-id":     "t1",
				"x-project-id": "p1",
				"x-cluster-id": "c1",
			},
			query:          "begin_time=1000&end_time=2000",
			expectedStatus: http.StatusBadRequest,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cp := &mockConProf{}
			if tt.setupMock != nil {
				tt.setupMock(cp)
			}
			service := &Service{
				ConProf:  cp,
				cacheDir: "/tmp/cache",
			}

			w := httptest.NewRecorder()
			c, _ := gin.CreateTestContext(w)
			c.Request = httptest.NewRequest("GET", "/continuous_profiling/group_profiles?"+tt.query, nil)
			for k, v := range tt.headers {
				c.Request.Header.Set(k, v)
			}

			service.handleList(c)

			assert.Equal(t, tt.expectedStatus, w.Code)
			if tt.validateResp != nil {
				tt.validateResp(t, w)
			}
		})
	}
}

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

	t.Run("success", func(t *testing.T) {
		cp := &mockConProf{}
		cp.On("List", mock.Anything, mock.AnythingOfType("*conprof.ListParams")).
			Return([]ProfileMeta{
				{
					Ts:          1000,
					Address:     "127.0.0.1:10080",
					Component:   "tidb",
					ProfileType: "cpu",
				},
			}, nil)
		service := &Service{
			ConProf:  cp,
			cacheDir: "/tmp/cache",
		}

		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		c.Request = httptest.NewRequest("GET", "/continuous_profiling/group_profile/detail?ts=1000", nil)
		c.Request.Header.Set("x-provider", "aws")
		c.Request.Header.Set("x-region", "us-west-2")
		c.Request.Header.Set("x-org-id", "t1")
		c.Request.Header.Set("x-project-id", "p1")
		c.Request.Header.Set("x-cluster-id", "c1")

		service.handleDetail(c)

		assert.Equal(t, http.StatusOK, w.Code)
	})

	t.Run("invalid header", func(t *testing.T) {
		cp := &mockConProf{}
		service := &Service{
			ConProf:  cp,
			cacheDir: "/tmp/cache",
		}

		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		// missing required headers
		c.Request = httptest.NewRequest("GET", "/continuous_profiling/group_profile/detail?ts=1000", nil)

		service.handleDetail(c)
		assert.Equal(t, http.StatusBadRequest, w.Code)
	})

	t.Run("invalid query", func(t *testing.T) {
		cp := &mockConProf{}
		service := &Service{
			ConProf:  cp,
			cacheDir: "/tmp/cache",
		}

		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		// missing ts
		c.Request = httptest.NewRequest("GET", "/continuous_profiling/group_profile/detail", nil)
		c.Request.Header.Set("x-provider", "aws")
		c.Request.Header.Set("x-region", "us-west-2")
		c.Request.Header.Set("x-org-id", "t1")
		c.Request.Header.Set("x-project-id", "p1")
		c.Request.Header.Set("x-cluster-id", "c1")

		service.handleDetail(c)
		assert.Equal(t, http.StatusBadRequest, w.Code)
	})

	t.Run("list errors", func(t *testing.T) {
		testCases := []struct {
			name        string
			err         error
			expectedMsg string
		}{
			{"aws", awserr.New(s3.ErrCodeNoSuchBucket, "not deployed", nil), "aws region"},
			{"gcp", gcpstorage.ErrBucketNotExist, "gcp region"},
			{"generic", errors.New("boom"), "failed to query conprof detail"},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				cp := &mockConProf{}
				cp.On("List", mock.Anything, mock.AnythingOfType("*conprof.ListParams")).
					Return(nil, tc.err)
				service := &Service{
					ConProf:  cp,
					cacheDir: "/tmp/cache",
				}

				w := httptest.NewRecorder()
				c, _ := gin.CreateTestContext(w)
				c.Request = httptest.NewRequest("GET", "/continuous_profiling/group_profile/detail?ts=1000", nil)
				c.Request.Header.Set("x-provider", "aws")
				c.Request.Header.Set("x-region", "us-west-2")
				c.Request.Header.Set("x-org-id", "t1")
				c.Request.Header.Set("x-project-id", "p1")
				c.Request.Header.Set("x-cluster-id", "c1")

				service.handleDetail(c)

				assert.Equal(t, http.StatusBadRequest, w.Code)
				assert.Contains(t, w.Body.String(), tc.expectedMsg)
			})
		}
	})
}

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

	service := &Service{
		cacheDir: "/tmp/cache",
	}

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("GET", "/continuous_profiling/action_token?q=ts=1000&address=127.0.0.1:10080&component=tidb&profile_type=cpu", nil)
	c.Request.Header.Set("x-provider", "aws")
	c.Request.Header.Set("x-region", "us-west-2")
	c.Request.Header.Set("x-org-id", "t1")
	c.Request.Header.Set("x-project-id", "p1")
	c.Request.Header.Set("x-cluster-id", "c1")

	service.handleActionToken(c)

	assert.Equal(t, http.StatusOK, w.Code)
	token := w.Body.String()
	assert.NotEmpty(t, token)

	// Verify token can be parsed
	payload, err := utils.ParseJWTString("conprof", token)
	require.NoError(t, err)
	var params downloadTokenParams
	err = json.Unmarshal([]byte(payload), &params)
	require.NoError(t, err)
	assert.Equal(t, "aws", params.Provider)
	assert.Equal(t, "us-west-2", params.Region)
}

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

	service := &Service{
		cacheDir: "/tmp/cache",
	}

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	// missing required headers
	c.Request = httptest.NewRequest("GET", "/continuous_profiling/action_token?q=ts=1000", nil)

	service.handleActionToken(c)

	assert.Equal(t, http.StatusBadRequest, w.Code)
}

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

	t.Run("success", func(t *testing.T) {
		cacheDir, err := os.MkdirTemp("", "conprof_test")
		require.NoError(t, err)
		defer os.RemoveAll(cacheDir)

		cp := &mockConProf{}
		cp.On("Download", mock.Anything, mock.AnythingOfType("*conprof.DownloadParams")).
			Return([]byte("test profile data"), nil)
		service := &Service{
			ConProf:  cp,
			cacheDir: cacheDir,
		}

		token := mustNewDownloadToken(t, downloadTokenParams{
			Provider:   "aws",
			Region:     "us-west-2",
			TenantID:   "t1",
			ProjectID:  "p1",
			ClusterID:  "c1",
			DeployType: "dedicated",
			Q:          "ts=1234567890&address=127.0.0.1:10080&component=tidb&profile_type=cpu",
		})

		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		c.Request = httptest.NewRequest("GET", "/continuous_profiling/download?token="+token+"&ts=1234567890&address=127.0.0.1:10080&component=tidb&profile_type=cpu", nil)

		service.handleDownload(c)

		assert.Equal(t, http.StatusOK, w.Code)
		assert.Contains(t, w.Header().Get("Content-Disposition"), "attachment")
	})

	t.Run("invalid token payload", func(t *testing.T) {
		service := &Service{cacheDir: "/tmp/cache"}
		token, err := utils.NewJWTString("conprof", "invalid json")
		require.NoError(t, err)

		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		c.Request = httptest.NewRequest("GET", "/continuous_profiling/download?token="+token, nil)

		service.handleDownload(c)

		assert.Equal(t, http.StatusInternalServerError, w.Code)
	})

	t.Run("invalid query params", func(t *testing.T) {
		cp := &mockConProf{}
		service := &Service{ConProf: cp, cacheDir: "/tmp/cache"}

		token := mustNewDownloadToken(t, downloadTokenParams{
			Provider:   "aws",
			Region:     "us-west-2",
			TenantID:   "t1",
			ProjectID:  "p1",
			ClusterID:  "c1",
			DeployType: "dedicated",
			Q:          "ts=123", // missing required fields
		})

		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		c.Request = httptest.NewRequest("GET", "/continuous_profiling/download?token="+token, nil)

		service.handleDownload(c)
		assert.Equal(t, http.StatusBadRequest, w.Code)
	})

	t.Run("download errors", func(t *testing.T) {
		testCases := []struct {
			name        string
			err         error
			expectedMsg string
		}{
			{"aws", awserr.New(s3.ErrCodeNoSuchBucket, "not deployed", nil), "aws region"},
			{"gcp", gcpstorage.ErrBucketNotExist, "gcp region"},
			{"generic", errors.New("boom"), "failed to download profile"},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				cp := &mockConProf{}
				cp.On("Download", mock.Anything, mock.AnythingOfType("*conprof.DownloadParams")).
					Return(nil, tc.err)
				service := &Service{
					ConProf:  cp,
					cacheDir: "/tmp/cache",
				}

				token := mustNewDownloadToken(t, downloadTokenParams{
					Provider:   "aws",
					Region:     "us-west-2",
					TenantID:   "t1",
					ProjectID:  "p1",
					ClusterID:  "c1",
					DeployType: "dedicated",
					Q:          "ts=1234567890&address=127.0.0.1:10080&component=tidb&profile_type=cpu",
				})

				w := httptest.NewRecorder()
				c, _ := gin.CreateTestContext(w)
				c.Request = httptest.NewRequest("GET", "/continuous_profiling/download?token="+token+"&ts=1234567890&address=127.0.0.1:10080&component=tidb&profile_type=cpu", nil)

				service.handleDownload(c)

				assert.Equal(t, http.StatusBadRequest, w.Code)
				assert.Contains(t, w.Body.String(), tc.expectedMsg)
			})
		}
	})
}

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

	service := &Service{
		cacheDir: "/tmp/cache",
	}

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("GET", "/continuous_profiling/download?token=invalid", nil)

	service.handleDownload(c)

	assert.Equal(t, http.StatusInternalServerError, w.Code)
}

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

	cacheDir, err := os.MkdirTemp("", "conprof_test")
	require.NoError(t, err)
	defer os.RemoveAll(cacheDir)

	cp := &mockConProf{}
	cp.On("Download", mock.Anything, mock.AnythingOfType("*conprof.DownloadParams")).
		Return([]byte("test profile data"), nil)
	service := &Service{
		ConProf:  cp,
		cacheDir: cacheDir,
	}

	token := mustNewDownloadToken(t, downloadTokenParams{
		Provider:   "aws",
		Region:     "us-west-2",
		TenantID:   "t1",
		ProjectID:  "p1",
		ClusterID:  "c1",
		DeployType: "dedicated",
		Q:          "ts=1234567890&address=127.0.0.1:10080&component=tidb&profile_type=cpu",
	})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("GET", "/continuous_profiling/single_profile/view?token="+token+"&ts=1234567890&address=127.0.0.1:10080&component=tidb&profile_type=cpu", nil)

	service.handleSingleView(c)

	assert.Equal(t, http.StatusOK, w.Code)
}

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

	cacheDir, err := os.MkdirTemp("", "conprof_heap")
	require.NoError(t, err)
	defer os.RemoveAll(cacheDir)

	cp := &mockConProf{}
	cp.On("Download", mock.Anything, mock.AnythingOfType("*conprof.DownloadParams")).
		Return([]byte("heap profile: 1: 1 [ 1: 1] @ heap\n"), nil)
	service := &Service{
		ConProf:  cp,
		cacheDir: cacheDir,
	}

	originalJeprof := jeprof
	defer func() { jeprof = originalJeprof }()
	jeprof = "print while <>;"

	token := mustNewDownloadToken(t, downloadTokenParams{
		Provider:   "aws",
		Region:     "us-west-2",
		TenantID:   "t1",
		ProjectID:  "p1",
		ClusterID:  "c1",
		DeployType: "dedicated",
		Q:          "ts=1234567890&address=127.0.0.1:10080&component=tikv&profile_type=heap",
	})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("GET", "/continuous_profiling/single_profile/view?token="+token+"&ts=1234567890&address=127.0.0.1:10080&component=tikv&profile_type=heap", nil)

	service.handleSingleView(c)

	assert.Equal(t, http.StatusOK, w.Code)
	assert.Equal(t, "text/plain", w.Header().Get("Content-Type"))
}

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

	cacheDir, err := os.MkdirTemp("", "conprof_heap")
	require.NoError(t, err)
	defer os.RemoveAll(cacheDir)

	cp := &mockConProf{}
	cp.On("Download", mock.Anything, mock.AnythingOfType("*conprof.DownloadParams")).
		Return([]byte("binary pprof data"), nil)
	service := &Service{
		ConProf:  cp,
		cacheDir: cacheDir,
	}

	token := mustNewDownloadToken(t, downloadTokenParams{
		Provider:   "aws",
		Region:     "us-west-2",
		TenantID:   "t1",
		ProjectID:  "p1",
		ClusterID:  "c1",
		DeployType: "dedicated",
		Q:          "ts=1234567890&address=127.0.0.1:10080&component=tikv&profile_type=heap",
	})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("GET", "/continuous_profiling/single_profile/view?token="+token+"&ts=1234567890&address=127.0.0.1:10080&component=tikv&profile_type=heap", nil)

	service.handleSingleView(c)

	assert.Equal(t, http.StatusOK, w.Code)
	assert.Empty(t, w.Header().Get("Content-Type"))
	assert.Equal(t, "binary pprof data", w.Body.String())
}

func Test_shouldConvertJeprofHeap(t *testing.T) {
	jeprofHeap := []byte("heap profile: 1: 1 [ 1: 1] @ heap\n")
	nonJeprof := []byte("binary pprof data")

	tests := []struct {
		name     string
		q        downloadTokenQ
		profile  []byte
		expected bool
	}{
		{
			name:     "tikv jeprof heap",
			q:        downloadTokenQ{Component: "tikv", ProfileType: "heap"},
			profile:  jeprofHeap,
			expected: true,
		},
		{
			name:     "tiflash jeprof heap",
			q:        downloadTokenQ{Component: "tiflash", ProfileType: "heap"},
			profile:  []byte("--- heap\n"),
			expected: true,
		},
		{
			name:     "tikv non-jeprof heap",
			q:        downloadTokenQ{Component: "tikv", ProfileType: "heap"},
			profile:  nonJeprof,
			expected: false,
		},
		{
			name:     "tidb heap",
			q:        downloadTokenQ{Component: "tidb", ProfileType: "heap"},
			profile:  jeprofHeap,
			expected: false,
		},
		{
			name:     "tikv cpu",
			q:        downloadTokenQ{Component: "tikv", ProfileType: "cpu"},
			profile:  jeprofHeap,
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, shouldConvertJeprofHeap(tt.q, tt.profile))
		})
	}
}

func Test_isJeprofHeapProfile(t *testing.T) {
	tests := []struct {
		name     string
		profile  []byte
		expected bool
	}{
		{name: "heap profile header", profile: []byte("heap profile: 1: 1"), expected: true},
		{name: "heap_v2 header", profile: []byte("heap_v2/524288"), expected: true},
		{name: "symbol header", profile: []byte("--- symbol\n"), expected: true},
		{name: "heap header", profile: []byte("--- heap\n"), expected: true},
		{name: "pprof binary", profile: []byte("\x1f\x8b\x08"), expected: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, isJeprofHeapProfile(tt.profile))
		})
	}
}

func Test_convertJeprofHeapProfileToCollapsed_RealTikvHeapV2(t *testing.T) {
	if _, err := exec.LookPath("perl"); err != nil {
		t.Skip("perl not available")
	}

	profilePath := "FT/profile-2026-06-11_09-04-00.zip"
	reader, err := zip.OpenReader(profilePath)
	require.NoError(t, err)
	defer reader.Close()

	var profile []byte
	for _, file := range reader.File {
		if strings.HasSuffix(file.Name, ".proto") && file.Name != "README.md" {
			rc, openErr := file.Open()
			require.NoError(t, openErr)
			var readErr error
			profile, readErr = io.ReadAll(rc)
			require.NoError(t, readErr)
			require.NoError(t, rc.Close())
			break
		}
	}
	require.NotEmpty(t, profile)
	require.True(t, isJeprofHeapProfile(profile))
	require.False(t, isJeprofSymbolizedProfile(profile))

	cacheDir, err := os.MkdirTemp("", "conprof_heap_real")
	require.NoError(t, err)
	defer os.RemoveAll(cacheDir)

	_, err = convertJeprofHeapProfileToCollapsed(profile, cacheDir)
	require.NoError(t, err)
}

func TestService_handleSingleView_TikvHeapV2RealProfile(t *testing.T) {
	if _, err := exec.LookPath("perl"); err != nil {
		t.Skip("perl not available")
	}

	gin.SetMode(gin.TestMode)

	profilePath := "FT/profile-2026-06-11_09-04-00.zip"
	reader, err := zip.OpenReader(profilePath)
	require.NoError(t, err)
	defer reader.Close()

	var profile []byte
	for _, file := range reader.File {
		if strings.HasSuffix(file.Name, ".proto") && file.Name != "README.md" {
			rc, openErr := file.Open()
			require.NoError(t, openErr)
			var readErr error
			profile, readErr = io.ReadAll(rc)
			require.NoError(t, readErr)
			require.NoError(t, rc.Close())
			break
		}
	}
	require.NotEmpty(t, profile)

	cacheDir, err := os.MkdirTemp("", "conprof_heap_real")
	require.NoError(t, err)
	defer os.RemoveAll(cacheDir)

	cp := &mockConProf{}
	cp.On("Download", mock.Anything, mock.AnythingOfType("*conprof.DownloadParams")).
		Return(profile, nil)
	service := &Service{
		ConProf:  cp,
		cacheDir: cacheDir,
	}

	token := mustNewDownloadToken(t, downloadTokenParams{
		Provider:   "aws",
		Region:     "us-west-2",
		TenantID:   "t1",
		ProjectID:  "p1",
		ClusterID:  "c1",
		DeployType: "premium",
		Q:          "ts=1781168640&profile_type=heap&component=tikv&address=db-67bdad2b-default-tikv-worker-zuliz6&data_format=text",
	})

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest(
		"GET",
		"/continuous_profiling/single_profile/view?output_type=text&token="+token,
		nil,
	)

	service.handleSingleView(c)

	assert.Equal(t, http.StatusOK, w.Code, w.Body.String())
	assert.Equal(t, "text/plain", w.Header().Get("Content-Type"))
}

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

	service := &Service{
		cacheDir: "/tmp/cache",
	}

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("GET", "/continuous_profiling/single_profile/view?token=invalid", nil)

	service.handleSingleView(c)

	assert.Equal(t, http.StatusInternalServerError, w.Code)
}

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

	t.Run("invalid query params", func(t *testing.T) {
		cp := &mockConProf{}
		service := &Service{ConProf: cp, cacheDir: "/tmp/cache"}

		token := mustNewDownloadToken(t, downloadTokenParams{
			Provider:   "aws",
			Region:     "us-west-2",
			TenantID:   "t1",
			ProjectID:  "p1",
			ClusterID:  "c1",
			DeployType: "dedicated",
			Q:          "ts=123", // missing fields
		})

		w := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(w)
		c.Request = httptest.NewRequest("GET", "/continuous_profiling/single_profile/view?token="+token, nil)

		service.handleSingleView(c)
		assert.Equal(t, http.StatusBadRequest, w.Code)
	})

	t.Run("download errors", func(t *testing.T) {
		testCases := []struct {
			name        string
			err         error
			expectedMsg string
		}{
			{"aws", awserr.New(s3.ErrCodeNoSuchBucket, "not deployed", nil), "aws region"},
			{"gcp", gcpstorage.ErrBucketNotExist, "gcp region"},
			{"generic", errors.New("boom"), "failed to download profile"},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				cp := &mockConProf{}
				cp.On("Download", mock.Anything, mock.AnythingOfType("*conprof.DownloadParams")).
					Return(nil, tc.err)
				service := &Service{ConProf: cp, cacheDir: "/tmp/cache"}

				token := mustNewDownloadToken(t, downloadTokenParams{
					Provider:   "aws",
					Region:     "us-west-2",
					TenantID:   "t1",
					ProjectID:  "p1",
					ClusterID:  "c1",
					DeployType: "dedicated",
					Q:          "ts=1234567890&address=127.0.0.1:10080&component=tidb&profile_type=cpu",
				})

				w := httptest.NewRecorder()
				c, _ := gin.CreateTestContext(w)
				c.Request = httptest.NewRequest("GET", "/continuous_profiling/single_profile/view?token="+token+"&ts=1234567890&address=127.0.0.1:10080&component=tidb&profile_type=cpu", nil)

				service.handleSingleView(c)
				assert.Equal(t, http.StatusBadRequest, w.Code)
				assert.Contains(t, w.Body.String(), tc.expectedMsg)
			})
		}
	})
}

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

	r := gin.New()
	s := &Service{
		ConProf:  &mockConProf{},
		cacheDir: "/tmp/cache",
	}

	InitConProfApi(r.Group("/api"), s)

	// Just ensure routes are registered and can be invoked without panic.
	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/api/continuous_profiling/group_profiles", nil)
	r.ServeHTTP(w, req)
}

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

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("GET", "/test", nil)

	finish := recordHttpMetric(c, "/test")
	c.Writer.WriteHeader(http.StatusOK)
	finish()

	// Verify metrics were recorded (no error means success)
	assert.Equal(t, http.StatusOK, w.Code)
}
