package smtpserver

import (
	"net/smtp"
	"strings"
	"testing"

	"mailgoserver/internal/db"
)

func TestBlockedSenderRejectedAtRcpt(t *testing.T) {
	backend, mailboxID := newTestBackendWithMailbox(t)
	if _, err := backend.DB.AddAllowBlockEntry(mailboxID, "block", "test@example.com"); err != nil {
		t.Fatal(err)
	}
	addr := startTestServer(t, backend)

	c, err := smtp.Dial(addr)
	if err != nil {
		t.Fatal(err)
	}
	defer c.Close()

	if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil {
		t.Fatalf("auth: %v", err)
	}
	if err := c.Mail("test@example.com"); err != nil {
		t.Fatalf("MAIL FROM: %v", err)
	}
	err = c.Rcpt("inbox@example.com")
	if err == nil {
		t.Fatal("expected RCPT to be rejected for a blocked sender")
	}
	if !strings.Contains(err.Error(), "550") {
		t.Fatalf("expected 550, got: %v", err)
	}
}

// TestAllowListBypassesSpamQuarantine confirms a zero reject threshold quarantines a
// non-allow-listed sender's mail into Spam (still accepted at SMTP level — spam is
// stored for review, not silently bounced), while an allow-listed sender's mail
// skips scoring entirely and lands in INBOX as normal.
func TestAllowListBypassesSpamQuarantine(t *testing.T) {
	backend, mailboxID := newTestBackendWithMailbox(t)
	// Force every non-allow-listed message to be quarantined as spam.
	backend.Cfg.Section("Mailstore").Key("spam_reject_score").SetValue("0")

	send := func(t *testing.T) error {
		t.Helper()
		c, err := smtp.Dial(startTestServer(t, backend))
		if err != nil {
			t.Fatal(err)
		}
		defer c.Close()
		if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil {
			t.Fatalf("auth: %v", err)
		}
		if err := c.Mail("test@example.com"); err != nil {
			t.Fatalf("MAIL FROM: %v", err)
		}
		if err := c.Rcpt("inbox@example.com"); err != nil {
			t.Fatalf("RCPT: %v", err)
		}
		w, err := c.Data()
		if err != nil {
			t.Fatal(err)
		}
		w.Write([]byte("Subject: hi\r\n\r\nhi"))
		return w.Close()
	}

	if err := send(t); err != nil {
		t.Fatalf("expected delivery accepted (quarantined) with a zero reject threshold and no allow-list entry, got: %v", err)
	}
	spamMsgs, err := backend.DB.ListMessagesInFolder(mailboxID, "Junk")
	if err != nil || len(spamMsgs) != 1 {
		t.Fatalf("expected 1 quarantined message in Spam, got %d (err=%v)", len(spamMsgs), err)
	}

	if _, err := backend.DB.AddAllowBlockEntry(mailboxID, "allow", "test@example.com"); err != nil {
		t.Fatal(err)
	}
	if err := send(t); err != nil {
		t.Fatalf("expected delivery to succeed once the sender is allow-listed, got: %v", err)
	}
	inboxMsgs, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
	if err != nil || len(inboxMsgs) != 1 {
		t.Fatalf("expected 1 message in INBOX once allow-listed (spam scoring skipped), got %d (err=%v)", len(inboxMsgs), err)
	}
}

// TestJunkListQuarantinesWithoutRejecting confirms a "junk" allowblock entry (the
// self-service webmail Blocklist — distinct from "block", which hard-rejects at RCPT
// instead) is still accepted at SMTP level but always lands in Junk, bypassing spam
// scoring entirely — the same soft-quarantine outcome as a heuristic/rspamd score hit,
// but decided unconditionally rather than computed.
func TestJunkListQuarantinesWithoutRejecting(t *testing.T) {
	backend, mailboxID := newTestBackendWithMailbox(t)
	if _, err := backend.DB.AddAllowBlockEntry(mailboxID, "junk", "test@example.com"); err != nil {
		t.Fatal(err)
	}
	addr := startTestServer(t, backend)

	c, err := smtp.Dial(addr)
	if err != nil {
		t.Fatal(err)
	}
	defer c.Close()
	if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil {
		t.Fatalf("auth: %v", err)
	}
	if err := c.Mail("test@example.com"); err != nil {
		t.Fatalf("MAIL FROM: %v", err)
	}
	if err := c.Rcpt("inbox@example.com"); err != nil {
		t.Fatalf("expected RCPT accepted (junk quarantines, doesn't reject), got: %v", err)
	}
	w, err := c.Data()
	if err != nil {
		t.Fatal(err)
	}
	w.Write([]byte("Subject: hi\r\n\r\nhi"))
	if err := w.Close(); err != nil {
		t.Fatalf("DATA: %v", err)
	}

	inbox, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
	if err != nil {
		t.Fatal(err)
	}
	if len(inbox) != 0 {
		t.Fatalf("expected nothing in INBOX, found %d", len(inbox))
	}
	junk, err := backend.DB.ListMessagesInFolder(mailboxID, "Junk")
	if err != nil {
		t.Fatal(err)
	}
	if len(junk) != 1 {
		t.Fatalf("expected 1 message quarantined in Junk, got %d", len(junk))
	}
}

func TestFilterRuleDeleteDropsMessage(t *testing.T) {
	backend, mailboxID := newTestBackendWithMailbox(t)
	if _, err := backend.DB.CreateRule(mailboxID, 0, "subject", "contains", "drop-me", "delete", ""); err != nil {
		t.Fatal(err)
	}
	addr := startTestServer(t, backend)

	c, err := smtp.Dial(addr)
	if err != nil {
		t.Fatal(err)
	}
	defer c.Close()
	if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil {
		t.Fatalf("auth: %v", err)
	}
	if err := c.Mail("test@example.com"); err != nil {
		t.Fatalf("MAIL FROM: %v", err)
	}
	if err := c.Rcpt("inbox@example.com"); err != nil {
		t.Fatalf("RCPT: %v", err)
	}
	w, err := c.Data()
	if err != nil {
		t.Fatal(err)
	}
	w.Write([]byte("Subject: drop-me please\r\n\r\nhi"))
	if err := w.Close(); err != nil {
		t.Fatalf("expected DATA to still report success even though the rule drops the message, got: %v", err)
	}

	msgs, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
	if err != nil {
		t.Fatal(err)
	}
	if len(msgs) != 0 {
		t.Fatalf("expected the delete rule to prevent storage, found %d messages", len(msgs))
	}
}

func TestFilterRuleMarkReadSetsSeenFlag(t *testing.T) {
	backend, mailboxID := newTestBackendWithMailbox(t)
	if _, err := backend.DB.CreateRule(mailboxID, 0, "subject", "contains", "newsletter", "mark_read", ""); err != nil {
		t.Fatal(err)
	}
	addr := startTestServer(t, backend)

	c, err := smtp.Dial(addr)
	if err != nil {
		t.Fatal(err)
	}
	defer c.Close()
	if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil {
		t.Fatalf("auth: %v", err)
	}
	if err := c.Mail("test@example.com"); err != nil {
		t.Fatalf("MAIL FROM: %v", err)
	}
	if err := c.Rcpt("inbox@example.com"); err != nil {
		t.Fatalf("RCPT: %v", err)
	}
	w, err := c.Data()
	if err != nil {
		t.Fatal(err)
	}
	w.Write([]byte("Subject: weekly newsletter\r\n\r\nhi"))
	if err := w.Close(); err != nil {
		t.Fatalf("DATA: %v", err)
	}

	msgs, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
	if err != nil {
		t.Fatal(err)
	}
	if len(msgs) != 1 {
		t.Fatalf("expected 1 message, got %d", len(msgs))
	}
	if !strings.Contains(msgs[0].Flags, `\Seen`) {
		t.Fatalf("expected the mark_read rule to set \\Seen, got flags %q", msgs[0].Flags)
	}
}

// TestFilterRuleForwardKeepCopyFalseDropsLocalCopy confirms a forward rule with
// keep_copy=false doesn't also store a local copy — forwarding itself happens
// fire-and-forget in a goroutine (see deliverLocally), so this only asserts on the
// local-storage decision, not on the (real, DNS-dependent) outbound delivery
// succeeding. ".invalid" is an RFC 2606 TLD guaranteed to never resolve, so the
// background forward attempt fails fast instead of hanging the test on a timeout.
func TestFilterRuleForwardKeepCopyFalseDropsLocalCopy(t *testing.T) {
	backend, mailboxID := newTestBackendWithMailbox(t)
	conditions := []db.RuleCondition{{Field: "subject", Op: "contains", Value: "fwd-drop"}}
	if _, err := backend.DB.CreateRuleMulti(mailboxID, 0, conditions, "all", "", "forward", "elsewhere@forward-test.invalid", `{"keep_copy":false}`); err != nil {
		t.Fatal(err)
	}
	addr := startTestServer(t, backend)

	c, err := smtp.Dial(addr)
	if err != nil {
		t.Fatal(err)
	}
	defer c.Close()
	if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil {
		t.Fatalf("auth: %v", err)
	}
	if err := c.Mail("test@example.com"); err != nil {
		t.Fatalf("MAIL FROM: %v", err)
	}
	if err := c.Rcpt("inbox@example.com"); err != nil {
		t.Fatalf("RCPT: %v", err)
	}
	w, err := c.Data()
	if err != nil {
		t.Fatal(err)
	}
	w.Write([]byte("Subject: fwd-drop this\r\n\r\nhi"))
	if err := w.Close(); err != nil {
		t.Fatalf("expected DATA to still report success even though the rule forwards-and-drops, got: %v", err)
	}

	msgs, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
	if err != nil {
		t.Fatal(err)
	}
	if len(msgs) != 0 {
		t.Fatalf("expected forward with keep_copy=false to skip local storage, found %d messages", len(msgs))
	}
}

// TestFilterRuleForwardKeepCopyTrueStillStoresLocally confirms the default
// (keep_copy=true) still stores the mailbox owner's own copy alongside forwarding.
func TestFilterRuleForwardKeepCopyTrueStillStoresLocally(t *testing.T) {
	backend, mailboxID := newTestBackendWithMailbox(t)
	conditions := []db.RuleCondition{{Field: "subject", Op: "contains", Value: "fwd-keep"}}
	if _, err := backend.DB.CreateRuleMulti(mailboxID, 0, conditions, "all", "", "forward", "elsewhere@forward-test.invalid", `{"keep_copy":true}`); err != nil {
		t.Fatal(err)
	}
	addr := startTestServer(t, backend)

	c, err := smtp.Dial(addr)
	if err != nil {
		t.Fatal(err)
	}
	defer c.Close()
	if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil {
		t.Fatalf("auth: %v", err)
	}
	if err := c.Mail("test@example.com"); err != nil {
		t.Fatalf("MAIL FROM: %v", err)
	}
	if err := c.Rcpt("inbox@example.com"); err != nil {
		t.Fatalf("RCPT: %v", err)
	}
	w, err := c.Data()
	if err != nil {
		t.Fatal(err)
	}
	w.Write([]byte("Subject: fwd-keep this\r\n\r\nhi"))
	if err := w.Close(); err != nil {
		t.Fatalf("DATA: %v", err)
	}

	msgs, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
	if err != nil {
		t.Fatal(err)
	}
	if len(msgs) != 1 {
		t.Fatalf("expected forward with keep_copy=true to still store locally, found %d messages", len(msgs))
	}
}

func TestFilterRuleMoveToFolderStoresInNamedFolder(t *testing.T) {
	backend, mailboxID := newTestBackendWithMailbox(t)
	if _, err := backend.DB.CreateRule(mailboxID, 0, "subject", "contains", "spam", "move_to_folder", "Junk"); err != nil {
		t.Fatal(err)
	}
	addr := startTestServer(t, backend)

	c, err := smtp.Dial(addr)
	if err != nil {
		t.Fatal(err)
	}
	defer c.Close()
	if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil {
		t.Fatalf("auth: %v", err)
	}
	if err := c.Mail("test@example.com"); err != nil {
		t.Fatalf("MAIL FROM: %v", err)
	}
	if err := c.Rcpt("inbox@example.com"); err != nil {
		t.Fatalf("RCPT: %v", err)
	}
	w, err := c.Data()
	if err != nil {
		t.Fatal(err)
	}
	w.Write([]byte("Subject: this looks like spam\r\n\r\nhi"))
	if err := w.Close(); err != nil {
		t.Fatalf("DATA: %v", err)
	}

	inbox, err := backend.DB.ListMessagesInFolder(mailboxID, "INBOX")
	if err != nil {
		t.Fatal(err)
	}
	if len(inbox) != 0 {
		t.Fatalf("expected nothing in INBOX, found %d", len(inbox))
	}
	spam, err := backend.DB.ListMessagesInFolder(mailboxID, "Junk")
	if err != nil {
		t.Fatal(err)
	}
	if len(spam) != 1 {
		t.Fatalf("expected 1 message in Spam, got %d", len(spam))
	}
}
