package wecombot_test

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

	"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/wecombot"
	"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"
)

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

type staticCredentialSource struct {
	credentials runner.Credentials
}

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

func validCredentials() runner.Credentials {
	return runner.Credentials{wecombot.APIKeyField: "key123"}
}

func invocation(action string, input string) registry.BuiltinInvocation {
	return registry.BuiltinInvocation{
		WorkspaceID:           "ws",
		ServiceInstallationID: "inst",
		ExtensionName:         wecombot.Domain,
		Action:                action,
		Input:                 json.RawMessage(input),
	}
}

// newWecomServer is a TLS loopback double that replies with the given status
// and body and captures the latest request.
func newWecomServer(t *testing.T, status int, body string) (*httptest.Server, *capturedRequest) {
	t.Helper()
	captured := &capturedRequest{}
	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		bodyBytes, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		captured.method = r.Method
		captured.path = r.URL.Path
		captured.query = r.URL.RawQuery
		captured.body = string(bodyBytes)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(status)
		_, _ = w.Write([]byte(body))
	}))
	t.Cleanup(server.Close)
	return server, captured
}

type capturedRequest struct {
	method string
	path   string
	query  string
	body   string
}

func newExecutor(
	t *testing.T,
	server *httptest.Server,
) *runner.RESTExecutor {
	t.Helper()
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:   server.URL,
		Auth:      wecombot.AuthSpec(),
		Transport: insecureTransport(),
	})
	require.NoError(t, err)
	return runner.NewRESTExecutor(wecombot.Domain, client,
		staticCredentialSource{credentials: validCredentials()}, wecombot.Handlers())
}

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

	extensionUnit, err := wecombot.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.Equal(t, "wecombot-rest-v1", extensionUnit.Implementation.Revision)
	require.Equal(t, appregistry.WecomBotAppID, extensionUnit.Distribution.AppID)
	require.Equal(t, appregistry.WecomBotAppSlug, 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 := wecombot.Extension(staticCredentialSource{})
	require.NoError(t, err)
	reg := registry.New()
	require.NoError(t, bootstrap.Register(reg, []extension.Extension{extensionUnit}))
	entry, ok := reg.LookupByIdentity(wecombot.Domain, 1)
	require.True(t, ok)
	require.Equal(t, model.WecomBotBuiltinExtensionDefinitionDigest, entry.Definition.Digest)
}

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

	spec := wecombot.AuthSpec()
	require.Equal(t, runner.AuthCredential, spec.Kind)
	require.NoError(t, spec.Validate(validCredentials()))
	require.Error(t, spec.Validate(runner.Credentials{}),
		"the webhook key is required")
}

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

	cases := []struct {
		name   string
		status int
		body   string
		code   extensionflow.InvocationErrorCode
	}{
		{
			name:   "business rate limit",
			status: http.StatusOK, body: `{"errcode":45009,"errmsg":"api freq out of limit"}`,
			code: extensionflow.InvocationRateLimited,
		},
		{
			name:   "invalid message type rejects input",
			status: http.StatusOK, body: `{"errcode":40008,"errmsg":"invalid msgtype"}`,
			code: extensionflow.InvocationValidation,
		},
		{
			name:   "capitalized legacy code still classifies",
			status: http.StatusOK, body: `{"ErrCode":40058,"ErrMsg":"invalid url"}`,
			code: extensionflow.InvocationValidation,
		},
		{
			name:   "numeric string code still classifies",
			status: http.StatusOK, body: `{"errcode":"93018","error_msg":"no robot"}`,
			code: extensionflow.InvocationValidation,
		},
		{
			name:   "webhook conflict blames the connection",
			status: http.StatusOK, body: `{"errcode":93000,"errmsg":"webhook not exists"}`,
			code: extensionflow.InvocationProviderAuthUnavailable,
		},
		{
			name:   "push disabled blames the connection",
			status: http.StatusOK, body: `{"errcode":93004,"errmsg":"push disabled"}`,
			code: extensionflow.InvocationProviderAuthUnavailable,
		},
		{
			name:   "invalid bot id blames the connection",
			status: http.StatusOK, body: `{"errcode":93019,"errmsg":"invalid webhook bot id"}`,
			code: extensionflow.InvocationProviderAuthUnavailable,
		},
		{
			name:   "unknown business code fails closed",
			status: http.StatusOK, body: `{"errcode":777777,"errmsg":"mystery"}`,
			code: extensionflow.InvocationUpstreamFailure,
		},
		{
			name:   "object envelope without errcode fails closed",
			status: http.StatusOK, body: `{"errmsg":"bad"}`,
			code: extensionflow.InvocationUpstreamFailure,
		},
		{
			name:   "malformed body fails closed",
			status: http.StatusOK, body: `plain text`,
			code: extensionflow.InvocationUpstreamFailure,
		},
		{
			name:   "http rate limit",
			status: http.StatusTooManyRequests, body: `{}`,
			code: extensionflow.InvocationRateLimited,
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()
			server, _ := newWecomServer(t, testCase.status, testCase.body)
			_, err := newExecutor(t, server).Invoke(context.Background(),
				invocation(wecombot.ActionSendTextMessage, `{"content":"hi"}`))
			require.Error(t, err)
			code, classified := extensionflow.InvocationErrorCodeOf(err)
			require.True(t, classified)
			require.Equal(t, testCase.code, code)
		})
	}
}

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

	server, _ := newWecomServer(t, http.StatusOK, `{"errcode":0,"errmsg":"ok"}`)
	result, err := newExecutor(t, server).Invoke(context.Background(),
		invocation(wecombot.ActionSendTextMessage, `{"content":"hi"}`))
	require.NoError(t, err)
	var payload map[string]any
	require.NoError(t, json.Unmarshal(result.Data, &payload))
	require.Equal(t, float64(0), payload["errcode"])
	require.Equal(t, "ok", payload["errmsg"])
}

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

	server, _ := newWecomServer(t, http.StatusOK, `{"errcode":0}`)
	result, err := newExecutor(t, server).Invoke(context.Background(),
		invocation(wecombot.ActionSendTextMessage, `{"content":"hi"}`))
	require.NoError(t, err)
	var payload map[string]any
	require.NoError(t, json.Unmarshal(result.Data, &payload))
	require.Equal(t, "ok", payload["errmsg"])
}

// TestUtf8ByteCapsRejectOversizedContent proves the per-message-type caps
// count UTF-8 bytes, not runes: 700 CJK runes exceed the 2048-byte text cap.
func TestUtf8ByteCapsRejectOversizedContent(t *testing.T) {
	t.Parallel()

	server, _ := newWecomServer(t, http.StatusOK, `{"errcode":0,"errmsg":"ok"}`)

	longText := strings.Repeat("中", 700)
	_, err := newExecutor(t, server).Invoke(context.Background(),
		invocation(wecombot.ActionSendTextMessage, `{"content":"`+longText+`"}`))
	require.Error(t, err)
	code, classified := extensionflow.InvocationErrorCodeOf(err)
	require.True(t, classified)
	require.Equal(t, extensionflow.InvocationValidation, code)

	longMarkdown := strings.Repeat("中", 1400)
	_, err = newExecutor(t, server).Invoke(context.Background(),
		invocation(wecombot.ActionSendMarkdownMessage, `{"content":"`+longMarkdown+`"}`))
	require.Error(t, err)
	code, classified = extensionflow.InvocationErrorCodeOf(err)
	require.True(t, classified)
	require.Equal(t, extensionflow.InvocationValidation, code)
}
