package mail

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/protocol"
	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
)

type definitionTestExecutor struct{}

func (definitionTestExecutor) Strategy() string { return "test" }
func (definitionTestExecutor) Invoke(context.Context, registry.BuiltinInvocation) (registry.BuiltinResult, error) {
	return registry.BuiltinResult{}, nil
}

func TestDefinitionRegistersAndPinsMailActionScopes(t *testing.T) {
	reg := registry.New()
	registered, err := reg.RegisterBuiltin(
		Definition(),
		definitionTestExecutor{},
		Revision,
	)
	require.NoError(t, err)
	require.Equal(t, Domain, registered.Definition.Domain)
	require.Len(t, registered.Definition.Actions, 9)
	require.Equal(
		t,
		DefinitionDigest,
		registered.Definition.Digest,
	)
	t.Logf("mail definition digest: %s", registered.Definition.Digest)

	actions := make(map[string]registry.DefinitionAction)
	for _, action := range registered.Definition.Actions {
		actions[action.Name] = action
	}
	require.ElementsMatch(t, []string{
		ActionSearchMessages,
		ActionGetMessage,
		ActionListAttachments,
		ActionGetAttachment,
		ActionCreateDraft,
		ActionUpdateDraft,
		ActionCreateReplyDraft,
		ActionCreateForwardDraft,
		ActionSendDraft,
	}, mapKeys(actions))
	require.Equal(t, []string{ScopeMetadataRead}, actions[ActionSearchMessages].RequiredScopes)
	require.ElementsMatch(t,
		[]string{ScopeMetadataRead, ScopeContentRead},
		actions[ActionGetMessage].RequiredScopes,
	)
	require.ElementsMatch(t,
		[]string{ScopeDraftWrite, ScopeSend},
		actions[ActionSendDraft].RequiredScopes,
	)
	require.Equal(t, "mutation", actions[ActionSendDraft].Safety.Class)
	require.Equal(t, "mutation", actions[ActionGetAttachment].Safety.Class)
	require.Contains(t, actions, "send-draft")
	require.NotContains(t, actions, "get-operation-status")

	expectedScopes := map[string][]string{
		ActionSearchMessages:     {ScopeMetadataRead},
		ActionGetMessage:         {ScopeMetadataRead, ScopeContentRead},
		ActionListAttachments:    {ScopeMetadataRead, ScopeContentRead},
		ActionGetAttachment:      {ScopeMetadataRead, ScopeContentRead},
		ActionCreateDraft:        {ScopeDraftWrite},
		ActionUpdateDraft:        {ScopeDraftWrite},
		ActionCreateReplyDraft:   {ScopeMetadataRead, ScopeContentRead, ScopeDraftWrite},
		ActionCreateForwardDraft: {ScopeMetadataRead, ScopeContentRead, ScopeDraftWrite},
		ActionSendDraft:          {ScopeDraftWrite, ScopeSend},
	}
	for name, expected := range expectedScopes {
		require.ElementsMatch(t, expected, actions[name].RequiredScopes, name)
		require.NotEmpty(t, actions[name].RequiredScopes, name)
		for _, scope := range actions[name].RequiredScopes {
			require.True(t, strings.HasPrefix(scope, "mail."), name)
			require.NotContains(t, scope, "*", name)
		}
	}
}

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

	_, currentFile, _, ok := runtime.Caller(0)
	require.True(t, ok)
	repositoryRoot := filepath.Clean(
		filepath.Join(filepath.Dir(currentFile), "..", "..", "..", ".."),
	)
	migrations, err := filepath.Glob(filepath.Join(
		repositoryRoot,
		"internal",
		"tidb",
		"migrations",
		"*_explicit_targets.sql",
	))
	require.NoError(t, err)
	require.Len(t, migrations, 1)
	payload, err := os.ReadFile(migrations[0])
	require.NoError(t, err)
	require.Contains(t, string(payload), DefinitionDigest)
}

func TestDraftSchemasBindAttachmentsByIDOnly(t *testing.T) {
	for _, name := range []string{
		ActionCreateDraft,
		ActionUpdateDraft,
		ActionCreateReplyDraft,
		ActionCreateForwardDraft,
	} {
		var action registry.DefinitionAction
		for _, candidate := range Definition().Actions {
			if candidate.Name == name {
				action = candidate
				break
			}
		}
		properties := action.InputSchema["properties"].(map[string]any)
		require.Equal(t, false, action.InputSchema["additionalProperties"], name)
		require.NotContains(t, properties, "attachmentRefs", name)
		require.NotContains(t, properties, "includeAttachments", name)
		attachments := properties["attachments"].(map[string]any)
		require.Equal(t, 10, attachments["maxItems"], name)
		require.Equal(t, true, attachments["uniqueItems"], name)
		items := attachments["items"].(map[string]any)
		itemProperties := items["properties"].(map[string]any)
		require.Equal(t, []any{"attachmentId"}, items["required"], name)
		attachmentID := itemProperties["attachmentId"].(map[string]any)
		require.Equal(t, 128, attachmentID["maxLength"], name)
		require.NotContains(t, itemProperties, "path", name)
		require.NotContains(t, itemProperties, "url", name)
		require.NotContains(t, itemProperties, "content", name)
	}
}

func TestSearchMessagesUsesProviderNeutralStructuredFilters(t *testing.T) {
	action := definitionActionsByName()[ActionSearchMessages]
	properties := action.InputSchema["properties"].(map[string]any)
	require.NotContains(t, properties, "query")
	filters := properties["filters"].(map[string]any)
	require.Equal(t, 1, filters["minProperties"])
	filterProperties := filters["properties"].(map[string]any)
	for _, name := range []string{
		"text",
		"folder",
		"from",
		"to",
		"receivedAfter",
		"receivedBefore",
		"unread",
		"hasAttachments",
	} {
		require.Contains(t, filterProperties, name)
	}
	require.Equal(t, false, filters["additionalProperties"])
	example := action.Example.(map[string]any)
	require.Contains(t, example, "filters")
	require.NotContains(t, example, "query")
}

func TestReadOutputsMarkExternalContentAndSanitizeHTML(t *testing.T) {
	actions := definitionActionsByName()
	getMessage := actions[ActionGetMessage]
	messageProperties := getMessage.OutputSchema["properties"].(map[string]any)
	require.Equal(t,
		true,
		messageProperties["untrustedExternalContent"].(map[string]any)["const"],
	)
	body := messageProperties["body"].(map[string]any)
	bodyProperties := body["properties"].(map[string]any)
	require.Equal(t,
		true,
		bodyProperties["untrustedExternalContent"].(map[string]any)["const"],
	)
	require.Equal(t,
		[]any{"text", "sanitized_html"},
		bodyProperties["format"].(map[string]any)["enum"],
	)

	attachments := actions[ActionListAttachments].
		OutputSchema["properties"].(map[string]any)["attachments"].(map[string]any)["items"].(map[string]any)
	attachmentProperties := attachments["properties"].(map[string]any)
	require.Equal(t,
		true,
		attachmentProperties["untrustedExternalContent"].(map[string]any)["const"],
	)
}

func TestDraftMutationOutputsDoNotExposeDurableOperationState(t *testing.T) {
	actions := definitionActionsByName()
	for _, name := range []string{
		ActionCreateDraft,
		ActionUpdateDraft,
		ActionCreateReplyDraft,
		ActionCreateForwardDraft,
	} {
		output := actions[name].OutputSchema
		properties := output["properties"].(map[string]any)
		require.NotContains(t, properties, "operationId", name)
		require.Equal(t,
			[]any{"accepted", "failed", "unknown"},
			properties["status"].(map[string]any)["enum"],
			name,
		)
	}
}

func TestActionInputsCannotSelectMailTransportOrIdentity(t *testing.T) {
	for name, action := range definitionActionsByName() {
		properties := action.InputSchema["properties"].(map[string]any)
		for _, forbidden := range []string{
			"host", "port", "tlsMode", "username", "password",
			"senderAddress", "from", "connectionId", "installationId",
		} {
			require.NotContains(t, properties, forbidden, name)
		}
		require.Equal(t, false, action.InputSchema["additionalProperties"], name)

		encoded, err := json.Marshal(action.InputSchema)
		require.NoError(t, err)
		lower := strings.ToLower(string(encoded))
		for _, forbidden := range []string{
			`"endpoint"`, `"auth"`, `"sender"`, `"senderaddress"`,
			`"connection"`, `"connectionid"`,
		} {
			require.NotContains(t, lower, forbidden, name)
		}
	}
}

func TestActionOutputsDeclareHardBudgetTruncation(t *testing.T) {
	for name, action := range definitionActionsByName() {
		if branches, conditional := action.OutputSchema["oneOf"].([]any); conditional {
			// Conditional contracts must declare truncation in every branch.
			for _, branch := range branches {
				properties := branch.(map[string]any)["properties"].(map[string]any)
				require.Contains(t, properties, "truncated", name)
				require.Equal(
					t,
					"boolean",
					properties["truncated"].(map[string]any)["type"],
					name,
				)
			}
			continue
		}
		properties := action.OutputSchema["properties"].(map[string]any)
		require.Contains(t, properties, "truncated", name)
		require.Equal(
			t,
			"boolean",
			properties["truncated"].(map[string]any)["type"],
			name,
		)
	}
}

func TestCapabilityGatesMatchDirectIMAPRequirements(t *testing.T) {
	base := map[string]bool{
		"IMAP4REV1":   true,
		"SPECIAL-USE": true,
		"UIDPLUS":     true,
	}
	capabilities := protocolCapabilitiesForDefinitionTest(base, "Drafts")
	require.True(
		t,
		actionSupportedByIMAPCapabilities(capabilities, ActionCreateDraft),
	)
	for _, action := range []string{
		ActionUpdateDraft,
		ActionCreateReplyDraft,
		ActionCreateForwardDraft,
		ActionSendDraft,
	} {
		require.False(
			t,
			actionSupportedByIMAPCapabilities(capabilities, action),
			action,
		)
	}

	base["CONDSTORE"] = true
	capabilities = protocolCapabilitiesForDefinitionTest(base, "Drafts")
	for _, action := range []string{
		ActionCreateDraft,
		ActionUpdateDraft,
		ActionCreateReplyDraft,
		ActionCreateForwardDraft,
		ActionSendDraft,
	} {
		require.True(
			t,
			actionSupportedByIMAPCapabilities(capabilities, action),
			action,
		)
	}

	capabilities = protocolCapabilitiesForDefinitionTest(base, "")
	for _, action := range []string{
		ActionCreateDraft,
		ActionUpdateDraft,
		ActionCreateReplyDraft,
		ActionCreateForwardDraft,
		ActionSendDraft,
	} {
		require.False(
			t,
			actionSupportedByIMAPCapabilities(capabilities, action),
			action,
		)
	}
}

func TestDefinitionContainsNoReplacedRuntimeVocabulary(t *testing.T) {
	encoded, err := json.Marshal(Definition())
	require.NoError(t, err)
	lower := strings.ToLower(string(encoded))
	for _, forbidden := range []string{
		"gmail",
		"microsoft_graph",
		"oauth",
		"get-operation-status",
		"operationid",
		"reconcile",
		"effect",
	} {
		require.NotContains(t, lower, forbidden)
	}
}

func TestReadSchemasPinSemanticCollectionLimits(t *testing.T) {
	actions := definitionActionsByName()
	messages := actions[ActionSearchMessages].
		OutputSchema["properties"].(map[string]any)["messages"].(map[string]any)
	require.Equal(t, 50, messages["maxItems"])

	attachments := actions[ActionListAttachments].
		OutputSchema["properties"].(map[string]any)["attachments"].(map[string]any)
	require.Equal(t, 100, attachments["maxItems"])
}

func definitionActionsByName() map[string]registry.DefinitionAction {
	actions := make(map[string]registry.DefinitionAction)
	for _, action := range Definition().Actions {
		actions[action.Name] = action
	}
	return actions
}

func mapKeys(values map[string]registry.DefinitionAction) []string {
	result := make([]string, 0, len(values))
	for key := range values {
		result = append(result, key)
	}
	return result
}

func protocolCapabilitiesForDefinitionTest(
	values map[string]bool,
	draftMailbox string,
) protocol.Capabilities {
	return protocol.Capabilities{
		Values:       values,
		DraftMailbox: draftMailbox,
	}
}

func TestGetAttachmentOutputSchemaIsConditional(t *testing.T) {
	output := definitionActionsByName()[ActionGetAttachment].OutputSchema
	branches, ok := output["oneOf"].([]any)
	require.True(t, ok, "output must be a conditional contract")
	require.Len(t, branches, 2)

	materialized := branches[0].(map[string]any)
	require.Equal(
		t,
		"materialized",
		materialized["properties"].(map[string]any)["status"].(map[string]any)["const"],
	)
	require.Equal(
		t,
		[]any{
			"status", "attachmentId", "filename", "mediaType",
			"sizeBytes", "truncated", "untrustedExternalContent",
		},
		materialized["required"],
	)

	unknown := branches[1].(map[string]any)
	require.Equal(
		t,
		"unknown",
		unknown["properties"].(map[string]any)["status"].(map[string]any)["const"],
	)
	unknownProperties := unknown["properties"].(map[string]any)
	require.NotContains(t, unknownProperties, "attachmentId")
	require.NotContains(t, unknownProperties, "filename")
	require.NotContains(t, unknownProperties, "mediaType")
	require.NotContains(t, unknownProperties, "sizeBytes")
}
