package mail

import (
	"context"
	"encoding/json"
	"fmt"
	"testing"

	"github.com/stretchr/testify/require"

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

type recordingMailInvoker struct {
	invocation mailruntime.Invocation
	result     json.RawMessage
	err        error
}

func (invoker *recordingMailInvoker) Invoke(
	_ context.Context,
	invocation mailruntime.Invocation,
) (json.RawMessage, error) {
	invoker.invocation = invocation
	return append([]byte(nil), invoker.result...), invoker.err
}

func TestExecutorForwardsExactConnectionIdentity(t *testing.T) {
	invoker := &recordingMailInvoker{
		result: json.RawMessage(`{"messages":[],"hasMore":false}`),
	}
	executor := NewExecutor(invoker)
	result, err := executor.Invoke(
		context.Background(),
		registry.BuiltinInvocation{
			WorkspaceID:           "wsp_one",
			AgentID:               "agt_one",
			AgentMemberID:         "mem_one",
			ServiceInstallationID: "mailinst_one",
			ServiceConnectionID:   "mailconn_exact",
			ServiceConnectionGeneration: "sha256:" +
				"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
			ServiceInstallationExtensionID: "instext_one",
			AuthorizedByMemberID:           "mem_human_one",
			ExtensionName:                  "work-mail",
			Action:                         ActionSearchMessages,
			DefinitionVersion:              1,
			DefinitionDigest:               DefinitionDigest,
			CatalogVersion:                 7,
			RequestHash: "sha256:" +
				"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
			Input: json.RawMessage(`{"filters":{"text":"safe"}}`),
		},
	)
	require.NoError(t, err)
	require.JSONEq(t, string(invoker.result), string(result.Data))
	require.Equal(t, "mailconn_exact", invoker.invocation.ServiceConnectionID)
	require.Equal(
		t,
		"sha256:"+
			"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
		invoker.invocation.ServiceConnectionGeneration,
	)
	require.Equal(t, "mailinst_one", invoker.invocation.ServiceInstallationID)
	require.Equal(t, "wsp_one", invoker.invocation.WorkspaceID)
	require.Equal(t, "agt_one", invoker.invocation.AgentID)
	require.Equal(
		t,
		"instext_one",
		invoker.invocation.ServiceInstallationExtensionID,
	)
	require.Equal(t, "work-mail", invoker.invocation.ExtensionName)
	require.Equal(
		t,
		"mem_human_one",
		invoker.invocation.AuthorizedByMemberID,
	)
	require.Equal(t, DefinitionDigest, invoker.invocation.DefinitionDigest)
	require.EqualValues(t, 7, invoker.invocation.CatalogVersion)
	require.NotEmpty(t, invoker.invocation.RequestHash)
}

func TestExecutorClassifiesUnknownMutationForGenericAudit(t *testing.T) {
	invoker := &recordingMailInvoker{
		result: json.RawMessage(
			`{"status":"unknown","updatedAt":"2026-08-15T12:00:00Z","truncated":false}`,
		),
	}
	result, err := NewExecutor(invoker).Invoke(
		context.Background(),
		registry.BuiltinInvocation{
			Action: ActionCreateDraft,
		},
	)
	require.NoError(t, err)
	require.Equal(t, "unknown", result.StatusClass)
	require.Equal(t, "unknown", result.ResultClassification)
}

func TestExecutorProvidesConservativeUnknownAfterMutationAuditFailure(
	t *testing.T,
) {
	invoker := &recordingMailInvoker{
		result: json.RawMessage(
			`{"status":"accepted","draft":"opaque-draft","sendDraft":"opaque-send","revision":9,"updatedAt":"2026-08-15T12:00:00Z","truncated":false}`,
		),
	}
	result, err := NewExecutor(invoker).Invoke(
		context.Background(),
		registry.BuiltinInvocation{Action: ActionCreateDraft},
	)
	require.NoError(t, err)
	require.JSONEq(
		t,
		`{"status":"unknown","updatedAt":"2026-08-15T12:00:00Z","truncated":false}`,
		string(result.AuditFailureData),
	)
	require.NotContains(t, string(result.AuditFailureData), "opaque-draft")
	require.NotContains(t, string(result.AuditFailureData), "opaque-send")
}

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

	composed := Extension(&recordingMailInvoker{})
	require.Equal(t, Domain, composed.Definition.Domain)
	require.Equal(t, registry.ConnectionRequired,
		composed.Implementation.ConnectionPolicy)
	require.Equal(t, Revision, composed.Implementation.Revision)
	require.Equal(t, "app_email", composed.Distribution.AppID)
	executor := composed.Implementation.Executor
	lister, ok := executor.(interface{ ActionNames() []string })
	require.True(t, ok, "executor must implement ActionLister")
	require.ElementsMatch(t, []string{
		ActionSearchMessages,
		ActionGetMessage,
		ActionListAttachments,
		ActionGetAttachment,
		ActionCreateDraft,
		ActionUpdateDraft,
		ActionCreateReplyDraft,
		ActionCreateForwardDraft,
		ActionSendDraft,
	}, lister.ActionNames())
}

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

	cases := []struct {
		status      string
		statusClass string
	}{
		{"accepted", "success"},
		{"failed", "failed"},
		{"unknown", "unknown"},
	}
	for _, testCase := range cases {
		invoker := &recordingMailInvoker{
			result: json.RawMessage(fmt.Sprintf(
				`{"status":%q,"recipientCount":2,"subjectExcerpt":"Excerpt","truncated":false}`,
				testCase.status,
			)),
		}
		result, err := NewExecutor(invoker).Invoke(
			context.Background(),
			registry.BuiltinInvocation{Action: ActionSendDraft},
		)
		require.NoError(t, err)
		require.Equal(t, testCase.statusClass, result.StatusClass, testCase.status)
		require.Equal(
			t,
			testCase.statusClass,
			result.ResultClassification,
			testCase.status,
		)
	}

	invoker := &recordingMailInvoker{
		result: json.RawMessage(
			`{"status":"accepted","recipientCount":2,"subjectExcerpt":"Excerpt","truncated":false}`,
		),
	}
	result, err := NewExecutor(invoker).Invoke(
		context.Background(),
		registry.BuiltinInvocation{Action: ActionSendDraft},
	)
	require.NoError(t, err)
	require.JSONEq(
		t,
		`{"status":"unknown","recipientCount":2,"truncated":false}`,
		string(result.AuditFailureData),
	)
	require.NotContains(t, string(result.AuditFailureData), "Excerpt")
}

func TestExecutorForwardsExplicitAttachmentTarget(t *testing.T) {
	invoker := &recordingMailInvoker{
		result: json.RawMessage(
			`{"status":"materialized","attachmentId":"att_one","filename":"notes.txt","mediaType":"text/plain","sizeBytes":5,"truncated":false,"untrustedExternalContent":true}`,
		),
	}
	_, err := NewExecutor(invoker).Invoke(
		context.Background(),
		registry.BuiltinInvocation{
			Action: ActionGetAttachment,
			Input:  json.RawMessage(`{"attachment":"att_1","target":"#general"}`),
		},
	)
	require.NoError(t, err)
	require.JSONEq(t, `{"attachment":"att_1","target":"#general"}`, string(invoker.invocation.Input))
}

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

	materialized := &recordingMailInvoker{
		result: json.RawMessage(
			`{"status":"materialized","attachmentId":"att_maybe","filename":"notes.txt","mediaType":"text/plain","sizeBytes":5,"truncated":false,"untrustedExternalContent":true}`,
		),
	}
	result, err := NewExecutor(materialized).Invoke(
		context.Background(),
		registry.BuiltinInvocation{Action: ActionGetAttachment},
	)
	require.NoError(t, err)
	require.Equal(t, "success", result.StatusClass)
	require.JSONEq(
		t,
		`{"status":"unknown","truncated":false,"untrustedExternalContent":true}`,
		string(result.AuditFailureData),
	)
	require.NotContains(t, string(result.AuditFailureData), "att_maybe")

	unknown := &recordingMailInvoker{
		result: json.RawMessage(
			`{"status":"unknown","truncated":false,"untrustedExternalContent":true}`,
		),
	}
	result, err = NewExecutor(unknown).Invoke(
		context.Background(),
		registry.BuiltinInvocation{Action: ActionGetAttachment},
	)
	require.NoError(t, err)
	require.Equal(t, "unknown", result.StatusClass)
	require.JSONEq(
		t,
		`{"status":"unknown","truncated":false,"untrustedExternalContent":true}`,
		string(result.AuditFailureData),
	)
}
