package tianyancha_test

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension/providers/tianyancha"
	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
	"github.com/tidbcloud/AgentTeam/internal/service/extensionflow"
)

// 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 (wire *wireServer) last() wireRequest {
	if len(wire.requests) == 0 {
		panic("no recorded request")
	}
	return wire.requests[len(wire.requests)-1]
}

// invoke runs one action against the double through the production handler
// table and classifier, with a loopback-friendly transport like every wire
// double in this package.
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:             tianyancha.AuthSpec(),
		StatusClassifier: tianyancha.ClassifyStatus,
		Transport:        insecureTransport(),
	})
	require.NoError(t, err)
	executor := runner.NewRESTExecutor(
		tianyancha.Domain,
		client,
		staticCredentialSource{credentials: runner.Credentials{"apiKey": "tyc-token"}},
		tianyancha.Handlers(),
	)
	result, err := executor.Invoke(context.Background(), registry.BuiltinInvocation{
		WorkspaceID:           "ws",
		ServiceInstallationID: "inst",
		ExtensionName:         tianyancha.Domain,
		Action:                action,
		Input:                 json.RawMessage(input),
	})
	if err != nil {
		return nil, err
	}
	return result.Data, nil
}

func requireTianyanchaHeaders(t *testing.T, request wireRequest) {
	t.Helper()
	require.Equal(t, "tyc-token", request.header.Get("Authorization"))
	require.Equal(t, "agentteam-extension", request.header.Get("User-Agent"))
}

func requireQuery(t *testing.T, request wireRequest, expected map[string]string) {
	t.Helper()
	require.Len(t, request.query, len(expected))
	for name, value := range expected {
		require.Equalf(t, value, request.query.Get(name), "query parameter %s", name)
	}
}

func pagedFixture() string {
	return `{"error_code":0,"reason":"ok","result":{"items":[{"id":1},{"id":2}],"total":57}}`
}

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

	cases := []wireCase{
		{
			action: "search-companies", input: `{"keyword":"阿里"}`,
			path: "/services/open/search/2.0", query: map[string]string{"word": "阿里"},
			response: pagedFixture(), outputPart: []string{`"total":57`, `"pageNum":1`, `"pageSize":20`, `"companies":[{"id":1},{"id":2}]`},
		},
		{
			action: "search-companies", input: `{"keyword":"阿里","pageNum":2,"pageSize":5}`,
			path: "/services/open/search/2.0", query: map[string]string{"word": "阿里", "pageNum": "2", "pageSize": "5"},
			response: pagedFixture(), outputPart: []string{`"pageNum":2`, `"pageSize":5`},
		},
		{
			action: "search-companies-advanced", input: `{"keyword":"阿里","industryCode":"I64","areaCode":"310000"}`,
			path:     "/services/open/searchx",
			query:    map[string]string{"word": "阿里", "categoryGuobiao": "I64", "areaCode": "310000"},
			response: pagedFixture(),
		},
		{
			action: "get-company-basic-info", input: `{"keyword":"91110000802100433C"}`,
			path:       "/services/open/ic/baseinfo/normal",
			query:      map[string]string{"keyword": "91110000802100433C"},
			response:   `{"error_code":0,"result":{"id":22822,"name":"北京百度网讯科技有限公司"}}`,
			outputPart: []string{`"company":{"id":22822,"name":"北京百度网讯科技有限公司"}`},
		},
		{
			action: "get-company-basic-info", input: `{"keyword":"不存在的公司zz"}`,
			path: "/services/open/ic/baseinfo/normal", query: map[string]string{"keyword": "不存在的公司zz"},
			response:   `{"error_code":300000,"reason":"查询无结果"}`,
			outputPart: []string{`"company":null`},
		},
		{
			action: "verify-company-identity",
			input:  `{"companyName":"北京百度网讯科技有限公司","legalPersonName":"梁志祥","registrationCode":"91110000802100433C"}`,
			path:   "/services/open/ic/verify/2.0",
			query: map[string]string{
				"name": "北京百度网讯科技有限公司", "legalPersonName": "梁志祥", "code": "91110000802100433C",
			},
			response:   `{"error_code":0,"result":{"result":1,"remark":"一致"}}`,
			outputPart: []string{`"matchStatus":1`, `"remark":"一致"`},
		},
		{
			action:     "verify-company-identity",
			input:      `{"companyName":"旧名称","legalPersonName":"张三","registrationCode":"123"}`,
			path:       "/services/open/ic/verify/2.0",
			query:      map[string]string{"name": "旧名称", "legalPersonName": "张三", "code": "123"},
			response:   `{"error_code":300000}`,
			outputPart: []string{`"matchStatus":null`, `"remark":null`},
		},
		{
			action: "get-company-contact-info", input: `{"keyword":"百度"}`,
			path: "/services/open/ic/contact", query: map[string]string{"keyword": "百度"},
			response:   `{"error_code":0,"result":{"phoneNumber":"010-59928888"}}`,
			outputPart: []string{`"contact":{"phoneNumber":"010-59928888"}`},
		},
		{
			action: "list-company-key-personnel", input: `{"keyword":"百度","pageNum":1,"pageSize":10}`,
			path: "/services/open/ic/staff/2.0", query: map[string]string{"keyword": "百度", "pageNum": "1", "pageSize": "10"},
			response: pagedFixture(), outputPart: []string{`"personnel":[{"id":1},{"id":2}]`},
		},
		{
			action: "list-company-shareholders", input: `{"keyword":"百度","source":2}`,
			path: "/services/open/ic/holder/2.0", query: map[string]string{"keyword": "百度", "source": "2"},
			response: pagedFixture(), outputPart: []string{`"shareholders":`},
		},
		{
			action: "list-company-investments", input: `{"keyword":"百度"}`,
			path: "/services/open/ic/inverst/2.0", query: map[string]string{"keyword": "百度"},
			response: pagedFixture(),
		},
		{
			action: "list-company-equity-changes", input: `{"keyword":"百度"}`,
			path: "/services/open/ic/holderChange/2.0", query: map[string]string{"keyword": "百度"},
			response: pagedFixture(),
		},
		{
			action: "list-company-historical-shareholders", input: `{"keyword":"百度"}`,
			path: "/services/open/hi/holder/2.0", query: map[string]string{"keyword": "百度"},
			response: pagedFixture(),
		},
		{
			action: "list-company-historical-investments", input: `{"keyword":"百度"}`,
			path: "/services/open/hi/invest/2.0", query: map[string]string{"keyword": "百度"},
			response: pagedFixture(),
		},
		{
			action: "list-company-branches", input: `{"keyword":"百度"}`,
			path: "/services/open/ic/branch/2.0", query: map[string]string{"keyword": "百度"},
			response: pagedFixture(),
		},
		{
			action: "list-company-changes", input: `{"keyword":"百度"}`,
			path: "/services/open/ic/changeinfo/2.0", query: map[string]string{"keyword": "百度"},
			response: pagedFixture(),
		},
		{
			action: "get-company-annual-reports", input: `{"keyword":"百度","year":"2023"}`,
			path: "/services/open/ic/annualreport/2.0", query: map[string]string{"keyword": "百度", "year": "2023"},
			response: pagedFixture(), outputPart: []string{`"annualReports":[{"id":1},{"id":2}]`, `"total":57`},
		},
	}

	runWireCases(t, cases)
}

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

	cases := []wireCase{
		{
			action: "get-company-risk", input: `{"keyword":"百度"}`,
			path: "/services/open/risk/riskInfo/2.0", query: map[string]string{"keyword": "百度"},
			response:   `{"error_code":0,"result":{"riskLevel":"中","riskList":[{"self":[]}]}}`,
			outputPart: []string{`"riskLevel":"中"`, `"risks":[{"self":[]}]`},
		},
		{
			action: "get-company-risk", input: `{"keyword":"无风险公司"}`,
			path: "/services/open/risk/riskInfo/2.0", query: map[string]string{"keyword": "无风险公司"},
			response:   `{"error_code":300000}`,
			outputPart: []string{`"riskLevel":null`, `"risks":[]`},
		},
		{
			action: "get-company-risk-detail", input: `{"riskId":123,"riskType":2,"pageNum":3,"pageSize":5}`,
			path:       "/services/v4/open/riskDetail",
			query:      map[string]string{"id": "123", "type": "2", "pageNum": "3", "pageSize": "5"},
			response:   `{"error_code":0,"result":{"type":3,"count":12,"dataList":[{"id":9}],"pn":2}}`,
			outputPart: []string{`"riskType":3`, `"records":[{"id":9}]`, `"total":12`, `"pageNum":2`, `"pageSize":5`},
		},
		{
			action: "get-company-risk-detail", input: `{"riskId":123,"riskType":2}`,
			path: "/services/v4/open/riskDetail", query: map[string]string{"id": "123", "type": "2"},
			response:   `{"error_code":300000}`,
			outputPart: []string{`"riskType":2`, `"records":[]`, `"total":0`, `"pageNum":1`, `"pageSize":20`},
		},
		{
			action: "get-company-judicial-risk", input: `{"keyword":"百度"}`,
			path: "/services/open/cb/judicial/2.0", query: map[string]string{"keyword": "百度"},
			response:   `{"error_code":0,"result":{"caseCount":3}}`,
			outputPart: []string{`"judicialRisk":{"caseCount":3}`},
		},
		{
			action: "get-company-development-info", input: `{"keyword":"百度"}`,
			path: "/services/open/cb/development/2.0", query: map[string]string{"keyword": "百度"},
			response:   `{"error_code":0,"result":{"team":[]}}`,
			outputPart: []string{`"development":{"team":[]}`},
		},
		{
			action: "get-company-intellectual-property", input: `{"keyword":"百度"}`,
			path: "/services/open/cb/ipr/3.0", query: map[string]string{"keyword": "百度"},
			response:   `{"error_code":0,"result":{"trademark":[]}}`,
			outputPart: []string{`"intellectualProperty":{"trademark":[]}`},
		},
		{
			action: "list-company-ultimate-beneficiaries", input: `{"keyword":"百度"}`,
			path: "/services/open/ic/humanholding/2.0", query: map[string]string{"keyword": "百度"},
			response: pagedFixture(),
		},
		{
			action: "get-company-actual-control", input: `{"keyword":"百度"}`,
			path: "/services/open/ic/actualControl/3.0", query: map[string]string{"keyword": "百度"},
			response:   `{"error_code":0,"result":{"actualControllerList":[{"name":"李彦宏"}],"pathMap":{"p_1":{"a":1}}}}`,
			outputPart: []string{`"controllers":[{"name":"李彦宏"}]`, `"paths":{"p_1":{"a":1}}`},
		},
		{
			action: "get-company-actual-control", input: `{"keyword":"无"}`,
			path: "/services/open/ic/actualControl/3.0", query: map[string]string{"keyword": "无"},
			response:   `{"error_code":300000}`,
			outputPart: []string{`"controllers":[]`, `"paths":{}`},
		},
		{
			action: "find-company-relationship-paths",
			input:  `{"sourceCompanyName":"甲公司","targetCompanyId":456,"relationshipTypes":["investment","branch"],"maxDepth":3}`,
			path:   "/services/open/rela/shortPath/2.0",
			query: map[string]string{
				"nameFrom": "甲公司", "idTo": "456", "types": "INVEST,BRANCH", "depth": "3",
			},
			response:   `{"error_code":0,"result":{"p_1":{"nodes":[]},"count":1}}`,
			outputPart: []string{`"hasRelationship":true`, `"paths":[{"nodes":[]}]`},
		},
		{
			action:     "find-company-relationship-paths",
			input:      `{"sourceCompanyId":1,"targetCompanyId":2}`,
			path:       "/services/open/rela/shortPath/2.0",
			query:      map[string]string{"idFrom": "1", "idTo": "2"},
			response:   `{"error_code":300000}`,
			outputPart: []string{`"hasRelationship":false`, `"paths":[]`},
		},
		{
			action: "search-tenders",
			input:  `{"keyword":"数据中心","searchTypes":["title","supplier"],"noticeType":"result","province":"北京","publishStartDate":"2024-01-01","publishEndDate":"2024-06-30","pageSize":20}`,
			path:   "/services/open/m/bids/search",
			query: map[string]string{
				"keyword": "数据中心", "searchType": "1,3", "type": "4", "province": "北京",
				"publishStartTime": "2024-01-01", "publishEndTime": "2024-06-30", "pageSize": "20",
			},
			response: pagedFixture(),
		},
		{
			action: "list-company-news",
			input:  `{"companyName":"百度","startDate":"2024-01-01","endDate":"2024-03-31","tags":["科技","财经"]}`,
			path:   "/services/open/ps/news/2.0",
			// Company-news is the one route whose dates go upstream compact.
			query: map[string]string{
				"name": "百度", "startTime": "20240101", "endTime": "20240331", "tags": "科技,财经",
			},
			response: pagedFixture(), outputPart: []string{`"news":`},
		},
		{
			action: "list-company-news", input: `{"companyId":22822}`,
			path: "/services/open/ps/news/2.0", query: map[string]string{"id": "22822"},
			response: pagedFixture(),
		},
	}

	runWireCases(t, cases)
}

// TestWireNormalizesScalarInputs locks the pinned input normalization on the
// wire: padded scalar strings arrive trimmed with internal spacing
// preserved, and schema-legal exponent/decimal integer lexicals re-encode
// as plain decimal digits before the query leaves the process.
func TestWireNormalizesScalarInputs(t *testing.T) {
	t.Parallel()

	cases := []wireCase{
		{
			action: "search-companies", input: `{"keyword":"  百度 "}`,
			path: "/services/open/search/2.0", query: map[string]string{"word": "百度"},
			response: pagedFixture(),
		},
		{
			action: "search-companies", input: `{"keyword":"北京 百度","pageNum":1e1,"pageSize":2.0}`,
			path:     "/services/open/search/2.0",
			query:    map[string]string{"word": "北京 百度", "pageNum": "10", "pageSize": "2"},
			response: pagedFixture(), outputPart: []string{`"pageNum":10`, `"pageSize":2`},
		},
		{
			action: "search-companies-advanced",
			input:  `{"keyword":" 百度 ","industryCode":" I64 ","areaCode":" 310000 ","pageNum":3e0}`,
			path:   "/services/open/searchx",
			query: map[string]string{
				"word": "百度", "categoryGuobiao": "I64", "areaCode": "310000", "pageNum": "3",
			},
			response: pagedFixture(),
		},
		{
			action: "get-company-basic-info", input: `{"keyword":" 91110000802100433C "}`,
			path:     "/services/open/ic/baseinfo/normal",
			query:    map[string]string{"keyword": "91110000802100433C"},
			response: `{"error_code":0,"result":{"id":22822,"name":"北京百度网讯科技有限公司"}}`,
		},
		{
			action: "verify-company-identity",
			input:  `{"companyName":" 北京百度网讯科技有限公司 ","legalPersonName":" 梁志祥 ","registrationCode":" 91110000802100433C "}`,
			path:   "/services/open/ic/verify/2.0",
			query: map[string]string{
				"name": "北京百度网讯科技有限公司", "legalPersonName": "梁志祥", "code": "91110000802100433C",
			},
			response: `{"error_code":0,"result":{"result":1,"remark":"一致"}}`,
		},
		{
			action: "get-company-annual-reports", input: `{"keyword":" 百度 ","year":" 2023 "}`,
			path: "/services/open/ic/annualreport/2.0", query: map[string]string{"keyword": "百度", "year": "2023"},
			response: pagedFixture(),
		},
		{
			action: "list-company-key-personnel",
			input:  `{"keyword":" 北京 百度 ","pageNum":1e1,"pageSize":2.0}`,
			path:   "/services/open/ic/staff/2.0",
			query: map[string]string{
				"keyword": "北京 百度", "pageNum": "10", "pageSize": "2",
			},
			response: pagedFixture(),
		},
		{
			action: "list-company-shareholders", input: `{"keyword":" 百度 ","source":2e0}`,
			path: "/services/open/ic/holder/2.0", query: map[string]string{"keyword": "百度", "source": "2"},
			response: pagedFixture(),
		},
		{
			action: "get-company-risk-detail", input: `{"riskId":1.23e2,"riskType":2e0,"pageNum":1e1,"pageSize":2.0}`,
			path: "/services/v4/open/riskDetail",
			query: map[string]string{
				"id": "123", "type": "2", "pageNum": "10", "pageSize": "2",
			},
			response: `{"error_code":0,"result":{"type":3,"count":12,"dataList":[{"id":9}],"pn":2}}`,
		},
		{
			action: "find-company-relationship-paths",
			input:  `{"sourceCompanyName":" 甲公司 ","targetCompanyId":4.56e2,"relationshipTypes":["investment"],"maxDepth":3e0}`,
			path:   "/services/open/rela/shortPath/2.0",
			query: map[string]string{
				"nameFrom": "甲公司", "idTo": "456", "types": "INVEST", "depth": "3",
			},
			response: `{"error_code":0,"result":{"p_1":{"nodes":[]},"count":1}}`,
		},
		{
			action:   "search-tenders",
			input:    `{"keyword":" 数据中心 ","province":" 北京 ","pageSize":2e1}`,
			path:     "/services/open/m/bids/search",
			query:    map[string]string{"keyword": "数据中心", "province": "北京", "pageSize": "20"},
			response: pagedFixture(),
		},
		{
			action: "list-company-news", input: `{"companyName":" 百度 ","companyId":2.2822e4}`,
			path: "/services/open/ps/news/2.0", query: map[string]string{"name": "百度", "id": "22822"},
			response: pagedFixture(),
		},
	}

	runWireCases(t, cases)
}

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

	cases := []struct {
		name       string
		action     string
		input      string
		response   string
		outputPart []string
	}{
		{
			name:       "company-verification",
			action:     "verify-company-identity",
			input:      `{"companyName":"百度","legalPersonName":"张三","registrationCode":"123"}`,
			response:   `{"error_code":0,"result":{"result":1.0,"remark":"一致"}}`,
			outputPart: []string{`"matchStatus":1`},
		},
		{
			name:       "paged-total",
			action:     "search-companies",
			input:      `{"keyword":"百度"}`,
			response:   `{"error_code":0,"result":{"items":[],"total":2e1}}`,
			outputPart: []string{`"total":20`},
		},
		{
			name:       "risk-detail",
			action:     "get-company-risk-detail",
			input:      `{"riskId":123,"riskType":2}`,
			response:   `{"error_code":0,"result":{"type":3.0,"count":1.2e1,"dataList":[],"pn":2e0}}`,
			outputPart: []string{`"riskType":3`, `"total":12`, `"pageNum":2`},
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()

			wire := newWireServer(t, http.StatusOK, testCase.response)
			output, err := invoke(t, wire, testCase.action, testCase.input)
			require.NoError(t, err)
			for _, part := range testCase.outputPart {
				require.Contains(t, string(output), part)
			}
		})
	}
}

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

	cases := []struct {
		name     string
		action   string
		input    string
		response string
	}{
		{
			name: "fractional-match-status", action: "verify-company-identity",
			input:    `{"companyName":"百度","legalPersonName":"张三","registrationCode":"123"}`,
			response: `{"error_code":0,"result":{"result":0.5}}`,
		},
		{
			name: "negative-match-status", action: "verify-company-identity",
			input:    `{"companyName":"百度","legalPersonName":"张三","registrationCode":"123"}`,
			response: `{"error_code":0,"result":{"result":-1}}`,
		},
		{
			name: "out-of-range-match-status", action: "verify-company-identity",
			input:    `{"companyName":"百度","legalPersonName":"张三","registrationCode":"123"}`,
			response: `{"error_code":0,"result":{"result":3}}`,
		},
		{
			name: "fractional-page-total", action: "search-companies",
			input:    `{"keyword":"百度"}`,
			response: `{"error_code":0,"result":{"items":[],"total":1.5}}`,
		},
		{
			name: "negative-page-total", action: "search-companies",
			input:    `{"keyword":"百度"}`,
			response: `{"error_code":0,"result":{"items":[],"total":-1}}`,
		},
		{
			name: "fractional-risk-type", action: "get-company-risk-detail",
			input:    `{"riskId":123,"riskType":2}`,
			response: `{"error_code":0,"result":{"type":1.5,"count":0,"dataList":[],"pn":1}}`,
		},
		{
			name: "non-positive-risk-type", action: "get-company-risk-detail",
			input:    `{"riskId":123,"riskType":2}`,
			response: `{"error_code":0,"result":{"type":0,"count":0,"dataList":[],"pn":1}}`,
		},
		{
			name: "fractional-risk-count", action: "get-company-risk-detail",
			input:    `{"riskId":123,"riskType":2}`,
			response: `{"error_code":0,"result":{"type":1,"count":0.5,"dataList":[],"pn":1}}`,
		},
		{
			name: "negative-risk-count", action: "get-company-risk-detail",
			input:    `{"riskId":123,"riskType":2}`,
			response: `{"error_code":0,"result":{"type":1,"count":-1,"dataList":[],"pn":1}}`,
		},
		{
			name: "fractional-page-number", action: "get-company-risk-detail",
			input:    `{"riskId":123,"riskType":2}`,
			response: `{"error_code":0,"result":{"type":1,"count":0,"dataList":[],"pn":1.5}}`,
		},
		{
			name: "non-positive-page-number", action: "get-company-risk-detail",
			input:    `{"riskId":123,"riskType":2}`,
			response: `{"error_code":0,"result":{"type":1,"count":0,"dataList":[],"pn":0}}`,
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()

			wire := newWireServer(t, http.StatusOK, testCase.response)
			_, err := invoke(t, wire, testCase.action, testCase.input)
			requireInvocationCode(t, err, extensionflow.InvocationUpstreamFailure)
		})
	}
}

// TestWireRejectsFractionalNumbers fails closed before any request leaves
// the process: the schema layer rejects fractional lexicals as
// non-integers, and the integer query formatter keeps the same verdict for
// anything that would slip past it.
func TestWireRejectsFractionalNumbers(t *testing.T) {
	t.Parallel()

	cases := []struct {
		action string
		input  string
	}{
		{action: "search-companies", input: `{"keyword":"百度","pageNum":1.5}`},
		{action: "get-company-risk-detail", input: `{"riskId":1.5,"riskType":2,"pageNum":1,"pageSize":1}`},
		{action: "find-company-relationship-paths", input: `{"sourceCompanyName":"甲","targetCompanyName":"乙","maxDepth":0.5}`},
		{action: "list-company-news", input: `{"companyId":1.5}`},
	}
	for _, testCase := range cases {
		t.Run(testCase.action, func(t *testing.T) {
			t.Parallel()

			wire := newWireServer(t, http.StatusOK, pagedFixture())
			_, err := invoke(t, wire, testCase.action, testCase.input)
			requireInvocationCode(t, err, extensionflow.InvocationValidation)
			require.Empty(t, wire.requests)
		})
	}
}

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

	wire := newWireServer(t, http.StatusOK, pagedFixture())
	_, err := invoke(t, wire, "list-company-news", `{"startDate":"2024-01-01"}`)
	requireInvocationCode(t, err, extensionflow.InvocationValidation)
	_, err = invoke(t, wire, "find-company-relationship-paths", `{"sourceCompanyName":"甲"}`)
	requireInvocationCode(t, err, extensionflow.InvocationValidation)
	require.Empty(t, wire.requests)
}

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: "http-404", status: 404, code: extensionflow.InvocationNotFound},
		{name: "http-500", status: 500, code: extensionflow.InvocationUpstreamFailure},
		{
			name:   "quota-code",
			status: 200, body: `{"error_code":300006,"reason":"超出当日调用量"}`,
			code: extensionflow.InvocationRateLimited,
		},
		{
			name:   "frequency-code",
			status: 200, body: `{"error_code":300007,"reason":"请求频率过高"}`,
			code: extensionflow.InvocationRateLimited,
		},
		{
			name:   "token-code",
			status: 200, body: `{"error_code":300002,"reason":"token错误"}`,
			code: extensionflow.InvocationProviderAuthUnavailable,
		},
		{
			name:   "entitlement-code",
			status: 200, body: `{"error_code":300005,"reason":"无权限"}`,
			code: extensionflow.InvocationPermissionDenied,
		},
		{
			name:   "entitlement-package-code",
			status: 200, body: `{"error_code":300011,"reason":"无此API套餐"}`,
			code: extensionflow.InvocationPermissionDenied,
		},
		{
			name:   "param-code",
			status: 200, body: `{"error_code":300008,"reason":"参数错误"}`,
			code: extensionflow.InvocationValidation,
		},
		{
			name:   "unknown-code",
			status: 200, body: `{"error_code":310000,"reason":"其他错误"}`,
			code: extensionflow.InvocationUpstreamFailure,
		},
		{
			name:   "missing-error-code",
			status: 200, body: `{"result":{}}`,
			code: extensionflow.InvocationUpstreamFailure,
		},
		{
			name:   "missing-result-member",
			status: 200, body: `{"error_code":0,"reason":"ok"}`,
			code: extensionflow.InvocationUpstreamFailure,
		},
		{
			name:   "malformed-paged-result",
			status: 200, body: `{"error_code":0,"result":{"items":"nope"}}`,
			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, "search-companies", `{"keyword":"百度"}`)
			requireInvocationCode(t, err, testCase.code)
		})
	}
}

func requireInvocationCode(
	t *testing.T, err error, code extensionflow.InvocationErrorCode,
) {
	t.Helper()
	mapped := runner.InvocationError(err)
	require.NotNil(t, mapped)
	var invocation *extensionflow.InvocationError
	require.ErrorAs(t, mapped, &invocation)
	require.Equal(t, code, invocation.Code)
}

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}}
}

// wireCase is one locked action contract row.
type wireCase struct {
	action     string
	input      string
	path       string
	query      map[string]string
	response   string
	outputPart []string
}

func runWireCases(t *testing.T, cases []wireCase) {
	t.Helper()
	for _, testCase := range cases {
		t.Run(testCase.action+" "+strconv.Itoa(len(testCase.query)), func(t *testing.T) {
			t.Parallel()
			wire := newWireServer(t, http.StatusOK, testCase.response)
			output, err := invoke(t, wire, testCase.action, testCase.input)
			require.NoError(t, err)
			request := wire.last()
			require.Equal(t, http.MethodGet, request.method)
			require.Equal(t, testCase.path, request.path)
			requireQuery(t, request, testCase.query)
			requireTianyanchaHeaders(t, request)
			for _, part := range testCase.outputPart {
				require.Truef(t, strings.Contains(string(output), part),
					"output %s must contain %s", output, part)
			}
		})
	}
}
