package dingtalkbot_test

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"net/http"
	"net/url"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension/providers/dingtalkbot"
	"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 with the unsigned
// credential so the query stays deterministic.
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 TestDingtalkWireContractCoversEveryAction(t *testing.T) {
	t.Parallel()

	cases := []wireCase{
		{
			action: dingtalkbot.ActionSendTextMessage,
			input:  `{"content":"Deploy finished","msgUuid":"uuid-1","atMobiles":["13800000000"],"isAtAll":false}`,
			bodyContains: []string{
				`"msgtype":"text"`,
				`"text":{"content":"Deploy finished"}`,
				`"msgUuid":"uuid-1"`,
				`"atMobiles":["13800000000"]`,
				`"isAtAll":false`,
			},
		},
		{
			action:     dingtalkbot.ActionSendTextMessage,
			input:      `{"content":"Minimal"}`,
			bodyAbsent: []string{"at", "msgUuid"},
		},
		{
			action: dingtalkbot.ActionSendLinkMessage,
			input:  `{"title":"Release","text":"v1","messageUrl":"https://example.com","picUrl":"https://example.com/p.png"}`,
			bodyContains: []string{
				`"msgtype":"link"`,
				`"title":"Release"`,
				`"messageUrl":"https://example.com"`,
				`"picUrl":"https://example.com/p.png"`,
			},
		},
		{
			action: dingtalkbot.ActionSendLinkMessage,
			input:  `{"title":"Release","text":"v1","messageUrl":"https://example.com"}`,
			bodyContains: []string{
				`"msgtype":"link"`,
				`"title":"Release"`,
				`"messageUrl":"https://example.com"`,
			},
			bodyAbsent: []string{"picUrl"},
		},
		{
			action: dingtalkbot.ActionSendMarkdownMessage,
			input:  `{"title":"Report","text":"## Build","atUserIds":["user-1"]}`,
			bodyContains: []string{
				`"msgtype":"markdown"`,
				`"title":"Report"`,
				`"text":"## Build"`,
				`"atUserIds":["user-1"]`,
			},
		},
		{
			action: dingtalkbot.ActionSendActionCardMessage,
			input:  `{"cardMode":"single","title":"Approval","text":"Approve?","singleTitle":"Open","singleUrl":"https://example.com","buttonOrientation":"vertical"}`,
			bodyContains: []string{
				`"msgtype":"actionCard"`,
				`"title":"Approval"`,
				`"singleTitle":"Open"`,
				`"singleURL":"https://example.com"`,
				`"btnOrientation":"0"`,
			},
		},
		{
			action: dingtalkbot.ActionSendActionCardMessage,
			input:  `{"cardMode":"buttons","title":"Approval","text":"Approve?","buttons":[{"title":"Yes","actionUrl":"https://example.com/yes"},{"title":"No","actionUrl":"https://example.com/no"}],"buttonOrientation":"horizontal"}`,
			bodyContains: []string{
				`"msgtype":"actionCard"`,
				`"btns":[{"actionURL":"https://example.com/yes","title":"Yes"},{"actionURL":"https://example.com/no","title":"No"}]`,
				`"btnOrientation":"1"`,
			},
			bodyAbsent: []string{"singleTitle", "singleURL"},
		},
		{
			action: dingtalkbot.ActionSendFeedCardMessage,
			input:  `{"links":[{"title":"Merge","messageUrl":"https://example.com/1","picUrl":"https://example.com/p.png"}]}`,
			bodyContains: []string{
				`"msgtype":"feedCard"`,
				`"links":[{"messageURL":"https://example.com/1","picURL":"https://example.com/p.png","title":"Merge"}]`,
			},
		},
	}
	definition := dingtalkbot.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 := newDingtalkServer(t, http.StatusOK, `{"errcode":0,"errmsg":"ok"}`)
			_, err := newExecutor(t, server, unsignedCredentials()).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, "/robot/send", captured.path, "path")
			require.Equal(t, "access_token=token123", 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)
			}
		})
	}
}

// TestDingtalkSignatureRidesTheQuery proves the signing secret path: the
// query carries the timestamp and a signature recomputable from that exact
// timestamp.
func TestDingtalkSignatureRidesTheQuery(t *testing.T) {
	t.Parallel()

	server, captured := newDingtalkServer(t, http.StatusOK, `{"errcode":0,"errmsg":"ok"}`)
	_, err := newExecutor(t, server, validCredentials()).Invoke(context.Background(),
		invocation(dingtalkbot.ActionSendTextMessage, `{"content":"hi"}`))
	require.NoError(t, err)

	values, err := url.ParseQuery(captured.query)
	require.NoError(t, err)
	require.Equal(t, "token123", values.Get("access_token"))
	timestamp := values.Get("timestamp")
	require.NotEmpty(t, timestamp, "signed request must carry a timestamp")
	require.NotEmpty(t, values.Get("sign"))

	mac := hmac.New(sha256.New, []byte("secret123"))
	mac.Write([]byte(timestamp + "\n" + "secret123"))
	require.Equal(t,
		base64.StdEncoding.EncodeToString(mac.Sum(nil)),
		values.Get("sign"),
		"signature must be HMAC-SHA256(secret, timestamp+\\n+secret)")
}

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

	server, captured := newDingtalkServer(t, http.StatusOK, `{"errcode":0,"errmsg":"ok"}`)
	_, err := newExecutor(t, server, runner.Credentials{
		dingtalkbot.APIKeyField: "https://oapi.dingtalk.com/robot/send?access_token=urltoken",
	}).Invoke(context.Background(),
		invocation(dingtalkbot.ActionSendTextMessage, `{"content":"hi"}`))
	require.NoError(t, err)
	require.Equal(t, "/robot/send", captured.path)
	require.True(t, strings.HasPrefix(captured.query, "access_token=urltoken"),
		"normalized query: %s", captured.query)
}

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

	// White-box coverage through the executor: a foreign URL credential is
	// unusable stored-connection state at execute time and must classify as
	// a connection problem before any request leaves.
	server, _ := newDingtalkServer(t, http.StatusOK, `{"errcode":0,"errmsg":"ok"}`)
	_, err := newExecutor(t, server, runner.Credentials{
		dingtalkbot.APIKeyField: "https://evil.example.com/robot/send?access_token=x",
	}).Invoke(context.Background(),
		invocation(dingtalkbot.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)
}
