package email

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func fixture(t *testing.T, name string) []byte {
	t.Helper()
	b, err := os.ReadFile(filepath.Join("testdata", name))
	if err != nil {
		t.Fatalf("read fixture %s: %v", name, err)
	}
	return b
}

func TestParsePlainRussian(t *testing.T) {
	msg, err := ParseMessage(7, fixture(t, "plain_ru.eml"))
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if msg.UID != 7 {
		t.Errorf("uid = %d, want 7", msg.UID)
	}
	if want := "Нужно закрыть задачу"; msg.Subject != want {
		t.Errorf("subject = %q, want %q", msg.Subject, want)
	}
	if !strings.Contains(msg.From, "Антон") {
		t.Errorf("from = %q, want the decoded display name", msg.From)
	}
	if !strings.Contains(msg.Body, "Надо отправить акт до пятницы.") {
		t.Errorf("body = %q, want the quoted-printable text decoded", msg.Body)
	}
	if msg.Junk {
		t.Errorf("a personal mail must not be junk (%s)", msg.JunkReason)
	}
}

func TestParseHTMLOnlyIsStripped(t *testing.T) {
	msg, err := ParseMessage(1, fixture(t, "html_only.eml"))
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if strings.Contains(msg.Body, "<") || strings.Contains(msg.Body, "color:red") || strings.Contains(msg.Body, "x()") {
		t.Errorf("body still has markup/script/style: %q", msg.Body)
	}
	for _, want := range []string{"Счёт за интернет: 700", "Оплатить до 5 августа."} {
		if !strings.Contains(msg.Body, want) {
			t.Errorf("body = %q, want it to contain %q", msg.Body, want)
		}
	}
	// &nbsp; must have become a real space, not vanished into the number.
	if strings.Contains(msg.Body, "&nbsp;") {
		t.Errorf("entity left unescaped: %q", msg.Body)
	}
}

func TestParsePrefersPlainAndSkipsAttachments(t *testing.T) {
	msg, err := ParseMessage(2, fixture(t, "mixed_attachment.eml"))
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if got := strings.TrimSpace(msg.Body); got != "Sign the contract before Monday." {
		t.Errorf("body = %q, want the text/plain alternative only", got)
	}
	if strings.Contains(msg.Body, "PDF") {
		t.Errorf("attachment bytes leaked into the body: %q", msg.Body)
	}
}

// windows-1251 is what older Russian senders still emit. Subject-only for those
// mails meant they could never produce a task candidate.
func TestParseCP1251(t *testing.T) {
	msg, err := ParseMessage(3, fixture(t, "cp1251.eml"))
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if want := "Счёт за интернет"; msg.Subject != want {
		t.Errorf("subject = %q, want %q", msg.Subject, want)
	}
	if want := "Оплати счёт до пятницы."; !strings.Contains(msg.Body, want) {
		t.Errorf("body = %q, want it to contain %q", msg.Body, want)
	}
}

// A charset with no table here must degrade to headers-only rather than to
// mojibake the model would then extract a task from.
func TestParseUnsupportedCharsetKeepsHeaders(t *testing.T) {
	msg, err := ParseMessage(3, fixture(t, "koi8r.eml"))
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if msg.Subject != "Legacy" {
		t.Errorf("subject = %q, want Legacy", msg.Subject)
	}
	if msg.Body != "" {
		t.Errorf("body = %q, want empty for an undecodable charset", msg.Body)
	}
}

// A nested multipart/alternative that only had HTML must not fill the plain
// bucket: a real text/plain sibling later in the message is the better text and
// used to be discarded.
func TestParseNestedHTMLDoesNotShadowLaterPlain(t *testing.T) {
	raw := "Subject: nested\r\n" +
		"Content-Type: multipart/mixed; boundary=OUT\r\n\r\n" +
		"--OUT\r\n" +
		"Content-Type: multipart/alternative; boundary=IN\r\n\r\n" +
		"--IN\r\n" +
		"Content-Type: text/html; charset=utf-8\r\n\r\n" +
		"<p>from the html part</p>\r\n" +
		"--IN--\r\n" +
		"--OUT\r\n" +
		"Content-Type: text/plain; charset=utf-8\r\n\r\n" +
		"the real plain text\r\n" +
		"--OUT--\r\n"
	msg, err := ParseMessage(5, []byte(raw))
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if got := strings.TrimSpace(msg.Body); got != "the real plain text" {
		t.Errorf("body = %q, want the text/plain part to win", got)
	}
}

func TestParseTruncatesLongBody(t *testing.T) {
	var b strings.Builder
	b.WriteString("Subject: long\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n")
	for i := 0; i < 2000; i++ {
		b.WriteString("длинная строка ")
	}
	msg, err := ParseMessage(4, []byte(b.String()))
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if len(msg.Body) > MaxBodyBytes+8 {
		t.Errorf("body kept %d bytes, want ≤ %d", len(msg.Body), MaxBodyBytes)
	}
	if !strings.HasSuffix(msg.Body, "…") {
		t.Errorf("truncated body should be marked: %q", msg.Body[len(msg.Body)-20:])
	}
}

// Nesting depth comes off the wire, so a hostile message must not get to pick
// the recursion depth. The walk stops and the headers still come through.
func TestParseMessageBoundsMIMEDepth(t *testing.T) {
	var b strings.Builder
	b.WriteString("Subject: deep\r\nMIME-Version: 1.0\r\n")
	for i := 0; i < MaxMIMEDepth+20; i++ {
		fmt.Fprintf(&b, "Content-Type: multipart/mixed; boundary=\"b%d\"\r\n\r\n--b%d\r\n", i, i)
	}
	b.WriteString("Content-Type: text/plain\r\n\r\nглубоко\r\n")
	msg, err := ParseMessage(7, []byte(b.String()))
	if err != nil {
		t.Fatalf("ParseMessage: %v", err)
	}
	if msg.Subject != "deep" {
		t.Errorf("Subject = %q, want the headers to survive", msg.Subject)
	}
}

func TestCollapseSqueezesBlankLines(t *testing.T) {
	got := collapse("  a  b \r\n\r\n\r\n\r\n  c  \r\n")
	if got != "a b\n\nc" {
		t.Errorf("collapse = %q, want %q", got, "a b\n\nc")
	}
}
