package message

import (
	"io"
	"log/slog"
	"strings"
	"testing"
	"time"

	"github.com/emersion/go-imap/v2"
	"github.com/emersion/go-imap/v2/imapclient"
	"github.com/emersion/go-message/mail"
	openai "github.com/sashabaranov/go-openai"
)

var log = slog.New(slog.NewTextHandler(io.Discard, nil))

var newMessageTests = []struct {
	name            string
	raw             string
	bufferOverrides func(*imapclient.FetchMessageBuffer)
	nilBuffer       bool
	Parts           []Part
	wantAttach      []Part
	wantRefs        []string
	wantTextBody    string
	shouldError     bool
}{
	{
		name: "Plain Text",
		raw: "Content-Type: text/plain; charset=utf-8\r\n" +
			"\r\n" +
			"Hello, world!\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Hello, world!\r\n",
			},
		},
		wantTextBody: "Hello, world!\r\n",
	},
	{
		name: "Multipart Alternative",
		raw: "Content-Type: multipart/alternative; boundary=bound\r\n" +
			"\r\n" +
			"--bound\r\n" +
			"Content-Type: text/plain\r\n" +
			"\r\n" +
			"Plain text\r\n" +
			"--bound\r\n" +
			"Content-Type: text/html\r\n" +
			"\r\n" +
			"<p>HTML</p>\r\n" +
			"--bound--\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Plain text",
			},
			{
				ContentType: "text/html",
				Content:     "<p>HTML</p>",
			},
		},
		wantTextBody: "Plain text\n<p>HTML</p>",
	},
	{
		name: "Multipart Mixed With Attachment",
		raw: "Content-Type: multipart/mixed; boundary=outer\r\n" +
			"\r\n" +
			"--outer\r\n" +
			"Content-Type: text/plain\r\n" +
			"\r\n" +
			"Message body\r\n" +
			"--outer\r\n" +
			"Content-Type: text/csv\r\n" +
			"Content-Disposition: attachment; filename=\"data.csv\"\r\n" +
			"\r\n" +
			"a,b,c\r\n" +
			"--outer--\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Message body",
			},
		},
		wantAttach: []Part{
			{
				ContentType:  "text/csv",
				Content:      "a,b,c",
				Filename:     "data.csv",
				IsAttachment: true,
			},
		},
		wantTextBody: "Message body",
	},
	{
		name: "Inline Image",
		raw: "Content-Type: multipart/mixed; boundary=bound\r\n" +
			"\r\n" +
			"--bound\r\n" +
			"Content-Type: text/plain\r\n" +
			"\r\n" +
			"See image:\r\n" +
			"--bound\r\n" +
			"Content-Type: image/png\r\n" +
			"Content-Disposition: inline\r\n" +
			"Content-Transfer-Encoding: base64\r\n" +
			"\r\n" +
			"iVBORw0KGgo=\r\n" +
			"--bound\r\n" +
			"Content-Type: text/plain\r\n" +
			"\r\n" +
			"What do you think?\r\n" +
			"--bound--\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "See image:",
			},
			{
				ContentType: "image/png",
				Data:        []byte{1},
			},
			{
				ContentType: "text/plain",
				Content:     "What do you think?",
			},
		},
		wantTextBody: "See image:\nWhat do you think?",
	},
	{
		name: "Image Attachment",
		raw: "Content-Type: multipart/mixed; boundary=bound\r\n" +
			"\r\n" +
			"--bound\r\n" +
			"Content-Type: text/plain\r\n" +
			"\r\n" +
			"Please review attached image.\r\n" +
			"--bound\r\n" +
			"Content-Type: image/jpeg\r\n" +
			"Content-Disposition: attachment; filename=\"photo.jpg\"\r\n" +
			"Content-Transfer-Encoding: base64\r\n" +
			"\r\n" +
			"/9j/4AAQ\r\n" +
			"--bound--\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Please review attached image.",
			},
		},
		wantAttach: []Part{
			{
				ContentType:  "image/jpeg",
				Data:         []byte{1},
				Filename:     "photo.jpg",
				IsAttachment: true,
			},
		},
		wantTextBody: "Please review attached image.",
	},
	{
		name: "Nested Multipart",
		raw: "Content-Type: multipart/mixed; boundary=outer\r\n" +
			"\r\n" +
			"--outer\r\n" +
			"Content-Type: multipart/alternative; boundary=inner\r\n" +
			"\r\n" +
			"--inner\r\n" +
			"Content-Type: text/plain\r\n" +
			"\r\n" +
			"Plain\r\n" +
			"--inner\r\n" +
			"Content-Type: text/html\r\n" +
			"\r\n" +
			"<b>HTML</b>\r\n" +
			"--inner--\r\n" +
			"--outer\r\n" +
			"Content-Type: image/png\r\n" +
			"Content-Disposition: attachment; filename=\"img.png\"\r\n" +
			"\r\n" +
			"PNG-DATA\r\n" +
			"--outer--\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Plain",
			},
			{
				ContentType: "text/html",
				Content:     "<b>HTML</b>",
			},
		},
		wantAttach: []Part{
			{
				ContentType:  "image/png",
				Data:         []byte{1},
				Filename:     "img.png",
				IsAttachment: true,
			},
		},
		wantTextBody: "Plain\n<b>HTML</b>",
	},
	{
		name: "References Header",
		raw: "References: <abc@example.com> <def@example.com>\r\n" +
			"Content-Type: text/plain\r\n" +
			"\r\n" +
			"Body\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Body\r\n",
			},
		},
		wantRefs: []string{
			"abc@example.com",
			"def@example.com",
		},
		wantTextBody: "Body\r\n",
	},
	{
		name: "Quoted Printable",
		raw: "Content-Type: text/plain; charset=utf-8\r\n" +
			"Content-Transfer-Encoding: quoted-printable\r\n" +
			"\r\n" +
			"Hello=20World\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Hello World\r\n",
			},
		},
		wantTextBody: "Hello World\r\n",
	},
	{
		name: "Base64 Text",
		raw: "Content-Type: text/plain; charset=utf-8\r\n" +
			"Content-Transfer-Encoding: base64\r\n" +
			"\r\n" +
			"SGVsbG8gV29ybGQ=\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Hello World",
			},
		},
		wantTextBody: "Hello World",
	},
	{
		name: "No Content Type",
		raw:  "\r\nPlain body with no headers\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Plain body with no headers\r\n",
			},
		},
		wantTextBody: "Plain body with no headers\r\n",
	},
	{
		name: "Multiple Text Parts",
		raw: "Content-Type: multipart/mixed; boundary=bound\r\n" +
			"\r\n" +
			"--bound\r\n" +
			"Content-Type: text/plain\r\n" +
			"\r\n" +
			"First part\r\n" +
			"--bound\r\n" +
			"Content-Type: text/plain\r\n" +
			"\r\n" +
			"Second part\r\n" +
			"--bound--\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "First part",
			},
			{
				ContentType: "text/plain",
				Content:     "Second part",
			},
		},
		wantTextBody: "First part\nSecond part",
	},
	{
		name: "Skips Unsupported Attachment",
		raw: "Content-Type: multipart/mixed; boundary=bound\r\n" +
			"\r\n" +
			"--bound\r\n" +
			"Content-Type: text/plain\r\n" +
			"\r\n" +
			"Message\r\n" +
			"--bound\r\n" +
			"Content-Type: application/pdf\r\n" +
			"Content-Disposition: attachment; filename=\"doc.pdf\"\r\n" +
			"\r\n" +
			"PDF-DATA\r\n" +
			"--bound--\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Message",
			},
		},
		wantAttach:   nil,
		wantTextBody: "Message",
	},
	{
		name: "Skips Unsupported Inline",
		raw: "Content-Type: multipart/mixed; boundary=bound\r\n" +
			"\r\n" +
			"--bound\r\n" +
			"Content-Type: text/plain\r\n" +
			"\r\n" +
			"Message\r\n" +
			"--bound\r\n" +
			"Content-Type: audio/mpeg\r\n" +
			"\r\n" +
			"AUDIO-DATA\r\n" +
			"--bound--\r\n",
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Message",
			},
		},
		wantTextBody: "Message",
	},
	{
		name:        "Nil Buffer",
		nilBuffer:   true,
		shouldError: true,
	},
	{
		name: "Missing UID",
		raw:  "Subject: x\r\n\r\nBody",
		bufferOverrides: func(
			mb *imapclient.FetchMessageBuffer,
		) {
			mb.UID = 0
		},
		shouldError: true,
	},
	{
		name: "Missing Envelope",
		raw:  "Subject: x\r\n\r\nBody",
		bufferOverrides: func(
			mb *imapclient.FetchMessageBuffer,
		) {
			mb.Envelope = nil
		},
		shouldError: true,
	},
	{
		name: "Empty Body Section",
		raw:  "Content-Type: text/plain\r\n\r\nBody\r\n",
		bufferOverrides: func(
			mb *imapclient.FetchMessageBuffer,
		) {
			// Prepend an empty section to verify it gets skipped.
			emptySec := imapclient.FetchBodySectionBuffer{
				Section: &imap.FetchItemBodySection{},
				Bytes:   []byte{},
			}
			mb.BodySection = append(
				[]imapclient.FetchBodySectionBuffer{
					emptySec,
				},
				mb.BodySection...,
			)
		},
		Parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Body\r\n",
			},
		},
		wantTextBody: "Body\r\n",
	},
}

// TestNew verifies MIME parsing across message structures:
// plain text, multipart/alternative, multipart/mixed with attachments,
// nested multipart, transfer encodings (quoted-printable, base64),
// inline images, and error conditions.
func TestNew(t *testing.T) {
	for _, tt := range newMessageTests {
		t.Run(tt.name, func(t *testing.T) {
			var mb *imapclient.FetchMessageBuffer

			if !tt.nilBuffer {
				mb = &imapclient.FetchMessageBuffer{
					UID:      1,
					Envelope: &imap.Envelope{},
					BodySection: []imapclient.FetchBodySectionBuffer{
						{
							Section: &imap.FetchItemBodySection{},
							Bytes:   []byte(tt.raw),
						},
					},
				}
				if tt.bufferOverrides != nil {
					tt.bufferOverrides(mb)
				}
			}

			msg, err := New(mb, log)

			if tt.shouldError {
				if err == nil {
					t.Fatal("expected error, got nil")
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if msg == nil {
				t.Fatal("expected message, got nil")
			}

			// Verify parts.
			if len(msg.Parts) != len(tt.Parts) {
				t.Fatalf(
					"Parts count: got %d, want %d",
					len(msg.Parts), len(tt.Parts),
				)
			}
			for i, want := range tt.Parts {
				got := msg.Parts[i]
				if !strings.HasPrefix(
					got.ContentType, want.ContentType,
				) {
					t.Errorf(
						"Parts[%d] content-type: got %q, want prefix %q",
						i, got.ContentType,
						want.ContentType,
					)
				}
				if want.Content != "" &&
					got.Content != want.Content {
					t.Errorf(
						"Parts[%d] Content: got %q, want %q",
						i, got.Content, want.Content,
					)
				}
				if len(want.Data) > 0 && len(got.Data) == 0 {
					t.Errorf(
						"Parts[%d] expected data, got empty",
						i,
					)
				}
			}

			// Verify attachments.
			if len(msg.Attachments) != len(tt.wantAttach) {
				t.Fatalf(
					"Attachments count: got %d, want %d",
					len(msg.Attachments),
					len(tt.wantAttach))
			}
			for i, want := range tt.wantAttach {
				got := msg.Attachments[i]
				if !strings.HasPrefix(
					got.ContentType, want.ContentType,
				) {
					t.Errorf(
						"Attachments[%d] type: got %q, want prefix %q",
						i, got.ContentType, want.ContentType,
					)
				}
				if want.Filename != "" &&
					got.Filename != want.Filename {
					t.Errorf(
						"Attachments[%d] Filename: got %q, want %q",
						i, got.Filename, want.Filename,
					)
				}
				if want.Content != "" && got.Content != want.Content {
					t.Errorf(
						"Attachments[%d] Content: got %q, want %q",
						i, got.Content, want.Content,
					)
				}
				if len(want.Data) > 0 && len(got.Data) == 0 {
					t.Errorf(
						"Attachments[%d] expected data, got empty",
						i,
					)
				}
				if !got.IsAttachment {
					t.Errorf(
						"Attachments[%d] IsAttachment: got false, want true",
						i,
					)
				}
			}

			// Verify TextBody.
			if tt.wantTextBody != "" &&
				msg.TextBody() != tt.wantTextBody {
				t.Errorf(
					"TextBody: got %q, want %q",
					msg.TextBody(), tt.wantTextBody,
				)
			}

			// Verify References.
			if len(tt.wantRefs) > 0 {
				if len(msg.References) != len(tt.wantRefs) {
					t.Errorf(
						"References count: got %d, want %d",
						len(msg.References),
						len(tt.wantRefs),
					)
				}
				for i, wantRef := range tt.wantRefs {
					if i < len(msg.References) &&
						msg.References[i] != wantRef {
						t.Errorf(
							"References[%d]: got %q, want %q",
							i,
							msg.References[i],
							wantRef,
						)
					}
				}
			}
		})
	}
}

var chatTests = []struct {
	name        string
	parts       []Part
	attachments []Part
	envelope    *imap.Envelope
	expected    []openai.ChatMessagePart
}{
	{
		name: "Text Only",
		parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "Hello",
			},
			{
				ContentType: "text/html",
				Content:     "<p>World</p>",
			},
		},
		expected: []openai.ChatMessagePart{
			{
				Type: openai.ChatMessagePartTypeText,
				Text: "Hello",
			},
			{
				Type: openai.ChatMessagePartTypeText,
				Text: "<p>World</p>",
			},
		},
	},
	{
		name: "Text And Image",
		parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "See this:",
			},
			{
				ContentType: "image/png",
				Data:        []byte("PNG"),
			},
		},
		expected: []openai.ChatMessagePart{
			{
				Type: openai.ChatMessagePartTypeText,
				Text: "See this:",
			},
			{
				Type: openai.ChatMessagePartTypeImageURL,
				ImageURL: &openai.ChatMessageImageURL{
					URL:    "data:image/png;base64,UE5H",
					Detail: openai.ImageURLDetailAuto,
				},
			},
		},
	},
	{
		name: "Parts Then Attachments",
		parts: []Part{
			{ContentType: "text/plain", Content: "Body"},
		},
		attachments: []Part{
			{
				ContentType:  "image/jpeg",
				Data:         []byte("JPG"),
				IsAttachment: true,
			},
		},
		expected: []openai.ChatMessagePart{
			{
				Type: openai.ChatMessagePartTypeText,
				Text: "Body",
			},
			{
				Type: openai.ChatMessagePartTypeImageURL,
				ImageURL: &openai.ChatMessageImageURL{
					URL:    "data:image/jpeg;base64,SlBH",
					Detail: openai.ImageURLDetailAuto,
				},
			},
		},
	},
	{
		name: "Skips Unsupported Types",
		parts: []Part{
			{ContentType: "text/plain", Content: "Text"},
			{ContentType: "application/pdf", Data: []byte("PDF")},
		},
		expected: []openai.ChatMessagePart{
			{Type: openai.ChatMessagePartTypeText, Text: "Text"},
		},
	},
	{
		name: "Order Preserved",
		parts: []Part{
			{ContentType: "text/plain", Content: "First"},
			{ContentType: "image/gif", Data: []byte("GIF1")},
			{ContentType: "text/csv", Content: "a,b,c"},
			{ContentType: "image/webp", Data: []byte("WEBP")},
			{ContentType: "text/plain", Content: "Last"},
		},
		expected: []openai.ChatMessagePart{
			{Type: openai.ChatMessagePartTypeText, Text: "First"},
			{
				Type: openai.ChatMessagePartTypeImageURL,
				ImageURL: &openai.ChatMessageImageURL{
					URL:    "data:image/gif;base64,R0lGMQ==",
					Detail: openai.ImageURLDetailAuto,
				},
			},
			{Type: openai.ChatMessagePartTypeText, Text: "a,b,c"},
			{Type: openai.ChatMessagePartTypeImageURL, ImageURL: &openai.ChatMessageImageURL{
				URL:    "data:image/webp;base64,V0VCUA==",
				Detail: openai.ImageURLDetailAuto,
			}},
			{Type: openai.ChatMessagePartTypeText, Text: "Last"},
		},
	},
	{
		name: "HeaderWithAllFields",
		parts: []Part{
			{ContentType: "text/plain", Content: "body"},
		},
		envelope: &imap.Envelope{
			From: []imap.Address{
				{
					Name:    "Alice",
					Mailbox: "alice",
					Host:    "example.com",
				},
				{
					Mailbox: "bob",
					Host:    "example.org",
				},
			},
			Date:    time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC),
			Subject: "Test Subject",
		},
		expected: []openai.ChatMessagePart{
			{
				// The header part – From, Date and Subject.
				Type: openai.ChatMessagePartTypeText,
				Text: "From: Alice <alice@example.com>, bob@example.org\n" +
					"Date: 2024-01-15T10:30:00Z\n" +
					"Subject: Test Subject\n",
			},
			{
				// The body part that follows the header.
				Type: openai.ChatMessagePartTypeText,
				Text: "body",
			},
		},
	},
	{
		name: "HeaderWithoutFrom",
		parts: []Part{
			{ContentType: "text/plain", Content: "only date"},
		},
		envelope: &imap.Envelope{
			Date:    time.Date(2022, 12, 31, 23, 59, 59, 0, time.FixedZone("-05:00", -5*3600)),
			Subject: "Year‑End",
		},
		expected: []openai.ChatMessagePart{
			{
				Type: openai.ChatMessagePartTypeText,
				Text: "Date: 2022-12-31T23:59:59-05:00\n" +
					"Subject: Year‑End\n",
			},
			{
				Type: openai.ChatMessagePartTypeText,
				Text: "only date",
			},
		},
	},
	{
		name: "HeaderWithoutDate",
		parts: []Part{
			{ContentType: "text/plain", Content: "no date"},
		},
		envelope: &imap.Envelope{
			From: []imap.Address{
				{
					Name:    "Charlie",
					Mailbox: "charlie",
					Host:    "example.net",
				},
			},
			Subject: "Missing‑Date",
		},
		expected: []openai.ChatMessagePart{
			{
				Type: openai.ChatMessagePartTypeText,
				Text: "From: Charlie <charlie@example.net>\n" +
					"Subject: Missing‑Date\n",
			},
			{
				Type: openai.ChatMessagePartTypeText,
				Text: "no date",
			},
		},
	},
	{
		name: "NilEnvelope",
		parts: []Part{
			{ContentType: "text/plain", Content: "just body"},
		},
		// envelope left nil – we expect *no* header part.
		expected: []openai.ChatMessagePart{
			{
				Type: openai.ChatMessagePartTypeText,
				Text: "just body",
			},
		},
	},
}

// TestToOpenAIMessages verifies conversion to OpenAI multimodal format.
func TestToOpenAIMessages(t *testing.T) {
	for _, tt := range chatTests {
		t.Run(tt.name, func(t *testing.T) {
			msg := &Message{
				Parts:       tt.parts,
				Attachments: tt.attachments,
				Envelope:    tt.envelope,
			}

			got := msg.ToOpenAIMessages()

			if len(got) != len(tt.expected) {
				t.Fatalf(
					"Part count: got %d, want %d",
					len(got), len(tt.expected),
				)
			}

			for i, want := range tt.expected {
				g := got[i]
				if g.Type != want.Type {
					t.Errorf(
						"[%d] Type: got %q, want %q",
						i, g.Type, want.Type,
					)
				}
				if want.Type == openai.ChatMessagePartTypeText {
					if g.Text != want.Text {
						t.Errorf(
							"[%d] Text: got %q, want %q",
							i, g.Text, want.Text,
						)
					}
				}
				if want.Type ==
					openai.ChatMessagePartTypeImageURL {
					if g.ImageURL == nil {
						t.Errorf(
							"[%d] ImageURL is nil",
							i,
						)
						continue
					}
					if g.ImageURL.URL != want.ImageURL.URL {
						t.Errorf(
							"[%d] ImageURL.URL: got %q, want %q",
							i, g.ImageURL.URL,
							want.ImageURL.URL,
						)
					}
					if g.ImageURL.Detail !=
						want.ImageURL.Detail {
						t.Errorf(
							"[%d] ImageURL.Detail: got %q, want %q",
							i, g.ImageURL.Detail,
							want.ImageURL.Detail,
						)
					}
				}
			}
		})
	}
}

var fixedDate = time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
var defaultFrom = &mail.Address{Name: "Bob", Address: "bob@example.com"}
var replyTests = []struct {
	name                 string
	msg                  *Message
	from                 *mail.Address
	replyText            string
	shouldError          bool
	expectedRecipients   []string
	expectedSubject      string
	expectedInReplyTo    string
	expectedReferences   []string
	expectedBodyContains []string
}{
	{
		name: "Basic Reply",
		msg: &Message{
			Envelope: &imap.Envelope{
				Subject:   "Test",
				MessageID: "orig@example.com",
				From: []imap.Address{{
					Name:    "Alice",
					Mailbox: "alice",
					Host:    "example.com",
				}},
				Date: fixedDate,
			},
			Parts: []Part{{
				ContentType: "text/plain",
				Content:     "Original message",
			}},
		},
		replyText:          "My response",
		expectedRecipients: []string{"alice@example.com"},
		expectedSubject:    "Re: Test",
		expectedInReplyTo:  "<orig@example.com>",
		expectedBodyContains: []string{
			"My response",
			"> Original message",
			"On Mon, 15 Jan 2024 10:30:00 +0000, Alice <alice@example.com> wrote:",
		},
	},
	{
		name: "Uses Reply-To",
		msg: &Message{
			Envelope: &imap.Envelope{
				Subject: "Test",
				From: []imap.Address{{
					Mailbox: "from", Host: "example.com",
				}},
				ReplyTo: []imap.Address{{
					Mailbox: "replyto",
					Host:    "example.com",
				}},
				Date: fixedDate,
			},
			Parts: []Part{{
				ContentType: "text/plain", Content: "Body",
			}},
		},
		from:               &mail.Address{Address: "me@example.com"},
		replyText:          "Response",
		expectedRecipients: []string{"replyto@example.com"},
	},
	{
		name: "Subject Already Re",
		msg: &Message{
			Envelope: &imap.Envelope{
				Subject: "Re: Already replied",
				From: []imap.Address{{
					Mailbox: "from", Host: "example.com",
				}},
			},
			Parts: []Part{{
				ContentType: "text/plain", Content: "Body",
			}},
		},
		replyText:       "Response",
		expectedSubject: "Re: Already replied",
	},
	{
		name: "Thread References",
		msg: &Message{
			Envelope: &imap.Envelope{
				Subject:   "Thread",
				MessageID: "msg3@example.com",
				From: []imap.Address{{
					Mailbox: "from", Host: "example.com",
				}},
			},
			References: []string{
				"msg1@example.com", "msg2@example.com",
			},
			Parts: []Part{{
				ContentType: "text/plain", Content: "Body",
			}},
		},
		replyText: "Response",
		expectedReferences: []string{
			"msg1@example.com",
			"msg2@example.com",
			"msg3@example.com",
		},
	},
	{
		name:        "Nil Message",
		msg:         nil,
		shouldError: true,
	},
	{
		name:        "Nil Envelope",
		msg:         &Message{},
		shouldError: true,
	},
	{
		name: "Nil From",
		msg: &Message{
			Envelope: &imap.Envelope{
				From: []imap.Address{{
					Mailbox: "from", Host: "example.com",
				}},
			},
		},
		from:        nil,
		shouldError: true,
	},
	{
		name: "Empty From Address",
		msg: &Message{
			Envelope: &imap.Envelope{
				From: []imap.Address{{
					Mailbox: "from", Host: "example.com",
				}},
			},
		},
		from:        &mail.Address{Name: "Name Only"},
		shouldError: true,
	},
	{
		name: "From Address No At Sign",
		msg: &Message{
			Envelope: &imap.Envelope{
				From: []imap.Address{{
					Mailbox: "from", Host: "example.com",
				}},
			},
		},
		from:        &mail.Address{Address: "localonly"},
		shouldError: false,
	},
	{
		name: "No Recipients",
		msg: &Message{
			Envelope: &imap.Envelope{Subject: "Test"},
			Parts: []Part{{
				ContentType: "text/plain", Content: "Body",
			}},
		},
		shouldError: true,
	},
}

// TestComposeReply verifies reply composition: recipient selection,
// subject handling, threading headers, and error conditions.
func TestComposeReply(t *testing.T) {
	for _, tt := range replyTests {
		t.Run(tt.name, func(t *testing.T) {
			useFrom := tt.from
			// Use default sender for non-error cases when from is
			// not specified.
			if useFrom == nil && !tt.shouldError {
				useFrom = defaultFrom
			}

			reply, err := tt.msg.ComposeReply(
				fixedDate, useFrom, tt.replyText,
			)
			if tt.shouldError {
				if err == nil {
					t.Fatal("expected error, got nil")
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			if len(tt.expectedRecipients) > 0 {
				recipients, err := reply.Recipients()
				if err != nil {
					t.Fatalf("Recipients() error: %v", err)
				}

				if len(recipients) !=
					len(tt.expectedRecipients) {
					t.Errorf(
						"Recipients count: got %d, want %d",
						len(recipients),
						len(tt.expectedRecipients),
					)
				}
				for i, want := range tt.expectedRecipients {
					if i < len(recipients) &&
						!strings.Contains(
							recipients[i], want,
						) {
						t.Errorf(
							"Recipient[%d]: got %q, want substring %q",
							i, recipients[i], want,
						)
					}
				}
			}

			data, err := reply.Bytes()
			if err != nil {
				t.Fatalf("Bytes() error: %v", err)
			}
			bodyStr := string(data)

			if tt.expectedSubject != "" &&
				!strings.Contains(
					bodyStr,
					"Subject: "+tt.expectedSubject,
				) {
				t.Errorf(
					"Subject missing or mismatch.\nBody: %s\nExpected Subject: %s",
					bodyStr,
					tt.expectedSubject,
				)
			}
			if tt.expectedInReplyTo != "" {
				if !strings.Contains(
					bodyStr,
					"In-Reply-To: "+tt.expectedInReplyTo,
				) {
					t.Errorf(
						"In-Reply-To missing.\nBody: %s\nExpected: %s",
						bodyStr,
						tt.expectedInReplyTo,
					)
				}
			}

			for _, ref := range tt.expectedReferences {
				if !strings.Contains(bodyStr, ref) {
					t.Errorf(
						"References header missing ID %q",
						ref,
					)
				}
			}

			for _, part := range tt.expectedBodyContains {
				if !strings.Contains(bodyStr, part) {
					t.Errorf(
						"Body content missing: %q",
						part,
					)
				}
			}
		})
	}
}

var attributionTests = []struct {
	name     string
	envelope *imap.Envelope
	expected string
}{
	{
		name:     "No From",
		envelope: &imap.Envelope{},
		expected: "> \n",
	},
	{
		name: "No Date",
		envelope: &imap.Envelope{
			From: []imap.Address{{
				Name:    "Alice",
				Mailbox: "alice",
				Host:    "example.com",
			}},
		},
		expected: "Alice <alice@example.com> wrote:\n",
	},
	{
		name: "No Name",
		envelope: &imap.Envelope{
			From: []imap.Address{{
				Mailbox: "alice", Host: "example.com",
			}},
		},
		expected: "alice@example.com wrote:\n",
	},
	{
		name: "Full",
		envelope: &imap.Envelope{
			From: []imap.Address{{
				Name: "Bob", Mailbox: "bob", Host: "ex.com",
			}},
			Date: time.Date(
				2024, 1, 2, 15, 4, 5, 0,
				time.FixedZone("", -25200),
			),
		},
		expected: "On Tue, 2 Jan 2024 15:04:05 -0700, Bob <bob@ex.com> wrote:\n",
	},
}

// TestComposeAttribution verifies attribution line formatting for various
// sender/date combinations.
func TestComposeAttribution(t *testing.T) {
	for _, tt := range attributionTests {
		t.Run(tt.name, func(t *testing.T) {
			msg := &Message{Envelope: tt.envelope}
			if got := msg.composeAttribution(); got != tt.expected {
				t.Errorf("got %q, want %q", got, tt.expected)
			}
		})
	}
}

var quoteTests = []struct {
	name     string
	parts    []Part
	expected string
}{
	{
		name: "Simple",
		parts: []Part{
			{ContentType: "text/plain", Content: "line1\nline2\n"},
		},
		expected: "> line1\n> line2\n> \n",
	},
	{
		name: "Strips CR",
		parts: []Part{
			{
				ContentType: "text/plain",
				Content:     "line1\r\nline2\r\n",
			},
		},
		expected: "> line1\n> line2\n> \n",
	},
	{
		name: "Multiple Parts",
		parts: []Part{
			{ContentType: "text/plain", Content: "first"},
			{ContentType: "text/plain", Content: "second"},
		},
		expected: "> first\n> second\n",
	},
	{
		name: "Ignores Images",
		parts: []Part{
			{ContentType: "text/plain", Content: "text"},
			{ContentType: "image/png", Data: []byte("PNG")},
		},
		expected: "> text\n",
	},
}

// TestQuotedBody verifies line quoting and CR stripping.
func TestQuotedBody(t *testing.T) {
	for _, tt := range quoteTests {
		t.Run(tt.name, func(t *testing.T) {
			msg := &Message{Parts: tt.parts, Envelope: &imap.Envelope{}}
			if got := msg.QuotedBody(); got != tt.expected {
				t.Errorf("got %q, want %q", got, tt.expected)
			}
		})
	}
}

var textBodyTests = []struct {
	name     string
	parts    []Part
	expected string
}{
	{
		name: "Single Text Part",
		parts: []Part{
			{ContentType: "text/plain", Content: "Hello"},
		},
		expected: "Hello",
	},
	{
		name: "Multiple Text Parts",
		parts: []Part{
			{ContentType: "text/plain", Content: "First"},
			{ContentType: "text/html", Content: "<p>Second</p>"},
		},
		expected: "First\n<p>Second</p>",
	},
	{
		name: "Mixed With Images",
		parts: []Part{
			{ContentType: "text/plain", Content: "Before"},
			{ContentType: "image/png", Data: []byte("PNG")},
			{ContentType: "text/plain", Content: "After"},
		},
		expected: "Before\nAfter",
	},
	{
		name: "No Text Parts",
		parts: []Part{{
			ContentType: "image/jpeg",
			Data:        []byte("JPG"),
		}},
		expected: "",
	},
}

// TestTextBody verifies text extraction from parts.
func TestTextBody(t *testing.T) {
	for _, tt := range textBodyTests {
		t.Run(tt.name, func(t *testing.T) {
			msg := &Message{Parts: tt.parts}
			if got := msg.TextBody(); got != tt.expected {
				t.Errorf("got %q, want %q", got, tt.expected)
			}
		})
	}
}

// TestTextFrom checks the formatting of the “From:” line produced by
// Message.TextFrom.
func TestTextFrom(t *testing.T) {
	tests := []struct {
		name     string
		env      *imap.Envelope
		expected string
	}{
		{
			name:     "Nil envelope",
			env:      nil,
			expected: "",
		},
		{
			name: "No From addresses",
			env: &imap.Envelope{
				From: []imap.Address{},
			},
			expected: "",
		},
		{
			name: "Single address with name",
			env: &imap.Envelope{
				From: []imap.Address{
					{
						Name:    "Alice",
						Mailbox: "alice",
						Host:    "example.com",
					},
				},
			},
			expected: "From: Alice <alice@example.com>",
		},
		{
			name: "Single address without name",
			env: &imap.Envelope{
				From: []imap.Address{
					{
						Mailbox: "bob",
						Host:    "example.org",
					},
				},
			},
			expected: "From: bob@example.org",
		},
		{
			name: "Multiple mixed addresses",
			env: &imap.Envelope{
				From: []imap.Address{
					{
						Name:    "Carol",
						Mailbox: "carol",
						Host:    "example.net",
					},
					{
						Mailbox: "dave",
						Host:    "example.net",
					},
				},
			},
			expected: "From: Carol <carol@example.net>, dave@example.net",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			msg := &Message{Envelope: tt.env}
			got := msg.TextFrom()
			if got != tt.expected {
				t.Errorf("TextFrom() = %q, want %q", got, tt.expected)
			}
		})
	}
}

// TestPartHelpers verifies Part.IsImage and Part.IsText methods.
func TestPartHelpers(t *testing.T) {
	var tests = []struct {
		contentType string
		isText      bool
		isImage     bool
	}{
		{"application/pdf", false, false},
		{"audio/mpeg", false, false},
		{"image/gif", false, true},
		{"image/jpeg", false, true},
		{"image/png", false, true},
		{"image/svg+xml", false, false},
		{"image/webp", false, true},
		{"text/csv", true, false},
		{"text/html", true, false},
		{"text/plain", true, false},
	}
	for _, tt := range tests {
		t.Run(tt.contentType, func(t *testing.T) {
			p := Part{ContentType: tt.contentType}
			if got := p.IsText(); got != tt.isText {
				t.Errorf(
					"IsText: got %v, want %v",
					got, tt.isText,
				)
			}
			if got := p.IsImage(); got != tt.isImage {
				t.Errorf(
					"IsImage: got %v, want %v",
					got, tt.isImage,
				)
			}
		})
	}
}
