package qichacha

import (
	"context"
	"crypto/md5"
	"crypto/tls"
	"encoding/hex"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
)

// probeCredentials mirrors the external wire fixtures: a visible AppKey and
// a SecretKey that must never ride the probe in plaintext.
var probeCredentials = runner.Credentials{"appKey": "qcc-appkey", "secretKey": "qcc-secretkey"}

// probeCapture records the raw material of every request the probe server
// receives, so the tests can assert on the exact wire shape.
type probeCapture struct {
	requests int
	query    url.Values
	rawQuery string
	header   http.Header
}

func newProbeServer(t *testing.T, status int, body string) (*httptest.Server, *probeCapture) {
	t.Helper()
	capture := &probeCapture{}
	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		capture.requests++
		query, err := url.ParseQuery(r.URL.RawQuery)
		require.NoError(t, err)
		capture.query = query
		capture.rawQuery = r.URL.RawQuery
		capture.header = r.Header.Clone()
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(status)
		_, _ = w.Write([]byte(body))
	}))
	t.Cleanup(server.Close)
	return server, capture
}

func newProbeClient(t *testing.T, server *httptest.Server) *runner.RESTClient {
	t.Helper()
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:          server.URL,
		Auth:             AuthSpec(),
		StatusClassifier: ClassifyStatus,
		Transport:        &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
	})
	require.NoError(t, err)
	return client
}

func TestValidateCredentialsProbeCarriesAppKeyAndSignature(t *testing.T) {
	t.Parallel()

	server, capture := newProbeServer(t, http.StatusOK, `{"Status":"200","OrderNumber":"ORD-1"}`)
	client := newProbeClient(t, server)

	require.NoError(t, validateCredentials(context.Background(), client, probeCredentials))
	require.Equal(t, 1, capture.requests)
	// The probe carries exactly one key parameter with the trimmed AppKey
	// plus the fixed safe search word, and nothing else.
	require.Equal(t, []string{"qcc-appkey"}, capture.query["key"])
	require.Equal(t, []string{"验证"}, capture.query["searchKey"])
	require.Len(t, capture.query, 2)

	// Timespan/Token recompute from the same AppKey/SecretKey pair, so the
	// probe signature is the production signature.
	timespan := capture.header.Get("Timespan")
	require.NotEmpty(t, timespan)
	parsed, err := strconv.ParseInt(timespan, 10, 64)
	require.NoError(t, err)
	require.InDelta(t, time.Now().Unix(), parsed, 120)
	digest := md5.Sum([]byte("qcc-appkey" + timespan + "qcc-secretkey"))
	require.Equal(t, strings.ToUpper(hex.EncodeToString(digest[:])), capture.header.Get("Token"))

	// The SecretKey never appears in plaintext anywhere on the probe.
	require.NotContains(t, capture.rawQuery, "qcc-secretkey")
	for _, values := range capture.query {
		for _, value := range values {
			require.NotContains(t, value, "qcc-secretkey")
		}
	}
	for _, values := range capture.header {
		for _, value := range values {
			require.NotContains(t, value, "qcc-secretkey")
		}
	}
}

// TestValidateCredentialsProbeEnvelope locks the success contract: Status
// 200 alone is the identity proof. OrderNumber is nullable on this provider
// and must not turn a working credential into a false negative.
func TestValidateCredentialsProbeEnvelope(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name     string
		body     string
		wantCode runner.ErrorCode
	}{
		{name: "processed-order", body: `{"Status":"200","OrderNumber":"ORD-1"}`},
		{name: "nullable-order", body: `{"Status":"200","OrderNumber":null}`},
		{name: "absent-order", body: `{"Status":"200"}`},
		{
			name: "auth-rejection", body: `{"Status":"401","Message":"Token验证失败"}`,
			wantCode: runner.ErrorProviderAuthUnavailable,
		},
		{
			name: "rate-rejection", body: `{"Status":"429","Message":"请求频率超限"}`,
			wantCode: runner.ErrorRateLimited,
		},
		{
			name: "upstream-rejection", body: `{"Status":"500","Message":"服务异常"}`,
			wantCode: runner.ErrorUpstreamFailure,
		},
		{
			name: "missing-status", body: `{"Message":"服务异常"}`,
			wantCode: runner.ErrorUpstreamFailure,
		},
		{
			name: "null-status", body: `{"Status":null}`,
			wantCode: runner.ErrorUpstreamFailure,
		},
		{
			name: "non-string-status", body: `{"Status":200}`,
			wantCode: runner.ErrorUpstreamFailure,
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()

			server, capture := newProbeServer(t, http.StatusOK, testCase.body)
			client := newProbeClient(t, server)
			err := validateCredentials(context.Background(), client, probeCredentials)
			if testCase.wantCode != "" {
				require.Equal(t, testCase.wantCode, runner.CodeOf(err))
				require.Equal(t, 1, capture.requests)
				return
			}
			require.NoError(t, err)
			require.Equal(t, 1, capture.requests)
		})
	}
}

func TestValidateCredentialsFailsClosedWithoutAppKey(t *testing.T) {
	t.Parallel()

	server, capture := newProbeServer(t, http.StatusOK, `{"Status":"200","OrderNumber":"ORD-1"}`)
	client := newProbeClient(t, server)

	for name, credentials := range map[string]runner.Credentials{
		"missing": {"secretKey": "qcc-secretkey"},
		"empty":   {"appKey": "", "secretKey": "qcc-secretkey"},
	} {
		err := validateCredentials(context.Background(), client, credentials)
		require.Errorf(t, err, "appKey %s must fail closed", name)
	}
	// No request left the process: the rejection is local.
	require.Zero(t, capture.requests)
}
