package service

import (
	"context"
	"errors"
	"strings"
	"testing"

	awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
	"github.com/aws/aws-sdk-go-v2/service/sesv2"
	sestypes "github.com/aws/aws-sdk-go-v2/service/sesv2/types"
	"github.com/aws/smithy-go"
	smithymiddleware "github.com/aws/smithy-go/middleware"

	"wucher/internal/config"
	"wucher/internal/queue"
)

type mockSESAPI struct {
	input  *sesv2.SendEmailInput
	output *sesv2.SendEmailOutput
	err    error
	calls  int
}

func (m *mockSESAPI) SendEmail(_ context.Context, params *sesv2.SendEmailInput, _ ...func(*sesv2.Options)) (*sesv2.SendEmailOutput, error) {
	m.calls++
	m.input = params
	if m.err != nil {
		return nil, m.err
	}
	if m.output != nil {
		return m.output, nil
	}
	return &sesv2.SendEmailOutput{}, nil
}

func TestSESSenderSendSimple(t *testing.T) {
	metadata := smithymiddleware.Metadata{}
	awsmiddleware.SetRequestIDMetadata(&metadata, "req-123")
	api := &mockSESAPI{
		output: &sesv2.SendEmailOutput{
			MessageId:      strPtr("ses-msg-1"),
			ResultMetadata: metadata,
		},
	}
	sender := NewSESSenderWithAPI(config.SESConfig{
		Region:               "ap-southeast-1",
		From:                 "Wucher <no-reply@example.com>",
		ConfigurationSetName: "auth-events",
	}, api)

	result, err := sender.Send(context.Background(), queue.EmailJob{
		MessageType: "password_reset",
		To:          "user@example.com",
		CC:          []string{"cc@example.com"},
		BCC:         []string{"bcc@example.com"},
		ReplyTo:     []string{"reply@example.com"},
		Subject:     "Reset Password",
		TextBody:    "plain body",
		HTMLBody:    "<p>html body</p>",
		Tags: map[string]string{
			"message_type": "password_reset",
			"template":     "auth_password_reset",
		},
	})
	if err != nil {
		t.Fatalf("send: %v", err)
	}
	if result.Provider != "ses" || result.ProviderMessageID != "ses-msg-1" || result.RequestID != "req-123" {
		t.Fatalf("unexpected result: %+v", result)
	}
	if api.calls != 1 || api.input == nil {
		t.Fatalf("expected SES SendEmail to be called once")
	}
	if got := *api.input.ConfigurationSetName; got != "auth-events" {
		t.Fatalf("unexpected configuration set: %q", got)
	}
	if got := *api.input.FromEmailAddress; got != "\"Wucher\" <no-reply@example.com>" {
		t.Fatalf("unexpected from address: %q", got)
	}
	if got := api.input.Destination.ToAddresses; len(got) != 1 || got[0] != "user@example.com" {
		t.Fatalf("unexpected to addresses: %#v", got)
	}
	if got := api.input.Destination.CcAddresses; len(got) != 1 || got[0] != "cc@example.com" {
		t.Fatalf("unexpected cc addresses: %#v", got)
	}
	if got := api.input.Destination.BccAddresses; len(got) != 1 || got[0] != "bcc@example.com" {
		t.Fatalf("unexpected bcc addresses: %#v", got)
	}
	if got := api.input.ReplyToAddresses; len(got) != 1 || got[0] != "reply@example.com" {
		t.Fatalf("unexpected reply-to addresses: %#v", got)
	}
	if api.input.Content == nil || api.input.Content.Simple == nil {
		t.Fatalf("expected simple SES content")
	}
	if got := *api.input.Content.Simple.Subject.Data; got != "Reset Password" {
		t.Fatalf("unexpected subject: %q", got)
	}
	if got := *api.input.Content.Simple.Body.Text.Data; got != "plain body" {
		t.Fatalf("unexpected text body: %q", got)
	}
	if got := *api.input.Content.Simple.Body.Html.Data; got != "<p>html body</p>" {
		t.Fatalf("unexpected html body: %q", got)
	}
	if len(api.input.EmailTags) != 2 {
		t.Fatalf("expected tags to be forwarded, got %#v", api.input.EmailTags)
	}
}

func TestSESSenderSendTemplate(t *testing.T) {
	api := &mockSESAPI{}
	sender := NewSESSenderWithAPI(config.SESConfig{
		Region: "ap-southeast-1",
		From:   "no-reply@example.com",
	}, api)

	_, err := sender.Send(context.Background(), queue.EmailJob{
		MessageType:  "invite_set_password",
		To:           "user@example.com",
		TemplateName: "auth.invite_set_password",
		TemplateData: map[string]any{"name": "John"},
	})
	if err != nil {
		t.Fatalf("send: %v", err)
	}
	if api.input == nil || api.input.Content == nil || api.input.Content.Template == nil {
		t.Fatalf("expected templated SES content")
	}
	if got := *api.input.Content.Template.TemplateName; got != "auth.invite_set_password" {
		t.Fatalf("unexpected template name: %q", got)
	}
	if got := *api.input.Content.Template.TemplateData; got != "{\"name\":\"John\"}" {
		t.Fatalf("unexpected template data: %q", got)
	}
}

func TestSESSenderSendInlineAttachmentUsesRawMIME(t *testing.T) {
	api := &mockSESAPI{}
	sender := NewSESSenderWithAPI(config.SESConfig{
		Region: "ap-southeast-1",
		From:   "no-reply@example.com",
	}, api)

	_, err := sender.Send(context.Background(), queue.EmailJob{
		MessageType: "security_pin_reset",
		To:          "user@example.com",
		Subject:     "Reset Security PIN",
		TextBody:    "plain body",
		HTMLBody:    `<img src="cid:branding-email-logo"><p>html body</p>`,
		Attachments: []queue.EmailAttachment{{
			Filename:    "email-logo.png",
			ContentType: "image/png",
			ContentID:   "branding-email-logo",
			Content:     []byte("png"),
			Inline:      true,
		}},
	})
	if err != nil {
		t.Fatalf("send: %v", err)
	}
	if api.input == nil || api.input.Content == nil || api.input.Content.Raw == nil {
		t.Fatalf("expected raw SES content")
	}

	raw := string(api.input.Content.Raw.Data)
	if !strings.Contains(raw, `Content-ID: <branding-email-logo>`) {
		t.Fatalf("expected inline content id in raw MIME: %q", raw)
	}
	if !strings.Contains(raw, `Content-Disposition: inline; filename="email-logo.png"`) {
		t.Fatalf("expected inline content disposition in raw MIME: %q", raw)
	}
	if !strings.Contains(raw, `cid:branding-email-logo`) {
		t.Fatalf("expected html body cid reference in raw MIME: %q", raw)
	}
}

func TestSESSenderSendValidationError(t *testing.T) {
	sender := NewSESSenderWithAPI(config.SESConfig{
		Region: "ap-southeast-1",
		From:   "no-reply@example.com",
	}, &mockSESAPI{})

	if _, err := sender.Send(context.Background(), queue.EmailJob{
		To:       "not-an-email",
		Subject:  "Hello",
		TextBody: "Body",
	}); err == nil {
		t.Fatalf("expected invalid recipient error")
	}

	invalidCfgSender := NewSESSenderWithAPI(config.SESConfig{}, &mockSESAPI{})
	if _, err := invalidCfgSender.Send(context.Background(), queue.EmailJob{
		To:       "user@example.com",
		Subject:  "Hello",
		TextBody: "Body",
	}); err == nil {
		t.Fatalf("expected missing SES config error")
	}
}

func TestClassifyDeliveryError(t *testing.T) {
	t.Run("throttling is transient", func(t *testing.T) {
		err := classifyDeliveryError(sesSenderAPIError{code: "TooManyRequestsException", fault: smithy.FaultClient})
		if !queue.IsTransient(err) {
			t.Fatalf("expected transient classification, got %v", err)
		}
	})

	t.Run("message rejected is permanent", func(t *testing.T) {
		err := classifyDeliveryError(sesSenderAPIError{code: "MessageRejected", fault: smithy.FaultClient})
		if !queue.IsPermanent(err) {
			t.Fatalf("expected permanent classification, got %v", err)
		}
	})

	t.Run("validation error is permanent", func(t *testing.T) {
		err := classifyDeliveryError(errors.New("invalid email job: recipient is required"))
		if !queue.IsPermanent(err) {
			t.Fatalf("expected permanent classification, got %v", err)
		}
	})
}

type sesSenderAPIError struct {
	code  string
	fault smithy.ErrorFault
}

func (e sesSenderAPIError) Error() string                 { return e.code }
func (e sesSenderAPIError) ErrorCode() string             { return e.code }
func (e sesSenderAPIError) ErrorMessage() string          { return e.code }
func (e sesSenderAPIError) ErrorFault() smithy.ErrorFault { return e.fault }

func strPtr(value string) *string {
	return &value
}

var _ sesAPI = (*mockSESAPI)(nil)
var _ = sestypes.MessageTag{}
