src/navigation.test.tsx

import { test } from "node:test";
import assert from "node:assert/strict";
import { act } from "react";
import { JSDOM } from "jsdom";
import type { Draft, Mail, State } from "../shared/types";
import { generatePalette, parseThemeFile, serializeThemeFile, type CustomTheme } from "./themes";

async function mount(native = false, setup?: (state: State, storage: Storage) => void, bridge: Partial<NonNullable<Window["desktop"]>> | boolean = {}) {
  const dom = new JSDOM("<!doctype html><div id='root'></div>", { url: "http://localhost/" });
  dom.window.HTMLElement.prototype.scrollTo = function (options?: ScrollToOptions | number, y?: number) {
    this.scrollTop = typeof options === "number" ? y || 0 : options?.top ?? this.scrollTop;
    this.dispatchEvent(new dom.window.Event("scroll"));
  };
  dom.window.HTMLElement.prototype.scrollIntoView = () => {};
  const globals = {
    window: dom.window, document: dom.window.document, localStorage: dom.window.localStorage,
    location: dom.window.location, history: dom.window.history, HTMLElement: dom.window.HTMLElement, Element: dom.window.Element,
    requestAnimationFrame: (callback: FrameRequestCallback) => { callback(0); return 0; },
    IS_REACT_ACT_ENVIRONMENT: true,
    confirm: () => true,
  };
  const previous = new Map(Object.keys(globals).map((key) =>
    [key, Object.getOwnPropertyDescriptor(globalThis, key)]));
  for (const [key, value] of Object.entries(globals))
    Object.defineProperty(globalThis, key, { configurable: true, value });
  const makeMail = (id: string, subject: string, accountId = "a", category: Mail["category"] = "important"): Mail => ({
    id, subject, accountId, category, remoteId: id, sender: "Sender", address: "sender@example.com",
    to: "reader@example.com", preview: subject, body: subject, date: 1000, unread: true,
    relevance: 2, reason: "", archived: false, snoozedUntil: null,
    html: null, images: [{ src: "https://images.example.com/photo.png", alt: "Photo" }],
  });
  const state: State = {
    accounts: [{ id: "a", email: "first@example.com", provider: "imap" },
      { id: "b", email: "second@example.com", provider: "imap" }],
    messages: [makeMail("a:1", "Alpha"), makeMail("a:2", "Beta"),
      makeMail("b:1", "Gamma", "b"),
      // Gmail files this one under Promotions, so it sits in a second Inbox tab.
      { ...makeMail("a:3", "Other message", "a", "other"), labelIds: ["CATEGORY_PROMOTIONS"] }],
    labels: [], rules: [], googleConfigured: false, googleClientId: null, jevConfigured: false,
    jevProvider: "typesafe", aiConfigured: false, lastSync: null, syncing: false,
  };
  setup?.(state, dom.window.localStorage);
  let changed: (() => void) | undefined;
  let navigate: ((direction: "back" | "forward") => void) | undefined;
  let subscriptions = 0;
  const desktop: NonNullable<Window["desktop"]> = {
    platform: native ? "linux" : "darwin", materials: false, oauthUrl: "", windowCommand() {},
    onWindowState: () => () => {}, onAccountConnected: () => () => {},
    onMailChanged: (callback) => { changed = callback; return () => { changed = undefined; }; },
    onNavigation: (callback) => {
      subscriptions++; navigate = callback;
      return () => { subscriptions--; navigate = undefined; };
    },
    ...(typeof bridge === "object" ? bridge : {}),
  };
  if (bridge === true) desktop.updates = {
    state: async () => ({ phase: "current" }),
    command: async () => { throw new Error("Opening update details must not issue a command"); },
    onState: () => () => {}, onDrain: () => () => {},
  };
  (dom.window as unknown as Window).desktop = desktop;
  const previousFetch = globalThis.fetch;
  const mutations: string[] = [];
  const renames: unknown[] = [];
  let renameFails = false;
  let savedDraft: Draft | undefined;
  let readWait: Promise<void> | undefined;
  let releaseRead: (() => void) | undefined;
  globalThis.fetch = async (input, options) => {
    const url = String(input);
    if (url.includes("/drafts") && ["POST", "PUT"].includes(options?.method || "")) {
      savedDraft = { ...JSON.parse(String(options?.body)), id: "synthetic-draft", accountId: url.split("/")[3],
        revision: (savedDraft?.revision || 0) + 1, updatedAt: 1 };
      return { ok: true, json: async () => savedDraft } as Response;
    }
    if (options?.method === "PATCH" && url.startsWith("/api/accounts/")) {
      const body = JSON.parse(String(options.body));
      renames.push({ url, body });
      if (renameFails) return { ok: false, json: async () => ({ error: "Synthetic rename failure" }) } as Response;
      const account = state.accounts.find(account => url.endsWith(encodeURIComponent(account.id)))!;
      if (body.name) account.name = body.name;
      else delete account.name;
    } else if (options?.method === "PATCH") {
      mutations.push(url);
      const mail = state.messages.find((mail) => url.endsWith(encodeURIComponent(mail.id)));
      await readWait;
      if (mail) mail.unread = false;
    }
    return { ok: true, json: async () => url === "/api/state" ? { ...state, csrf: "synthetic" } :
      url === "/api/sync" ? { errors: [] } : {} } as Response;
  };
  const { createRoot } = await import("react-dom/client");
  const { App } = await import("./App");
  const container = dom.window.document.getElementById("root")!;
  const root = createRoot(container);
  await act(async () => root.render(<App />));
  const button = (label: string): HTMLButtonElement => {
    if (label === "Filter accounts") return accountButtons().find(item => item.getAttribute("aria-pressed") === "true")!;
    const found = [...container.querySelectorAll<HTMLButtonElement>("button")].find((button) =>
      button.getAttribute("aria-label") === label || button.textContent?.trim() === label || button.querySelector(".sidebar-name, .tab-label")?.textContent === label || [...button.childNodes].filter(node => node.nodeType === 3).map(node => node.textContent).join("").trim() === label);
    assert.ok(found, `button ${label}`);
    return found;
  };
  const click = async (target: HTMLElement) => { await act(async () => target.click()); };
  // The filter controls live in a popover behind the Filters button.
  const openFilters = async () => {
    const toggle = button("Filters");
    if (toggle.getAttribute("aria-expanded") !== "true") await click(toggle);
  };
  const choose = async (label: string, value: string) => {
    if (label !== "Filter accounts" && !container.querySelector(`[aria-label="${label}"]`)) await openFilters();
    if (label === "Filter accounts") { await click(account(value)); return; }
    await click(button(label));
    const option = [...container.querySelectorAll<HTMLElement>('[role="option"]')]
      .find((option) => option.textContent?.trim() === value);
    assert.ok(option, `option ${value}`);
    await click(option);
  };
  const accountButtons = () => [...container.querySelectorAll<HTMLButtonElement>('[aria-label="Filter accounts"] button[aria-pressed]')];
  const account = (name: string) => {
    const found = accountButtons().find((item) => item.querySelector(".sidebar-name")?.textContent === name);
    assert.ok(found, `account ${name}`);
    return found;
  };
  const activeAccount = () => accountButtons().find((item) => item.getAttribute("aria-pressed") === "true")
    ?.querySelector(".sidebar-name")?.textContent;
  const back = () => button("Back");
  const forward = () => button("Forward");
  const open = async (id: string) => {
    const row = container.querySelector<HTMLElement>(`[id='mail-${id}']`)!;
    assert.ok(row);
    await click(row);
    await act(async () => row.dispatchEvent(new dom.window.MouseEvent("dblclick", { bubbles: true })));
  };
  const mouse = async (side: number) => {
    const events = ["mousedown", "mouseup", "auxclick"].map((type) =>
      new dom.window.MouseEvent(type, { button: side, bubbles: true, cancelable: true }));
    await act(async () => { for (const event of events) dom.window.dispatchEvent(event); });
    assert.ok(events.every((event) => event.defaultPrevented));
  };
  const key = async (key: string, options: KeyboardEventInit = {}) => {
    await act(async () => dom.window.dispatchEvent(new dom.window.KeyboardEvent("keydown", { key, bubbles: true, ...options })));
  };
  const input = async (field: HTMLInputElement | HTMLTextAreaElement, value: string) => {
    const prototype = field.tagName === "TEXTAREA" ? dom.window.HTMLTextAreaElement.prototype : dom.window.HTMLInputElement.prototype;
    await act(async () => {
      Object.getOwnPropertyDescriptor(prototype, "value")!.set!.call(field, value);
      field.dispatchEvent(new dom.window.Event("input", { bubbles: true }));
    });
  };
  return { state, storage: dom.window.localStorage, window: dom.window, container, button, click, choose, openFilters, account, activeAccount, accountButtons, back, forward, open, mouse, key, input, mutations,
    renames, failRename: (value: boolean) => { renameFails = value; },
    title: () => container.querySelector(".message h1")?.textContent,
    native: async (direction: "back" | "forward") => { await act(async () => navigate?.(direction)); },
    refresh: async () => { await act(async () => changed?.()); },
    menu: async (id: string) => {
      const row = container.querySelector<HTMLElement>(`[id='mail-${id}']`)!;
      await act(async () => row.dispatchEvent(new dom.window.MouseEvent("contextmenu", { bubbles: true, cancelable: true })));
    },
    subscriptions: () => subscriptions,
    holdRead: () => { readWait = new Promise<void>((resolve) => { releaseRead = resolve; }); },
    finishRead: async () => { await act(async () => { releaseRead?.(); await readWait; readWait = undefined; }); },
    savedDraft: () => savedDraft,
    async close() {
      await act(async () => root.unmount());
      assert.equal(subscriptions, 0);
      assert.equal(changed, undefined);
      dom.window.close();
      globalThis.fetch = previousFetch;
      for (const [key, descriptor] of previous) {
        if (descriptor) Object.defineProperty(globalThis, key, descriptor);
        else Reflect.deleteProperty(globalThis, key);
      }
    },
  };
}

function themeButton(app: Awaited<ReturnType<typeof mount>>, label: string): HTMLButtonElement {
  const document = app.window.document;
  const scope = document.querySelector('[role="dialog"]') ?? app.container;
  const button = [...scope.querySelectorAll<HTMLButtonElement>("button")]
    .find((button) => button.textContent?.trim() === label || button.getAttribute("aria-label") === label);
  assert.ok(button, `theme button ${label}`);
  return button;
}

const sampleTheme = (): CustomTheme => ({
  id: `custom:${crypto.randomUUID()}`, name: "Nightfall", palettes: {
    dark: generatePalette("#1a1b26", "#7aa2f7", "dark"),
    light: generatePalette("#eef0f8", "#3456bc", "light"),
  },
});

test("theme dialogs retain edits and block Back/Forward until closed", async () => {
  const theme = sampleTheme();
  const app = await mount(true, (_state, storage) => storage.setItem("emc2.appearance.v1", JSON.stringify({ theme: theme.id, customThemes: [theme] })));
  try {
    const document = app.window.document;
    await app.click(app.button("Settings"));
    await app.click(app.button("Appearance"));
    await app.click(app.button("Accounts"));
    await app.native("back");
    for (const action of ["Create theme", "Edit theme", "Import theme", "Delete theme"]) {
      await app.click(themeButton(app, action));
      const dialog = document.querySelector('[role="dialog"]')!;
      const color = dialog.querySelector<HTMLInputElement>('[aria-label="Background hex"]');
      if (color) await app.input(color, "#123456");
      for (const direction of ["back", "forward"] as const) {
        await app.native(direction);
        await app.key(direction === "back" ? "ArrowLeft" : "ArrowRight", { altKey: true });
        assert.equal(dialog.isConnected, true, `${action} must survive ${direction}`);
        assert.equal(app.container.querySelector(".settings h1")?.textContent, "Appearance");
        if (color) assert.equal(color.value, "#123456");
      }
      await app.click(themeButton(app, "Cancel"));
    }
    await app.native("forward");
    assert.equal(app.container.querySelector(".settings h1")?.textContent, "Accounts", "closing restores navigation");
    await app.native("back");
    assert.equal(app.container.querySelector(".settings h1")?.textContent, "Appearance");
  } finally { await app.close(); }
});

for (const mac of [false, true]) test(`${mac ? "Cmd" : "Ctrl"}+K cannot stack Commands over a theme dialog`, async () => {
  const theme = sampleTheme();
  const app = await mount(!mac, (_state, storage) => storage.setItem("emc2.appearance.v1", JSON.stringify({ theme: theme.id, customThemes: [theme] })));
  try {
    const document = app.window.document;
    await app.click(app.button("Settings"));
    await app.click(app.button("Appearance"));
    for (const action of ["Create theme", "Edit theme", "Import theme", "Delete theme"]) {
      await app.click(themeButton(app, action));
      const dialog = document.querySelector('[role="dialog"]')!;
      await app.key("k", mac ? { metaKey: true } : { ctrlKey: true });
      assert.equal(document.querySelectorAll('[role="dialog"]').length, 1, action);
      assert.equal(document.querySelector('[role="dialog"]'), dialog);
      if (mac) {
        await app.key("[", { metaKey: true });
        await app.key("]", { metaKey: true });
        assert.equal(dialog.isConnected, true);
      }
      await app.key("Escape");
      assert.equal(document.querySelector('[role="dialog"]'), null);
    }
    await app.key("k", mac ? { metaKey: true } : { ctrlKey: true });
    assert.equal(document.querySelector('[role="dialog"]')?.getAttribute("aria-label"), "Commands", "closing restores Commands");
  } finally { await app.close(); }
});

test("custom theme preview cancels without saving; both palettes persist through an App restart", async () => {
  const app = await mount();
  let saved = "";
  try {
    await app.click(app.button("Settings"));
    await app.click(app.button("Appearance"));
    await app.click(app.button("Midnight"));
    const before = app.storage.getItem("emc2.appearance.v1");
    const document = app.window.document;
    const root = document.documentElement;
    await app.click(themeButton(app, "Create theme"));
    await app.input(document.querySelector<HTMLInputElement>('[aria-label="Background hex"]')!, "#202530");
    assert.equal(root.style.getPropertyValue("--bg"), "#202530");
    const color = document.querySelector<HTMLInputElement>('[aria-label="Background hex"]')!;
    await app.input(color, "#123");
    assert.equal(color.value, "#123", "shorthand must not interrupt typing a six-digit color");
    await app.input(color, "#1234");
    assert.equal(color.getAttribute("aria-invalid"), "true");
    await app.click(themeButton(app, "Save theme"));
    assert.ok(document.querySelector('[role="dialog"]'), "invalid colors block saving");
    await app.input(color, "#123456");
    assert.equal(root.style.getPropertyValue("--bg"), "#123456");
    assert.equal(app.storage.getItem("emc2.appearance.v1"), before);
    await app.key("Escape");
    assert.equal(document.querySelector('[role="dialog"]'), null);
    assert.equal(root.style.getPropertyValue("--bg"), "");
    assert.equal(root.dataset.theme, "midnight");
    assert.equal(app.storage.getItem("emc2.appearance.v1"), before);

    await app.click(themeButton(app, "Create theme"));
    await app.input(document.querySelector<HTMLInputElement>('.theme-name input')!, "Nightfall");
    await app.input(document.querySelector<HTMLInputElement>('[aria-label="Background hex"]')!, "#202530");
    await app.click(themeButton(app, "Light"));
    await app.input(document.querySelector<HTMLInputElement>('[aria-label="Background hex"]')!, "#eef0f8");
    assert.equal(root.dataset.scheme, "light");
    await app.click(themeButton(app, "Save theme"));
    assert.equal(document.querySelector('[role="dialog"]'), null);
    assert.equal(root.dataset.scheme, "dark", "saving returns to the selected appearance mode");
    assert.equal(root.style.getPropertyValue("--bg"), "#202530");
    saved = app.storage.getItem("emc2.appearance.v1")!;
    const value = JSON.parse(saved);
    assert.equal(value.customThemes.length, 1);
    assert.equal(value.customThemes[0].palettes.light.canvas, "#eef0f8");
    assert.equal(value.theme, value.customThemes[0].id);
    await app.click(app.button("Close settings"));
    assert.equal(root.style.getPropertyValue("--bg"), "#202530");
    assert.equal(app.mutations.length, 0, "theme work never mutates mail");
  } finally { await app.close(); }
  const restarted = await mount(false, (_state, storage) => storage.setItem("emc2.appearance.v1", saved));
  try {
    assert.equal(restarted.window.document.documentElement.style.getPropertyValue("--bg"), "#202530");
    await restarted.click(restarted.button("Settings"));
    await restarted.click(restarted.button("Appearance"));
    assert.equal(restarted.button("Nightfall").getAttribute("aria-checked"), "true");
    await restarted.click(themeButton(restarted, "Light"));
    assert.equal(restarted.window.document.documentElement.style.getPropertyValue("--bg"), "#eef0f8");
  } finally { await restarted.close(); }
});

test("edit, duplicate, rename conflicts and confirmed deletion preserve independent themes", async () => {
  const original = sampleTheme();
  const app = await mount(false, (_state, storage) => storage.setItem("emc2.appearance.v1", JSON.stringify({ theme: original.id, customThemes: [original] })));
  try {
    const document = app.window.document;
    const read = () => JSON.parse(app.storage.getItem("emc2.appearance.v1")!);
    await app.click(app.button("Settings"));
    await app.click(app.button("Appearance"));
    await app.click(themeButton(app, "Edit theme"));
    await app.input(document.querySelector<HTMLInputElement>('[aria-label="Links hex"]')!, "#00ccaa");
    await app.click(themeButton(app, "Save theme"));
    assert.equal(read().customThemes[0].palettes.dark.link, "#00ccaa");
    await app.click(themeButton(app, "Duplicate theme"));
    await app.input(document.querySelector<HTMLInputElement>('[aria-label="Background hex"]')!, "#323232");
    await app.click(themeButton(app, "Save theme"));
    assert.equal(read().customThemes.length, 2);
    assert.notEqual(read().customThemes[0].id, read().customThemes[1].id);
    assert.equal(read().customThemes[0].palettes.dark.canvas, original.palettes.dark.canvas);
    assert.equal(read().customThemes[1].palettes.dark.canvas, "#323232");
    await app.click(themeButton(app, "Edit theme"));
    await app.input(document.querySelector<HTMLInputElement>('.theme-name input')!, "Nightfall");
    await app.click(themeButton(app, "Save theme"));
    assert.match(document.querySelector('[role="dialog"] [role="alert"]')!.textContent!, /already has this name/);
    assert.equal(read().customThemes[1].name, "Nightfall copy");
    await app.click(themeButton(app, "Cancel"));
    await app.click(themeButton(app, "Delete theme"));
    await app.click(themeButton(app, "Cancel"));
    assert.equal(read().customThemes.length, 2);
    await app.click(themeButton(app, "Delete theme"));
    await app.click(themeButton(app, "Delete theme"));
    assert.equal(read().customThemes.length, 1);
    assert.equal(read().theme, "black");
    assert.equal(document.documentElement.style.getPropertyValue("--bg"), "");
  } finally { await app.close(); }
});

test("JSON import validates before saving and export downloads a portable copy", async () => {
  const app = await mount();
  const createObjectURL = URL.createObjectURL;
  const revokeObjectURL = URL.revokeObjectURL;
  let downloaded: Blob | undefined;
  let filename = "";
  let revoked = "";
  try {
    const document = app.window.document;
    URL.createObjectURL = (blob) => {
      assert.ok(blob instanceof Blob);
      downloaded = blob;
      return "blob:synthetic-theme";
    };
    URL.revokeObjectURL = (url) => { revoked = url; };
    app.window.HTMLAnchorElement.prototype.click = function () { filename = this.download; };
    await app.click(app.button("Settings"));
    await app.click(app.button("Appearance"));
    await app.click(themeButton(app, "Import theme"));
    await app.input(document.querySelector<HTMLTextAreaElement>("textarea")!, '{"version":2}');
    await app.click(themeButton(app, "Import theme"));
    assert.match(document.querySelector('[role="dialog"] [role="alert"]')!.textContent!, /Emc² version 1/);
    assert.equal(app.storage.getItem("emc2.appearance.v1"), null);
    const theme = sampleTheme();
    await app.input(document.querySelector<HTMLTextAreaElement>("textarea")!, serializeThemeFile(theme));
    await app.click(themeButton(app, "Import theme"));
    assert.equal(document.querySelector('[role="dialog"]'), null);
    const imported = JSON.parse(app.storage.getItem("emc2.appearance.v1")!).customThemes[0];
    assert.notEqual(imported.id, theme.id);
    await app.click(themeButton(app, "Export theme"));
    assert.equal(filename, "nightfall.emc2-theme.json");
    assert.ok(downloaded);
    assert.deepEqual(parseThemeFile(await downloaded.text()), { name: theme.name, palettes: theme.palettes });
    await new Promise((resolve) => setTimeout(resolve, 5));
    assert.equal(revoked, "blob:synthetic-theme");
    await app.click(themeButton(app, "Import theme"));
    await app.input(document.querySelector<HTMLTextAreaElement>("textarea")!, await downloaded.text());
    await app.click(themeButton(app, "Import theme"));
    const themes = JSON.parse(app.storage.getItem("emc2.appearance.v1")!).customThemes;
    assert.deepEqual(themes.map((item: CustomTheme) => item.name), ["Nightfall", "Nightfall (2)"]);
    assert.notEqual(themes[0].id, themes[1].id);
  } finally {
    URL.createObjectURL = createObjectURL;
    URL.revokeObjectURL = revokeObjectURL;
    await app.close();
  }
});

test("failed theme saves keep the editor and draft available and Cancel restores the saved appearance", async () => {
  const app = await mount();
  try {
    await app.click(app.button("Settings"));
    await app.click(app.button("Appearance"));
    await app.click(app.button("Midnight"));
    const saved = app.storage.getItem("emc2.appearance.v1");
    Object.defineProperty(app.window.Storage.prototype, "setItem", { configurable: true, value() { throw new Error("quota"); } });
    await app.click(themeButton(app, "Create theme"));
    await app.input(app.window.document.querySelector<HTMLInputElement>('[aria-label="Background hex"]')!, "#202530");
    await app.click(themeButton(app, "Save theme"));
    assert.ok(app.window.document.querySelector('[role="dialog"]'));
    assert.match(app.window.document.querySelector('[role="dialog"] [role="alert"]')!.textContent!, /could not be saved/);
    assert.equal(app.storage.getItem("emc2.appearance.v1"), saved);
    await app.click(themeButton(app, "Cancel"));
    assert.equal(app.window.document.documentElement.dataset.theme, "midnight");
    assert.equal(app.window.document.documentElement.style.getPropertyValue("--bg"), "");
  } finally { await app.close(); }
});

test("theme file input reads bounded files and ignores a read completed after Cancel", async () => {
  const app = await mount();
  try {
    const document = app.window.document;
    const exported = serializeThemeFile(sampleTheme());
    await app.click(app.button("Settings"));
    await app.click(app.button("Appearance"));
    await app.click(themeButton(app, "Import theme"));
    const input = document.querySelector<HTMLInputElement>('input[type="file"]')!;
    Object.defineProperty(input, "files", { configurable: true, value: [new File(["x".repeat(65537)], "large.json")] });
    await act(async () => input.dispatchEvent(new app.window.Event("change", { bubbles: true })));
    assert.match(document.querySelector('[role="dialog"] [role="alert"]')!.textContent!, /64 KB/);
    Object.defineProperty(input, "files", { configurable: true, value: [new File([exported], "theme.json")] });
    await act(async () => input.dispatchEvent(new app.window.Event("change", { bubbles: true })));
    assert.equal(document.querySelector<HTMLTextAreaElement>("textarea")!.value, exported);
    await app.click(themeButton(app, "Import theme"));
    const saved = app.storage.getItem("emc2.appearance.v1");
    await app.click(themeButton(app, "Import theme"));
    let finish = (_text: string) => {};
    const pending = new Promise<string>((resolve) => { finish = resolve; });
    const nextInput = document.querySelector<HTMLInputElement>('input[type="file"]')!;
    Object.defineProperty(nextInput, "files", { value: [{ size: 100, text: () => pending }] });
    await act(async () => nextInput.dispatchEvent(new app.window.Event("change", { bubbles: true })));
    assert.ok(themeButton(app, "Reading file…").disabled);
    await app.click(themeButton(app, "Cancel"));
    await act(async () => { finish(exported); await pending; });
    assert.equal(document.querySelector('[role="dialog"]'), null);
    assert.equal(app.storage.getItem("emc2.appearance.v1"), saved);
  } finally { await app.close(); }
});

test("a full theme library keeps editing available and the theme picker supports keyboard wrapping", async () => {
  const themes = Array.from({ length: 32 }, (_, index) => ({ ...sampleTheme(), name: `Palette ${index + 1}` }));
  const app = await mount(false, (_state, storage) => storage.setItem("emc2.appearance.v1", JSON.stringify({ theme: themes[0].id, customThemes: themes })));
  try {
    await app.click(app.button("Settings"));
    await app.click(app.button("Appearance"));
    assert.ok(themeButton(app, "Create theme").disabled);
    assert.ok(themeButton(app, "Import theme").disabled);
    assert.ok(themeButton(app, "Duplicate theme").disabled);
    await app.click(themeButton(app, "Edit theme"));
    await app.input(app.window.document.querySelector<HTMLInputElement>('.theme-name input')!, "Renamed palette");
    await app.click(themeButton(app, "Save theme"));
    assert.equal(JSON.parse(app.storage.getItem("emc2.appearance.v1")!).customThemes.length, 32);
    const chosen = app.button("Renamed palette");
    chosen.focus();
    await act(async () => chosen.dispatchEvent(new app.window.KeyboardEvent("keydown", { key: "Home", bubbles: true })));
    assert.equal(app.button("Black").getAttribute("aria-checked"), "true");
    assert.equal(app.window.document.activeElement, app.button("Black"));
    await act(async () => app.button("Black").dispatchEvent(new app.window.KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true })));
    assert.equal(app.button("Palette 32").getAttribute("aria-checked"), "true");
    assert.equal(app.window.document.activeElement, app.button("Palette 32"));
    assert.equal(app.container.querySelectorAll('.theme-choices [tabindex="0"]').length, 1);
  } finally { await app.close(); }
});

test("context menu Open navigates and marks read without a menu guard blocking it", async () => {
  const app = await mount();
  try {
    await app.menu("a:1");
    assert.equal(app.title(), undefined);
    assert.equal(app.mutations.length, 0);
    await app.click(app.container.querySelector<HTMLButtonElement>('[role="menuitem"]')!);
    assert.equal(app.title(), "Alpha");
    assert.equal(app.mutations.length, 1);
  } finally { await app.close(); }
});

test("Settings persist opening mode and custom keys while single-click opens and marks read once", async () => {
  const app = await mount();
  try {
    const first = app.container.querySelector<HTMLElement>("[id='mail-a:1']")!;
    await app.click(first);
    assert.equal(app.title(), undefined);
    assert.equal(app.mutations.length, 0);
    await app.click(app.button("Settings"));
    const opening = app.container.querySelector<HTMLSelectElement>('[data-interaction-settings] select')!;
    await act(async () => { opening.value = "single"; opening.dispatchEvent(new app.window.Event("change", { bubbles: true })); });
    const search = [...app.container.querySelectorAll<HTMLInputElement>('[data-interaction-settings] input')]
      .find((input) => input.value === "/")!;
    search.focus();
    await act(async () => search.dispatchEvent(new app.window.KeyboardEvent("keydown", { key: "f", bubbles: true, cancelable: true })));
    assert.equal(search.value, "F");
    assert.equal(app.container.querySelector('[aria-label="Search all synced mail"]'), null);
    const saved = JSON.parse(app.storage.getItem("emc2.interaction.v1")!);
    assert.equal(saved.opening, "single"); assert.equal(saved.shortcuts.search.key, "f");
    const preferences = app.storage.getItem("emc2.interaction.v1");
    assert.equal(app.window.document.activeElement, search);
    for (const options of [{ ctrlKey: true }, { metaKey: true }, { altKey: true },
      { shiftKey: true }, { isComposing: true }, { repeat: true }]) {
      await act(async () => search.dispatchEvent(new app.window.KeyboardEvent("keydown", {
        key: "Escape", bubbles: true, cancelable: true, ...options,
      })));
      assert.ok(app.container.querySelector(".settings"), "modified/repeated/composing Escape stays guarded");
    }
    const consumed = new app.window.KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true });
    consumed.preventDefault();
    await act(async () => search.dispatchEvent(consumed));
    assert.ok(app.container.querySelector(".settings"), "consumed Escape stays guarded");
    await act(async () => search.dispatchEvent(new app.window.KeyboardEvent("keydown", {
      key: "Escape", bubbles: true, cancelable: true,
    })));
    assert.equal(app.container.querySelector(".settings"), null, "focused shortcut Escape closes Settings");
    assert.equal(app.storage.getItem("emc2.interaction.v1"), preferences, "Escape does not change a binding");
    assert.equal(app.title(), undefined);
    assert.equal(app.mutations.length, 0, "Escape does not run a mail action");
    assert.equal(app.container.querySelector('[role="dialog"]'), null);
    assert.equal(app.container.querySelector('[aria-label="Search all synced mail"]'), null);
    const row = app.container.querySelector<HTMLElement>("[id='mail-a:1']")!;
    await act(async () => {
      row.dispatchEvent(new app.window.MouseEvent("click", { detail: 1, bubbles: true }));
      row.dispatchEvent(new app.window.MouseEvent("click", { detail: 2, bubbles: true }));
      row.dispatchEvent(new app.window.MouseEvent("dblclick", { detail: 2, bubbles: true }));
    });
    assert.equal(app.title(), "Alpha"); assert.equal(app.mutations.length, 1);
    await app.key("ArrowLeft", { altKey: true });
    assert.equal(app.title(), undefined);
    await app.key("ArrowRight", { altKey: true });
    assert.equal(app.title(), "Alpha"); assert.equal(app.mutations.length, 1);
    await app.key("Escape");
    await app.key("f");
    assert.ok(app.container.querySelector('[aria-label="Search all synced mail"]'));
  } finally { await app.close(); }
});

test("privacy removes connected recipient names and addresses from list, headers, body and attributes", async () => {
  const app = await mount(false, (state, storage) => {
    storage.setItem("privacy", "on");
    state.messages[0] = { ...state.messages[0], to: '"Nora Lane" <first@example.com>',
      cc: "Kai Rivers <second@example.com>", subject: "For Nora Lane", preview: "Hello Nora and Kai",
      body: "Hello Nora Lane", reason: "Priority for Nora Lane",
      html: '<table style="background-color:#fff;color:#222"><tbody><tr><td>Hello <b>Nora</b> Lane and Kai first<span>@example.com<img src="https://images.example.com/photo.png" alt="Nora Lane first@example.com" title="Nora Lane" aria-label="Nora Lane"></span></td></tr><tr><td><a href="https://example.com/?name=Nora%2520Lane" title="Nora Lane">Private destination</a><img src="https://images.example.com/?to=first%40example.com" alt="Nora Lane"></td></tr></tbody></table>' };
  });
  try {
    const assertHidden = () => {
      const markup = app.container.innerHTML;
      for (const value of ["Nora", "Lane", "Kai", "first@example.com", "second@example.com", "%2520", "%40"])
        assert.ok(!markup.includes(value), `hidden ${value} must not be in markup`);
    };
    assertHidden();
    await app.open("a:1");
    assertHidden();
    await app.key("s"); assert.ok(app.container.querySelector('[role="dialog"]'));
    assertHidden(); await app.click(app.button("Close dialog"));
    assert.equal(app.container.querySelector(".message-body img"), null);
    assert.equal(app.button("Show images").disabled, false);
    await app.click(app.button("Show images"));
    assert.equal(app.container.querySelectorAll(".message-body img").length, 1);
    assertHidden();
    assert.ok(app.container.textContent?.includes("Image unavailable while personal information is hidden"));
    await app.click(app.button("Hide images"));
    assert.equal(app.container.querySelector(".message-body img"), null);
    assert.ok(app.container.querySelector(".formatted-mail table"));
    const control = app.container.querySelector(".image-controls")!;
    const body = app.container.querySelector(".message-body")!;
    assert.ok(control.compareDocumentPosition(body) & 4, "Show images precedes body");
    await app.click(app.button("Show personal information"));
    assert.ok(app.container.textContent?.includes("Nora"));
    await app.click(app.button("Show images"));
    assert.equal(app.container.querySelectorAll(".message-body img").length, 2);
    assert.equal(app.container.querySelectorAll(".message-images figure").length, 0, "formatted images are not duplicated");
    await app.key("p");
    assertHidden(); assert.equal(app.container.querySelector(".message-body img"), null);
    await app.click(app.button("Show images"));
    assert.equal(app.container.querySelectorAll(".message-body img").length, 1);
    assertHidden();
    await app.click(app.back()); await app.click(app.forward());
    assert.equal(app.container.querySelector(".message-body img"), null, "history return requires new consent in privacy mode");
    assert.equal(app.button("Show images").disabled, false);
  } finally { await app.close(); }
});

test("formatted privacy keeps image order and keyboard reveal across image boundaries", async () => {
  const app = await mount(false, (state, storage) => {
    storage.setItem("privacy", "on");
    state.messages[0] = { ...state.messages[0], to: '"Nora Lane" <first@example.com>',
      html: '<p>😀BEFORE <b>No<img src="https://images.example.com/first.png" alt="Nora Lane">ra</b> Lane BETWEEN first<span>@<img src="https://images.example.com/second.png" alt="first@example.com">example.com</span> AFTER<img src="https://images.example.com/?to=first%40example.com" alt="Nora Lane"> Separate Lane.</p>' };
  });
  try {
    await app.open("a:1");
    const body = app.container.querySelector<HTMLElement>(".message-body")!;
    const assertHidden = () => {
      for (const token of ["Nora", "Lane", "first@example.com", "%40"])
        assert.ok(!body.innerHTML.includes(token), token);
      for (const bar of body.querySelectorAll(".address.veiled")) {
        assert.equal(bar.textContent, "");
        assert.equal(bar.getAttribute("tabindex"), "0");
        assert.equal(bar.getAttribute("role"), "button");
      }
    };
    assertHidden();
    assert.equal(body.querySelector("img"), null);
    await app.click(app.button("Show images"));
    assertHidden();
    assert.equal(body.querySelectorAll("img").length, 2);
    assert.equal(body.querySelectorAll("[src]").length, 2);
    assert.ok(body.textContent?.includes("Image unavailable while personal information is hidden"));
    const copy = body.cloneNode(true) as HTMLElement;
    copy.querySelectorAll(".formatted-mail-image-status").forEach((status) => status.remove());
    copy.querySelectorAll("img").forEach((image, index) => image.replaceWith(`[image${index + 1}]`));
    assert.match(copy.textContent || "", /😀BEFORE.*\[image1\].*BETWEEN.*\[image2\].*AFTER/s);
    const name = body.querySelector<HTMLElement>('[aria-label="Reveal hidden name"]')!;
    name.focus();
    await act(async () => name.dispatchEvent(new app.window.KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true })));
    assert.equal(app.window.document.activeElement, name);
    assert.match(body.textContent || "", /NoLoading image…ra Lane/);
    assert.ok(!body.innerHTML.includes("first@example.com"));
    assert.ok(body.textContent?.endsWith(" Separate ."), "revealing the full name leaves a separate surname hidden");
    await act(async () => name.dispatchEvent(new app.window.KeyboardEvent("keydown", { key: " ", bubbles: true, cancelable: true })));
    assertHidden();
    const address = body.querySelector<HTMLElement>('[aria-label="Reveal hidden address"]')!;
    await app.click(address);
    assert.match(body.textContent || "", /first@Loading image…example.com/);
    assert.ok(!body.innerHTML.includes("Nora") && !body.innerHTML.includes("Lane"));
    await app.click(address);
    assertHidden();
    await app.click(app.button("Hide images"));
    assert.equal(body.querySelector("img"), null);
    await app.click(app.button("Show images"));
    assert.equal(body.querySelectorAll("img").length, 2);
    await app.key("Escape"); await app.open("a:1");
    assert.equal(app.container.querySelector(".message-body img"), null);
  } finally { await app.close(); }
});

for (const target of ["filter", "editor"]) {
  test(`privacy hides message-derived names and destinations in Gmail label ${target}`, async () => {
    const app = await mount(false, (state, storage) => {
      storage.setItem("privacy", "on");
      state.accounts[0].provider = "gmail";
      state.labels = [{ id: "private", accountId: "a", name: "Nora Lane", type: "user" }];
      state.messages[0] = { ...state.messages[0], to: "Nora Lane <first@example.com>",
        subject: "For Nora", preview: "Hello Lane", body: "Hello Nora Lane", labelIds: ["private"],
        html: '<p>Hello Nora Lane <a href="https://example.com/?name=Nora%2520Lane">Private destination</a></p>' };
    });
    try {
      await app.open("a:1");
      assert.ok(app.container.querySelector(".message-labels .veiled"), "reader label is hidden");
      const assertHidden = () => {
        for (const value of ["Nora", "Lane", "first@example.com", "second@example.com", "%2520"])
          assert.ok(!app.container.innerHTML.includes(value), `hidden ${value} must not be in markup`);
      };
      assertHidden();
      if (target === "filter") await app.openFilters();
      await app.click(app.button(target === "filter" ? "Filter by Gmail label" : "Edit labels"));
      assertHidden();
      if (target === "filter") {
        assert.equal(app.container.querySelectorAll('.label-filter [role="option"]').length, 2);
      } else {
        assert.ok(app.container.querySelector('[role="dialog"] .label-options .veiled'));
        assert.equal(app.container.querySelector<HTMLInputElement>('.label-options input')?.checked, true);
      }
      assert.equal(app.container.querySelector(".message-body a"), null, "private destination has no anchor");
      assert.equal(app.mutations.length, 1, "only read-on-open runs");
    } finally { await app.close(); }
  });
}

test("privacy hides one-character recipient names and encoded destinations in mounted mail", async () => {
  const app = await mount(false, (state, storage) => {
    storage.setItem("privacy", "on");
    state.accounts[0].email = "reader@gmail.com";
    state.messages[0] = { ...state.messages[0], sender: "张 文", address: "reader@gmail.com", to: "张 文 <reader@gmail.com>",
      subject: "For 张", preview: "Hello 文", body: "Hello 张 and 文", reason: "Priority for 文",
      html: '<p>Hello <b>张</b> and 文 <a href="https://example.com/?name=%E5%BC%A0">Private destination</a> <a href="https://example.com/public">Public destination</a></p>' };
  });
  try {
    const assertHidden = () => {
      for (const value of ["张", "文", "reader@gmail.com", "%E5%BC%A0"])
        assert.ok(!app.container.innerHTML.includes(value), `hidden ${value} must not be in markup`);
      assert.equal(app.container.querySelector('[id="mail-a:1"] .avatar-letter')?.textContent ||
        app.container.querySelector('.message .avatar-letter')?.textContent, "·");
    };
    assertHidden();
    await app.open("a:1");
    assertHidden();
    assert.equal(app.container.querySelectorAll(".message-body a").length, 1);
    assert.equal(app.container.querySelector(".message-body a")?.getAttribute("href"), "https://example.com/public");
    await app.click(app.button("Show personal information"));
    assert.ok(app.container.textContent?.includes("张"));
    assert.ok(app.container.textContent?.includes("文"));
    assert.equal(app.container.querySelector('.message .avatar-letter')?.textContent, "张");
    assert.ok(app.container.querySelector('a[href="https://example.com/?name=%E5%BC%A0"]'));
    await app.key("p");
    assertHidden();
  } finally { await app.close(); }
});


test("unread navigation sends independent read requests while earlier reads are pending", async () => {
  const app = await mount();
  try {
    app.holdRead(); await app.open("a:1");
    assert.equal(app.title(), "Alpha"); assert.equal(app.mutations.length, 1);
    await app.key("j");
    assert.equal(app.title(), "Beta"); assert.equal(app.mutations.length, 2);
    await app.finishRead();
    assert.equal(app.state.messages.find((mail) => mail.id === "a:2")?.unread, false);
  } finally { await app.close(); }
});


test("empty and rejected formatting retain readable text and consent-controlled fallback images", async () => {
  for (const html of [null, "", "<script>discard</script>", "<br>".repeat(4001),
    "<html><head><style>p { color: red }</style></head><body><form><p>Discarded form</p></form></body></html>",
    "<div><span>   </span></div>", '<div><img src="javascript:invalid"></div>']) {
    const app = await mount(false, (state, storage) => {
      storage.setItem("privacy", "on");
      state.messages[0] = { ...state.messages[0], html, body: "First paragraph\n\nSecond paragraph" };
    });
    try {
      await app.open("a:1");
      assert.equal(app.container.querySelector(".message-body p")?.textContent, "First paragraph");
      assert.equal(app.container.querySelector(".message-images img"), null);
      await app.click(app.button("Show images"));
      assert.equal(app.container.querySelectorAll(".message-images figure img").length, 1);
      assert.equal(app.container.querySelectorAll(".message-body p").length, 2);
      await app.click(app.button("Hide images"));
      assert.equal(app.container.querySelector(".message-images img"), null);
      await app.click(app.button("Show images"));
      await app.key("Escape"); await app.open("a:1");
      assert.equal(app.container.querySelector(".message-images img"), null, "reopening requires fresh consent");
    } finally { await app.close(); }
  }
});

test("mounted cached-mail controls combine filters, retain the read-on-open reader and restore history", async () => {
  const app = await mount(false, (state) => {
    state.accounts[0].provider = "gmail";
    state.labels = [{ id: "team", accountId: "a", name: "Team", type: "user" }];
    state.messages[0] = { ...state.messages[0], relevance: 3, labelIds: ["team"], date: 30 };
    state.messages[1] = { ...state.messages[1], relevance: 1, labelIds: ["team"], date: 20 };
    state.messages[2] = { ...state.messages[2], relevance: 3, date: 40 };
  });
  const rows = () => [...app.container.querySelectorAll(".row")].map(row => row.id);
  try {
    assert.deepEqual(rows(), ["mail-b:1", "mail-a:1", "mail-a:2"]);
    await app.click(app.account("first@example.com"));
    await app.choose("Filter by Gmail label", "Team");
    await app.choose("Read status", "Unread");
    await app.choose("Relevance filter", "High relevance");
    await app.choose("Sort mail", "Oldest");
    await app.click(app.button("Search mail"));
    await app.input(app.container.querySelector<HTMLInputElement>(".search input")!, "ALPHA");
    assert.deepEqual(rows(), ["mail-a:1"]);
    await app.open("a:1");
    assert.equal(app.title(), "Alpha");
    assert.equal(app.state.messages[0].unread, false);
    assert.equal(app.button("Read status").textContent, "Unread");
    await app.refresh();
    assert.equal(app.title(), "Alpha", "automatic cache updates retain the opened reader");
    await app.choose("Sort mail", "Relevance");
    assert.equal(app.title(), undefined, "explicit sort change closes the reader");
    assert.deepEqual(rows(), []);
    assert.match(app.container.querySelector(".empty")!.textContent!, /No matching mail/);
    await app.click(app.back());
    assert.equal(app.title(), undefined, "menu history restores its list");
    assert.equal(app.button("Sort mail").textContent, "Oldest");
    await app.click(app.forward());
    assert.equal(app.title(), undefined);
    const reset = app.container.querySelector<HTMLButtonElement>(".empty button")!;
    assert.equal(reset.textContent, "Reset filters and sort");
    await app.click(reset);
    assert.deepEqual(rows(), ["mail-b:1", "mail-a:1", "mail-a:2"]);
    assert.equal(app.activeAccount(), "All accounts");
    for (const [label, value] of [["Read status", "All mail"],
      ["Relevance filter", "All relevance"], ["Sort mail", "Newest"], ["Filter by Gmail label", "All labels"]])
      assert.equal(app.button(label).textContent, value);
    assert.equal(app.container.querySelector(".search input"), null);
    await app.click(app.back());
    assert.deepEqual(rows(), []);
    assert.equal(app.button("Sort mail").textContent, "Relevance");
    assert.equal(app.activeAccount(), "first@example.com");
    assert.equal(app.button("Read status").textContent, "Unread");
    assert.equal(app.button("Relevance filter").textContent, "High relevance");
    assert.equal(app.button("Filter by Gmail label").textContent, "Team");
    assert.equal(app.container.querySelector<HTMLInputElement>(".search input")?.value, "ALPHA");
    await app.choose("Read status", "All mail");
    assert.deepEqual(rows(), ["mail-a:1"]);
    await app.click(app.forward());
    assert.equal(app.button("Read status").textContent, "All mail", "new control visit truncates Forward");
    assert.equal(app.storage.length, 0, "mail filter history remains in memory");
  } finally { await app.close(); }
});

test("multiple IMAP accounts stay distinguishable and private, choose compose From and reuse Add account", async () => {
  const app = await mount(false, (state, storage) => {
    storage.setItem("privacy", "on");
    state.accounts[1].error = "second@example.com connection failed";
  });
  try {
    const hidden = () => {
      for (const token of ["first@example.com", "second@example.com"])
        assert.ok(!app.container.innerHTML.includes(token), `${token} hidden in text and attributes`);
    };
    assert.equal(app.accountButtons().length, 3);
    hidden();
    await app.click(app.account("Account 2 · Custom IMAP"));
    assert.equal(app.activeAccount(), "Account 2 · Custom IMAP");
    assert.ok(app.account("Account 2 · Custom IMAP").querySelector('[aria-label="Needs attention"]'));
    hidden();
    assert.deepEqual([...app.container.querySelectorAll(".row")].map(row => row.id), ["mail-b:1"]);
    await app.key("c");
    assert.equal(app.container.querySelector<HTMLSelectElement>(".compose-form select")?.value, "b");
    await app.key("Escape");
    await app.click(app.button("Add account"));
    assert.equal(app.container.querySelector('[role="dialog"] h2')?.textContent, "Connect account");
    assert.ok(app.container.querySelector(".provider-row"), "existing provider choice is reused");
    assert.ok(app.accountButtons().every((item) => item.disabled));
    await app.key("Escape");
    await app.click(app.button("Show personal information"));
    assert.ok(app.account("first@example.com"));
    assert.ok(app.account("second@example.com").querySelector('[aria-label="Needs attention"]'));
    assert.equal(app.mutations.length, 0, "filter and setup entry do not mutate mail");
  } finally { await app.close(); }
});

test("sidebar account choices close the reader, keep history and leave number shortcuts working", async () => {
  const app = await mount();
  try {
    await app.open("a:1");
    await app.click(app.account("first@example.com"));
    assert.equal(app.activeAccount(), "first@example.com");
    assert.equal(app.title(), undefined);
    await app.click(app.back());
    assert.equal(app.title(), undefined, "account history returns to its list");
    await app.click(app.account("second@example.com"));
    assert.equal(app.title(), undefined);
    assert.equal(app.container.querySelector(".row.selected")?.id, "mail-b:1");
    await app.key("0");
    assert.equal(app.activeAccount(), "All accounts", "number shortcuts still work");
  } finally { await app.close(); }
});

test("mounted Oldest and Relevance controls reorder cached rows and history restores the sort", async () => {
  const app = await mount(false, (state) => {
    state.messages[0] = { ...state.messages[0], date: 30, relevance: 1 };
    state.messages[1] = { ...state.messages[1], date: 20, relevance: 3 };
    state.messages[2] = { ...state.messages[2], date: 10, relevance: 2 };
  });
  const rows = () => [...app.container.querySelectorAll(".row")].map(row => row.id);
  try {
    assert.deepEqual(rows(), ["mail-a:1", "mail-a:2", "mail-b:1"]);
    await app.choose("Sort mail", "Oldest");
    assert.deepEqual(rows(), ["mail-b:1", "mail-a:2", "mail-a:1"]);
    await app.choose("Sort mail", "Relevance");
    assert.deepEqual(rows(), ["mail-a:2", "mail-b:1", "mail-a:1"]);
    assert.equal(app.container.querySelector(".group"), null, "date groups do not interrupt relevance ordering");
    await app.click(app.back());
    assert.equal(app.button("Sort mail").textContent, "Oldest");
    assert.deepEqual(rows(), ["mail-b:1", "mail-a:2", "mail-a:1"]);
    await app.click(app.forward());
    assert.equal(app.button("Sort mail").textContent, "Relevance");
    assert.deepEqual(rows(), ["mail-a:2", "mail-b:1", "mail-a:1"]);
  } finally { await app.close(); }
});

test("Unread reader navigation keeps the opened message as an ordering anchor", async () => {
  const scenarios = [
    { sort: "Newest", opened: "a:2", key: "j", expected: "Gamma" },
    { sort: "Newest", opened: "b:1", key: "k", expected: "Beta" },
    { sort: "Oldest", opened: "a:2", key: "j", expected: "Alpha" },
    { sort: "Relevance", opened: "a:2", key: "j", expected: "Gamma" },
    { sort: "Newest", opened: "b:1", key: "j", expected: "Gamma" },
    { sort: "Newest", opened: "a:1", key: "k", expected: "Alpha" },
  ];
  for (const scenario of scenarios) {
    const app = await mount(false, (state) => {
      state.messages[0] = { ...state.messages[0], date: 30, relevance: 3 };
      state.messages[1] = { ...state.messages[1], date: 20, relevance: 2 };
      state.messages[2] = { ...state.messages[2], date: 10, relevance: 1 };
    });
    try {
      await app.choose("Read status", "Unread");
      await app.choose("Sort mail", scenario.sort);
      await app.open(scenario.opened);
      assert.equal(app.state.messages.find(mail => mail.id === scenario.opened)?.unread, false);
      await app.key(scenario.key);
      assert.equal(app.title(), scenario.expected, JSON.stringify(scenario));
      await app.key("Escape");
      assert.ok(!app.container.querySelector(`[id="mail-${scenario.opened}"]`), "read anchor stays out of the Unread list");
    } finally { await app.close(); }
  }
});

test("sidebar toggle stays first in its header, hides the panel and survives a restart", async () => {
  const app = await mount();
  try {
    assert.equal(app.container.querySelector(".sidebar-head")?.firstElementChild, app.button("Hide sidebar"));
    assert.equal(app.container.querySelector('.main-head [aria-controls="sidebar"]'), null);
    await app.click(app.button("Hide sidebar"));
    assert.equal(app.container.querySelector(".main-head")?.firstElementChild, app.button("Show sidebar"));
    // The panel stays mounted so it can animate closed, but leaves the focus order.
    assert.ok(app.container.querySelector(".sidebar.closed[inert]"));
    assert.equal(app.window.localStorage.getItem("sidebar"), "closed");
    await app.click(app.button("Show sidebar"));
    assert.equal(app.container.querySelector(".sidebar.closed, .sidebar[inert]"), null);
    await app.click(app.button("Settings"));
    await app.click(app.button("Hide sidebar"));
    assert.equal(app.container.querySelector(".main-head")?.firstElementChild, app.button("Show sidebar"));
  } finally { await app.close(); }
  const restarted = await mount(false, (_state, storage) => storage.setItem("sidebar", "closed"));
  try {
    assert.ok(restarted.container.querySelector(".sidebar.closed[inert]"));
  } finally { await restarted.close(); }
});

test("Settings replaces the mail sidebar with its sections and shows one at a time", async () => {
  const app = await mount();
  try {
    const settings = app.button("Settings");
    const footer = settings.closest(".sidebar-footer");
    assert.ok(footer, "Settings belongs beside Sync at the bottom of the sidebar");
    assert.ok(footer.querySelector(".sync"));
    assert.ok(settings.querySelector("svg"));
    assert.equal(settings.textContent, "", "Settings is an accessible icon button");
    await app.click(settings);
    assert.equal(app.container.querySelector('[aria-label="Mail views"]'), null);
    assert.ok(app.container.querySelector("[data-interaction-settings]"));
    await app.click(app.button("Appearance"));
    assert.equal(app.container.querySelector("[data-interaction-settings]"), null);
    await app.click(app.container.querySelector<HTMLButtonElement>('[role="radio"][aria-label="Purple"]')!);
    assert.equal(app.window.document.documentElement.style.getPropertyValue("--accent"), "#a78bfa");
    assert.equal(JSON.parse(app.window.localStorage.getItem("emc2.appearance.v1")!).accent, "purple");
    await app.click(app.button("Close settings"));
    assert.ok(app.container.querySelector('[aria-label="Mail views"]'));
  } finally { await app.close(); }
});

test("side by side keeps the list beside the opened email; full width opens it in place", async () => {
  const split = await mount();
  try {
    assert.ok(split.container.querySelector(".reader-pane .reader-empty"), "the reader waits for an email");
    await split.open("a:1");
    assert.equal(split.title(), "Alpha");
    assert.ok(split.container.querySelector(".reader-pane .message"));
    assert.ok(split.container.querySelector("[id='mail-a:2']"), "the list stays beside the reader");
    assert.equal(split.container.querySelector(".reader-pane .mail-actions")?.getAttribute("aria-label"), "Opened email actions");
    await split.click(split.button("Close email"));
    assert.equal(split.title(), undefined);
    assert.ok(split.container.querySelector(".reader-empty"));
  } finally { await split.close(); }
  const full = await mount(false, (_state, storage) =>
    storage.setItem("emc2.appearance.v1", JSON.stringify({ reading: "full" })));
  try {
    assert.equal(full.container.querySelector(".reader-pane"), null);
    await full.open("a:1");
    assert.equal(full.title(), "Alpha");
    assert.equal(full.container.querySelector("[id='mail-a:2']"), null, "the email replaces the list");
  } finally { await full.close(); }
});

test("sender details fold away and the choice survives a restart", async () => {
  const app = await mount();
  try {
    assert.ok(app.container.querySelector(".context"));
    await app.click(app.button("Sender details"));
    assert.equal(app.container.querySelector(".context"), null);
    assert.equal(app.storage.getItem("sender-panel"), "closed");
  } finally { await app.close(); }
  const restarted = await mount(false, (_state, storage) => storage.setItem("sender-panel", "closed"));
  try {
    assert.equal(restarted.container.querySelector(".context"), null);
    assert.equal(restarted.button("Sender details").getAttribute("aria-pressed"), "false");
  } finally { await restarted.close(); }
});

test("filters open as a popover that Escape closes, and the button stays lit while filtered", async () => {
  const app = await mount();
  try {
    await app.open("a:1");
    await app.choose("Read status", "Unread");
    assert.ok(app.container.querySelector('[role="dialog"][aria-label="Mail filters and sort"]'));
    await app.key("Escape");
    assert.equal(app.container.querySelector('[aria-label="Mail filters and sort"]'), null);
    assert.equal(app.button("Filters").getAttribute("aria-expanded"), "false");
    assert.ok(app.container.querySelector(".filter-dot"), "a filtered list keeps the button lit");
    assert.equal(app.container.querySelector(".mail-filters"), null, "no filter row under the header");
  } finally { await app.close(); }
});

test("desktop mail rows use a native context menu and run the chosen action", async () => {
  const menus: { items: { id: string; label: string; enabled: boolean; separator?: boolean }[]; position?: { x: number; y: number } }[] = [];
  const app = await mount(false, undefined, {
    mailMenu: async (items, position) => { menus.push({ items, position }); return "archive"; },
  });
  try {
    await app.menu("a:1");
    await new Promise((resolve) => setTimeout(resolve, 0));
    assert.equal(app.container.querySelector('[role="menu"]'), null, "no drawn menu on the desktop");
    assert.deepEqual(menus[0].items.map((item) => item.label),
      ["Open", "Mark as read", "Pin email", "Reply", "Archive", "Snooze", "Move to Trash", "Teach sorting"]);
    assert.deepEqual(menus[0].items.filter((item) => item.separator).map((item) => item.id), ["reply", "archive", "teach"]);
    assert.ok(app.mutations.some((url) => url.endsWith(encodeURIComponent("a:1"))), "the chosen action ran");
  } finally { await app.close(); }
});

test("sidebar sections fold away and stay folded after a restart", async () => {
  const app = await mount();
  try {
    await app.click(app.button("Accounts"));
    assert.equal(app.accountButtons().length, 0);
    assert.equal(app.button("Accounts").getAttribute("aria-expanded"), "false");
  } finally { await app.close(); }
  const restarted = await mount(false, (_state, storage) => storage.setItem("sidebar-folded", JSON.stringify(["accounts"])));
  try {
    assert.equal(restarted.accountButtons().length, 0);
    await restarted.click(restarted.button("Accounts"));
    assert.ok(restarted.accountButtons().length > 0);
  } finally { await restarted.close(); }
});

test("initial reader Back and Forward restore the latest reader without traversal visits", { timeout: 10000 }, async () => {
  const app = await mount();
  try {
    await app.open("a:1");
    assert.equal(app.back().disabled, false);
    await app.click(app.back());
    assert.equal(app.title(), undefined);
    assert.equal(app.back().disabled, true);
    await app.click(app.forward());
    assert.equal(app.title(), "Alpha");
    await app.key("j");
    await app.click(app.button("Next email"));
    assert.equal(app.title(), "Gamma");
    await app.click(app.button("Show images"));
    await app.click(app.back());
    assert.equal(app.title(), undefined);
    assert.equal(app.back().disabled, true, "reader traversal adds no history entries");
    await app.click(app.forward());
    assert.equal(app.title(), "Gamma");
    assert.ok(app.button("Show images"), "restoring a reader requires fresh image consent");
    await app.click(app.back());
    await app.open("a:2");
    await app.key("Escape");
    assert.equal(app.forward().disabled, true, "opening another reader clears the previous return");
    await app.open("a:1");
    await app.click(app.back());
    await app.click(app.button("Promotions"));
    assert.equal(app.forward().disabled, true, "a new menu visit clears the reader return");
  } finally { await app.close(); }
});


test("mouse side buttons leave and restore an initial reader while dialogs guard both directions", { timeout: 10000 }, async () => {
  const app = await mount();
  try {
    await app.open("a:1");
    await app.mouse(3);
    assert.equal(app.title(), undefined);
    await app.mouse(4);
    assert.equal(app.title(), "Alpha");
    for (const shortcut of ["c", "m", "s", "?"]) {
      await app.key(shortcut);
      assert.ok(app.container.querySelector('[role="dialog"]'));
      await app.mouse(3); await app.mouse(4);
      assert.equal(app.title(), "Alpha");
      assert.ok(app.container.querySelector('[role="dialog"]'));
      await app.key("Escape");
    }
    await app.mouse(3);
    assert.equal(app.title(), undefined);
    await app.mouse(4);
    assert.equal(app.title(), "Alpha");
  } finally { await app.close(); }
});


test("menu history restores view, account, search and Settings without restoring readers", { timeout: 10000 }, async () => {
  const app = await mount();
  try {
    assert.ok(app.back().disabled && app.forward().disabled);
    await app.open("a:1");
    await app.key("j");
    assert.equal(app.title(), "Beta");
    assert.ok(!app.back().disabled, "an opened reader can return to its list");
    await app.key("Escape");
    assert.ok(app.back().disabled, "closing email creates no menu visit");
    await app.click(app.button("Promotions"));
    await app.choose("Filter accounts", "first@example.com");
    await app.click(app.button("Search mail"));
    const search = app.container.querySelector<HTMLInputElement>(".search input")!;
    await app.input(search, "Other"); await app.input(search, "Other message");
    await app.click(app.button("Settings"));
    assert.ok(app.container.querySelector(".settings"));
    assert.ok(!app.back().disabled, "history remains reachable in Settings");
    await app.click(app.back());
    assert.equal(app.container.querySelector(".settings"), null);
    assert.equal(app.container.querySelector<HTMLInputElement>(".search input")?.value, "Other message");
    assert.equal(app.activeAccount(), "first@example.com");
    await app.open("a:3");
    assert.equal(app.title(), "Other message");
    await app.key("Escape");
    assert.ok(!app.forward().disabled, "opening and closing do not clear Forward");
    await app.click(app.forward());
    assert.ok(app.container.querySelector(".settings"));
    await app.mouse(3);
    assert.equal(app.title(), undefined);
    await app.mouse(3);
    assert.equal(app.container.querySelector(".search input"), null, "search keystrokes form one visit");
    await app.mouse(4);
    assert.equal(app.container.querySelector<HTMLInputElement>(".search input")?.value, "Other message");
    await app.click(app.back());
    await app.click(app.button("Archive"));
    assert.ok(app.forward().disabled, "new menu navigation truncates Forward");
    await app.choose("Filter accounts", "second@example.com");
    await app.click(app.button("Settings"));
    app.state.accounts = app.state.accounts.filter(account => account.id !== "b");
    await app.refresh(); await app.click(app.back());
    assert.equal(app.activeAccount(), "All accounts", "missing accounts resolve to All");
    assert.equal(app.title(), undefined);
    assert.equal(app.storage.length, 0, "history is memory-only");
  } finally { await app.close(); }
});



test("reader buttons reuse unread anchors, edge guards and image resets without history visits", { timeout: 10000 }, async () => {
  const app = await mount();
  try {
    await app.choose("Read status", "Unread");
    await app.click(app.button("Settings")); await app.click(app.back());
    await app.open("a:1");
    assert.equal(app.button("Previous email").disabled, true);
    await app.click(app.button("Show images"));
    await app.click(app.button("Next email"));
    assert.equal(app.title(), "Beta");
    assert.ok(app.button("Show images"));
    assert.ok(!app.forward().disabled, "reader buttons preserve menu Forward");
    await app.click(app.button("Next email"));
    assert.equal(app.title(), "Gamma");
    assert.equal(app.button("Next email").disabled, true);
    await app.click(app.button("Reply"));
    assert.equal(app.button("Previous email").disabled, true);
    assert.equal(app.button("Next email").disabled, true);
  } finally { await app.close(); }
});



test("updater Escape dismisses only update details and preserves reader focus", { timeout: 10000 }, async () => {
  const app = await mount(false, undefined, true);
  try {
    await app.open("a:1");
    const trigger = app.container.querySelector<HTMLButtonElement>("[aria-controls='update-details']")!;
    trigger.focus();
    await app.click(trigger);
    assert.ok(app.container.querySelector("#update-details"));
    const escape = new app.window.KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true });
    await act(async () => trigger.dispatchEvent(escape));
    assert.equal(escape.defaultPrevented, true);
    assert.equal(app.container.querySelector("#update-details"), null);
    assert.equal(app.title(), "Alpha");
    assert.ok(app.window.document.activeElement === trigger, "focus returns to the updater icon");
    await app.key("Escape");
    assert.equal(app.title(), undefined, "the next Escape still closes the reader");
  } finally { await app.close(); }
});


test("updater Escape from Search preserves Search and the opened reader", { timeout: 10000 }, async () => {
  const app = await mount(false, undefined, true);
  try {
    await app.open("a:1");
    await app.key("/");
    const search = app.container.querySelector<HTMLInputElement>("[aria-label='Search all synced mail']")!;
    assert.ok(search);
    const trigger = app.container.querySelector<HTMLButtonElement>("[aria-controls='update-details']")!;
    await app.click(trigger);
    search.focus();
    const escape = new app.window.KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true });
    await act(async () => search.dispatchEvent(escape));
    assert.equal(escape.defaultPrevented, true);
    assert.equal(app.container.querySelector("#update-details"), null);
    assert.equal(app.title(), "Alpha");
    assert.ok(search.isConnected, "Escape leaves Search open beneath the dismissed panel");
    assert.ok(app.window.document.activeElement === trigger, "focus returns to the updater icon");
  } finally { await app.close(); }
});


test("reader button traversal keeps focus in the reader so Tab preserves the mail view", { timeout: 10000 }, async () => {
  const app = await mount();
  try {
    await app.open("a:1");
    for (const [label, title] of [["Next email", "Beta"], ["Previous email", "Alpha"]]) {
      if (label === "Previous email") await app.key("j");
      const button = app.button(label);
      button.focus();
      await app.click(button);
      const reader = app.container.querySelector<HTMLElement>(".message")!;
      assert.equal(app.title(), title);
      assert.ok(app.window.document.activeElement === reader, "focus stays inside the replacement reader");
      const tab = new app.window.KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
      await act(async () => reader.dispatchEvent(tab));
      assert.equal(tab.defaultPrevented, false);
      assert.equal(app.title(), title);
      assert.ok(app.container.querySelector('.sidebar-group button.active')?.textContent?.includes("Inbox"));
      const action = app.button("Reply"); action.focus();
      const key = new app.window.KeyboardEvent("keydown", { key: title === "Beta" ? "k" : "j", bubbles: true, cancelable: true });
      await act(async () => action.dispatchEvent(key));
      const nextReader = app.container.querySelector<HTMLElement>(".message")!;
      assert.ok(app.window.document.activeElement === nextReader, "j/k moves focus off the replaced reader action");
      const nextTitle = title === "Beta" ? "Alpha" : "Beta";
      assert.equal(app.title(), nextTitle);
      const afterKeyTab = new app.window.KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
      await act(async () => nextReader.dispatchEvent(afterKeyTab));
      assert.equal(afterKeyTab.defaultPrevented, false);
      assert.equal(app.title(), nextTitle);
      assert.ok(app.container.querySelector('.sidebar-group [aria-current="page"]')?.textContent?.includes("Inbox"));
    }
  } finally { await app.close(); }
});



test("private rename confirms saving and displays the custom account label", { timeout: 10000 }, async () => {
  const app = await mount();
  try {
    await app.click(app.button("Hide personal information"));
    await app.click(app.button("Settings"));
    await app.click(app.button("Accounts"));
    await app.click(app.button("Rename"));
    const form = app.container.querySelector<HTMLFormElement>(".account-name-form")!;
    const input = form.querySelector<HTMLInputElement>("input")!;
    await act(async () => input.focus());
    await app.input(input, "Work inbox");
    await act(async () => input.blur());
    assert.equal(input.value, "", "blur hides the private draft without discarding it");
    await app.click(form.querySelector<HTMLButtonElement>('button[type="submit"]')!);
    assert.equal(app.state.accounts[0].name, "Work inbox");
    assert.match(app.container.querySelector(".toast-text")?.textContent || "", /^Account name saved\.$/);
    assert.match(app.container.querySelector(".settings")!.textContent!, /Work inbox/);
    await app.click(app.button("Show personal information"));
    assert.match(app.container.querySelector(".settings")!.textContent!, /Work inbox/, "the saved name appears when privacy is off");
  } finally { await app.close(); }
});


test("Settings rename persists through PATCH and reload, clears to email, cancels and retains failed input", { timeout: 10000 }, async () => {
  const app = await mount();
  const form = () => app.container.querySelector<HTMLFormElement>(".account-name-form")!;
  const submit = async () => { await act(async () => form().dispatchEvent(new app.window.Event("submit", { bubbles: true, cancelable: true }))); };
  try {
    await app.click(app.button("Settings")); await app.click(app.button("Accounts")); await app.click(app.button("Rename"));
    assert.equal(form().querySelector<HTMLInputElement>("input")!.maxLength, 80);
    await app.input(form().querySelector("input")!, "  Personal inbox  "); await submit();
    assert.deepEqual(app.renames, [{ url: "/api/accounts/a", body: { name: "Personal inbox" } }]);
    assert.equal(app.state.accounts[0].name, "Personal inbox");
    assert.match(app.container.querySelector(".settings")!.textContent!, /Personal inbox/); assert.equal(form(), null);
    await app.click(app.button("Rename")); await app.input(form().querySelector("input")!, "Discarded");
    await app.click(app.button("Cancel")); assert.equal(app.renames.length, 1);
    await app.click(app.button("Rename")); await app.input(form().querySelector("input")!, "Keep this name");
    app.failRename(true); await submit();
    assert.equal(form().querySelector<HTMLInputElement>("input")!.value, "Keep this name");
    assert.match(form().querySelector('[role="alert"]')!.textContent!, /Could not rename/);
    assert.equal(app.state.accounts[0].name, "Personal inbox");
    app.failRename(false); await app.input(form().querySelector("input")!, "x".repeat(81)); await submit();
    assert.deepEqual(app.renames.at(-1), { url: "/api/accounts/a", body: { name: "x".repeat(80) } });
    await app.click(app.button("Rename")); await app.input(form().querySelector("input")!, "   "); await submit();
    assert.equal(app.state.accounts[0].name, undefined); assert.match(app.container.querySelector(".settings")!.textContent!, /first@example.com/);
  } finally { await app.close(); }
});


test("custom account labels remain visible in privacy while addresses and unfocused edit values are hidden", { timeout: 10000 }, async () => {
  const app = await mount(false, (state, storage) => { state.accounts[0].name = "Private Identity"; storage.setItem("privacy", "on"); });
  try {
    const hidden = () => {
      assert.doesNotMatch(app.container.innerHTML, /first@example.com|second@example.com/);
      assert.match(app.container.textContent!, /Private Identity/);
    };
    hidden(); await app.click(app.button("Hide sidebar")); hidden();
    await app.click(app.button("Show sidebar"));
    await app.click(app.button("Settings")); await app.click(app.button("Accounts")); hidden();
    await app.click(app.button("Rename")); hidden();
    const input = app.container.querySelector<HTMLInputElement>(".account-name-form input")!;
    await act(async () => input.focus()); assert.equal(input.value, "Private Identity");
    await act(async () => input.blur()); assert.equal(input.value, ""); hidden();
    await app.click(app.button("Cancel")); assert.equal(app.renames.length, 0);
    await app.click(app.button("Show personal information"));
    assert.match(app.container.textContent!, /Private Identity/);
  } finally { await app.close(); }
});


test("menu Back and Forward shortcuts work from focused sidebar controls", { timeout: 10000 }, async () => {
  const app = await mount();
  const historyKey = async (key: string) => {
    const target = app.window.document.activeElement!;
    assert.ok(target.closest("#sidebar"), "dispatch from the sidebar control that retained focus");
    const event = new app.window.KeyboardEvent("keydown", { key, altKey: true, bubbles: true, cancelable: true });
    await act(async () => target.dispatchEvent(event));
    assert.equal(event.defaultPrevented, true);
  };
  try {
    for (const label of ["Archive", "second@example.com"]) {
      const control = app.button(label);
      await act(async () => control.focus()); await app.click(control);
      assert.equal(app.back().disabled, false);
      await historyKey("ArrowLeft");
      assert.equal(app.container.querySelector(".settings"), null);
      assert.equal(app.button("Inbox").getAttribute("aria-current"), "page");
      assert.equal(app.activeAccount(), "All accounts");
      await historyKey("ArrowRight");
      if (label === "Settings") assert.ok(app.container.querySelector(".settings"));
      else if (label === "Archive") assert.equal(app.button(label).getAttribute("aria-current"), "page");
      else assert.equal(app.activeAccount(), label);
      await historyKey("ArrowLeft");
    }
  } finally { await app.close(); }
});



test("focused closed filters accept Ctrl density shortcuts while editors and dialogs guard them", { timeout: 10000 }, async () => {
  const app = await mount();
  const density = () => app.container.querySelector(".list")!.classList.contains("compact");
  const send = async (target: HTMLElement, key: string) => {
    const event = new app.window.KeyboardEvent("keydown", { key, ctrlKey: true, bubbles: true, cancelable: true });
    await act(async () => target.dispatchEvent(event));
    return event.defaultPrevented;
  };
  try {
    await app.openFilters();
    const filter = app.button("Read status"); await act(async () => filter.focus());
    assert.equal(await send(filter, "-"), true);
    assert.equal(density(), true);
    assert.equal(await send(filter, "+"), true);
    assert.equal(density(), false);
    await app.click(app.button("Search mail"));
    const search = app.container.querySelector<HTMLInputElement>(".search input")!;
    search.focus(); assert.equal(await send(search, "-"), false); assert.equal(density(), false);
    await app.key("Escape");
    await app.open("a:1"); await app.key("c");
    const textarea = app.container.querySelector<HTMLTextAreaElement>("textarea")!;
    assert.ok(textarea); assert.equal(await send(textarea, "-"), false); assert.equal(density(), false);
    const dialogButton = app.container.querySelector<HTMLElement>('[role="dialog"] button')!;
    assert.equal(await send(dialogButton, "-"), false); assert.equal(density(), false);
  } finally { await app.close(); }
});


for (const reading of ["split", "full"] as const) test(`3000-row ${reading} App window retains tail mail, navigation, actions and passive-scroll focus`, { timeout: 15000 }, async () => {
  const app = await mount(false, (state, storage) => {
    storage.setItem("emc2.appearance.v1", JSON.stringify({ reading }));
    const sample = state.messages[0];
    state.messages = Array.from({ length: 3000 }, (_, index) => ({ ...sample,
      id: `window:${index}`, remoteId: String(index), subject: `Window message ${index}`,
      body: `Window message ${index}`, preview: `Window message ${index}`, unread: false,
      date: Date.now() - index * 1000, accountId: index % 2 ? "b" : "a",
    }));
  });
  const list = () => app.container.querySelector<HTMLElement>(".list")!;
  const bounded = () => assert.ok(app.container.querySelectorAll(".row").length < 60);
  const tail = async () => {
    const window = app.container.querySelector<HTMLElement>(".mail-window")!;
    assert.ok(window);
    await act(async () => list().scrollTo({ top: Math.max(0, parseFloat(window.style.height) - 600) }));
    bounded();
  };
  try {
    bounded();
    const first = app.container.querySelector<HTMLElement>('[id="mail-window:0"]')!;
    await act(async () => first.focus());
    await tail();
    assert.equal(app.window.document.activeElement, first, "ordinary scrolling preserves focused row");
    const tailTop = list().scrollTop;
    await app.refresh();
    assert.equal(list().scrollTop, tailTop, "refresh never snaps scrolling to selection");
    assert.ok(app.container.querySelector('[id="mail-window:2999"]'), "last message stays reachable");
    await app.open("window:2999"); assert.equal(app.title(), "Window message 2999");
    for (let index = 0; index < 31; index++) await app.key("k");
    assert.equal(app.title(), "Window message 2968", "reader navigation crosses the mounted viewport");
    await app.click(app.back()); bounded(); assert.ok(app.container.querySelector('[id="mail-window:2968"]'));
    await app.click(app.forward()); assert.equal(app.title(), "Window message 2968");
    await app.click(app.back());
    await app.key("j"); await app.key("Enter"); assert.equal(app.title(), "Window message 2969", "keyboard uses the full ordered data");
    await app.key("Escape");
    await app.key("-", { ctrlKey: true }); assert.ok(list().classList.contains("compact")); await tail();
    assert.ok(app.container.querySelector('[id="mail-window:2999"]'));
    await app.choose("Sort mail", "Oldest"); await app.key("Escape"); bounded();
    assert.ok(app.container.querySelector('[id="mail-window:2999"]'), "sort includes the tail of the original data");
    await app.choose("Filter accounts", "first@example.com"); await tail();
    assert.ok(app.container.querySelector('[id="mail-window:0"]'), "filtered sorted tail remains reachable");
    await app.choose("Filter accounts", "All accounts");
    await app.click(app.button("Search mail"));
    await app.input(app.container.querySelector<HTMLInputElement>(".search input")!, "Window message 2999");
    assert.equal(app.container.querySelectorAll(".row").length, 1, "search considers all cached messages");
    const row = app.container.querySelector<HTMLElement>(".mail-row")!;
    await act(async () => row.dispatchEvent(new app.window.MouseEvent("mouseover", { bubbles: true })));
    assert.ok(row.querySelector('[aria-label="Move to Trash"]'), "offscreen mail retains its action when reached");
  } finally { await app.close(); }
});


for (const removed of ["message", "account"]) test(`Forward rejects a removed reader ${removed}`, { timeout: 10000 }, async () => {
  const app = await mount();
  try {
    await app.open("a:1"); await app.click(app.back());
    if (removed === "message") app.state.messages = app.state.messages.filter(mail => mail.id !== "a:1");
    else app.state.accounts = app.state.accounts.filter(account => account.id !== "a");
    await app.refresh(); await app.click(app.forward());
    assert.equal(app.title(), undefined);
    assert.ok(app.container.querySelector(".list"));
  } finally { await app.close(); }
});


test("privacy masks addresses embedded in custom account labels", { timeout: 10000 }, async () => {
  const app = await mount(false, (state, storage) => {
    state.accounts[0].name = "Work first@example.com";
    storage.setItem("privacy", "on");
  });
  try {
    assert.match(app.container.querySelector('[aria-label="Filter accounts"]')!.textContent!, /Work/);
    assert.doesNotMatch(app.container.innerHTML, /first@example.com|second@example.com/);
    await app.click(app.button("Settings")); await app.click(app.button("Accounts"));
    assert.match(app.container.querySelector(".settings")!.textContent!, /Work/);
    assert.doesNotMatch(app.container.innerHTML, /first@example.com|second@example.com/);
  } finally { await app.close(); }
});



test("reader history restores row focus from a removed reader and preserves focused controls", { timeout: 10000 }, async () => {
  const app = await mount();
  try {
    await app.open("a:1");
    const reader = app.container.querySelector<HTMLElement>(".message")!;
    await act(async () => reader.focus());
    await app.mouse(3);
    assert.equal(app.window.document.activeElement?.id, "mail-a:1");
    await app.mouse(4);
    assert.equal(app.title(), "Alpha");
    assert.ok(app.window.document.activeElement?.closest(".message"));
    const control = app.back(); await act(async () => control.focus());
    await app.click(control);
    assert.equal(app.window.document.activeElement, control, "returning to the list preserves control focus");
  } finally { await app.close(); }
});

test("mounted App guards drafts/dialogs and uses one native side-button source with cleanup", { timeout: 10000 }, async () => {
  const app = await mount(true);
  try {
    await app.open("a:1");
    await app.click(app.button("Promotions"));
    assert.equal(app.subscriptions(), 1);
    await app.mouse(3);
    assert.equal(app.title(), undefined); // DOM delivery cannot duplicate Electron's command.
    await app.native("back");
    assert.equal(app.title(), undefined);
    await app.native("forward");
    assert.equal(app.title(), undefined);
    await app.native("back");
    await app.open("a:1");
    const mutations = app.mutations.length;
    for (const shortcut of ["c", "m", "s", "?"]) {
      await app.key(shortcut);
      assert.ok(app.container.querySelector('[role="dialog"]'));
      const body = app.container.querySelector<HTMLTextAreaElement>("textarea");
      if (shortcut === "c") {
        assert.ok(body);
        await app.input(body, "Keep this draft");
      }
      assert.ok(app.back().disabled && app.forward().disabled);
      await app.native("back");
      await app.native("forward");
      assert.equal(app.title(), "Alpha");
      if (shortcut === "c") assert.equal(body!.value, "Keep this draft");
      await app.key("Escape");
      if (shortcut === "c") {
        assert.equal(app.container.querySelector('[role="dialog"]'), null);
        assert.equal(app.savedDraft()?.body, "Keep this draft", "Escape retains the acknowledged draft");
      }
    }
    await app.key("Escape");
    assert.equal(app.title(), undefined);
    assert.equal(app.mutations.length, mutations);
    await app.click(app.button("Settings"));
    await app.native("back");
    assert.equal(app.container.querySelector(".settings"), null);
    await app.native("forward");
    assert.ok(app.container.querySelector(".settings"));
    await app.native("back");
    assert.equal(app.title(), undefined);
    assert.ok(app.back().disabled);
  } finally { await app.close(); }
});

test("menu history clears saved readers before resolving and never marks them read", { timeout: 10000 }, async () => {
  const app = await mount();
  try {
    await app.open("a:1");
    await app.click(app.button("Archive"));
    app.state.messages.find(mail => mail.id === "a:1")!.unread = true;
    await app.refresh();
    const reads = app.mutations.length;
    await app.click(app.back());
    assert.equal(app.title(), undefined);
    assert.equal(app.mutations.length, reads, "returning to a menu never opens its saved reader");
    assert.equal(app.state.messages.find(mail => mail.id === "a:1")!.unread, true);
    await app.click(app.forward());
    assert.equal(app.title(), undefined);
    assert.equal(app.mutations.length, reads);
  } finally { await app.close(); }
});