package smtp

import (
	"bytes"
	"encoding/base64"
	"io"
	"mime"
	"mime/multipart"
	"net/mail"
	"strings"
	"testing"
)

func TestBuildMessage_plainOnly(t *testing.T) {
	msg := buildMessage(&SendRequest{
		From:     "alice@example.com",
		To:       []string{"bob@example.com"},
		Subject:  "Hello",
		BodyText: "Plain body",
	})

	if strings.Contains(msg, "multipart/") {
		t.Fatal("plain-only message must not be multipart")
	}
	if !strings.Contains(msg, "Content-Type: text/plain; charset=UTF-8") {
		t.Fatal("missing text/plain content type")
	}
	if !strings.Contains(msg, "Plain body") {
		t.Fatal("missing body text")
	}
}

func TestBuildMessage_alternativeOnly(t *testing.T) {
	msg := buildMessage(&SendRequest{
		From:     "alice@example.com",
		To:       []string{"bob@example.com"},
		Subject:  "Hello",
		BodyText: "Plain body",
		BodyHTML: "<p>HTML body</p>",
	})

	if !strings.Contains(msg, "Content-Type: multipart/alternative") {
		t.Fatal("missing multipart/alternative top-level content type")
	}
	if strings.Contains(msg, "multipart/mixed") {
		t.Fatal("alternative-only message must not be multipart/mixed")
	}
	if !strings.Contains(msg, "Content-Type: text/html; charset=UTF-8") {
		t.Fatal("missing text/html part")
	}
}

func TestBuildMessage_mixedWithPlainAttachment(t *testing.T) {
	payload := []byte("file-bytes")
	msg := buildMessage(&SendRequest{
		From:     "alice@example.com",
		To:       []string{"bob@example.com"},
		Subject:  "With file",
		BodyText: "See attached",
		Attachments: []SendAttachment{{
			Filename:    "doc.pdf",
			ContentType: "application/pdf",
			Data:        payload,
		}},
	})

	parsed, err := mail.ReadMessage(strings.NewReader(msg))
	if err != nil {
		t.Fatalf("ReadMessage: %v", err)
	}

	mediaType, params, err := mime.ParseMediaType(parsed.Header.Get("Content-Type"))
	if err != nil {
		t.Fatalf("ParseMediaType: %v", err)
	}
	if mediaType != "multipart/mixed" {
		t.Fatalf("top-level content type = %q, want multipart/mixed", mediaType)
	}

	parts := readAllParts(t, parsed.Body, params["boundary"])
	if len(parts) != 2 {
		t.Fatalf("part count = %d, want 2", len(parts))
	}

	if got := parts[0].Header.Get("Content-Type"); got != "text/plain; charset=UTF-8" {
		t.Fatalf("body part content type = %q", got)
	}
	if string(parts[0].Body) != "See attached" {
		t.Fatalf("body part = %q", parts[0].Body)
	}

	attType, attParams, err := mime.ParseMediaType(parts[1].Header.Get("Content-Type"))
	if err != nil {
		t.Fatalf("ParseMediaType attachment: %v", err)
	}
	if attType != "application/pdf" {
		t.Fatalf("attachment content type = %q", attType)
	}
	if attParams["name"] != "doc.pdf" {
		t.Fatalf("attachment name = %q", attParams["name"])
	}
	if parts[1].Header.Get("Content-Transfer-Encoding") != "base64" {
		t.Fatal("attachment missing base64 transfer encoding")
	}
	disp, dispParams, err := mime.ParseMediaType(parts[1].Header.Get("Content-Disposition"))
	if err != nil {
		t.Fatalf("ParseMediaType disposition: %v", err)
	}
	if disp != "attachment" {
		t.Fatalf("disposition = %q, want attachment", disp)
	}
	if dispParams["filename"] != "doc.pdf" {
		t.Fatalf("filename = %q", dispParams["filename"])
	}
	if !bytes.Equal(decodeBase64Part(t, parts[1].Body), payload) {
		t.Fatal("attachment payload mismatch")
	}
}

func TestBuildMessage_mixedWithAlternativeAndAttachment(t *testing.T) {
	msg := buildMessage(&SendRequest{
		From:     "alice@example.com",
		To:       []string{"bob@example.com"},
		Subject:  "Rich mail",
		BodyText: "Plain",
		BodyHTML: "<b>HTML</b>",
		Attachments: []SendAttachment{{
			Filename:    "note.txt",
			ContentType: "text/plain",
			Data:        []byte("attachment"),
		}},
	})

	parsed, err := mail.ReadMessage(strings.NewReader(msg))
	if err != nil {
		t.Fatalf("ReadMessage: %v", err)
	}

	mediaType, params, err := mime.ParseMediaType(parsed.Header.Get("Content-Type"))
	if err != nil {
		t.Fatalf("ParseMediaType: %v", err)
	}
	if mediaType != "multipart/mixed" {
		t.Fatalf("top-level content type = %q, want multipart/mixed", mediaType)
	}

	parts := readAllParts(t, parsed.Body, params["boundary"])
	if len(parts) != 2 {
		t.Fatalf("part count = %d, want 2", len(parts))
	}

	altType, altParams, err := mime.ParseMediaType(parts[0].Header.Get("Content-Type"))
	if err != nil {
		t.Fatalf("ParseMediaType alternative: %v", err)
	}
	if altType != "multipart/alternative" {
		t.Fatalf("first part type = %q, want multipart/alternative", altType)
	}

	altParts := readAllParts(t, bytes.NewReader(parts[0].Body), altParams["boundary"])
	if len(altParts) != 2 {
		t.Fatalf("alternative part count = %d, want 2", len(altParts))
	}
	if string(altParts[0].Body) != "Plain" {
		t.Fatalf("plain part = %q", altParts[0].Body)
	}
	if string(altParts[1].Body) != "<b>HTML</b>" {
		t.Fatalf("html part = %q", altParts[1].Body)
	}

	disp, _, err := mime.ParseMediaType(parts[1].Header.Get("Content-Disposition"))
	if err != nil {
		t.Fatalf("ParseMediaType disposition: %v", err)
	}
	if disp != "attachment" {
		t.Fatalf("attachment disposition = %q", disp)
	}
}

func TestBuildMessage_inlineAttachmentHeaders(t *testing.T) {
	msg := buildMessage(&SendRequest{
		From:     "alice@example.com",
		To:       []string{"bob@example.com"},
		Subject:  "Inline",
		BodyHTML: `<img src="cid:logo@ultimail">`,
		BodyText: "Logo",
		Attachments: []SendAttachment{{
			Filename:    "logo.png",
			ContentType: "image/png",
			ContentID:   "logo@ultimail",
			IsInline:    true,
			Data:        []byte{0x89, 0x50, 0x4e, 0x47},
		}},
	})

	parsed, err := mail.ReadMessage(strings.NewReader(msg))
	if err != nil {
		t.Fatalf("ReadMessage: %v", err)
	}

	_, params, err := mime.ParseMediaType(parsed.Header.Get("Content-Type"))
	if err != nil {
		t.Fatalf("ParseMediaType: %v", err)
	}

	parts := readAllParts(t, parsed.Body, params["boundary"])
	if len(parts) != 2 {
		t.Fatalf("part count = %d, want 2", len(parts))
	}

	inline := parts[1]
	disp, _, err := mime.ParseMediaType(inline.Header.Get("Content-Disposition"))
	if err != nil {
		t.Fatalf("ParseMediaType disposition: %v", err)
	}
	if disp != "inline" {
		t.Fatalf("disposition = %q, want inline", disp)
	}
	if inline.Header.Get("Content-ID") != "<logo@ultimail>" {
		t.Fatalf("content-id = %q", inline.Header.Get("Content-ID"))
	}
}

type mimePart struct {
	Header mail.Header
	Body   []byte
}

func readAllParts(t *testing.T, r io.Reader, boundary string) []mimePart {
	t.Helper()

	mr := multipart.NewReader(r, boundary)
	var parts []mimePart
	for {
		part, err := mr.NextPart()
		if err == io.EOF {
			break
		}
		if err != nil {
			t.Fatalf("NextPart: %v", err)
		}
		body, err := io.ReadAll(part)
		if err != nil {
			t.Fatalf("ReadAll part: %v", err)
		}
		parts = append(parts, mimePart{
			Header: mail.Header(part.Header),
			Body:   body,
		})
	}
	return parts
}

func decodeBase64Part(t *testing.T, raw []byte) []byte {
	t.Helper()
	clean := strings.Map(func(r rune) rune {
		if r == '\r' || r == '\n' {
			return -1
		}
		return r
	}, string(raw))
	out, err := base64.StdEncoding.DecodeString(clean)
	if err != nil {
		t.Fatalf("DecodeString: %v", err)
	}
	return out
}
