package db

import "testing"

// TestCreateMailboxFolderDoesNotDisturbOrder is a regression test for a real bug
// caught via live testing: CreateMailboxFolder's INSERT OR IGNORE used to leave a
// fresh row's position at the column's SQL default (0), which is indistinguishable
// from "explicitly dragged to the very top" — so creating a brand-new custom folder
// made it jump above INBOX in the sidebar the instant it was created, before the user
// ever touched drag-and-drop. Fixed by writing an explicit sentinel (unpositionedFolder,
// -1) for a row that exists but was never dragged.
func TestCreateMailboxFolderDoesNotDisturbOrder(t *testing.T) {
	d := openTestDB(t)
	const mailboxID = int64(1)

	if err := d.CreateMailboxFolder(mailboxID, "Receipts"); err != nil {
		t.Fatal(err)
	}
	folders, err := d.AllFoldersForMailbox(mailboxID)
	if err != nil {
		t.Fatal(err)
	}
	if len(folders) == 0 || folders[0] != "INBOX" {
		t.Fatalf("expected INBOX first, got %v", folders)
	}
	if folders[len(folders)-1] != "Receipts" {
		t.Fatalf("expected Receipts last (never dragged), got %v", folders)
	}
}

// treeNames flattens a []*FolderNode into a depth-first name list for easy assertion.
func treeNames(nodes []*FolderNode) []string {
	var out []string
	var walk func(n *FolderNode)
	walk = func(n *FolderNode) {
		out = append(out, n.Name)
		for _, c := range n.Children {
			walk(c)
		}
	}
	for _, n := range nodes {
		walk(n)
	}
	return out
}

// TestAllFoldersForMailboxRootOrderIsFixed confirms the 5 standard folders are always
// returned in their fixed StandardMailboxFolders order — root folders are static and
// were deliberately made non-reorderable, so even a stray saved position for one
// (however it got there) must never change their relative order.
func TestAllFoldersForMailboxRootOrderIsFixed(t *testing.T) {
	d := openTestDB(t)
	const mailboxID = int64(1)

	if err := d.SetFolderOrder(mailboxID, []string{"Trash", "Junk", "INBOX", "Drafts", "Sent"}); err != nil {
		t.Fatal(err)
	}
	folders, err := d.AllFoldersForMailbox(mailboxID)
	if err != nil {
		t.Fatal(err)
	}
	want := []string{"INBOX", "Junk", "Sent", "Drafts", "Trash"}
	if len(folders) != len(want) {
		t.Fatalf("folders = %v, want %v", folders, want)
	}
	for i, name := range want {
		if folders[i] != name {
			t.Fatalf("folders = %v, want %v", folders, want)
		}
	}
}

// TestFolderTreeNestsUnderParentAndOrdersSiblings confirms CreateMailboxFolderUnder
// actually nests a folder under its chosen parent in FolderTree, and that a saved
// sibling order (SetFolderOrder, scoped to just that parent's children) is respected
// — a folder created afterwards (never dragged) appends after the ordered siblings.
func TestFolderTreeNestsUnderParentAndOrdersSiblings(t *testing.T) {
	d := openTestDB(t)
	const mailboxID = int64(1)

	if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
		t.Fatal(err)
	}
	if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
		t.Fatal(err)
	}
	if err := d.CreateMailboxFolderUnder(mailboxID, "ClientB", "Projects"); err != nil {
		t.Fatal(err)
	}
	if err := d.SetFolderOrder(mailboxID, []string{"ClientB", "ClientA"}); err != nil {
		t.Fatal(err)
	}

	tree, err := d.FolderTree(mailboxID)
	if err != nil {
		t.Fatal(err)
	}
	got := treeNames(tree)
	want := []string{"INBOX", "Projects", "ClientB", "ClientA", "Junk", "Sent", "Drafts", "Trash"}
	if len(got) != len(want) {
		t.Fatalf("tree = %v, want %v", got, want)
	}
	for i, name := range want {
		if got[i] != name {
			t.Fatalf("tree = %v, want %v", got, want)
		}
	}

	if err := d.CreateMailboxFolderUnder(mailboxID, "ClientC", "Projects"); err != nil {
		t.Fatal(err)
	}
	tree, err = d.FolderTree(mailboxID)
	if err != nil {
		t.Fatal(err)
	}
	got = treeNames(tree)
	if got[len(got)-5] != "ClientC" { // last of Projects' 3 children, before Junk/Sent/Drafts/Trash
		t.Fatalf("expected ClientC to append after the ordered siblings, got %v", got)
	}
}

// TestRenameMailboxFolderKeepsChildrenNested confirms renaming a folder that has
// children leaves them correctly nested (they reference their parent by row id, not
// by name, so a plain single-row UPDATE never orphans them).
func TestRenameMailboxFolderKeepsChildrenNested(t *testing.T) {
	d := openTestDB(t)
	const mailboxID = int64(1)

	if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
		t.Fatal(err)
	}
	if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
		t.Fatal(err)
	}
	if err := d.RenameMailboxFolder(mailboxID, "Projects", "Work"); err != nil {
		t.Fatal(err)
	}

	root, err := d.FolderRoot(mailboxID, "ClientA")
	if err != nil || root != "INBOX" {
		t.Fatalf("expected ClientA to still resolve under INBOX after its parent was renamed, root=%q err=%v", root, err)
	}
	tree, err := d.FolderTree(mailboxID)
	if err != nil {
		t.Fatal(err)
	}
	got := treeNames(tree)
	want := []string{"INBOX", "Work", "ClientA", "Junk", "Sent", "Drafts", "Trash"}
	if len(got) != len(want) {
		t.Fatalf("tree = %v, want %v", got, want)
	}
	for i, name := range want {
		if got[i] != name {
			t.Fatalf("tree = %v, want %v", got, want)
		}
	}
}

// TestMoveFolderToTrashKeepsSubtreeIntact confirms deleting a folder that has its own
// children re-parents the whole subtree under Trash in one move — the children,
// referencing their parent by id, come along automatically with zero further writes.
func TestMoveFolderToTrashKeepsSubtreeIntact(t *testing.T) {
	d := openTestDB(t)
	const mailboxID = int64(1)

	if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
		t.Fatal(err)
	}
	if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
		t.Fatal(err)
	}
	if err := d.MoveFolderToTrash(mailboxID, "Projects"); err != nil {
		t.Fatal(err)
	}

	for _, name := range []string{"Projects", "ClientA"} {
		root, err := d.FolderRoot(mailboxID, name)
		if err != nil || root != "Trash" {
			t.Fatalf("expected %s to resolve under Trash, got root=%q (err=%v)", name, root, err)
		}
	}
	names, err := d.FolderSubtreeNames(mailboxID, "Trash")
	if err != nil {
		t.Fatal(err)
	}
	want := map[string]bool{"Trash": true, "Projects": true, "ClientA": true}
	if len(names) != len(want) {
		t.Fatalf("FolderSubtreeNames(Trash) = %v, want exactly %v", names, want)
	}
	for _, n := range names {
		if !want[n] {
			t.Fatalf("FolderSubtreeNames(Trash) = %v, want exactly %v", names, want)
		}
	}
}

// TestRestoreFolderReturnsToOriginalParent confirms deleting a nested folder and then
// restoring it puts it back exactly where it was (not just dumped at INBOX's top
// level) — its sibling, untouched throughout, proves the parent (Projects) itself
// never moved.
func TestRestoreFolderReturnsToOriginalParent(t *testing.T) {
	d := openTestDB(t)
	const mailboxID = int64(1)

	if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
		t.Fatal(err)
	}
	if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
		t.Fatal(err)
	}
	if err := d.MoveFolderToTrash(mailboxID, "ClientA"); err != nil {
		t.Fatal(err)
	}
	if root, err := d.FolderRoot(mailboxID, "ClientA"); err != nil || root != "Trash" {
		t.Fatalf("expected ClientA under Trash after delete, root=%q err=%v", root, err)
	}

	if err := d.RestoreFolder(mailboxID, "ClientA"); err != nil {
		t.Fatal(err)
	}
	tree, err := d.FolderTree(mailboxID)
	if err != nil {
		t.Fatal(err)
	}
	got := treeNames(tree)
	want := []string{"INBOX", "Projects", "ClientA", "Junk", "Sent", "Drafts", "Trash"}
	if len(got) != len(want) {
		t.Fatalf("tree after restore = %v, want %v", got, want)
	}
	for i, name := range want {
		if got[i] != name {
			t.Fatalf("tree after restore = %v, want %v", got, want)
		}
	}
}

// TestRestoreFolderFallsBackToInboxWhenOriginalParentStillTrashed confirms restoring
// a folder whose former parent is itself still in Trash doesn't leave it looking
// un-restored (nested inside another trashed folder) — it falls back to INBOX
// instead.
func TestRestoreFolderFallsBackToInboxWhenOriginalParentStillTrashed(t *testing.T) {
	d := openTestDB(t)
	const mailboxID = int64(1)

	if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
		t.Fatal(err)
	}
	if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
		t.Fatal(err)
	}
	if err := d.MoveFolderToTrash(mailboxID, "ClientA"); err != nil {
		t.Fatal(err)
	}
	// Now trash Projects too — ClientA's restore target (Projects) is itself trashed.
	if err := d.MoveFolderToTrash(mailboxID, "Projects"); err != nil {
		t.Fatal(err)
	}
	if err := d.RestoreFolder(mailboxID, "ClientA"); err != nil {
		t.Fatal(err)
	}
	root, err := d.FolderRoot(mailboxID, "ClientA")
	if err != nil || root != "INBOX" {
		t.Fatalf("expected ClientA to fall back under INBOX, got root=%q (err=%v)", root, err)
	}
}

// TestDeleteFolderRowsRemovesRecords confirms DeleteFolderRows actually removes the
// esrv_mailbox_folders rows (not just messages) — used by permanent deletes so a
// permanently-removed folder doesn't linger as an empty shell in FolderTree.
func TestDeleteFolderRowsRemovesRecords(t *testing.T) {
	d := openTestDB(t)
	const mailboxID = int64(1)

	if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
		t.Fatal(err)
	}
	if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
		t.Fatal(err)
	}
	if err := d.DeleteFolderRows(mailboxID, []string{"Projects", "ClientA"}); err != nil {
		t.Fatal(err)
	}
	tree, err := d.FolderTree(mailboxID)
	if err != nil {
		t.Fatal(err)
	}
	got := treeNames(tree)
	want := []string{"INBOX", "Junk", "Sent", "Drafts", "Trash"}
	if len(got) != len(want) {
		t.Fatalf("tree after DeleteFolderRows = %v, want %v (Projects/ClientA gone)", got, want)
	}
	for i, name := range want {
		if got[i] != name {
			t.Fatalf("tree after DeleteFolderRows = %v, want %v", got, want)
		}
	}
}
