package dingtalkbot_test

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"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/dingtalkbot"
	"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{
		dingtalkbot.APIKeyField:        "token123",
		dingtalkbot.SigningSecretField: "secret123",
	}
}

func unsignedCredentials() runner.Credentials {
	return runner.Credentials{dingtalkbot.APIKeyField: "token123"}
}

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

// newDingtalkServer is a TLS loopback double that replies with the given
// status and body and captures the latest request.
func newDingtalkServer(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 agentteamClient(t *testing.T, server *httptest.Server) *runner.RESTClient {
	t.Helper()
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:   server.URL,
		Auth:      dingtalkbot.AuthSpec(),
		Transport: insecureTransport(),
	})
	require.NoError(t, err)
	return client
}

func newExecutor(
	t *testing.T,
	server *httptest.Server,
	credentials runner.Credentials,
) *runner.RESTExecutor {
	t.Helper()
	return runner.NewRESTExecutor(dingtalkbot.Domain,
		agentteamClient(t, server),
		staticCredentialSource{credentials: credentials},
		dingtalkbot.Handlers())
}

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

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

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

	spec := dingtalkbot.AuthSpec()
	require.Equal(t, runner.AuthCredential, spec.Kind)
	require.NoError(t, spec.Validate(validCredentials()))
	require.NoError(t, spec.Validate(unsignedCredentials()))
	require.Error(t, spec.Validate(runner.Credentials{}),
		"the webhook access token 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":410100,"errmsg":"too fast"}`,
			code: extensionflow.InvocationRateLimited,
		},
		{
			name:   "token rejection",
			status: http.StatusOK, body: `{"errcode":400013,"errmsg":"invalid token"}`,
			code: extensionflow.InvocationProviderAuthUnavailable,
		},
		{
			name:   "sign mismatch blames the connection",
			status: http.StatusOK, body: `{"errcode":310000,"errmsg":"sign not match"}`,
			code: extensionflow.InvocationProviderAuthUnavailable,
		},
		{
			name:   "keyword filter miss rejects input",
			status: http.StatusOK, body: `{"errcode":310000,"errmsg":"keywords not in content"}`,
			code: extensionflow.InvocationValidation,
		},
		{
			name:   "ip whitelist rejection blames the connection",
			status: http.StatusOK, body: `{"errcode":310000,"errmsg":"ip 1.2.3.4 not in whitelist"}`,
			code: extensionflow.InvocationProviderAuthUnavailable,
		},
		{
			name:   "invalid timestamp blames the connection",
			status: http.StatusOK, body: `{"errcode":310000,"errmsg":"invalid timestamp"}`,
			code: extensionflow.InvocationProviderAuthUnavailable,
		},
		{
			name:   "unsafe link rejects input",
			status: http.StatusOK, body: `{"errcode":430102,"errmsg":"unsafe link"}`,
			code: extensionflow.InvocationValidation,
		},
		{
			name:   "unrecognized 310000 fails closed",
			status: http.StatusOK, body: `{"errcode":310000,"errmsg":"something else entirely"}`,
			code: extensionflow.InvocationUpstreamFailure,
		},
		{
			name:   "invalid msgtype rejects input",
			status: http.StatusOK, body: `{"errcode":40035,"errmsg":"bad msgtype"}`,
			code: extensionflow.InvocationValidation,
		},
		{
			name:   "unknown business code fails closed",
			status: http.StatusOK, body: `{"errcode":999999,"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, _ := newDingtalkServer(t, testCase.status, testCase.body)
			_, err := newExecutor(t, server, validCredentials()).Invoke(context.Background(),
				invocation(dingtalkbot.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, _ := newDingtalkServer(t, http.StatusOK, `{"errcode":0,"errmsg":"ok"}`)
	result, err := newExecutor(t, server, validCredentials()).Invoke(context.Background(),
		invocation(dingtalkbot.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, _ := newDingtalkServer(t, http.StatusOK, `{"errcode":0}`)
	result, err := newExecutor(t, server, unsignedCredentials()).Invoke(context.Background(),
		invocation(dingtalkbot.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"])
}
