import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createSettingsOverlay } from "@components/SettingsOverlay";

// Mock logger
vi.mock("@lib/logger", () => ({
  createLogger: () => ({
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  }),
  getLogBuffer: () => [],
  clearLogBuffer: vi.fn(),
  addLogListener: () => () => {},
  setLogLevel: vi.fn(),
}));

// Mock stores
const mockSetTheme = vi.fn();
vi.mock("@stores/ui.store", () => ({
  uiStore: {
    getState: () => ({ settingsOpen: false }),
    subscribe: () => () => {},
    subscribeSelector: vi.fn((_sel: unknown, _listener: unknown) => () => {}),
  },
  setTheme: (...args: unknown[]) => mockSetTheme(...args),
}));

vi.mock("@lib/livekitSession", () => ({
  switchInputDevice: vi.fn().mockResolvedValue(undefined),
  switchOutputDevice: vi.fn().mockResolvedValue(undefined),
  setVoiceSensitivity: vi.fn(),
  setInputVolume: vi.fn(),
  setOutputVolume: vi.fn(),
  reapplyAudioProcessing: vi.fn().mockResolvedValue(undefined),
  getSessionDebugInfo: vi.fn().mockReturnValue({}),
}));

// Held in a mutable object (rather than baked into the factory literal) so
// individual tests can simulate the store having a display_name set — e.g.
// to reproduce the header/username desync in OC-0188.
const mockAuthState = vi.hoisted(() => ({
  user: {
    id: 1,
    username: "testuser",
    totp_enabled: false,
    display_name: null as string | null,
  },
}));

vi.mock("@stores/auth.store", () => ({
  authStore: {
    getState: () => mockAuthState,
    subscribeSelector: vi.fn(() => () => {}),
  },
  updateUser: vi.fn(),
}));

function clickEl(el: Element | null): void {
  expect(el).not.toBeNull();
  (el as HTMLElement).click();
}

function getTab(container: HTMLDivElement, index: number): HTMLElement {
  const tabs = container.querySelectorAll(".settings-sidebar > button.settings-nav-item");
  const tab = tabs[index];
  expect(tab).toBeDefined();
  return tab as HTMLElement;
}

describe("SettingsOverlay", () => {
  let container: HTMLDivElement;

  const defaultOptions = {
    onClose: vi.fn(),
    onChangePassword: vi.fn().mockResolvedValue(undefined),
    onUpdateProfile: vi.fn().mockResolvedValue(undefined),
    onUploadAvatar: vi.fn().mockResolvedValue("/api/v1/files/test"),
    onLogout: vi.fn(),
    onDeleteAccount: vi.fn().mockResolvedValue(undefined),
    onStatusChange: vi.fn(),
    onEnableTotp: vi.fn().mockResolvedValue({ qr_uri: "otpauth://test", backup_codes: [] }),
    onConfirmTotp: vi.fn().mockResolvedValue(undefined),
    onDisableTotp: vi.fn().mockResolvedValue(undefined),
  };

  beforeEach(() => {
    container = document.createElement("div");
    document.body.appendChild(container);
    localStorage.clear();
    vi.clearAllMocks();
    mockAuthState.user = { id: 1, username: "testuser", totp_enabled: false, display_name: null };
  });

  afterEach(() => {
    container.remove();
  });

  it("mounts with all tabs", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const tabs = container.querySelectorAll(".settings-sidebar > button.settings-nav-item");
    const tabNames = Array.from(tabs).map((t) => t.textContent);
    expect(tabNames).toEqual([
      "Account",
      "Appearance",
      "Notifications",
      "Text & Images",
      "Accessibility",
      "Voice & Audio",
      "Keybinds",
      "Advanced",
      "Logs",
    ]);

    overlay.destroy?.();
  });

  it("starts on Account tab", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const activeTab = container.querySelector(
      ".settings-sidebar > button.settings-nav-item.active",
    );
    expect(activeTab?.textContent).toBe("Account");

    overlay.destroy?.();
  });

  it("switches tabs on click", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const appearanceTab = getTab(container, 1);
    appearanceTab.click();

    expect(appearanceTab.classList.contains("active")).toBe(true);
    const prevActive = getTab(container, 0);
    expect(prevActive.classList.contains("active")).toBe(false);

    overlay.destroy?.();
  });

  it("renders close button that calls onClose", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    clickEl(container.querySelector(".settings-close-btn"));
    expect(defaultOptions.onClose).toHaveBeenCalled();

    overlay.destroy?.();
  });

  it("closes on Escape key", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    overlay.open();

    document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
    expect(defaultOptions.onClose).toHaveBeenCalled();

    overlay.destroy?.();
  });

  // --- Appearance tab tests ---

  it("applies theme on click", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    getTab(container, 1).click();

    const themeOptions = container.querySelectorAll(".theme-opt");
    expect(themeOptions.length).toBe(4);

    const midnight = themeOptions[2] as HTMLElement;
    midnight.click();

    expect(midnight.classList.contains("active")).toBe(true);
    expect(document.documentElement.style.getPropertyValue("--bg-primary")).toBe("#1a1a2e");
    // Theme persists only via themes.ts (owncord:theme:active), not via savePref
    expect(localStorage.getItem("owncord:theme:active")).toBe("midnight");
    expect(localStorage.getItem("owncord:settings:theme")).toBeNull();
    expect(mockSetTheme).toHaveBeenCalledWith("midnight");

    overlay.destroy?.();
  });

  it("persists and restores font size", () => {
    localStorage.setItem("owncord:settings:fontSize", "18");

    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    getTab(container, 1).click();

    const slider = container.querySelector(".settings-slider") as HTMLInputElement;
    expect(slider.value).toBe("18");
    expect(document.documentElement.style.getPropertyValue("--font-size")).toBe("18px");

    overlay.destroy?.();
  });

  it("changes font size via slider", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    getTab(container, 1).click();

    const slider = container.querySelector(".settings-slider") as HTMLInputElement;
    slider.value = "14";
    slider.dispatchEvent(new Event("input"));

    expect(document.documentElement.style.getPropertyValue("--font-size")).toBe("14px");
    expect(localStorage.getItem("owncord:settings:fontSize")).toBe("14");

    overlay.destroy?.();
  });

  it("toggles compact mode", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    getTab(container, 1).click();

    const toggle = container.querySelector(".toggle") as HTMLElement;
    expect(toggle).not.toBeNull();
    toggle.click();

    expect(toggle.classList.contains("on")).toBe(true);
    expect(document.documentElement.classList.contains("compact-mode")).toBe(true);
    expect(localStorage.getItem("owncord:settings:compactMode")).toBe("true");

    overlay.destroy?.();
  });

  // --- Notifications tab tests ---

  it("renders notification toggles", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    getTab(container, 2).click();

    const toggles = container.querySelectorAll(".toggle");
    expect(toggles.length).toBe(4);

    overlay.destroy?.();
  });

  it("persists notification toggle state", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    getTab(container, 2).click();

    const toggles = container.querySelectorAll(".toggle");
    const suppressToggle = toggles[2] as HTMLElement;
    suppressToggle.click();

    expect(suppressToggle.classList.contains("on")).toBe(true);
    expect(localStorage.getItem("owncord:settings:suppressEveryone")).toBe("true");

    overlay.destroy?.();
  });

  // --- Voice & Audio tab tests ---

  it("renders Voice & Audio tab with device selectors", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    getTab(container, 5).click();

    const selects = container.querySelectorAll("select.form-input");
    // input device, output device, video quality, screen share fps, video device = 5
    expect(selects.length).toBe(5);

    const sliders = container.querySelectorAll(".settings-slider");
    expect(sliders.length).toBeGreaterThanOrEqual(1);

    const toggles = container.querySelectorAll(".toggle");
    // 4 toggles: echo cancellation, noise suppression, auto gain control,
    // enhanced noise suppression (RNNoise)
    expect(toggles.length).toBe(4);

    overlay.destroy?.();
  });

  it("renders voice sensitivity meter bar", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    getTab(container, 5).click();

    // Sensitivity is now a draggable meter bar, not a slider.
    const meterBar = container.querySelector(".mic-meter-bar") as HTMLElement;
    expect(meterBar).not.toBeNull();
    const threshold = container.querySelector(".mic-meter-threshold") as HTMLElement;
    expect(threshold).not.toBeNull();

    overlay.destroy?.();
  });

  it("persists audio device selection on change", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    getTab(container, 5).click();

    const selects = container.querySelectorAll("select.form-input");
    const inputSelect = selects[0] as HTMLSelectElement;
    inputSelect.dispatchEvent(new Event("change"));

    expect(localStorage.getItem("owncord:settings:audioInputDevice")).toBe('""');

    overlay.destroy?.();
  });

  it("toggles echo cancellation", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    getTab(container, 5).click();

    const toggles = container.querySelectorAll(".toggle");
    const echoToggle = toggles[0] as HTMLElement;

    // Default is on
    expect(echoToggle.classList.contains("on")).toBe(true);
    echoToggle.click();
    expect(echoToggle.classList.contains("on")).toBe(false);
    expect(localStorage.getItem("owncord:settings:echoCancellation")).toBe("false");

    overlay.destroy?.();
  });

  // --- Account tab tests ---

  it("shows current username", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const acName = container.querySelector(".account-header-name");
    expect(acName?.textContent).toBe("testuser");

    overlay.destroy?.();
  });

  it("calls onLogout when logout button clicked", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    clickEl(container.querySelector(".settings-nav-item.danger"));
    expect(defaultOptions.onLogout).toHaveBeenCalled();

    overlay.destroy?.();
  });

  it("validates password change requires minimum length", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const inputs = container.querySelectorAll("input[type='password']");
    (inputs[0] as HTMLInputElement).value = "oldpass123";
    (inputs[1] as HTMLInputElement).value = "short";
    (inputs[2] as HTMLInputElement).value = "short";

    const changePwBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Change Password",
    ) as HTMLElement;
    changePwBtn.click();

    expect(defaultOptions.onChangePassword).not.toHaveBeenCalled();

    overlay.destroy?.();
  });

  it("validates password confirmation matches", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const inputs = container.querySelectorAll("input[type='password']");
    (inputs[0] as HTMLInputElement).value = "oldpass123";
    (inputs[1] as HTMLInputElement).value = "newpassword123";
    (inputs[2] as HTMLInputElement).value = "differentpassword";

    const changePwBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Change Password",
    ) as HTMLElement;
    changePwBtn.click();

    expect(defaultOptions.onChangePassword).not.toHaveBeenCalled();

    overlay.destroy?.();
  });

  it("requires the current password before calling the server", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const inputs = container.querySelectorAll("input[type='password']");
    (inputs[0] as HTMLInputElement).value = "";
    (inputs[1] as HTMLInputElement).value = "newpassword123";
    (inputs[2] as HTMLInputElement).value = "newpassword123";

    const changePwBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Change Password",
    ) as HTMLElement;
    changePwBtn.click();

    // An empty current password is a guaranteed 403 — and each one counts
    // against the server's lockout counter.
    expect(defaultOptions.onChangePassword).not.toHaveBeenCalled();
    expect(container.textContent).toContain("Enter your current password.");

    overlay.destroy?.();
  });

  it("blocks a double submit while the password change is in flight", async () => {
    let resolveChange: (() => void) | null = null;
    const onChangePassword = vi.fn(
      () =>
        new Promise<void>((resolve) => {
          resolveChange = resolve;
        }),
    );
    const overlay = createSettingsOverlay({ ...defaultOptions, onChangePassword });
    overlay.mount(container);

    const inputs = container.querySelectorAll("input[type='password']");
    (inputs[0] as HTMLInputElement).value = "oldpass123";
    (inputs[1] as HTMLInputElement).value = "newpassword123";
    (inputs[2] as HTMLInputElement).value = "newpassword123";

    const changePwBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Change Password",
    ) as HTMLButtonElement;
    changePwBtn.click();
    expect(changePwBtn.disabled).toBe(true);
    expect(changePwBtn.textContent).toBe("Changing...");

    changePwBtn.click();
    expect(onChangePassword).toHaveBeenCalledTimes(1);

    resolveChange!();
    await vi.waitFor(() => {
      expect(changePwBtn.disabled).toBe(false);
      expect(changePwBtn.textContent).toBe("Change Password");
    });

    overlay.destroy?.();
  });

  it("calls onChangePassword and clears inputs on success", async () => {
    const onChangePassword = vi.fn().mockResolvedValue(undefined);
    const overlay = createSettingsOverlay({ ...defaultOptions, onChangePassword });
    overlay.mount(container);

    const inputs = container.querySelectorAll("input[type='password']");
    (inputs[0] as HTMLInputElement).value = "oldpass123";
    (inputs[1] as HTMLInputElement).value = "newpassword123";
    (inputs[2] as HTMLInputElement).value = "newpassword123";

    const changePwBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Change Password",
    ) as HTMLElement;
    changePwBtn.click();

    await vi.waitFor(() => {
      expect(onChangePassword).toHaveBeenCalledWith("oldpass123", "newpassword123");
    });

    // Inputs should be cleared after success
    await vi.waitFor(() => {
      expect((inputs[0] as HTMLInputElement).value).toBe("");
      expect((inputs[1] as HTMLInputElement).value).toBe("");
      expect((inputs[2] as HTMLInputElement).value).toBe("");
    });

    overlay.destroy?.();
  });

  it("shows error when password change fails", async () => {
    const onChangePassword = vi.fn().mockRejectedValue(new Error("Incorrect old password"));
    const overlay = createSettingsOverlay({ ...defaultOptions, onChangePassword });
    overlay.mount(container);

    const inputs = container.querySelectorAll("input[type='password']");
    (inputs[0] as HTMLInputElement).value = "wrongold";
    (inputs[1] as HTMLInputElement).value = "newpassword123";
    (inputs[2] as HTMLInputElement).value = "newpassword123";

    const changePwBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Change Password",
    ) as HTMLElement;
    changePwBtn.click();

    await vi.waitFor(() => {
      // Find the error element near the password fields
      const errorEls = container.querySelectorAll("div[style*='color:var(--red)']");
      const pwError = Array.from(errorEls).find(
        (el) => el.textContent === "Incorrect old password",
      );
      expect(pwError).not.toBeUndefined();
    });

    overlay.destroy?.();
  });

  it("shows error when username update fails", async () => {
    const onUpdateProfile = vi.fn().mockRejectedValue(new Error("Username taken"));
    const overlay = createSettingsOverlay({ ...defaultOptions, onUpdateProfile });
    overlay.mount(container);

    const editBtn = container.querySelector(".account-field-edit") as HTMLElement;
    editBtn.click();

    const editInput = container.querySelector(
      '[data-testid="username-edit-input"]',
    ) as HTMLInputElement;
    editInput.value = "taken-name";

    const saveBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Save",
    ) as HTMLElement;
    saveBtn.click();

    await vi.waitFor(() => {
      const errorEls = container.querySelectorAll("div[style*='color:var(--red)']");
      const nameError = Array.from(errorEls).find((el) => el.textContent === "Username taken");
      expect(nameError).not.toBeUndefined();
    });

    overlay.destroy?.();
  });

  it("Edit User Profile button opens the same edit form", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    // Click "Edit User Profile" button instead of "Edit" button
    const editProfileBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Edit User Profile",
    ) as HTMLElement;
    editProfileBtn.click();

    const editInput = container.querySelector(
      '[data-testid="username-edit-input"]',
    ) as HTMLInputElement;
    expect(editInput).not.toBeNull();
    // The edit form should be visible
    const editForm = editInput.closest(".setting-row") as HTMLElement;
    expect(editForm.style.display).toBe("flex");

    overlay.destroy?.();
  });

  it("Cancel button hides the username edit form", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    // Open edit form
    const editBtn = container.querySelector(".account-field-edit") as HTMLElement;
    editBtn.click();

    // Click cancel
    const cancelBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Cancel",
    ) as HTMLElement;
    cancelBtn.click();

    // Edit form should be hidden
    const editInput = container.querySelector(
      '[data-testid="username-edit-input"]',
    ) as HTMLInputElement;
    const editForm = editInput.closest(".setting-row") as HTMLElement;
    expect(editForm.style.display).toBe("none");

    overlay.destroy?.();
  });

  // --- Delete account tests ---

  it("shows confirmation area when Delete Account is clicked", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const triggerBtn = container.querySelector(
      "[data-testid='delete-account-trigger']",
    ) as HTMLElement;
    expect(triggerBtn).not.toBeNull();

    const confirmArea = container.querySelector(
      "[data-testid='delete-account-confirm-area']",
    ) as HTMLElement;
    expect(confirmArea.style.display).toBe("none");

    triggerBtn.click();

    expect(confirmArea.style.display).toBe("block");
    expect(triggerBtn.style.display).toBe("none");

    overlay.destroy?.();
  });

  it("hides confirmation area when Cancel is clicked", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const triggerBtn = container.querySelector(
      "[data-testid='delete-account-trigger']",
    ) as HTMLElement;
    triggerBtn.click();

    const confirmArea = container.querySelector(
      "[data-testid='delete-account-confirm-area']",
    ) as HTMLElement;
    expect(confirmArea.style.display).toBe("block");

    const cancelBtn = confirmArea.querySelector("button:not(.account-delete-btn)") as HTMLElement;
    cancelBtn.click();

    expect(confirmArea.style.display).toBe("none");
    expect(triggerBtn.style.display).toBe("");

    overlay.destroy?.();
  });

  it("shows error when confirming delete without password", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const triggerBtn = container.querySelector(
      "[data-testid='delete-account-trigger']",
    ) as HTMLElement;
    triggerBtn.click();

    const confirmBtn = container.querySelector(
      "[data-testid='delete-account-confirm']",
    ) as HTMLElement;
    confirmBtn.click();

    const errorEl = container.querySelector("[data-testid='delete-account-error']") as HTMLElement;
    expect(errorEl.textContent).toBe("Password is required.");
    expect(defaultOptions.onDeleteAccount).not.toHaveBeenCalled();

    overlay.destroy?.();
  });

  it("calls onDeleteAccount with password on confirm", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const triggerBtn = container.querySelector(
      "[data-testid='delete-account-trigger']",
    ) as HTMLElement;
    triggerBtn.click();

    const passwordInput = container.querySelector(
      "[data-testid='delete-account-password']",
    ) as HTMLInputElement;
    passwordInput.value = "mypassword123";

    const confirmBtn = container.querySelector(
      "[data-testid='delete-account-confirm']",
    ) as HTMLButtonElement;
    confirmBtn.click();

    expect(defaultOptions.onDeleteAccount).toHaveBeenCalledWith("mypassword123");

    overlay.destroy?.();
  });

  it("disables confirm button and shows 'Deleting...' during delete", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const triggerBtn = container.querySelector(
      "[data-testid='delete-account-trigger']",
    ) as HTMLElement;
    triggerBtn.click();

    const passwordInput = container.querySelector(
      "[data-testid='delete-account-password']",
    ) as HTMLInputElement;
    passwordInput.value = "mypassword123";

    const confirmBtn = container.querySelector(
      "[data-testid='delete-account-confirm']",
    ) as HTMLButtonElement;
    confirmBtn.click();

    expect(confirmBtn.disabled).toBe(true);
    expect(confirmBtn.textContent).toBe("Deleting...");

    overlay.destroy?.();
  });

  it("shows error and re-enables button on delete failure", async () => {
    const failOptions = {
      ...defaultOptions,
      onDeleteAccount: vi.fn().mockRejectedValue(new Error("Wrong password")),
    };

    const overlay = createSettingsOverlay(failOptions);
    overlay.mount(container);

    const triggerBtn = container.querySelector(
      "[data-testid='delete-account-trigger']",
    ) as HTMLElement;
    triggerBtn.click();

    const passwordInput = container.querySelector(
      "[data-testid='delete-account-password']",
    ) as HTMLInputElement;
    passwordInput.value = "wrongpassword";

    const confirmBtn = container.querySelector(
      "[data-testid='delete-account-confirm']",
    ) as HTMLButtonElement;
    confirmBtn.click();

    // Wait for the rejected promise to settle
    await vi.waitFor(() => {
      expect(confirmBtn.disabled).toBe(false);
    });

    const errorEl = container.querySelector("[data-testid='delete-account-error']") as HTMLElement;
    expect(errorEl.textContent).toBe("Wrong password");
    expect(confirmBtn.textContent).toBe("Confirm Delete");

    overlay.destroy?.();
  });

  it("clears password input when reopening confirmation area", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const triggerBtn = container.querySelector(
      "[data-testid='delete-account-trigger']",
    ) as HTMLElement;
    triggerBtn.click();

    const passwordInput = container.querySelector(
      "[data-testid='delete-account-password']",
    ) as HTMLInputElement;
    passwordInput.value = "typed-something";

    // Cancel and reopen
    const confirmArea = container.querySelector(
      "[data-testid='delete-account-confirm-area']",
    ) as HTMLElement;
    const cancelBtn = confirmArea.querySelector("button:not(.account-delete-btn)") as HTMLElement;
    cancelBtn.click();
    triggerBtn.click();

    expect(passwordInput.value).toBe("");

    overlay.destroy?.();
  });

  // --- Open/Close ---

  it("open() adds .open class, close() removes it", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const root = container.querySelector(".settings-overlay");
    expect(root?.classList.contains("open")).toBe(false);

    overlay.open();
    expect(root?.classList.contains("open")).toBe(true);

    overlay.close();
    expect(root?.classList.contains("open")).toBe(false);

    overlay.destroy?.();
  });

  // --- Dialog & tablist semantics (DC-13) ---

  it("stamps dialog semantics on the settings panel", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const panel = container.querySelector(".settings-panel") as HTMLElement;
    expect(panel.getAttribute("role")).toBe("dialog");
    expect(panel.getAttribute("aria-modal")).toBe("true");
    expect(panel.getAttribute("aria-label")).toBe("Settings");

    overlay.destroy?.();
  });

  it("marks the sidebar as a vertical tablist", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const sidebar = container.querySelector(".settings-sidebar") as HTMLElement;
    expect(sidebar.getAttribute("role")).toBe("tablist");
    expect(sidebar.getAttribute("aria-orientation")).toBe("vertical");
    expect(sidebar.getAttribute("aria-label")).toBe("Settings sections");

    overlay.destroy?.();
  });

  it("moves the roving tabindex to the tab activated by setActiveTab", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const accountTab = getTab(container, 0);
    const appearanceTab = getTab(container, 1);
    expect(accountTab.getAttribute("tabindex")).toBe("0");
    expect(appearanceTab.getAttribute("tabindex")).toBe("-1");

    appearanceTab.click();

    expect(accountTab.getAttribute("tabindex")).toBe("-1");
    expect(appearanceTab.getAttribute("tabindex")).toBe("0");

    overlay.destroy?.();
  });

  it("ArrowDown moves focus and activation to the next tab", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const accountTab = getTab(container, 0);
    const appearanceTab = getTab(container, 1);
    accountTab.focus();
    accountTab.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));

    expect(document.activeElement).toBe(appearanceTab);
    expect(appearanceTab.classList.contains("active")).toBe(true);
    expect(appearanceTab.getAttribute("aria-selected")).toBe("true");
    expect(accountTab.classList.contains("active")).toBe(false);

    overlay.destroy?.();
  });

  it("labels the content tabpanel with the active tab", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const content = container.querySelector(".settings-content") as HTMLElement;
    expect(content.getAttribute("role")).toBe("tabpanel");
    expect(getTab(container, 0).id).toBe("settings-tab-account");
    expect(content.getAttribute("aria-labelledby")).toBe("settings-tab-account");

    // "Text & Images" — the slugged id drops the ampersand
    getTab(container, 3).click();
    expect(content.getAttribute("aria-labelledby")).toBe("settings-tab-text-images");

    overlay.destroy?.();
  });

  it("restores focus to the opener when the overlay closes", () => {
    const opener = document.createElement("button");
    document.body.appendChild(opener);
    opener.focus();

    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    overlay.open();

    const panel = container.querySelector(".settings-panel") as HTMLElement;
    expect(panel.contains(document.activeElement)).toBe(true);

    overlay.close();
    expect(document.activeElement).toBe(opener);

    overlay.destroy?.();
    opener.remove();
  });

  // --- Username validation ---

  it("rejects single-character username (min 2)", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    // Click "Edit" to open username edit form
    const editBtn = container.querySelector(".account-field-edit") as HTMLElement;
    editBtn.click();

    // Type a single character
    const editInput = container.querySelector(
      '[data-testid="username-edit-input"]',
    ) as HTMLInputElement;
    editInput.value = "A";

    // Click Save
    const saveBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Save",
    ) as HTMLElement;
    saveBtn.click();

    // Should NOT call onUpdateProfile
    expect(defaultOptions.onUpdateProfile).not.toHaveBeenCalled();

    overlay.destroy?.();
  });

  it("accepts two-character username (min 2)", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const editBtn = container.querySelector(".account-field-edit") as HTMLElement;
    editBtn.click();

    const editInput = container.querySelector(
      '[data-testid="username-edit-input"]',
    ) as HTMLInputElement;
    editInput.value = "AB";

    const saveBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Save",
    ) as HTMLElement;
    saveBtn.click();

    expect(defaultOptions.onUpdateProfile).toHaveBeenCalledWith({ username: "AB" });

    overlay.destroy?.();
  });

  // OC-0188: renaming the username must not stomp the profile card's
  // header with the raw username when a display name is set — the header
  // is a resolveDisplayName() slot, not a mirror of whichever field was
  // last saved.
  it("keeps the display name in the header after a username rename", async () => {
    mockAuthState.user = {
      id: 1,
      username: "testuser",
      totp_enabled: false,
      display_name: "Alice Smith",
    };

    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    // Header starts out showing the display name, not the username.
    const acName = container.querySelector(".account-header-name");
    expect(acName?.textContent).toBe("Alice Smith");

    const editBtn = container.querySelector(".account-field-edit") as HTMLElement;
    editBtn.click();

    const editInput = container.querySelector(
      '[data-testid="username-edit-input"]',
    ) as HTMLInputElement;
    editInput.value = "alice2";

    const saveBtn = Array.from(container.querySelectorAll(".ac-btn")).find(
      (b) => b.textContent === "Save",
    ) as HTMLElement;
    saveBtn.click();

    await vi.waitFor(() => {
      expect(defaultOptions.onUpdateProfile).toHaveBeenCalledWith({ username: "alice2" });
    });

    // The server merge leaves display_name untouched; the header must keep
    // showing it rather than the raw username that was just saved.
    await vi.waitFor(() => {
      expect(acName?.textContent).toBe("Alice Smith");
    });

    overlay.destroy?.();
  });

  // --- Status selector ---

  // Phase 6 flipped this: "invisible" is a real, settable status now (the
  // server stores it and shows everyone else offline), so the option says what
  // it does instead of borrowing "offline"'s name.
  it("offers Invisible as a status, not Offline", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const statusLabels = container.querySelectorAll(".settings-status-label");
    const labels = Array.from(statusLabels).map((el) => el.textContent);
    expect(labels).toContain("Invisible");
    expect(labels).not.toContain("Offline");

    overlay.destroy?.();
  });

  it("status rows have role=button and tabindex for keyboard access", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const rows = container.querySelectorAll(".settings-status-option");
    expect(rows.length).toBeGreaterThan(0);
    for (const row of rows) {
      expect(row.getAttribute("role")).toBe("button");
      expect(row.getAttribute("tabindex")).toBe("0");
    }

    overlay.destroy?.();
  });

  it("status row activates on Enter key", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const rows = container.querySelectorAll(".settings-status-option");
    const idleRow = Array.from(rows).find(
      (r) => r.querySelector(".settings-status-label")?.textContent === "Idle",
    ) as HTMLElement;
    expect(idleRow).toBeDefined();
    idleRow.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));

    expect(idleRow.classList.contains("active")).toBe(true);
    expect(defaultOptions.onStatusChange).toHaveBeenCalledWith("idle");

    overlay.destroy?.();
  });

  it("status row activates on Space key", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const rows = container.querySelectorAll(".settings-status-option");
    const dndRow = Array.from(rows).find(
      (r) => r.querySelector(".settings-status-label")?.textContent === "Do Not Disturb",
    ) as HTMLElement;
    expect(dndRow).toBeDefined();
    dndRow.dispatchEvent(new KeyboardEvent("keydown", { key: " ", bubbles: true }));

    expect(dndRow.classList.contains("active")).toBe(true);
    expect(defaultOptions.onStatusChange).toHaveBeenCalledWith("dnd");

    overlay.destroy?.();
  });

  // --- Connect page: Account tab gating ---

  it("hides Account tab when isAuthenticated is false", () => {
    const overlay = createSettingsOverlay({ ...defaultOptions, isAuthenticated: false });
    overlay.mount(container);

    const tabs = container.querySelectorAll(".settings-sidebar > button.settings-nav-item");
    const tabNames = Array.from(tabs).map((t) => t.textContent);
    expect(tabNames).not.toContain("Account");
    // Should start on Appearance instead
    const activeTab = container.querySelector(
      ".settings-sidebar > button.settings-nav-item.active",
    );
    expect(activeTab?.textContent).toBe("Appearance");

    overlay.destroy?.();
  });

  it("hides 'Edit Profile' link when isAuthenticated is false", () => {
    const overlay = createSettingsOverlay({ ...defaultOptions, isAuthenticated: false });
    overlay.mount(container);

    const editLink = container.querySelector(".settings-sidebar-edit") as HTMLElement;
    expect(editLink.style.display).toBe("none");

    overlay.destroy?.();
  });

  it("hides the logout action when isAuthenticated is false", () => {
    const overlay = createSettingsOverlay({ ...defaultOptions, isAuthenticated: false });
    overlay.mount(container);

    expect(container.querySelector(".settings-sidebar-logout")).toBeNull();
    expect(container.querySelector(".settings-nav-item.danger")).toBeNull();

    overlay.destroy?.();
  });

  it("shows Account tab by default (isAuthenticated not set)", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    const tabs = container.querySelectorAll(".settings-sidebar > button.settings-nav-item");
    const tabNames = Array.from(tabs).map((t) => t.textContent);
    expect(tabNames).toContain("Account");

    overlay.destroy?.();
  });

  // --- Reopen rebuilds live content ---

  it("rebuilds the active tab when the panel is reopened", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    overlay.open();
    const firstPane = container.querySelector(".settings-content .settings-pane");
    expect(firstPane).not.toBeNull();

    // Closing tears down the live parts of the tab (mic meter, camera preview,
    // log listener) — reopening must build a fresh pane, not show the corpse.
    overlay.close();
    overlay.open();

    const secondPane = container.querySelector(".settings-content .settings-pane");
    expect(secondPane).not.toBeNull();
    expect(secondPane).not.toBe(firstPane);
    // Exactly one pane — the old one was replaced, not appended to.
    expect(container.querySelectorAll(".settings-content .settings-pane").length).toBe(1);
  });

  it("re-reads preferences when reopened", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    overlay.open();
    const appearanceTab = Array.from(
      container.querySelectorAll(".settings-sidebar > button.settings-nav-item"),
    ).find((b) => b.textContent === "Appearance") as HTMLElement;
    appearanceTab.click();

    let slider = container.querySelector(".settings-slider") as HTMLInputElement;
    expect(slider.value).toBe("16");

    overlay.close();
    localStorage.setItem("owncord:settings:fontSize", JSON.stringify(20));
    overlay.open();

    slider = container.querySelector(".settings-slider") as HTMLInputElement;
    expect(slider.value).toBe("20");
  });

  // --- Listener retention across tab switches (OC-0268) ---
  //
  // Every TAB_BUILDERS entry registers its element listeners on the single
  // overlay-lifetime `ac.signal`. Switching away from a tab detaches its pane
  // via clearChildren() but never aborts that build's listener registrations,
  // so the discarded subtree stays reachable (and live) through the shared
  // AbortController for as long as the overlay itself is mounted.
  it("stops a discarded tab's listeners from firing once the tab is switched away", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);
    overlay.open();

    getTab(container, 1).click(); // Appearance
    const fontSlider = container.querySelector(".settings-slider") as HTMLInputElement;
    expect(fontSlider).not.toBeNull();

    getTab(container, 2).click(); // Notifications — discards the Appearance pane
    expect(container.contains(fontSlider)).toBe(false);

    document.documentElement.style.removeProperty("--font-size");
    localStorage.removeItem("owncord:settings:fontSize");

    // The pane is gone from the DOM, but nothing has told its listener to
    // stop — dispatching directly on the detached element still reaches it
    // if the registration was never aborted.
    fontSlider.value = "19";
    fontSlider.dispatchEvent(new Event("input"));

    expect(document.documentElement.style.getPropertyValue("--font-size")).toBe("");
    expect(localStorage.getItem("owncord:settings:fontSize")).toBeNull();

    overlay.destroy?.();
  });

  // --- Cleanup ---

  it("destroy removes root from DOM", () => {
    const overlay = createSettingsOverlay(defaultOptions);
    overlay.mount(container);

    expect(container.querySelector(".settings-overlay")).not.toBeNull();
    overlay.destroy?.();
    expect(container.querySelector(".settings-overlay")).toBeNull();
  });
});

// ---------------------------------------------------------------------------
// OC-0181: mount() when the store already reports settingsOpen === true
// ---------------------------------------------------------------------------
//
// ConnectPage creates the overlay lazily: it only calls mount() once
// uiStore.settingsOpen is already true (the settings gear flips the store
// first, and only then does ensureSettingsOverlay() run). So on the very
// first open from the connect page, mount() runs its "sync initial state"
// show() call synchronously inside mount() itself — before the caller has
// had a chance to see `root` come back and attach it anywhere.
//
// The module-level mock above pins settingsOpen to a permanent `false`, so
// every other test in this file mounts into a closed overlay and only calls
// open() afterward (root already attached by then). This block re-imports
// the component fresh with settingsOpen already true at mount time, the one
// path that exercises the bug.
describe("SettingsOverlay - mount() with settingsOpen already true", () => {
  afterEach(() => {
    vi.doUnmock("@stores/ui.store");
    vi.resetModules();
  });

  it("moves focus into the dialog when the store is already open at mount time", async () => {
    vi.resetModules();
    vi.doMock("@stores/ui.store", () => ({
      uiStore: {
        getState: () => ({ settingsOpen: true }),
        subscribe: () => () => {},
        subscribeSelector: vi.fn((_sel: unknown, _listener: unknown) => () => {}),
      },
      setTheme: vi.fn(),
    }));

    const { createSettingsOverlay: createReopenedOverlay } =
      await import("@components/SettingsOverlay");

    const opener = document.createElement("button");
    document.body.appendChild(opener);
    opener.focus();
    expect(document.activeElement).toBe(opener);

    const localContainer = document.createElement("div");
    document.body.appendChild(localContainer);

    const overlay = createReopenedOverlay({
      onClose: vi.fn(),
      onChangePassword: vi.fn().mockResolvedValue(undefined),
      onUpdateProfile: vi.fn().mockResolvedValue(undefined),
      onUploadAvatar: vi.fn().mockResolvedValue("/api/v1/files/test"),
      onLogout: vi.fn(),
      onDeleteAccount: vi.fn().mockResolvedValue(undefined),
      onStatusChange: vi.fn(),
      onEnableTotp: vi.fn().mockResolvedValue({ qr_uri: "otpauth://test", backup_codes: [] }),
      onConfirmTotp: vi.fn().mockResolvedValue(undefined),
      onDisableTotp: vi.fn().mockResolvedValue(undefined),
    });

    overlay.mount(localContainer);

    // The overlay must actually be in the document by the time mount()
    // returns, or nothing below it can matter.
    expect(document.body.contains(localContainer.querySelector(".settings-overlay"))).toBe(true);

    const panel = localContainer.querySelector(".settings-panel") as HTMLElement;
    expect(panel).not.toBeNull();
    // focusDialog() only succeeds once root is attached to the document;
    // called against a detached subtree, .focus() is a silent no-op and
    // activeElement never leaves the opener button.
    expect(panel.contains(document.activeElement)).toBe(true);

    overlay.destroy?.();
    localContainer.remove();
    opener.remove();
  });
});
