package qichacha_test

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension"
	"github.com/tidbcloud/AgentTeam/internal/extension/bootstrap"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/qichacha"
	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
	"github.com/tidbcloud/AgentTeam/internal/service/extensionflow"
	appregistry "github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

type staticCredentialSource struct {
	credentials runner.Credentials
}

func (source staticCredentialSource) ResolveProviderCredentials(
	context.Context, string, string,
) (runner.Credentials, error) {
	return source.credentials, nil
}

func insecureTransport() *http.Transport {
	return &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
}

var testCredentials = runner.Credentials{"appKey": "qcc-appkey", "secretKey": "qcc-secretkey"}

// TestSearchKeySchemaAllowsInternalSpaces locks the upstream search-key
// semantics: a value needs at least one non-whitespace character, so real
// company names with internal spaces are legal inputs, while empty and
// whitespace-only values stay rejected.
func TestSearchKeySchemaAllowsInternalSpaces(t *testing.T) {
	t.Parallel()

	var pattern string
	found := false
	for _, action := range qichacha.Definition().Actions {
		if action.Name != "list-company-shareholders" {
			continue
		}
		found = true
		properties, ok := action.InputSchema["properties"].(map[string]any)
		require.True(t, ok)
		propertySchema, ok := properties["searchKey"].(map[string]any)
		require.True(t, ok)
		pattern, ok = propertySchema["pattern"].(string)
		require.True(t, ok)
	}
	require.True(t, found, "list-company-shareholders not found in the catalog")

	expression := regexp.MustCompile(pattern)
	require.True(t, expression.MatchString("北京百度网讯科技有限公司"))
	require.True(t, expression.MatchString("ACME Holdings Ltd"))
	require.False(t, expression.MatchString(""))
	require.False(t, expression.MatchString("   "))
}

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

	extensionUnit, err := qichacha.Extension(staticCredentialSource{})
	require.NoError(t, err)

	names := make([]string, 0)
	for _, action := range extensionUnit.Definition.Actions {
		names = append(names, action.Name)
	}
	lister, ok := extensionUnit.Implementation.Executor.(runner.ActionLister)
	require.True(t, ok)
	require.ElementsMatch(t, names, lister.ActionNames())
	require.Len(t, names, 2)

	require.Equal(t, "qichacha-rest-v1", extensionUnit.Implementation.Revision)
	require.Equal(t, appregistry.QichachaAppID, extensionUnit.Distribution.AppID)
	require.Equal(t, appregistry.QichachaAppSlug, extensionUnit.Distribution.AppSlug)
	require.True(t, appregistry.IsProductApp(
		extensionUnit.Distribution.AppID, extensionUnit.Distribution.AppSlug))
	require.NoError(t, bootstrap.Register(registry.New(), []extension.Extension{extensionUnit}))
}

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

	extensionUnit, err := qichacha.Extension(staticCredentialSource{})
	require.NoError(t, err)
	reg := registry.New()
	require.NoError(t, bootstrap.Register(reg, []extension.Extension{extensionUnit}))
	entry, ok := reg.LookupByIdentity(qichacha.Domain, 1)
	require.True(t, ok)
	require.Equal(t, model.QichachaBuiltinExtensionDefinitionDigest, entry.Definition.Digest)
}

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

	spec := qichacha.AuthSpec()
	require.Equal(t, runner.AuthHeaders, spec.Kind)
	require.NoError(t, spec.ValidateSpec())
	require.NoError(t, spec.Validate(testCredentials))
	require.Error(t, spec.Validate(runner.Credentials{"appKey": "only"}))
	require.NotNil(t, spec.Signature)
	for _, field := range spec.Fields {
		require.Empty(t, field.HeaderName)
	}
	// The secretKey stays secret; the appKey rides the query string on the
	// wire, exactly like the upstream contract.
	for _, field := range spec.Fields {
		if field.Name == "secretKey" {
			require.True(t, field.Secret)
		}
	}
}

// wireServer records every request and answers with the scripted status and
// body for every call.
type wireServer struct {
	server   *httptest.Server
	requests []wireRequest
	status   int
	body     string
}

type wireRequest struct {
	method string
	path   string
	query  url.Values
	body   string
	header http.Header
}

func newWireServer(t *testing.T, status int, body string) *wireServer {
	t.Helper()
	wire := &wireServer{status: status, body: body}
	wire.server = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		bodyBytes, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		query, err := url.ParseQuery(r.URL.RawQuery)
		require.NoError(t, err)
		wire.requests = append(wire.requests, wireRequest{
			method: r.Method,
			path:   r.URL.Path,
			query:  query,
			body:   string(bodyBytes),
			header: r.Header.Clone(),
		})
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(status)
		_, _ = w.Write([]byte(body))
	}))
	t.Cleanup(wire.server.Close)
	return wire
}

func invoke(
	t *testing.T,
	wire *wireServer,
	action string,
	input string,
) (json.RawMessage, error) {
	t.Helper()
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:          wire.server.URL,
		Auth:             qichacha.AuthSpec(),
		StatusClassifier: qichacha.ClassifyStatus,
		Transport:        insecureTransport(),
	})
	require.NoError(t, err)
	executor := runner.NewRESTExecutor(
		qichacha.Domain,
		client,
		staticCredentialSource{credentials: testCredentials},
		qichacha.Handlers(),
	)
	result, err := executor.Invoke(context.Background(), registry.BuiltinInvocation{
		WorkspaceID:           "ws",
		ServiceInstallationID: "inst",
		ExtensionName:         qichacha.Domain,
		Action:                action,
		Input:                 json.RawMessage(input),
	})
	if err != nil {
		return nil, err
	}
	return result.Data, nil
}

// requireSignatureHeaders recomputes the expected MD5 token from the
// observed Timespan so the signature is verified against live values.
func requireSignatureHeaders(t *testing.T, header http.Header) {
	t.Helper()
	timespan := 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[:])), header.Get("Token"))
	require.Equal(t, "agentteam-extension", header.Get("User-Agent"))
}

const shareholdersFixture = `{
  "Status": "200",
  "OrderNumber": "ORD-1",
  "Paging": {"PageIndex": 1, "PageSize": 10, "TotalRecords": 2},
  "Result": {
    "VerifyResult": 1,
    "Data": [
      {"KeyNo": "p1", "StockName": "张三", "StockPercent": "60.00%",
       "ShouldCapi": "120万元", "SubscribedList": [
         {"ContributionType": "货币", "Capital": "60万元", "Date": "2020-01-01"}]},
      {"KeyNo": "p2", "StockName": "李四", "StockPercent": "40.00%"}
    ]
  }
}`

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

	wire := newWireServer(t, http.StatusOK, shareholdersFixture)
	output, err := invoke(t, wire, "list-company-shareholders",
		`{"searchKey":"北京百度网讯科技有限公司","pageIndex":1,"pageSize":10}`)
	require.NoError(t, err)
	request := wire.requests[0]
	require.Equal(t, http.MethodGet, request.method)
	require.Equal(t, "/ECIPartner/GetList", request.path)
	require.Equal(t, []string{"qcc-appkey"}, request.query["key"])
	require.Equal(t, "北京百度网讯科技有限公司", request.query.Get("searchKey"))
	require.Equal(t, "1", request.query.Get("pageIndex"))
	require.Equal(t, "10", request.query.Get("pageSize"))
	requireSignatureHeaders(t, request.header)

	for _, part := range []string{
		`"shareholders":[`,
		`"keyNo":"p1"`,
		`"name":"张三"`,
		`"stockPercent":"60.00%"`,
		`"subscriptions":[{"capital":"60万元","contributionType":"货币","date":"2020-01-01"}]`,
		`"keyNo":"p2"`,
		`"totalRecords":2`,
		`"verifyResult":1`,
		`"orderNumber":"ORD-1"`,
		`"pageIndex":1`,
		`"pageSize":10`,
	} {
		require.Truef(t, strings.Contains(string(output), part),
			"output %s must contain %s", output, part)
	}
}

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

	fixture := `{
	  "Status": "200",
	  "Result": [
	    {"KeyNo": "c1", "CompanyName": "子公司甲", "OperInfo": {"KeyNo": "p1", "Name": "王五"},
	     "RegistCapi": "500万元", "FundedRatio": "100.00%", "ExitDate": null}
	  ]
	}`
	wire := newWireServer(t, http.StatusOK, fixture)
	output, err := invoke(t, wire, "list-company-historical-investments", `{"searchKey":"百度"}`)
	require.NoError(t, err)
	request := wire.requests[0]
	require.Equal(t, "/HistoryInvestmentCheck/GetList", request.path)
	require.Equal(t, []string{"qcc-appkey"}, request.query["key"])
	require.Equal(t, "1", request.query.Get("pageIndex"))
	require.Equal(t, "10", request.query.Get("pageSize"))
	require.Equal(t, "百度", request.query.Get("searchKey"))
	requireSignatureHeaders(t, request.header)

	for _, part := range []string{
		`"historicalInvestments":[`,
		`"companyName":"子公司甲"`,
		`"legalRepresentative":{"keyNo":"p1","name":"王五"}`,
		`"registeredCapital":"500万元"`,
		`"fundedRatio":"100.00%"`,
		`"exitDate":null`,
		`"totalRecords":1`,
		`"verifyResult":null`,
		`"orderNumber":null`,
	} {
		require.Truef(t, strings.Contains(string(output), part),
			"output %s must contain %s", output, part)
	}
}

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

	cases := []struct {
		name   string
		status int
		body   string
		code   extensionflow.InvocationErrorCode
	}{
		{name: "http-401", status: 401, code: extensionflow.InvocationProviderAuthUnavailable},
		{name: "http-429", status: 429, code: extensionflow.InvocationRateLimited},
		{
			name: "signature-message", status: 200,
			body: `{"Status":"401","Message":"Token验证失败"}`,
			code: extensionflow.InvocationProviderAuthUnavailable,
		},
		{
			name: "secret-message", status: 200,
			body: `{"Status":"403","Message":"AppKey不存在"}`,
			code: extensionflow.InvocationProviderAuthUnavailable,
		},
		{
			name: "rate-message", status: 200,
			body: `{"Status":"429","Message":"请求频率超限"}`,
			code: extensionflow.InvocationRateLimited,
		},
		{
			name: "plain-business-failure", status: 200,
			body: `{"Status":"500","Message":"服务异常"}`,
			code: extensionflow.InvocationUpstreamFailure,
		},
		{
			name: "malformed-envelope", status: 200,
			body: `[]`,
			code: extensionflow.InvocationUpstreamFailure,
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()
			wire := newWireServer(t, testCase.status, testCase.body)
			_, err := invoke(t, wire, "list-company-shareholders", `{"searchKey":"百度"}`)
			mapped := runner.InvocationError(err)
			require.NotNil(t, mapped)
			var invocation *extensionflow.InvocationError
			require.ErrorAs(t, mapped, &invocation)
			require.Equal(t, testCase.code, invocation.Code)
		})
	}
}

// TestWireNormalizesScalarInputs locks the pinned input normalization on the
// wire: a padded searchKey arrives trimmed with internal spacing preserved,
// and schema-legal exponent/decimal pagination lexicals re-encode as plain
// decimal digits.
func TestWireNormalizesScalarInputs(t *testing.T) {
	t.Parallel()

	wire := newWireServer(t, http.StatusOK, shareholdersFixture)
	output, err := invoke(t, wire, "list-company-shareholders",
		`{"searchKey":" 北京百度网讯科技有限公司 ","pageIndex":1e0,"pageSize":1.0e1}`)
	require.NoError(t, err)
	request := wire.requests[0]
	require.Equal(t, "北京百度网讯科技有限公司", request.query.Get("searchKey"))
	require.Equal(t, "1", request.query.Get("pageIndex"))
	require.Equal(t, "10", request.query.Get("pageSize"))
	requireSignatureHeaders(t, request.header)
	require.Truef(t, strings.Contains(string(output), `"pageIndex":1`),
		"output %s must echo the plain-decimal page index", output)
	require.Truef(t, strings.Contains(string(output), `"pageSize":10`),
		"output %s must echo the plain-decimal page size", output)
}

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

	wire := newWireServer(t, http.StatusOK, `{
	  "Status":"200",
	  "Paging":{"PageIndex":1e0,"PageSize":1.0e1,"TotalRecords":2.0},
	  "Result":{"VerifyResult":1e0,"Data":[]}
	}`)
	output, err := invoke(t, wire, "list-company-shareholders", `{"searchKey":"百度"}`)
	require.NoError(t, err)
	for _, part := range []string{
		`"pageIndex":1`,
		`"pageSize":10`,
		`"totalRecords":2`,
		`"verifyResult":1`,
	} {
		require.Contains(t, string(output), part)
	}
}

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

	cases := []struct {
		name string
		body string
	}{
		{
			name: "fractional-page-index",
			body: `{"Status":"200","Paging":{"PageIndex":1.5,"PageSize":10,"TotalRecords":0},"Result":[]}`,
		},
		{
			name: "non-positive-page-index",
			body: `{"Status":"200","Paging":{"PageIndex":0,"PageSize":10,"TotalRecords":0},"Result":[]}`,
		},
		{
			name: "non-positive-page-size",
			body: `{"Status":"200","Paging":{"PageIndex":1,"PageSize":0,"TotalRecords":0},"Result":[]}`,
		},
		{
			name: "negative-total-records",
			body: `{"Status":"200","Paging":{"PageIndex":1,"PageSize":10,"TotalRecords":-1},"Result":[]}`,
		},
		{
			name: "fractional-verify-result",
			body: `{"Status":"200","Result":{"VerifyResult":0.5,"Data":[]}}`,
		},
		{
			name: "out-of-range-verify-result",
			body: `{"Status":"200","Result":{"VerifyResult":2,"Data":[]}}`,
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()

			wire := newWireServer(t, http.StatusOK, testCase.body)
			_, err := invoke(t, wire, "list-company-shareholders", `{"searchKey":"百度"}`)
			mapped := runner.InvocationError(err)
			require.NotNil(t, mapped)
			var invocation *extensionflow.InvocationError
			require.ErrorAs(t, mapped, &invocation)
			require.Equal(t, extensionflow.InvocationUpstreamFailure, invocation.Code)
		})
	}
}

// TestWireRejectsFractionalPagination fails closed before any request leaves
// the process: fractional lexicals are not integers.
func TestWireRejectsFractionalPagination(t *testing.T) {
	t.Parallel()

	wire := newWireServer(t, http.StatusOK, shareholdersFixture)
	_, err := invoke(t, wire, "list-company-shareholders",
		`{"searchKey":"百度","pageIndex":1.5}`)
	var invocation *extensionflow.InvocationError
	require.ErrorAs(t, err, &invocation)
	require.Equal(t, extensionflow.InvocationValidation, invocation.Code)
	require.Empty(t, wire.requests)
}
