package mailapi

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/mailstore"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/protocol"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/app/runtime"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

type fakeIMAPProber struct {
	capabilities protocol.Capabilities
	err          error
	endpoint     protocol.Endpoint
	order        *[]string
	calls        int
}

func (fake *fakeIMAPProber) Probe(
	_ context.Context,
	endpoint protocol.Endpoint,
) (protocol.Capabilities, error) {
	fake.calls++
	fake.endpoint = endpoint
	if fake.order != nil {
		*fake.order = append(*fake.order, "imap")
	}
	return fake.capabilities, fake.err
}

type fakeSMTPProber struct {
	capabilities protocol.Capabilities
	err          error
	endpoint     protocol.Endpoint
	sender       string
	order        *[]string
	calls        int
}

func (fake *fakeSMTPProber) ProbeSender(
	_ context.Context,
	endpoint protocol.Endpoint,
	sender string,
) (protocol.Capabilities, error) {
	fake.calls++
	fake.endpoint = endpoint
	fake.sender = sender
	if fake.order != nil {
		*fake.order = append(*fake.order, "smtp")
	}
	return fake.capabilities, fake.err
}

type fakePersistence struct {
	installView        mailstore.StatelessInstallationView
	identity           mailstore.ReconnectIdentity
	reconnect          runtime.ConnectionView
	err                error
	authorizationErr   error
	authorizationCalls int
	identityLoadCalls  int
	installCalls       int
	reconnectCalls     int
	disconnectCalls    int
	material           mailstore.StatelessConnectionMaterial
	order              *[]string
}

func (fake *fakePersistence) RequireWorkspaceAppManagementForUser(
	_ context.Context,
	_ string,
	_ string,
) error {
	fake.authorizationCalls++
	if fake.order != nil {
		*fake.order = append(*fake.order, "authorize")
	}
	return fake.authorizationErr
}

func (fake *fakePersistence) FinalizeStatelessInstallation(
	_ context.Context,
	_ string,
	_ string,
	material mailstore.StatelessConnectionMaterial,
) (mailstore.StatelessInstallationView, error) {
	fake.installCalls++
	fake.material = material
	if fake.order != nil {
		*fake.order = append(*fake.order, "persist")
	}
	return fake.installView, fake.err
}

func (fake *fakePersistence) LoadStatelessReconnectIdentity(
	_ context.Context,
	_ string,
	_ string,
) (mailstore.ReconnectIdentity, error) {
	fake.identityLoadCalls++
	return fake.identity, fake.err
}

func (fake *fakePersistence) FinalizeStatelessReconnect(
	_ context.Context,
	_ string,
	_ string,
	_ string,
	material mailstore.StatelessConnectionMaterial,
) (runtime.ConnectionView, error) {
	fake.reconnectCalls++
	fake.material = material
	if fake.order != nil {
		*fake.order = append(*fake.order, "persist")
	}
	return fake.reconnect, fake.err
}

func (fake *fakePersistence) DisconnectStatelessConnection(
	context.Context,
	string,
	string,
	string,
	string,
) error {
	fake.disconnectCalls++
	return fake.err
}

func TestConnectionServiceVerifiesBeforePersistingSecretFreeConfig(
	t *testing.T,
) {
	t.Parallel()

	order := []string{}
	imap := &fakeIMAPProber{
		capabilities: protocol.Capabilities{
			Values: map[string]bool{
				"IMAP4REV1":   true,
				"UIDPLUS":     true,
				"CONDSTORE":   true,
				"SPECIAL-USE": true,
			},
			DraftMailbox: "Drafts",
		},
		order: &order,
	}
	smtp := &fakeSMTPProber{
		capabilities: protocol.Capabilities{
			Values: map[string]bool{"SMTPUTF8": true},
		},
		order: &order,
	}
	persistence := &fakePersistence{
		order: &order,
		installView: mailstore.StatelessInstallationView{
			Installation: app.AppInstallationView{
				ID: "appinst_email",
			},
			Connection: runtime.ConnectionView{ID: "conn_email"},
			Extension: serviceactor.InstallationExtension{
				ExtensionName: "work-mail",
			},
		},
	}
	service := NewService(
		imap,
		smtp,
		persistence,
		[]byte(strings.Repeat("k", 32)),
	)
	request := validServiceInstallRequest()
	request.SMTPSameAsIMAP = false
	request.SMTP.Auth = &mail.Auth{
		Username: "submit-user",
		Password: "submit-secret",
	}
	result, err := service.Install(
		context.Background(),
		"usr_1",
		"wsp_1",
		request,
	)
	require.NoError(t, err)
	require.Equal(
		t,
		[]string{"authorize", "imap", "smtp", "persist"},
		order,
	)
	require.Equal(t, "CaseSensitive@example.com", smtp.sender)
	require.Equal(t, "imap-user", imap.endpoint.Username)
	require.Equal(t, "submit-user", smtp.endpoint.Username)
	require.Equal(t, "appinst_email", result.InstallationID)
	require.Regexp(
		t,
		`^email:h1:[0-9a-f]{64}$`,
		persistence.material.ExternalSubjectRef,
	)
	config, err := json.Marshal(persistence.material.Config)
	require.NoError(t, err)
	require.NotContains(t, string(config), "imap-secret")
	require.NotContains(t, string(config), "submit-secret")
	require.Contains(t, string(config), `"senderAddressAccepted":true`)
	require.Contains(t, string(config), `"draftMailbox":"Drafts"`)
	credentials, err := json.Marshal(persistence.material.Credentials)
	require.NoError(t, err)
	require.Contains(t, string(credentials), "imap-secret")
	require.Contains(t, string(credentials), "submit-secret")
}

func TestConnectionServiceRequiresFreshWorkspaceAppManagementBeforeExternalIO(
	t *testing.T,
) {
	t.Parallel()

	for _, test := range []struct {
		name string
		run  func(*Service) error
	}{
		{
			name: "ordinary member install",
			run: func(service *Service) error {
				_, err := service.Install(
					context.Background(),
					"usr_member",
					"wsp_1",
					validServiceInstallRequest(),
				)
				return err
			},
		},
		{
			name: "revoked admin reconnect",
			run: func(service *Service) error {
				request := validServiceInstallRequest()
				_, err := service.Reconnect(
					context.Background(),
					"usr_revoked_admin",
					"wsp_1",
					"appinst_email",
					mail.ReconnectRequest{
						IMAP:           request.IMAP,
						SMTPSameAsIMAP: request.SMTPSameAsIMAP,
						SMTP:           request.SMTP,
					},
				)
				return err
			},
		},
	} {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			imap := &fakeIMAPProber{
				capabilities: protocol.Capabilities{
					Values: map[string]bool{"IMAP4REV1": true},
				},
			}
			smtp := &fakeSMTPProber{
				capabilities: protocol.Capabilities{
					Values: map[string]bool{"SMTPUTF8": true},
				},
			}
			persistence := &fakePersistence{
				authorizationErr: app.ErrForbidden,
				identity: mailstore.ReconnectIdentity{
					SenderAddress:      "Frozen@example.com",
					ExternalSubjectRef: "email:h1:" + strings.Repeat("a", 64),
				},
			}
			service := NewService(
				imap,
				smtp,
				persistence,
				[]byte(strings.Repeat("k", 32)),
			)

			err := test.run(service)

			require.ErrorIs(t, err, app.ErrForbidden)
			require.Equal(t, 1, persistence.authorizationCalls)
			require.Zero(t, persistence.identityLoadCalls)
			require.Zero(t, imap.calls)
			require.Zero(t, smtp.calls)
			require.Zero(t, persistence.installCalls)
			require.Zero(t, persistence.reconnectCalls)
		})
	}
}

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

	persistence := &fakePersistence{}
	service := NewService(
		&fakeIMAPProber{err: errors.New("unreachable")},
		&fakeSMTPProber{},
		persistence,
		[]byte(strings.Repeat("k", 32)),
	)
	_, err := service.Install(
		context.Background(),
		"usr_1",
		"wsp_1",
		validServiceInstallRequest(),
	)
	require.ErrorContains(t, err, "IMAP")
	require.Zero(t, persistence.installCalls)
}

func TestConnectionServiceReconnectReusesFrozenSenderIdentity(
	t *testing.T,
) {
	t.Parallel()

	externalRef := "email:h1:" + strings.Repeat("a", 64)
	smtp := &fakeSMTPProber{
		capabilities: protocol.Capabilities{
			Values: map[string]bool{"8BITMIME": true},
		},
	}
	persistence := &fakePersistence{
		identity: mailstore.ReconnectIdentity{
			SenderAddress:      "Frozen@example.com",
			ExternalSubjectRef: externalRef,
		},
		reconnect: runtime.ConnectionView{ID: "conn_email"},
	}
	service := NewService(
		&fakeIMAPProber{
			capabilities: protocol.Capabilities{
				Values: map[string]bool{"IMAP4REV1": true},
			},
		},
		smtp,
		persistence,
		[]byte(strings.Repeat("k", 32)),
	)
	install := validServiceInstallRequest()
	reconnect := mail.ReconnectRequest{
		IMAP:           install.IMAP,
		SMTPSameAsIMAP: install.SMTPSameAsIMAP,
		SMTP:           install.SMTP,
	}
	result, err := service.Reconnect(
		context.Background(),
		"usr_1",
		"wsp_1",
		"appinst_email",
		reconnect,
	)
	require.NoError(t, err)
	require.Equal(t, "Frozen@example.com", smtp.sender)
	require.Equal(t, externalRef, persistence.material.ExternalSubjectRef)
	require.Equal(t, "Frozen@example.com", result.SenderAddress)
}

func validServiceInstallRequest() mail.InstallRequest {
	return mail.InstallRequest{
		ExtensionName: "work-mail",
		DisplayName:   "Work Mail",
		SenderAddress: "CaseSensitive@EXAMPLE.COM",
		IMAP: mail.IMAPSettings{
			Host:    "imap.example.com",
			Port:    993,
			TLSMode: mail.TLSModeImplicit,
			Auth: mail.Auth{
				Username: "imap-user",
				Password: "imap-secret",
			},
		},
		SMTPSameAsIMAP: true,
		SMTP: mail.SMTPSettings{
			Host:    "smtp.example.com",
			Port:    587,
			TLSMode: mail.TLSModeStartTLS,
		},
	}
}
