package server

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

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

	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/config"
)

func TestRegisterClinicResourceRoutesFailsClosedOnConfigLoadError(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	missingConfig := filepath.Join(t.TempDir(), "missing.yaml")

	require.Panics(t, func() {
		registerClinicResourceRoutes(context.Background(), router, missingConfig)
	})
}

func TestDataServiceAccessLogRedactsBYOCStatementsQuery(t *testing.T) {
	request := httptest.NewRequest(http.MethodGet,
		"/data-proxy/api/v1/orgs/123/clusters/logical-cluster/statements?text=customer-secret-sql&begin_time=1&end_time=2",
		nil,
	)
	line := formatDataServiceAccessLog(gin.LogFormatterParams{
		Request:   request,
		Method:    http.MethodGet,
		Path:      request.URL.RequestURI(),
		TimeStamp: time.Unix(0, 0),
	})
	require.Contains(t, line, request.URL.Path)
	require.NotContains(t, line, "customer-secret-sql")
	require.NotContains(t, line, "begin_time")

	otherRequest := httptest.NewRequest(http.MethodGet, "/healthz?probe=kept", nil)
	otherLine := formatDataServiceAccessLog(gin.LogFormatterParams{
		Request:   otherRequest,
		Method:    http.MethodGet,
		Path:      otherRequest.URL.RequestURI(),
		TimeStamp: time.Unix(0, 0),
	})
	require.Contains(t, otherLine, "?probe=kept", "other routes must preserve the existing access-log format")
}

func TestRegisterClinicResourceRoutes(t *testing.T) {
	gin.SetMode(gin.TestMode)
	o11y := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Contains(t, []string{
			"/internal/diagnosis/api/v1/topru/summary",
			"/internal/diagnosis/api/v1/topru/summaryplan",
			"/internal/diagnosis/api/v1/topru/user_stats",
			"/internal/byoc-o11y_cluster-1/diagnosis/api/v1/topsql/instances",
			"/internal/byoc-o11y_cluster-1/diagnosis/api/v1/topsql/summary",
			"/internal/byoc-o11y_cluster-1/diagnosis/api/v1/continuous_profiling/group_profiles",
			"/internal/byoc-o11y_cluster-1/diagnosis/api/v1/continuous_profiling/group_profile/detail",
		}, r.URL.Path)
		switch r.URL.Path {
		case "/internal/diagnosis/api/v1/topru/summary":
			require.Equal(t, "cluster-a", r.Header.Get("X-Cluster-Id"))
			require.Equal(t, "alice", r.URL.Query().Get("user"))
		case "/internal/diagnosis/api/v1/topru/summaryplan":
			require.Equal(t, "cluster-a", r.Header.Get("X-Cluster-Id"))
			require.Equal(t, "sql-digest", r.URL.Query().Get("digest"))
		case "/internal/diagnosis/api/v1/topru/user_stats":
			require.Equal(t, "cluster-a", r.Header.Get("X-Cluster-Id"))
		case "/internal/byoc-o11y_cluster-1/diagnosis/api/v1/topsql/instances":
			require.Equal(t, "1", r.URL.Query().Get("start"))
			require.Equal(t, "2", r.URL.Query().Get("end"))
			require.Empty(t, r.Header.Get("X-Cluster-Id"))
			require.Equal(t, "byoc", r.Header.Get("X-Biz-Type"))
			require.Equal(t, "123", r.Header.Get("X-Org-Id"))
			require.Equal(t, "aws-us-west-2", r.Header.Get("X-Region-Id"))
			require.Equal(t, "cluster-a", r.Header.Get("X-Application-Id"))
		case "/internal/byoc-o11y_cluster-1/diagnosis/api/v1/topsql/summary":
			require.Equal(t, "1s", r.URL.Query().Get("window"))
			require.Equal(t, "5", r.URL.Query().Get("top"))
			require.Equal(t, "db-tidb-0.db-tidb-peer.tidb123.svc:10080", r.URL.Query().Get("instance"))
			require.Equal(t, "tidb", r.URL.Query().Get("instance_type"))
		case "/internal/byoc-o11y_cluster-1/diagnosis/api/v1/continuous_profiling/group_profiles":
			require.Equal(t, "1", r.URL.Query().Get("begin_time"))
			require.Equal(t, "2", r.URL.Query().Get("end_time"))
			require.Equal(t, "byoc", r.Header.Get("X-Biz-Type"))
			require.Equal(t, "123", r.Header.Get("X-Org-Id"))
			require.Equal(t, "aws-us-west-2", r.Header.Get("X-Region-Id"))
			require.Equal(t, "cluster-a", r.Header.Get("X-Application-Id"))
			require.Equal(t, "aws", r.Header.Get("X-Provider"))
			require.Equal(t, "us-west-2", r.Header.Get("X-Region"))
			require.Equal(t, "cluster-a", r.Header.Get("X-Cluster-Id"))
			require.Equal(t, "byoc-host", r.Header.Get("X-Deploy-Type"))
			_, _ = w.Write([]byte(`[]`))
			return
		case "/internal/byoc-o11y_cluster-1/diagnosis/api/v1/continuous_profiling/group_profile/detail":
			require.Equal(t, "1", r.URL.Query().Get("ts"))
			_, _ = w.Write([]byte(`{"ts":1,"profile_duration_secs":10,"state":"finished","target_profiles":[]}`))
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"status":"success","data":[]}`))
	}))
	defer o11y.Close()

	// Production targets use a configured, non-loopback BYOC data-service host.
	// Route that safe test hostname to the local fake without weakening target
	// validation in the typed client or service.
	defaultTransport, ok := http.DefaultTransport.(*http.Transport)
	require.True(t, ok)
	testTransport := defaultTransport.Clone()
	testTransport.Proxy = nil
	defaultDial := testTransport.DialContext
	testTransport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
		if address == "byoc-ds.example.test:80" {
			return (&net.Dialer{}).DialContext(ctx, network, o11y.Listener.Addr().String())
		}
		return defaultDial(ctx, network, address)
	}
	http.DefaultTransport = testTransport
	t.Cleanup(func() { http.DefaultTransport = defaultTransport })

	clinic := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		switch r.URL.Path {
		case "/clinic/api/v1/internal/data-proxy/v1/clusters/cluster-a":
			_, _ = w.Write([]byte(`{"clusterId":"cluster-a","orgId":"123","diagnosticContext":{}}`))
		case "/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-a/activities":
			_, _ = w.Write([]byte(`{"activities":[],"partial":false,"cursor":""}`))
		case "/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-a/components":
			_, _ = w.Write([]byte(`[]`))
		case "/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-a/health-check/reports":
			_, _ = w.Write([]byte(`{"items":[],"page":1,"pageSize":20,"total":0}`))
		case "/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-a/health-check/reports/report-a":
			_, _ = w.Write([]byte(`{"reportId":"report-a","orgId":"123","clusterId":"cluster-a","status":"success","createdAt":"2026-07-19T00:00:00Z","updatedAt":"2026-07-19T00:00:00Z","items":[]}`))
		case "/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-a/targets/top-ru":
			_, _ = w.Write([]byte(fmt.Sprintf(`{"baseUrl":%q,"headers":{"env":"staging","bizType":"staging-tidbcloud-nextgen","provider":"aws","region":"us-west-2","orgId":"123","clusterId":"cluster-a","deployType":"premium","deployTypeV2":"premium"}}`, o11y.URL)))
		case "/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-a/targets/slow-query":
			_, _ = w.Write([]byte(`{"baseUrl":"http://byoc-ds.example.test","o11yClusterId":"byoc-o11y_cluster-1","headers":{"env":"staging","bizType":"byoc","orgId":"123","regionId":"aws-us-west-2","applicationId":"cluster-a"},"provider":"aws","region":"us-west-2","deployType":"byoc-host","deployTypeV2":"byoc resource pool"}`))
		default:
			http.NotFound(w, r)
		}
	}))
	defer clinic.Close()

	configPath := filepath.Join(t.TempDir(), "config.yaml")
	require.NoError(t, os.WriteFile(configPath, []byte(fmt.Sprintf(
		"clinic_api_base: %q\ntidb_mgmt_http_base: %q\nticdc:\n  dataflow_addr: localhost:10103\n  dataflow_ng_addr: localhost:10104\n  serverless_addr: localhost:10101\n",
		clinic.URL, clinic.URL,
	)), 0o600))

	router := gin.New()
	registerClinicResourceRoutes(context.Background(), router, configPath)

	for _, path := range []string{
		"/data-proxy/api/v1/clusters/cluster-a",
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/activities?begin_ts=1&end_ts=2",
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/components",
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/health-check/reports",
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/health-check/reports/report-a",
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/summary?start=1&end=2&top=10&user=alice",
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/summary-plan?start=1&end=2&top=10&digest=sql-digest",
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-ru/user-stats?start=1&end=2&top=20",
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-sql/instances?start=1&end=2",
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/top-sql/summary?start=1&end=2&window=1s&top=5&instance=db-tidb-0.db-tidb-peer.tidb123.svc%3A10080&instance_type=tidb",
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/continuous-profiling/group-profiles?begin_time=1&end_time=2",
		"/data-proxy/api/v1/orgs/123/clusters/cluster-a/continuous-profiling/group-profiles/1",
	} {
		response := httptest.NewRecorder()
		request := httptest.NewRequest(http.MethodGet, path, nil)
		router.ServeHTTP(response, request)
		require.Equal(t, http.StatusOK, response.Code, path+": "+response.Body.String())
	}

	wantDiagnosisRoutes := map[string]struct{}{
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/ticdc/changefeeds":                       {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/ticdc/changefeeds/:changefeedId":         {},
		http.MethodGet + " /api/v1/orgs/:orgId/data-proxy/clusters/:clusterId/ticdc/changefeeds":                       {},
		http.MethodGet + " /api/v1/orgs/:orgId/data-proxy/clusters/:clusterId/ticdc/changefeeds/:changefeedId":         {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/health-check/reports":                    {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/health-check/reports/:reportId":          {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/slow-queries":                            {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/slow-queries/available-fields":           {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/slow-queries/detail":                     {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/top-sql/instances":                       {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/top-sql/summary":                         {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/continuous-profiling/group-profiles":     {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/continuous-profiling/group-profiles/:ts": {},
	}
	wantStatementsRoutes := map[string]struct{}{
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/statements":                  {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/statements/available-fields": {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/statements/statement-types":  {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/statements/plans":            {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/statements/plan-detail":      {},
	}
	for _, route := range router.Routes() {
		delete(wantDiagnosisRoutes, route.Method+" "+route.Path)
		delete(wantStatementsRoutes, route.Method+" "+route.Path)
	}
	require.Empty(t, wantDiagnosisRoutes, "BYOC diagnosis routes must be registered without the warehouse Data Proxy")
	require.Empty(t, wantStatementsRoutes, "BYOC Statements routes must be registered without the warehouse Data Proxy")
}

func TestRegisterDataProxyRoutesIncludesCanonicalTelemetryAliases(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	cfg := config.DefaultConfig()
	cfg.MetricsQuery.Enabled = true

	registerDataProxyRoutes(context.Background(), router, cfg, nil)

	want := map[string]struct{}{
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/metrics/query":                  {},
		http.MethodPost + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/metrics/query-range":           {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/logs/query-range":               {},
		http.MethodGet + " /data-proxy/api/v1/orgs/:orgId/clusters/:clusterId/logs/labels/:name/values":       {},
		http.MethodGet + " /data-proxy/api/v1/system-log-datasources":                                         {},
		http.MethodGet + " /data-proxy/api/v1/system-log-datasources/:datasourceUID/logs/labels/:name/values": {},
		http.MethodGet + " /data-proxy/metrics/:cluster_id/api/v1/query":                                      {},
		http.MethodGet + " /data-proxy/loki/:cluster_id/api/v1/query_range":                                   {},
		http.MethodGet + " /data-proxy/system-logs/api/v1/query_range":                                        {},
	}
	for _, route := range router.Routes() {
		delete(want, route.Method+" "+route.Path)
	}
	require.Empty(t, want)
}
