package imapserver_test

import (
	"net"
	"path/filepath"
	"testing"

	"github.com/emersion/go-imap/v2"
	"github.com/emersion/go-imap/v2/imapclient"

	"mailgoserver/internal/db"
	"mailgoserver/internal/imapserver"
	"mailgoserver/internal/mailstore"
)

// newTestMailboxWithAppPassword seeds a domain + mailbox + app password (returning the
// plaintext, since only its bcrypt hash is stored) and one stored message, and starts
// a plain IMAP listener against it.
func newTestMailboxWithAppPassword(t *testing.T) (client *imapclient.Client, mailboxEmail, appPassword string, mailboxID int64) {
	t.Helper()
	dir := t.TempDir()
	database, err := db.Open(filepath.Join(dir, "test.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { database.Close() })

	domainID, err := database.CreateDomain("example.com")
	if err != nil {
		t.Fatal(err)
	}

	store := mailstore.New(database, mailstore.GenerateDEK(), t.TempDir())
	dek := mailstore.GenerateDEK()
	wrapped, nonce, err := store.WrapDEK(dek)
	if err != nil {
		t.Fatal(err)
	}
	portalHash, err := db.HashPassword("portal-password-unused")
	if err != nil {
		t.Fatal(err)
	}
	mailboxID, err = database.CreateMailbox("inbox@example.com", portalHash, domainID, 5*1024*1024*1024, wrapped, nonce)
	if err != nil {
		t.Fatal(err)
	}

	appPassword = db.GenerateAppPassword(25)
	appHash, err := db.HashPassword(appPassword)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := database.CreateAppPassword(mailboxID, "test client", appHash, nil); err != nil {
		t.Fatal(err)
	}

	raw := []byte("From: Alice <alice@example.com>\r\nSubject: Hello there\r\n\r\nBody text.")
	if _, err := store.StoreMessage(mailboxID, "INBOX", raw, "<abc@example.com>", "Alice <alice@example.com>", "Hello there"); err != nil {
		t.Fatal(err)
	}

	backend := &imapserver.Backend{DB: database, Mailstore: store}
	srv := imapserver.NewPlainServer(backend)
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	go srv.Serve(ln)
	t.Cleanup(func() { srv.Close() })

	conn, err := net.Dial("tcp", ln.Addr().String())
	if err != nil {
		t.Fatal(err)
	}
	client = imapclient.New(conn, nil)
	t.Cleanup(func() { client.Close() })

	return client, "inbox@example.com", appPassword, mailboxID
}

func TestIMAPLoginSelectFetch(t *testing.T) {
	client, email, appPassword, _ := newTestMailboxWithAppPassword(t)

	if err := client.Login(email, appPassword).Wait(); err != nil {
		t.Fatalf("login with app password: %v", err)
	}

	selectData, err := client.Select("INBOX", nil).Wait()
	if err != nil {
		t.Fatalf("select INBOX: %v", err)
	}
	if selectData.NumMessages != 1 {
		t.Fatalf("NumMessages = %d, want 1", selectData.NumMessages)
	}

	msgs, err := client.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{
		Envelope:    true,
		Flags:       true,
		BodySection: []*imap.FetchItemBodySection{{}},
	}).Collect()
	if err != nil {
		t.Fatalf("fetch: %v", err)
	}
	if len(msgs) != 1 {
		t.Fatalf("got %d messages, want 1", len(msgs))
	}
	msg := msgs[0]
	if msg.Envelope == nil || msg.Envelope.Subject != "Hello there" {
		t.Fatalf("envelope subject = %+v, want %q", msg.Envelope, "Hello there")
	}
	if len(msg.Envelope.From) != 1 || msg.Envelope.From[0].Addr() != "alice@example.com" {
		t.Fatalf("envelope from = %+v, want alice@example.com", msg.Envelope.From)
	}
	if len(msg.BodySection) != 1 {
		t.Fatalf("expected one body section, got %d", len(msg.BodySection))
	}
	got := string(msg.BodySection[0].Bytes)
	want := "From: Alice <alice@example.com>\r\nSubject: Hello there\r\n\r\nBody text."
	if got != want {
		t.Fatalf("body section = %q, want %q", got, want)
	}

	if err := client.Logout().Wait(); err != nil {
		t.Fatalf("logout: %v", err)
	}
}

func TestIMAPLoginRejectsPortalPassword(t *testing.T) {
	client, email, _, _ := newTestMailboxWithAppPassword(t)

	// The mailbox's own portal password must never work for IMAP login — only an
	// app password does (see Session.Login).
	err := client.Login(email, "portal-password-unused").Wait()
	if err == nil {
		t.Fatal("expected login with the portal password to fail")
	}
}

func TestIMAPListAndSelectAdditionalFolder(t *testing.T) {
	dir := t.TempDir()
	database, err := db.Open(filepath.Join(dir, "test.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { database.Close() })

	domainID, err := database.CreateDomain("example.com")
	if err != nil {
		t.Fatal(err)
	}
	store := mailstore.New(database, mailstore.GenerateDEK(), t.TempDir())
	dek := mailstore.GenerateDEK()
	wrapped, nonce, err := store.WrapDEK(dek)
	if err != nil {
		t.Fatal(err)
	}
	portalHash, err := db.HashPassword("portal-password-unused")
	if err != nil {
		t.Fatal(err)
	}
	mailboxID, err := database.CreateMailbox("inbox@example.com", portalHash, domainID, 5*1024*1024*1024, wrapped, nonce)
	if err != nil {
		t.Fatal(err)
	}
	appPassword := db.GenerateAppPassword(25)
	appHash, err := db.HashPassword(appPassword)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := database.CreateAppPassword(mailboxID, "test client", appHash, nil); err != nil {
		t.Fatal(err)
	}

	if _, err := store.StoreMessage(mailboxID, "INBOX", []byte("Subject: normal\r\n\r\nhi"), "<a@example.com>", "a@example.com", "normal"); err != nil {
		t.Fatal(err)
	}
	if _, err := store.StoreMessage(mailboxID, "Junk", []byte("Subject: junk\r\n\r\nspam"), "<b@example.com>", "b@example.com", "junk"); err != nil {
		t.Fatal(err)
	}

	backend := &imapserver.Backend{DB: database, Mailstore: store}
	srv := imapserver.NewPlainServer(backend)
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	go srv.Serve(ln)
	t.Cleanup(func() { srv.Close() })
	conn, err := net.Dial("tcp", ln.Addr().String())
	if err != nil {
		t.Fatal(err)
	}
	client := imapclient.New(conn, nil)
	t.Cleanup(func() { client.Close() })

	if err := client.Login("inbox@example.com", appPassword).Wait(); err != nil {
		t.Fatalf("login: %v", err)
	}

	mailboxes, err := client.List("", "%", nil).Collect()
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	// LIST must report every standard folder (INBOX, Junk, Sent, Drafts, Trash) even
	// though only INBOX and Junk actually hold a message here — a desktop client that
	// only sees folders with existing mail never learns Trash/Drafts/Sent exist. See
	// db.AllFoldersForMailbox / imapserver.Session.List.
	byName := map[string]*imap.ListData{}
	for _, m := range mailboxes {
		byName[m.Mailbox] = m
	}
	wantFolders := []string{"INBOX", "Junk", "Sent", "Drafts", "Trash"}
	if len(mailboxes) != len(wantFolders) {
		names := make([]string, 0, len(mailboxes))
		for _, m := range mailboxes {
			names = append(names, m.Mailbox)
		}
		t.Fatalf("expected folders %v, got %v", wantFolders, names)
	}
	for _, name := range wantFolders {
		if _, ok := byName[name]; !ok {
			t.Errorf("LIST is missing folder %q", name)
		}
	}
	// Trash/Junk/Sent/Drafts should each carry their RFC 6154 SPECIAL-USE attribute
	// so a desktop client (Thunderbird, Apple Mail, etc.) recognizes them regardless
	// of the exact folder name.
	wantAttrs := map[string]imap.MailboxAttr{
		"Trash":  imap.MailboxAttrTrash,
		"Junk":   imap.MailboxAttrJunk,
		"Sent":   imap.MailboxAttrSent,
		"Drafts": imap.MailboxAttrDrafts,
	}
	for name, attr := range wantAttrs {
		found := false
		for _, a := range byName[name].Attrs {
			if a == attr {
				found = true
			}
		}
		if !found {
			t.Errorf("folder %q missing SPECIAL-USE attr %q, got %v", name, attr, byName[name].Attrs)
		}
	}

	inboxData, err := client.Select("INBOX", nil).Wait()
	if err != nil {
		t.Fatalf("select INBOX: %v", err)
	}
	if inboxData.NumMessages != 1 {
		t.Fatalf("INBOX NumMessages = %d, want 1", inboxData.NumMessages)
	}

	spamData, err := client.Select("Junk", nil).Wait()
	if err != nil {
		t.Fatalf("select Junk: %v", err)
	}
	if spamData.NumMessages != 1 {
		t.Fatalf("Junk NumMessages = %d, want 1", spamData.NumMessages)
	}

	msgs, err := client.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{Envelope: true}).Collect()
	if err != nil {
		t.Fatalf("fetch in Junk: %v", err)
	}
	if len(msgs) != 1 || msgs[0].Envelope.Subject != "junk" {
		t.Fatalf("expected the Junk-folder message (subject %q), got %+v", "junk", msgs)
	}
}

func TestIMAPStoreSeenFlag(t *testing.T) {
	client, email, appPassword, _ := newTestMailboxWithAppPassword(t)

	if err := client.Login(email, appPassword).Wait(); err != nil {
		t.Fatalf("login: %v", err)
	}
	if _, err := client.Select("INBOX", nil).Wait(); err != nil {
		t.Fatalf("select: %v", err)
	}

	storeFlags := &imap.StoreFlags{Op: imap.StoreFlagsAdd, Flags: []imap.Flag{imap.FlagSeen}}
	msgs, err := client.Store(imap.SeqSetNum(1), storeFlags, nil).Collect()
	if err != nil {
		t.Fatalf("store: %v", err)
	}
	if len(msgs) != 1 {
		t.Fatalf("got %d fetch responses from STORE, want 1", len(msgs))
	}
	found := false
	for _, f := range msgs[0].Flags {
		if f == imap.FlagSeen {
			found = true
		}
	}
	if !found {
		t.Fatalf("expected \\Seen in returned flags, got %v", msgs[0].Flags)
	}
}
