package webui

import (
	"bytes"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"testing"

	"mailgoserver/internal/mailview"
)

// TestWebmailComposeSendLocalDelivery confirms a composed message reaches another
// local mailbox's INBOX with the right content, and a copy lands in the sender's own
// Sent folder — the core send/receive round trip.
func TestWebmailComposeSendLocalDelivery(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	domainID := domains[0].ID

	senderID := createTestMailboxWithPassword(t, app, "sender@example.com", domainID, "sender-password-1!")
	recipientID := createTestMailboxWithPassword(t, app, "recipient@example.com", domainID, "recipient-password-1!")
	cookie := webmailLoginSession(t, app, senderID)

	form := url.Values{
		"to": {"recipient@example.com"}, "subject": {"Hello there"}, "body_html": {"This is the message body."},
	}
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("compose send: status=%d body=%s", rec.Code, rec.Body.String())
	}

	recipientMsgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX")
	if err != nil {
		t.Fatal(err)
	}
	if len(recipientMsgs) != 1 {
		t.Fatalf("expected 1 message in recipient's INBOX, got %d", len(recipientMsgs))
	}
	if recipientMsgs[0].CachedSubject != "Hello there" {
		t.Errorf("recipient subject = %q", recipientMsgs[0].CachedSubject)
	}

	senderSent, err := app.DB.ListMessagesInFolder(senderID, "Sent")
	if err != nil {
		t.Fatal(err)
	}
	if len(senderSent) != 1 {
		t.Fatalf("expected 1 message in sender's Sent folder, got %d", len(senderSent))
	}
	if isUnread(senderSent[0].Flags) {
		t.Error("expected the Sent copy to be marked read, not unread")
	}
	if !isUnread(recipientMsgs[0].Flags) {
		t.Error("expected the recipient's INBOX copy to still be unread")
	}

	// Recipient can actually read it via the message view.
	recipientCookie := webmailLoginSession(t, app, recipientID)
	viewReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(recipientMsgs[0].ID, 10), nil)
	viewReq.AddCookie(recipientCookie)
	viewRec := httptest.NewRecorder()
	mux.ServeHTTP(viewRec, viewReq)
	if viewRec.Code != http.StatusOK {
		t.Fatalf("view message: status=%d", viewRec.Code)
	}
	if !strings.Contains(viewRec.Body.String(), "This is the message body.") {
		t.Error("expected the message body in the rendered view")
	}

	// It's also recorded in the admin email log for visibility — but never with the
	// real body content (privacy default; the Sent folder above already keeps the
	// real, encrypted-at-rest copy).
	logs, _ := app.DB.ListEmailLogsPage(0, 10)
	found := false
	for _, l := range logs {
		if l.Subject == "Hello there" && l.MailFrom == "sender@example.com" {
			found = true
			if strings.Contains(l.MessageBody, "This is the message body.") {
				t.Errorf("expected the real body not to be logged, got %q", l.MessageBody)
			}
		}
	}
	if !found {
		t.Error("expected the webmail send to show up in the admin email log")
	}
}

// TestWebmailMessageHTMLBodyIsSanitized confirms a malicious HTML body (e.g. from a
// received message, not something webmail's own plain-text compose can produce) never
// reaches the page unsanitized — this is the actual stored-XSS defense.
func TestWebmailMessageHTMLBodyIsSanitized(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "victim@example.com", domains[0].ID, "victim-password-1!")

	raw := "From: attacker@evil.example\r\nTo: victim@example.com\r\nSubject: gotcha\r\n" +
		"Content-Type: text/html\r\n\r\n" +
		`<p>hello</p><script>alert(document.cookie)</script><img src=x onerror="alert(1)">`
	uid, err := app.Mailstore.StoreMessage(mailboxID, "INBOX", []byte(raw), "x@example.com", "attacker@evil.example", "gotcha")
	if err != nil {
		t.Fatal(err)
	}

	cookie := webmailLoginSession(t, app, mailboxID)
	req := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(uid, 10), nil)
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("view message: status=%d", rec.Code)
	}
	body := rec.Body.String()
	// The page legitimately has its own <script> tags (Bootstrap/toast JS) — check
	// for the actual injected payload surviving, not the literal substring "<script>".
	if strings.Contains(body, "alert(document.cookie)") || strings.Contains(body, "onerror=") {
		t.Error("HTML body was not sanitized — script/event-handler survived into the rendered page")
	}
	if !strings.Contains(body, "<p>hello</p>") {
		t.Error("expected the safe formatting to survive sanitization")
	}
}

// TestWebmailAttachmentRoundTrip confirms a file attached during compose survives
// send, local delivery, and download with identical bytes.
func TestWebmailAttachmentRoundTrip(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	domainID := domains[0].ID

	senderID := createTestMailboxWithPassword(t, app, "sender2@example.com", domainID, "sender-password-1!")
	recipientID := createTestMailboxWithPassword(t, app, "recipient2@example.com", domainID, "recipient-password-1!")
	cookie := webmailLoginSession(t, app, senderID)

	var buf bytes.Buffer
	mw := multipart.NewWriter(&buf)
	mw.WriteField("to", "recipient2@example.com")
	mw.WriteField("subject", "With attachment")
	mw.WriteField("body_html", "see attached")
	fw, err := mw.CreateFormFile("attachments", "notes.txt")
	if err != nil {
		t.Fatal(err)
	}
	fw.Write([]byte("attachment file contents"))
	mw.Close()

	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", &buf)
	req.Header.Set("Content-Type", mw.FormDataContentType())
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("compose send with attachment: status=%d body=%s", rec.Code, rec.Body.String())
	}

	recipientMsgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX")
	if err != nil || len(recipientMsgs) != 1 {
		t.Fatalf("recipient INBOX: got %d messages, err=%v", len(recipientMsgs), err)
	}
	uid := recipientMsgs[0].ID

	recipientCookie := webmailLoginSession(t, app, recipientID)
	dlReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(uid, 10)+"/attachment/0", nil)
	dlReq.AddCookie(recipientCookie)
	dlRec := httptest.NewRecorder()
	mux.ServeHTTP(dlRec, dlReq)
	if dlRec.Code != http.StatusOK {
		t.Fatalf("download attachment: status=%d", dlRec.Code)
	}
	if got := dlRec.Body.String(); got != "attachment file contents" {
		t.Errorf("downloaded attachment = %q, want original content", got)
	}
}

// TestWebmailComposeSendMultipleAttachments confirms sending 2+ files under the
// "attachments" field name — the shape the accumulating dropzone picker in
// webmail_compose.html produces — delivers all of them, not just the first. The
// server-side loop over r.MultipartForm.File["attachments"] already handled this
// before the dropzone UI existed; this closes the test-coverage gap that let "only
// one attachment works" go unnoticed (it was a frontend picker limitation, not a
// server one — see the accumulating DataTransfer-backed picker in Milestone C).
func TestWebmailComposeSendMultipleAttachments(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	domainID := domains[0].ID

	senderID := createTestMailboxWithPassword(t, app, "multisender@example.com", domainID, "sender-password-1!")
	recipientID := createTestMailboxWithPassword(t, app, "multirecip@example.com", domainID, "recipient-password-1!")
	cookie := webmailLoginSession(t, app, senderID)

	var buf bytes.Buffer
	mw := multipart.NewWriter(&buf)
	mw.WriteField("to", "multirecip@example.com")
	mw.WriteField("subject", "Three files")
	mw.WriteField("body_html", "see attached")
	for i, name := range []string{"a.txt", "b.txt", "c.txt"} {
		fw, err := mw.CreateFormFile("attachments", name)
		if err != nil {
			t.Fatal(err)
		}
		fw.Write([]byte("contents of file " + strconv.Itoa(i)))
	}
	mw.Close()

	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", &buf)
	req.Header.Set("Content-Type", mw.FormDataContentType())
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("compose send: status=%d body=%s", rec.Code, rec.Body.String())
	}

	recipientMsgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX")
	if err != nil || len(recipientMsgs) != 1 {
		t.Fatalf("recipient INBOX: got %d messages, err=%v", len(recipientMsgs), err)
	}

	recipientCookie := webmailLoginSession(t, app, recipientID)
	viewReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(recipientMsgs[0].ID, 10), nil)
	viewReq.AddCookie(recipientCookie)
	viewRec := httptest.NewRecorder()
	mux.ServeHTTP(viewRec, viewReq)
	if viewRec.Code != http.StatusOK {
		t.Fatalf("view message: status=%d", viewRec.Code)
	}
	for _, name := range []string{"a.txt", "b.txt", "c.txt"} {
		if !strings.Contains(viewRec.Body.String(), name) {
			t.Errorf("expected attachment %q listed on the message page", name)
		}
	}
	if !strings.Contains(viewRec.Body.String(), "see attached") {
		t.Errorf("expected the message body rendered alongside the attachments, got: %s", viewRec.Body.String())
	}

	for i, name := range []string{"a.txt", "b.txt", "c.txt"} {
		dlReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(recipientMsgs[0].ID, 10)+"/attachment/"+strconv.Itoa(i), nil)
		dlReq.AddCookie(recipientCookie)
		dlRec := httptest.NewRecorder()
		mux.ServeHTTP(dlRec, dlReq)
		if dlRec.Code != http.StatusOK {
			t.Fatalf("download attachment %d (%s): status=%d", i, name, dlRec.Code)
		}
		want := "contents of file " + strconv.Itoa(i)
		if got := dlRec.Body.String(); got != want {
			t.Errorf("attachment %d content = %q, want %q", i, got, want)
		}
	}
}

// TestWebmailComposeRejectsEmptySend confirms the server-side guard (not just the
// client-side JS, which a test can't exercise) refuses to send a message with no
// subject, and separately one with no body and no attachments.
func TestWebmailComposeRejectsEmptySend(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	domainID := domains[0].ID

	senderID := createTestMailboxWithPassword(t, app, "emptysender@example.com", domainID, "sender-password-1!")
	recipientID := createTestMailboxWithPassword(t, app, "emptyrecip@example.com", domainID, "recipient-password-1!")
	cookie := webmailLoginSession(t, app, senderID)

	send := func(t *testing.T, form url.Values) *httptest.ResponseRecorder {
		t.Helper()
		req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.AddCookie(cookie)
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		return rec
	}

	// A rejected send redisplays the compose form (status 200) with the posted
	// content still filled in, rather than redirecting to a blank one.
	noSubject := url.Values{"to": {"emptyrecip@example.com"}, "subject": {""}, "body_html": {"hello"}}
	rec := send(t, noSubject)
	if rec.Code != http.StatusOK {
		t.Fatalf("no-subject send: status=%d", rec.Code)
	}

	noBody := url.Values{"to": {"emptyrecip@example.com"}, "subject": {"hi"}, "body_html": {"   "}}
	rec = send(t, noBody)
	if rec.Code != http.StatusOK {
		t.Fatalf("no-body send: status=%d", rec.Code)
	}

	msgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX")
	if err != nil || len(msgs) != 0 {
		t.Fatalf("expected neither blank send delivered, got %d messages (err=%v)", len(msgs), err)
	}
}

// TestWebmailComposeHTMLBodyRoundTrip confirms an HTML compose body (what Quill
// submits) survives send/delivery as a proper multipart/alternative — both the
// formatted HTML and a derived plain-text fallback reach the recipient — and renders
// with its formatting intact on the message page.
func TestWebmailComposeHTMLBodyRoundTrip(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	domainID := domains[0].ID

	senderID := createTestMailboxWithPassword(t, app, "htmlsender@example.com", domainID, "sender-password-1!")
	recipientID := createTestMailboxWithPassword(t, app, "htmlrecip@example.com", domainID, "recipient-password-1!")
	cookie := webmailLoginSession(t, app, senderID)

	form := url.Values{
		"to": {"htmlrecip@example.com"}, "subject": {"Formatted"},
		"body_html": {"<p><strong>Bold</strong> and <em>italic</em> text.</p>"},
	}
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("compose send: status=%d body=%s", rec.Code, rec.Body.String())
	}

	msgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX")
	if err != nil || len(msgs) != 1 {
		t.Fatalf("expected 1 message, got %d (err=%v)", len(msgs), err)
	}
	raw, err := app.Mailstore.FetchMessage(recipientID, msgs[0].ID)
	if err != nil {
		t.Fatal(err)
	}
	parsed, err := mailview.Parse(raw)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(parsed.HTMLBody, "<strong>Bold</strong>") {
		t.Errorf("expected the HTML body to survive formatting, got %q", parsed.HTMLBody)
	}
	if !strings.Contains(parsed.TextBody, "Bold and italic text") {
		t.Errorf("expected a plain-text fallback part derived from the HTML, got %q", parsed.TextBody)
	}

	recipientCookie := webmailLoginSession(t, app, recipientID)
	viewReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(msgs[0].ID, 10), nil)
	viewReq.AddCookie(recipientCookie)
	viewRec := httptest.NewRecorder()
	mux.ServeHTTP(viewRec, viewReq)
	if viewRec.Code != http.StatusOK {
		t.Fatalf("view: status=%d", viewRec.Code)
	}
	if !strings.Contains(viewRec.Body.String(), "<strong>Bold</strong>") {
		t.Error("expected the formatted HTML rendered on the message page")
	}
}

// TestWebmailComposePastedImageSurvivesRoundTrip confirms a pasted screenshot
// (Quill's clipboard module embeds it as a base64 data: URI <img>) survives compose,
// delivery, and sanitize-on-view unchanged — this is what makes "paste a screenshot"
// actually work end to end, not just accepted at compose time.
func TestWebmailComposePastedImageSurvivesRoundTrip(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	domainID := domains[0].ID

	senderID := createTestMailboxWithPassword(t, app, "imgsender@example.com", domainID, "sender-password-1!")
	recipientID := createTestMailboxWithPassword(t, app, "imgrecip@example.com", domainID, "recipient-password-1!")
	cookie := webmailLoginSession(t, app, senderID)

	imgSrc := "data:image/png;base64,aGVsbG8="
	form := url.Values{
		"to": {"imgrecip@example.com"}, "subject": {"Screenshot"},
		"body_html": {`<p>See attached: <img src="` + imgSrc + `"></p>`},
	}
	req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusFound {
		t.Fatalf("compose send: status=%d body=%s", rec.Code, rec.Body.String())
	}

	msgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX")
	if err != nil || len(msgs) != 1 {
		t.Fatalf("expected 1 message, got %d (err=%v)", len(msgs), err)
	}

	recipientCookie := webmailLoginSession(t, app, recipientID)
	viewReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(msgs[0].ID, 10), nil)
	viewReq.AddCookie(recipientCookie)
	viewRec := httptest.NewRecorder()
	mux.ServeHTTP(viewRec, viewReq)
	if viewRec.Code != http.StatusOK {
		t.Fatalf("view: status=%d", viewRec.Code)
	}
	if !strings.Contains(viewRec.Body.String(), imgSrc) {
		t.Errorf("expected the pasted base64 image to survive sanitize-on-view, got body: %s", viewRec.Body.String())
	}
}

// TestWebmailComposeReplyPrefill confirms replying prefills To/Subject/quoted body
// from the original message.
func TestWebmailComposeReplyPrefill(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "replier@example.com", domains[0].ID, "replier-password-1!")

	raw := "From: original@example.com\r\nTo: replier@example.com\r\nSubject: Original subject\r\nMessage-Id: <orig123@example.com>\r\n\r\noriginal body text"
	uid, err := app.Mailstore.StoreMessage(mailboxID, "INBOX", []byte(raw), "orig123@example.com", "original@example.com", "Original subject")
	if err != nil {
		t.Fatal(err)
	}

	cookie := webmailLoginSession(t, app, mailboxID)
	req := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/compose?reply="+strconv.FormatInt(uid, 10)+"&folder=INBOX", nil)
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("compose reply prefill: status=%d", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "original@example.com") {
		t.Error("expected the To field prefilled with the original sender")
	}
	if !strings.Contains(body, "Re: Original subject") {
		t.Error("expected the subject prefilled with a Re: prefix")
	}
	if !strings.Contains(body, "original body text") {
		t.Error("expected the original body quoted")
	}
	// The new message goes ABOVE the quoted original, not mixed into or after it —
	// composeCursorHome (webmail_compose.go) prepends an empty line the cursor gets
	// placed in (see webmail_compose.html's seed-loading JS), so the seed must start
	// with that empty paragraph before the "On ... wrote:" quote line.
	seedIdx := strings.Index(body, `id="body_html_seed"`)
	quoteIdx := strings.Index(body, "wrote:")
	emptyLineIdx := strings.Index(body, "<p><br></p>")
	if seedIdx < 0 || quoteIdx < 0 || emptyLineIdx < 0 || !(seedIdx < emptyLineIdx && emptyLineIdx < quoteIdx) {
		t.Errorf("expected an empty line before the quoted original (for the new message to go above it), got: %s", body)
	}
}

// extractSeed pulls out #body_html_seed's own content from a rendered compose page —
// the seed div can itself contain a nested <div> (a signature's wrapper), so its end
// is located via the exact markup immediately following the seed div's closing tag in
// webmail_compose.html, not just "the next </div>" (which would match the nested one).
func extractSeed(t *testing.T, page string) string {
	t.Helper()
	const openMarker = `id="body_html_seed" style="display: none;">`
	const closeMarker = "</div>\n    </form>"
	start := strings.Index(page, openMarker)
	if start < 0 {
		t.Fatal("body_html_seed not found in rendered page")
	}
	start += len(openMarker)
	end := strings.Index(page[start:], closeMarker)
	if end < 0 {
		t.Fatal("body_html_seed's closing markup not found in rendered page")
	}
	return page[start : start+end]
}

// TestWebmailComposeDefaultSignatureInsertion confirms a mailbox's default-for-new
// signature is auto-inserted into a brand-new compose, and its (possibly different)
// default-for-reply signature is auto-inserted into a reply — both above the quoted
// original, matching where a real signature belongs.
func TestWebmailComposeDefaultSignatureInsertion(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "sigcompose@example.com", domains[0].ID, "sigcompose-password-1!")
	cookie := webmailLoginSession(t, app, mailboxID)

	newSigID, err := app.DB.CreateSignature(mailboxID, "New", "<p>Sent from my desk</p>")
	if err != nil {
		t.Fatal(err)
	}
	replySigID, err := app.DB.CreateSignature(mailboxID, "Reply", "<p>Sent on the go</p>")
	if err != nil {
		t.Fatal(err)
	}
	if err := app.DB.SetDefaultSignature(mailboxID, newSigID, false); err != nil {
		t.Fatal(err)
	}
	if err := app.DB.SetDefaultSignature(mailboxID, replySigID, true); err != nil {
		t.Fatal(err)
	}

	// Brand-new compose gets the "new" default.
	req := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/compose", nil)
	req.AddCookie(cookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	body := rec.Body.String()
	// The picker's hidden .sig-data divs legitimately contain every signature's HTML
	// (so the JS can swap between them client-side) — only the seed div (what actually
	// loads into the editor) should be checked for which one was auto-inserted. The
	// seed div's own content can itself contain a nested <div> (the signature's
	// wrapper), so its end is found via the exact markup that follows the seed div's
	// closing tag in the template, not just "the next </div>".
	seed := extractSeed(t, body)
	if !strings.Contains(seed, "Sent from my desk") {
		t.Errorf("expected the default-for-new signature inserted, got seed:\n%s", seed)
	}
	if strings.Contains(seed, "Sent on the go") {
		t.Error("did not expect the reply signature in a brand-new compose's seed")
	}

	// Reply gets the "reply" default instead, above the quote.
	raw := "From: original@example.com\r\nTo: sigcompose@example.com\r\nSubject: Hi\r\nMessage-Id: <o1@example.com>\r\n\r\noriginal body"
	uid, err := app.Mailstore.StoreMessage(mailboxID, "INBOX", []byte(raw), "o1@example.com", "original@example.com", "Hi")
	if err != nil {
		t.Fatal(err)
	}
	replyReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/compose?reply="+strconv.FormatInt(uid, 10)+"&folder=INBOX", nil)
	replyReq.AddCookie(cookie)
	replyRec := httptest.NewRecorder()
	mux.ServeHTTP(replyRec, replyReq)
	replySeed := extractSeed(t, replyRec.Body.String())
	if !strings.Contains(replySeed, "Sent on the go") {
		t.Errorf("expected the default-for-reply signature inserted into the seed, got:\n%s", replySeed)
	}
	sigIdx := strings.Index(replySeed, "Sent on the go")
	quoteIdx := strings.Index(replySeed, "wrote:")
	if sigIdx < 0 || quoteIdx < 0 || sigIdx > quoteIdx {
		t.Error("expected the signature above the quoted original")
	}
}

// TestWebmailMoveAndDeleteMessage confirms moving a message changes its folder, and
// deleting from a non-Trash folder moves to Trash first, requiring a second delete to
// actually remove it.
func TestWebmailMoveAndDeleteMessage(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	mailboxID := createTestMailboxWithPassword(t, app, "organizer@example.com", domains[0].ID, "organizer-password-1!")
	cookie := webmailLoginSession(t, app, mailboxID)

	raw := "From: a@example.com\r\nTo: organizer@example.com\r\nSubject: sort me\r\n\r\nbody"
	uid, err := app.Mailstore.StoreMessage(mailboxID, "INBOX", []byte(raw), "m1@example.com", "a@example.com", "sort me")
	if err != nil {
		t.Fatal(err)
	}
	uidStr := strconv.FormatInt(uid, 10)

	moveReq := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/INBOX/"+uidStr+"/move", strings.NewReader("target_folder=Work"))
	moveReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	moveReq.AddCookie(cookie)
	moveRec := httptest.NewRecorder()
	mux.ServeHTTP(moveRec, moveReq)
	if moveRec.Code != http.StatusFound {
		t.Fatalf("move: status=%d", moveRec.Code)
	}
	moved, err := app.DB.GetMessageByUID(mailboxID, uid)
	if err != nil || moved == nil || moved.Folder != "Work" {
		t.Fatalf("expected message moved to Work, got %+v (err=%v)", moved, err)
	}

	delReq := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/Work/"+uidStr+"/delete", nil)
	delReq.AddCookie(cookie)
	delRec := httptest.NewRecorder()
	mux.ServeHTTP(delRec, delReq)
	if delRec.Code != http.StatusFound {
		t.Fatalf("delete (to trash): status=%d", delRec.Code)
	}
	trashed, err := app.DB.GetMessageByUID(mailboxID, uid)
	if err != nil || trashed == nil || trashed.Folder != "Trash" {
		t.Fatalf("expected message moved to Trash, got %+v (err=%v)", trashed, err)
	}

	del2Req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/Trash/"+uidStr+"/delete", nil)
	del2Req.AddCookie(cookie)
	del2Rec := httptest.NewRecorder()
	mux.ServeHTTP(del2Rec, del2Req)
	if del2Rec.Code != http.StatusFound {
		t.Fatalf("delete (permanent): status=%d", del2Rec.Code)
	}
	gone, err := app.DB.GetMessageByUID(mailboxID, uid)
	if err != nil || gone != nil {
		t.Fatalf("expected message permanently gone, got %+v (err=%v)", gone, err)
	}
}

// TestWebmailMessageAccessControlAcrossMailboxes confirms one mailbox owner can't
// view another mailbox's message by guessing its UID, even in a folder name they
// both happen to have.
func TestWebmailMessageAccessControlAcrossMailboxes(t *testing.T) {
	app := newTestApp(t)
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	domainID := domains[0].ID

	ownerID := createTestMailboxWithPassword(t, app, "owner3@example.com", domainID, "owner-password-1!")
	attackerID := createTestMailboxWithPassword(t, app, "attacker3@example.com", domainID, "attacker-password-1!")

	raw := "From: a@example.com\r\nTo: owner3@example.com\r\nSubject: private\r\n\r\nsecret body"
	uid, err := app.Mailstore.StoreMessage(ownerID, "INBOX", []byte(raw), "m2@example.com", "a@example.com", "private")
	if err != nil {
		t.Fatal(err)
	}

	attackerCookie := webmailLoginSession(t, app, attackerID)
	req := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(uid, 10), nil)
	req.AddCookie(attackerCookie)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if rec.Code != http.StatusNotFound {
		t.Fatalf("expected 404 for another mailbox's message, got %d", rec.Code)
	}
}
