package email

import (
	"net"
	"strings"
	"testing"
	"time"
)

func TestFetchSinceRun(t *testing.T) {
	mk := func(subject string) string {
		return "Subject: " + subject + "\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nbody\r\n"
	}
	f := &fakeIMAP{
		uids: []uint32{1, 2, 3},
		msgs: map[uint32]string{1: mk("one"), 2: mk("two"), 3: mk("three")},
	}
	fs := FetchSince{
		Addr: "mail.example:993", User: "kami", Mailbox: "INBOX",
		Timeout: 5 * time.Second,
		Since:   time.Date(2026, 7, 30, 0, 0, 0, 0, time.UTC),
		Max:     2,
		Skip:    func(uid uint32) bool { return uid == 3 },
	}
	msgs, err := fs.WithDial(dialer(t, f)).Run("secret")
	if err != nil {
		t.Fatalf("run: %v", err)
	}
	// Newest first, the already-seen UID skipped, Max respected.
	if len(msgs) != 2 {
		t.Fatalf("got %d messages, want 2: %+v", len(msgs), msgs)
	}
	if msgs[0].Subject != "two" || msgs[1].Subject != "one" {
		t.Errorf("subjects = %q,%q, want two,one (newest first)", msgs[0].Subject, msgs[1].Subject)
	}
	if strings.Contains(strings.Join(f.cmds, " "), "UID FETCH 3") {
		t.Error("a skipped UID must not be fetched again")
	}
}

func TestFetchSinceRequiresConfig(t *testing.T) {
	if _, err := (FetchSince{}).Run("secret"); err == nil {
		t.Fatal("an unconfigured mailbox must not be read")
	}
}

// Timeout zero disables the dial timeout AND every socket deadline, so a dead
// server parks the poller forever with his credential live in a TLS state.
func TestFetchSinceRejectsZeroTimeout(t *testing.T) {
	fs := FetchSince{Addr: "mail.example:993", User: "kami", Mailbox: "INBOX"}
	if _, err := fs.Run("secret"); err == nil {
		t.Fatal("a zero timeout must be rejected like an empty address")
	}
}

// dialer wires a client Conn to an in-process fake over net.Pipe.
func dialer(t *testing.T, f *fakeIMAP) func(string, time.Duration) (*Conn, error) {
	t.Helper()
	return func(addr string, timeout time.Duration) (*Conn, error) {
		cli, srv := net.Pipe()
		go f.serve(t, srv)
		return NewConn(cli, timeout)
	}
}

// One message that cannot be read must not hide the older ones behind it. The
// old code returned on the first failure, so an oversized or unreadable UID
// blocked every message below it on every poll, forever.
func TestFetchSinceContinuesPastABadMessage(t *testing.T) {
	mk := func(subject string) string {
		return "Subject: " + subject + "\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nbody\r\n"
	}
	f := &fakeIMAP{
		uids:   []uint32{1, 2, 3},
		msgs:   map[uint32]string{1: mk("one"), 3: mk("three")},
		quoted: map[uint32]bool{2: true},
	}
	fs := FetchSince{
		Addr: "mail.example:993", User: "kami", Mailbox: "INBOX",
		Timeout: 5 * time.Second,
		Since:   time.Date(2026, 7, 30, 0, 0, 0, 0, time.UTC),
	}
	msgs, err := fs.WithDial(dialer(t, f)).Run("secret")
	if err == nil {
		t.Fatal("the unreadable UID must still be reported")
	}
	if !strings.Contains(err.Error(), "uid 2") {
		t.Errorf("error should name the UID: %v", err)
	}
	if len(msgs) != 2 {
		t.Fatalf("got %d messages, want the two readable ones: %+v", len(msgs), msgs)
	}
	if msgs[0].Subject != "three" || msgs[1].Subject != "one" {
		t.Errorf("subjects = %q,%q, want three,one", msgs[0].Subject, msgs[1].Subject)
	}
}

// An oversized message is retired as bulk rather than retried: it will be the
// same size next poll, and the poller marks bulk seen without a model call.
func TestFetchSinceRetiresOversizedMessage(t *testing.T) {
	f := &fakeIMAP{uids: []uint32{7}, oversize: map[uint32]int{7: MaxMessageBytes + 1}}
	fs := FetchSince{
		Addr: "mail.example:993", User: "kami", Mailbox: "INBOX",
		Timeout: 5 * time.Second,
		Since:   time.Date(2026, 7, 30, 0, 0, 0, 0, time.UTC),
	}
	msgs, err := fs.WithDial(dialer(t, f)).Run("secret")
	if err != nil {
		t.Fatalf("run: %v", err)
	}
	if len(msgs) != 1 || !msgs[0].Junk || msgs[0].JunkReason != "oversize" {
		t.Fatalf("want one oversize-bulk message, got %+v", msgs)
	}
	if msgs[0].Body != "" || msgs[0].Subject != "" {
		t.Error("nothing from an oversized message may be kept")
	}
}
