package wecombot_test

import (
	"context"
	"net/http"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

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

// wireCase pins one action's upstream wire contract: method, path, query, and
// body semantics, exercised against a TLS loopback double.
type wireCase struct {
	action string
	input  string
	// bodyContains asserts substrings of the JSON request body.
	bodyContains []string
	// bodyAbsent asserts keys absent from the request body.
	bodyAbsent []string
}

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

	cases := []wireCase{
		{
			action: wecombot.ActionSendTextMessage,
			input:  `{"content":"Deploy finished","mentionedList":["user-1","@all"],"mentionedMobileList":["13800000000"]}`,
			bodyContains: []string{
				`"msgtype":"text"`,
				`"content":"Deploy finished"`,
				`"mentioned_list":["user-1","@all"]`,
				`"mentioned_mobile_list":["13800000000"]`,
			},
		},
		{
			action:     wecombot.ActionSendTextMessage,
			input:      `{"content":"Minimal"}`,
			bodyAbsent: []string{"mentioned_list", "mentioned_mobile_list"},
		},
		{
			action:       wecombot.ActionSendMarkdownMessage,
			input:        `{"content":"## Build passed"}`,
			bodyContains: []string{`"msgtype":"markdown"`, `"markdown":{"content":"## Build passed"}`},
		},
		{
			action:       wecombot.ActionSendMarkdownV2Message,
			input:        `{"content":"## Build passed"}`,
			bodyContains: []string{`"msgtype":"markdown_v2"`, `"markdown_v2":{"content":"## Build passed"}`},
			bodyAbsent:   []string{"markdown\":"},
		},
		{
			action:       wecombot.ActionSendImageMessage,
			input:        `{"base64":"aW1hZ2U=","md5":"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"}`,
			bodyContains: []string{`"msgtype":"image"`, `"base64":"aW1hZ2U="`, `"md5":"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"`},
		},
		{
			action: wecombot.ActionSendNewsMessage,
			input:  `{"articles":[{"title":"Release","description":"v1","url":"https://example.com","picurl":"https://example.com/p.png"}]}`,
			bodyContains: []string{
				`"msgtype":"news"`,
				`"articles":[{"description":"v1","picurl":"https://example.com/p.png","title":"Release","url":"https://example.com"}]`,
			},
		},
		{
			action: wecombot.ActionSendNewsMessage,
			input:  `{"articles":[{"title":"Bare","url":"https://example.com"}]}`,
			bodyContains: []string{
				`"articles":[{"title":"Bare","url":"https://example.com"}]`,
			},
		},
	}
	definition := wecombot.Definition()
	covered := make(map[string]struct{}, len(cases))
	for _, testCase := range cases {
		covered[testCase.action] = struct{}{}
	}
	for _, action := range definition.Actions {
		require.Containsf(t, covered, action.Name,
			"every catalog action needs a wire case: %s", action.Name)
	}

	for _, testCase := range cases {
		t.Run(testCase.action, func(t *testing.T) {
			t.Parallel()
			server, captured := newWecomServer(t, http.StatusOK, `{"errcode":0,"errmsg":"ok"}`)
			_, err := newExecutor(t, server).Invoke(context.Background(),
				invocation(testCase.action, testCase.input))
			require.NoErrorf(t, err, "%s must succeed against the double", testCase.action)

			require.Equal(t, http.MethodPost, captured.method, "method")
			require.Equal(t, "/cgi-bin/webhook/send", captured.path, "path")
			require.Equal(t, "key=key123", captured.query, "query")
			for _, fragment := range testCase.bodyContains {
				require.Containsf(t, captured.body, fragment,
					"%s body must contain %s", testCase.action, fragment)
			}
			for _, key := range testCase.bodyAbsent {
				require.NotContainsf(t, captured.body, `"`+key,
					"%s body must omit the %q field", testCase.action, key)
			}
		})
	}
}

// TestWecomWebhookURLCredentialNormalizesToQuery proves a full webhook URL
// credential normalizes onto the same wire query as a bare key.
func TestWecomWebhookURLCredentialNormalizesToQuery(t *testing.T) {
	t.Parallel()

	server, captured := newWecomServer(t, http.StatusOK, `{"errcode":0,"errmsg":"ok"}`)
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:   server.URL,
		Auth:      wecombot.AuthSpec(),
		Transport: insecureTransport(),
	})
	require.NoError(t, err)
	executor := runner.NewRESTExecutor(wecombot.Domain, client,
		staticCredentialSource{credentials: runner.Credentials{
			wecombot.APIKeyField: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=urlkey",
		}}, wecombot.Handlers())
	_, err = executor.Invoke(context.Background(),
		invocation(wecombot.ActionSendTextMessage, `{"content":"hi"}`))
	require.NoError(t, err)
	require.Equal(t, "/cgi-bin/webhook/send", captured.path)
	require.True(t, strings.HasPrefix(captured.query, "key=urlkey"),
		"normalized query: %s", captured.query)
}

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

	server, _ := newWecomServer(t, http.StatusOK, `{"errcode":0,"errmsg":"ok"}`)
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:   server.URL,
		Auth:      wecombot.AuthSpec(),
		Transport: insecureTransport(),
	})
	require.NoError(t, err)
	executor := runner.NewRESTExecutor(wecombot.Domain, client,
		staticCredentialSource{credentials: runner.Credentials{
			wecombot.APIKeyField: "https://evil.example.com/cgi-bin/webhook/send?key=x",
		}}, wecombot.Handlers())
	_, err = executor.Invoke(context.Background(),
		invocation(wecombot.ActionSendTextMessage, `{"content":"hi"}`))
	require.Error(t, err)
	code, classified := extensionflow.InvocationErrorCodeOf(err)
	require.True(t, classified)
	// A stored credential that cannot be resolved at execute time is a
	// connection problem, never an action-input validation failure.
	require.Equal(t, extensionflow.InvocationProviderAuthUnavailable, code)
}
