package service

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

	"github.com/stretchr/testify/require"

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

type fakeBYOCTopSQLTargetResolver struct {
	target    *clinic_internal_api.BYOCDiagnosisTarget
	err       error
	orgID     uint64
	clusterID string
}

func (f *fakeBYOCTopSQLTargetResolver) GetBYOCDiagnosisTarget(
	_ context.Context,
	orgID uint64,
	clusterID string,
) (*clinic_internal_api.BYOCDiagnosisTarget, error) {
	f.orgID = orgID
	f.clusterID = clusterID
	return f.target, f.err
}

func validBYOCTopSQLTarget(baseURL string) *clinic_internal_api.BYOCDiagnosisTarget {
	return &clinic_internal_api.BYOCDiagnosisTarget{
		BaseURL:       baseURL,
		O11yClusterID: "o11y-cluster_a",
		Provider:      "aws",
		Region:        "us-east-1",
		DeployType:    "byoc",
		DeployTypeV2:  "byoc",
		Headers: clinic_internal_api.BYOCDiagnosisTargetHeaders{
			Env:           "prod",
			BizType:       "byoc",
			OrgID:         "123",
			RegionID:      "aws-us-east-1",
			ApplicationID: "cluster-a",
		},
	}
}

func TestNewBYOCTopSQLServiceUsesFixedTimeout(t *testing.T) {
	svc := NewBYOCTopSQLService(&fakeBYOCTopSQLTargetResolver{})
	require.Equal(t, 2*time.Minute, svc.httpClient.Timeout)
}

func TestBYOCTopSQLServiceCallsOnlyFixedO11yEndpoints(t *testing.T) {
	tests := []struct {
		name      string
		operation BYOCTopSQLOperation
		path      string
		query     url.Values
		body      string
	}{
		{
			name:      "instances",
			operation: BYOCTopSQLOperationInstances,
			path:      "/internal/o11y-cluster_a/diagnosis/api/v1/topsql/instances",
			query:     url.Values{"start": {"1"}, "end": {"2"}},
			body:      `{"status":"ok","data":[{"instance":"tidb-0","instance_type":"tidb"}]}`,
		},
		{
			name:      "summary",
			operation: BYOCTopSQLOperationSummary,
			path:      "/internal/o11y-cluster_a/diagnosis/api/v1/topsql/summary",
			query: url.Values{
				"start": {"1"}, "end": {"2"}, "window": {"1s"}, "top": {"5"},
				"instance": {"tidb-0"}, "instance_type": {"tidb"},
			},
			body: `{"status":"ok","data":[{"sql_digest":"abc","future_field":true}]}`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				require.Equal(t, http.MethodGet, r.Method)
				require.Equal(t, tt.path, r.URL.Path)
				require.Equal(t, tt.query.Encode(), r.URL.Query().Encode())
				require.Equal(t, "application/json", r.Header.Get("Accept"))
				require.Equal(t, "prod", r.Header.Get("X-Env"))
				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-east-1", r.Header.Get("X-Region-Id"))
				require.Equal(t, "cluster-a", r.Header.Get("X-Application-Id"))
				require.Empty(t, r.Header.Get("Authorization"))
				require.Empty(t, r.Header.Get("Cookie"))
				require.Empty(t, r.Header.Get("X-Forwarded-Host"))
				_, _ = io.WriteString(w, tt.body)
			}))
			defer upstream.Close()

			baseURL, client := byocTopSQLTestUpstream(upstream)
			resolver := &fakeBYOCTopSQLTargetResolver{target: validBYOCTopSQLTarget(baseURL)}
			svc := newBYOCTopSQLService(resolver, client)
			result, err := svc.Query(context.Background(), 123, "cluster-a", tt.operation, tt.query)
			require.NoError(t, err)
			require.Equal(t, []byte(tt.body), result.Body)
			require.Equal(t, "aws", result.Provider)
			require.Equal(t, "us-east-1", result.Region)
			require.Equal(t, "byoc", result.DeployTypeV2)
			require.Equal(t, uint64(123), resolver.orgID)
			require.Equal(t, "cluster-a", resolver.clusterID)
		})
	}
}

func TestBYOCTopSQLServiceAcceptsLogicalAndPhysicalBYOCTargets(t *testing.T) {
	for _, tt := range []struct {
		name         string
		deployType   string
		deployTypeV2 string
	}{
		{name: "logical", deployType: "byoc", deployTypeV2: "byoc"},
		{name: "physical", deployType: "byoc-host", deployTypeV2: "byoc resource pool"},
	} {
		t.Run(tt.name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				_, _ = io.WriteString(w, `{"status":"ok","data":[]}`)
			}))
			defer upstream.Close()
			baseURL, client := byocTopSQLTestUpstream(upstream)
			target := validBYOCTopSQLTarget(baseURL)
			target.DeployType = tt.deployType
			target.DeployTypeV2 = tt.deployTypeV2
			svc := newBYOCTopSQLService(&fakeBYOCTopSQLTargetResolver{target: target}, client)

			result, err := svc.Query(context.Background(), 123, "cluster-a", BYOCTopSQLOperationInstances, nil)
			require.NoError(t, err)
			require.Equal(t, tt.deployTypeV2, result.DeployTypeV2)
		})
	}
}

func TestBYOCTopSQLServiceForbidsStorageSelection(t *testing.T) {
	target := validBYOCTopSQLTarget("https://byoc-ds.example.test")
	svc := newBYOCTopSQLService(&fakeBYOCTopSQLTargetResolver{target: target}, nil)

	_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCTopSQLOperationSummary, url.Values{
		"data_source": {"deltalake"},
	})
	require.EqualError(t, err, "unsupported BYOC Top SQL query parameter")
}

func TestBYOCTopSQLServiceDoesNotFollowRedirect(t *testing.T) {
	destinationCalled := false
	destination := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		destinationCalled = true
		_, _ = io.WriteString(w, `{"status":"ok","data":[]}`)
	}))
	defer destination.Close()
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, destination.URL, http.StatusFound)
	}))
	defer upstream.Close()

	baseURL, client := byocTopSQLTestUpstream(upstream)
	svc := newBYOCTopSQLService(
		&fakeBYOCTopSQLTargetResolver{target: validBYOCTopSQLTarget(baseURL)},
		client,
	)
	_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCTopSQLOperationInstances, nil)
	var upstreamError *BYOCTopSQLUpstreamError
	require.ErrorAs(t, err, &upstreamError)
	require.Equal(t, http.StatusFound, upstreamError.StatusCode)
	require.False(t, destinationCalled)
}

func TestBYOCTopSQLServiceRedactsUpstreamAndCauseErrors(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		http.Error(w, "sensitive customer SQL and routing detail", http.StatusServiceUnavailable)
	}))
	defer upstream.Close()
	baseURL, client := byocTopSQLTestUpstream(upstream)
	svc := newBYOCTopSQLService(
		&fakeBYOCTopSQLTargetResolver{target: validBYOCTopSQLTarget(baseURL)},
		client,
	)
	_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCTopSQLOperationInstances, nil)
	var upstreamError *BYOCTopSQLUpstreamError
	require.ErrorAs(t, err, &upstreamError)
	require.Equal(t, http.StatusServiceUnavailable, upstreamError.StatusCode)
	require.NotContains(t, err.Error(), "sensitive customer SQL")

	secretCause := errors.New("secret target URL and token")
	svc = newBYOCTopSQLService(&fakeBYOCTopSQLTargetResolver{err: secretCause}, nil)
	_, err = svc.Query(context.Background(), 123, "cluster-a", BYOCTopSQLOperationInstances, nil)
	require.ErrorIs(t, err, secretCause)
	require.EqualError(t, err, "resolve BYOC Top SQL target failed")
}

func TestBYOCTopSQLServiceRejectsOversizedResponse(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.CopyN(w, strings.NewReader(strings.Repeat("x", maxBYOCTopSQLResponse+1)), maxBYOCTopSQLResponse+1)
	}))
	defer upstream.Close()
	baseURL, client := byocTopSQLTestUpstream(upstream)
	svc := newBYOCTopSQLService(
		&fakeBYOCTopSQLTargetResolver{target: validBYOCTopSQLTarget(baseURL)},
		client,
	)

	_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCTopSQLOperationInstances, nil)
	require.ErrorIs(t, err, ErrInvalidBYOCTopSQLResponse)
}

func TestBYOCTopSQLServiceRejectsInvalidEnvelope(t *testing.T) {
	for _, tt := range []struct {
		name string
		body string
	}{
		{name: "invalid JSON", body: `not-json`},
		{name: "multiple JSON values", body: `{"status":"ok","data":[]} {}`},
		{name: "array envelope", body: `[]`},
		{name: "missing status", body: `{"data":[]}`},
		{name: "empty status", body: `{"status":"","data":[]}`},
		{name: "non-string status", body: `{"status":true,"data":[]}`},
		{name: "missing data", body: `{"status":"ok"}`},
		{name: "null data", body: `{"status":"ok","data":null}`},
		{name: "object data", body: `{"status":"ok","data":{}}`},
	} {
		t.Run(tt.name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				_, _ = io.WriteString(w, tt.body)
			}))
			defer upstream.Close()
			baseURL, client := byocTopSQLTestUpstream(upstream)
			svc := newBYOCTopSQLService(
				&fakeBYOCTopSQLTargetResolver{target: validBYOCTopSQLTarget(baseURL)},
				client,
			)

			_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCTopSQLOperationInstances, nil)
			require.ErrorIs(t, err, ErrInvalidBYOCTopSQLResponse)
		})
	}
}

func TestBYOCTopSQLServiceRejectsInvalidTargets(t *testing.T) {
	for _, tt := range []struct {
		name   string
		mutate func(*clinic_internal_api.BYOCDiagnosisTarget)
	}{
		{name: "userinfo", mutate: func(target *clinic_internal_api.BYOCDiagnosisTarget) { target.BaseURL = "https://user@example.com" }},
		{name: "path", mutate: func(target *clinic_internal_api.BYOCDiagnosisTarget) { target.BaseURL = "https://example.com/caller" }},
		{name: "localhost", mutate: func(target *clinic_internal_api.BYOCDiagnosisTarget) { target.BaseURL = "http://localhost:8080" }},
		{name: "IPv4 loopback", mutate: func(target *clinic_internal_api.BYOCDiagnosisTarget) { target.BaseURL = "http://127.0.0.1:8080" }},
		{name: "cluster slash", mutate: func(target *clinic_internal_api.BYOCDiagnosisTarget) { target.O11yClusterID = "../other" }},
		{name: "org mismatch", mutate: func(target *clinic_internal_api.BYOCDiagnosisTarget) { target.Headers.OrgID = "999" }},
		{name: "application mismatch", mutate: func(target *clinic_internal_api.BYOCDiagnosisTarget) { target.Headers.ApplicationID = "cluster-b" }},
		{name: "caller header injection", mutate: func(target *clinic_internal_api.BYOCDiagnosisTarget) { target.Headers.RegionID += "\r\nX-Evil: true" }},
		{name: "other tier", mutate: func(target *clinic_internal_api.BYOCDiagnosisTarget) { target.DeployType = "premium" }},
	} {
		t.Run(tt.name, func(t *testing.T) {
			target := validBYOCTopSQLTarget("https://example.com")
			tt.mutate(target)
			svc := newBYOCTopSQLService(&fakeBYOCTopSQLTargetResolver{target: target}, nil)

			_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCTopSQLOperationInstances, nil)
			require.ErrorIs(t, err, ErrInvalidBYOCTopSQLTarget)
		})
	}
}

func TestBYOCTopSQLServicePreservesTypedTargetResolutionError(t *testing.T) {
	want := &clinic_internal_api.UpstreamError{StatusCode: http.StatusNotFound}
	svc := newBYOCTopSQLService(&fakeBYOCTopSQLTargetResolver{err: want}, nil)

	_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCTopSQLOperationInstances, nil)
	var got *clinic_internal_api.UpstreamError
	require.True(t, errors.As(err, &got))
	require.Equal(t, http.StatusNotFound, got.StatusCode)
}

func byocTopSQLTestUpstream(upstream *httptest.Server) (string, *http.Client) {
	client := *upstream.Client()
	transport := client.Transport.(*http.Transport).Clone()
	upstreamAddress := upstream.Listener.Addr().String()
	transport.DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
		return (&net.Dialer{}).DialContext(ctx, network, upstreamAddress)
	}
	client.Transport = transport
	return "http://byoc-ds.example.test", &client
}
