package usecase_test

import (
	"context"
	"testing"

	errs "gateway/internal/library/errors"
	"gateway/internal/model/notification"
	"gateway/internal/model/notification/config"
	domentity "gateway/internal/model/notification/domain/entity"
	"gateway/internal/model/notification/domain/enum"
	domrepo "gateway/internal/model/notification/domain/repository"
	domtpl "gateway/internal/model/notification/domain/template"
	domusecase "gateway/internal/model/notification/domain/usecase"
	mocknotifrepo "gateway/internal/model/notification/mock/repository"
	"gateway/internal/model/notification/provider/email"
	"gateway/internal/model/notification/provider/sms"
	"gateway/internal/model/notification/repository"
	"gateway/internal/model/notification/template"
	"gateway/internal/model/notification/usecase"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/mock/gomock"
)

const (
	testTenantID    = "tenant-1"
	testEmailFrom   = "noreply@test.com"
	testEmailTarget = "testEmailTarget"
	testTenantName  = "Acme"
	varTenantName   = "tenant_name"
)

func newTestNotifier(t *testing.T, repo domrepo.NotificationRepository, queue domrepo.RetryQueue) domusecase.NotifierUseCase {
	t.Helper()
	return usecase.MustNotifierUseCase(usecase.NotifierUseCaseParam{
		Repo:        repo,
		Idempotency: repository.NewMemoryIdempotencyCache(),
		Quota:       repository.NewMemoryQuotaCounter(),
		RetryQueue:  queue,
		Renderer:    template.NewRenderer(template.DefaultRegistry(), domtpl.LocaleZhTW, domtpl.LocaleEnUS),
		Email: email.NewChain(email.NewMockSender(
			email.WithMockName("mock"),
			email.WithMockMessageID("email-msg-1"),
		)),
		SMS: sms.NewChain(sms.NewMockSender(
			sms.WithMockName("mock"),
			sms.WithMockMessageID("sms-msg-1"),
		)),
		Config: config.Config{
			DefaultLocale: domtpl.LocaleZhTW,
			RatePerTenant: config.RatePerTenantConfig{Email: 100, SMS: 100},
			Email:         config.EmailConfig{From: testEmailFrom},
		},
	})
}

func expectIdempotencyMiss(repo *mocknotifrepo.MockNotificationRepository, kind enum.NotifyKind, idemKey string) {
	repo.EXPECT().
		FindByIdempotency(gomock.Any(), testTenantID, kind, idemKey).
		Return(nil, notification.ErrNotFound)
}

func expectInsertAssignsID(repo *mocknotifrepo.MockNotificationRepository) {
	repo.EXPECT().Insert(gomock.Any(), gomock.Any()).DoAndReturn(
		func(_ context.Context, data *domentity.Notification) error {
			if data.ID.IsZero() {
				data.ID = bson.NewObjectID()
			}
			return nil
		},
	)
}

func TestNotifier_Send_EmailSuccess(t *testing.T) {
	ctrl := gomock.NewController(t)
	repo := mocknotifrepo.NewMockNotificationRepository(ctrl)

	expectIdempotencyMiss(repo, enum.NotifyVerifyEmail, "challenge-1")
	expectInsertAssignsID(repo)
	repo.EXPECT().UpdateDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)

	uc := newTestNotifier(t, repo, nil)
	dto, err := uc.Send(context.Background(), &domusecase.SendRequest{
		TenantID:       testTenantID,
		UID:            "uid-1",
		Channel:        enum.ChannelEmail,
		Kind:           enum.NotifyVerifyEmail,
		Target:         testEmailTarget,
		Locale:         domtpl.LocaleZhTW,
		Data:           map[string]any{domtpl.VarCode: "123456", domtpl.VarExpiresIn: 300},
		IdempotencyKey: "challenge-1",
		Severity:       enum.SeverityInfo,
	})
	require.NoError(t, err)
	assert.Equal(t, enum.NotifyStatusSent, dto.Status)
	assert.Equal(t, "mock", dto.Provider)
	assert.Equal(t, "email-msg-1", dto.ProviderMessageID)
	assert.NotEmpty(t, dto.TargetHash)
}

func TestNotifier_Send_IdempotentReplay(t *testing.T) {
	ctrl := gomock.NewController(t)
	repo := mocknotifrepo.NewMockNotificationRepository(ctrl)

	expectIdempotencyMiss(repo, enum.NotifyVerifyPhone, "challenge-2")
	expectInsertAssignsID(repo)
	repo.EXPECT().UpdateDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)

	uc := newTestNotifier(t, repo, nil)
	req := &domusecase.SendRequest{
		TenantID:       testTenantID,
		Channel:        enum.ChannelSMS,
		Kind:           enum.NotifyVerifyPhone,
		Target:         "+886912345678",
		Data:           map[string]any{domtpl.VarCode: "111111", domtpl.VarExpiresIn: 300},
		IdempotencyKey: "challenge-2",
	}

	first, err := uc.Send(context.Background(), req)
	require.NoError(t, err)

	second, err := uc.Send(context.Background(), req)
	require.NoError(t, err)
	assert.Equal(t, first.ID, second.ID)
}

func TestNotifier_Send_DoNotPersistBody(t *testing.T) {
	ctrl := gomock.NewController(t)
	repo := mocknotifrepo.NewMockNotificationRepository(ctrl)

	expectIdempotencyMiss(repo, enum.NotifyStepUpEmail, "challenge-3")
	expectInsertAssignsID(repo)
	repo.EXPECT().UpdateDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
		func(_ context.Context, _, _ string, update *domrepo.NotificationDeliveryUpdate) error {
			assert.Empty(t, update.Body)
			return nil
		},
	)

	uc := newTestNotifier(t, repo, nil)
	_, err := uc.Send(context.Background(), &domusecase.SendRequest{
		TenantID:         testTenantID,
		Channel:          enum.ChannelEmail,
		Kind:             enum.NotifyStepUpEmail,
		Target:           "testEmailTarget",
		Data:             map[string]any{domtpl.VarCode: "999999", domtpl.VarExpiresIn: 300},
		IdempotencyKey:   "challenge-3",
		DoNotPersistBody: true,
	})
	require.NoError(t, err)
}

func TestNotifier_Enqueue_Pending(t *testing.T) {
	ctrl := gomock.NewController(t)
	repo := mocknotifrepo.NewMockNotificationRepository(ctrl)
	queue := mocknotifrepo.NewMockRetryQueue(ctrl)

	expectIdempotencyMiss(repo, enum.NotifyTenantWelcome, "welcome-1")
	expectInsertAssignsID(repo)
	queue.EXPECT().Schedule(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)

	uc := newTestNotifier(t, repo, queue)
	dto, err := uc.Enqueue(context.Background(), &domusecase.SendRequest{
		TenantID:       testTenantID,
		Channel:        enum.ChannelEmail,
		Kind:           enum.NotifyTenantWelcome,
		Target:         "admin@example.com",
		Data:           map[string]any{varTenantName: testTenantName},
		IdempotencyKey: "welcome-1",
	})
	require.NoError(t, err)
	assert.Equal(t, enum.NotifyStatusPending, dto.Status)
}

func TestNotifier_Send_QuotaExceeded(t *testing.T) {
	ctrl := gomock.NewController(t)
	repo := mocknotifrepo.NewMockNotificationRepository(ctrl)

	gomock.InOrder(
		repo.EXPECT().FindByIdempotency(gomock.Any(), testTenantID, enum.NotifyVerifyEmail, "q1").
			Return(nil, notification.ErrNotFound),
		repo.EXPECT().Insert(gomock.Any(), gomock.Any()).DoAndReturn(
			func(_ context.Context, data *domentity.Notification) error {
				if data.ID.IsZero() {
					data.ID = bson.NewObjectID()
				}
				return nil
			},
		),
		repo.EXPECT().UpdateDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil),
		repo.EXPECT().FindByIdempotency(gomock.Any(), testTenantID, enum.NotifyVerifyEmail, "q2").
			Return(nil, notification.ErrNotFound),
	)

	uc := usecase.MustNotifierUseCase(usecase.NotifierUseCaseParam{
		Repo:     repo,
		Quota:    repository.NewMemoryQuotaCounter(),
		Renderer: template.NewRenderer(template.DefaultRegistry(), domtpl.LocaleZhTW),
		Email:    email.NewChain(email.NewMockSender()),
		Config: config.Config{
			RatePerTenant: config.RatePerTenantConfig{Email: 1},
			Email:         config.EmailConfig{From: testEmailFrom},
		},
	})

	_, err := uc.Send(context.Background(), &domusecase.SendRequest{
		TenantID:       testTenantID,
		Channel:        enum.ChannelEmail,
		Kind:           enum.NotifyVerifyEmail,
		Target:         "a@test.com",
		Data:           map[string]any{domtpl.VarCode: "1", domtpl.VarExpiresIn: 60},
		IdempotencyKey: "q1",
	})
	require.NoError(t, err)

	_, err = uc.Send(context.Background(), &domusecase.SendRequest{
		TenantID:       testTenantID,
		Channel:        enum.ChannelEmail,
		Kind:           enum.NotifyVerifyEmail,
		Target:         "b@test.com",
		Data:           map[string]any{domtpl.VarCode: "2", domtpl.VarExpiresIn: 60},
		IdempotencyKey: "q2",
	})
	require.Error(t, err)
	var appErr *errs.Error
	require.ErrorAs(t, err, &appErr)
}
