import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/svelte";

// Controllable store shims + spies, declared via vi.hoisted so they exist when
// the hoisted vi.mock factories run. A tiny writable shim avoids importing
// svelte inside the hoisted block.
const h = vi.hoisted(() => {
  function shim<T>(initial: T) {
    let value = initial;
    const subs = new Set<(v: T) => void>();
    return {
      set(v: T) {
        value = v;
        subs.forEach((fn) => fn(value));
      },
      subscribe(fn: (v: T) => void) {
        subs.add(fn);
        fn(value);
        return () => subs.delete(fn);
      },
    };
  }
  return {
    currentUserStore: shim<{ name: string } | null>({ name: "Ada" }),
    serverNameStore: shim<string | null>("Home Server"),
    serverUrlStore: shim<string | null>("https://media.example.com"),
    logout: vi.fn(async () => {}),
    reset: vi.fn(),
    goto: vi.fn(),
  };
});

vi.mock("$lib/stores/auth", () => ({
  auth: { logout: h.logout },
  currentUser: { subscribe: h.currentUserStore.subscribe },
  serverName: { subscribe: h.serverNameStore.subscribe },
  serverUrl: { subscribe: h.serverUrlStore.subscribe },
}));

vi.mock("$lib/stores/library", () => ({
  library: { reset: h.reset },
}));

vi.mock("$app/navigation", () => ({ goto: h.goto }));

import AccountMenu from "./AccountMenu.svelte";

function openMenu() {
  const trigger = screen.getByRole("button", { name: "Account menu" });
  fireEvent.click(trigger);
  return trigger;
}

describe("AccountMenu", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    h.currentUserStore.set({ name: "Ada" });
    h.serverNameStore.set("Home Server");
    h.serverUrlStore.set("https://media.example.com");
  });

  it("trigger toggles aria-expanded", async () => {
    render(AccountMenu);
    const trigger = screen.getByRole("button", { name: "Account menu" });
    expect(trigger.getAttribute("aria-expanded")).toBe("false");
    await fireEvent.click(trigger);
    expect(trigger.getAttribute("aria-expanded")).toBe("true");
    await fireEvent.click(trigger);
    expect(trigger.getAttribute("aria-expanded")).toBe("false");
  });

  it("renders the documented items in order", async () => {
    render(AccountMenu);
    openMenu();
    const items = screen.getAllByRole("menuitem").map((el) => el.textContent?.trim());
    expect(items).toEqual(["Downloads", "Settings", "Display", "Sign out"]);
  });

  it("shows the identity block with name and server host", async () => {
    render(AccountMenu);
    openMenu();
    expect(screen.getByText("Signed in as")).toBeTruthy();
    // "Ada" appears in both the trigger label and the identity block.
    expect(screen.getAllByText("Ada").length).toBeGreaterThanOrEqual(1);
    expect(screen.getByText("Home Server")).toBeTruthy();
  });

  it("falls back to the URL host when no server name is set", async () => {
    h.serverNameStore.set(null);
    render(AccountMenu);
    openMenu();
    expect(screen.getByText("media.example.com")).toBeTruthy();
  });

  it("Escape closes the menu", async () => {
    render(AccountMenu);
    const trigger = openMenu();
    expect(trigger.getAttribute("aria-expanded")).toBe("true");
    await fireEvent.keyDown(window, { key: "Escape" });
    expect(trigger.getAttribute("aria-expanded")).toBe("false");
  });

  it("backdrop click closes the menu", async () => {
    render(AccountMenu);
    const trigger = openMenu();
    const backdrop = screen.getByRole("button", { name: "Close account menu" });
    await fireEvent.click(backdrop);
    expect(trigger.getAttribute("aria-expanded")).toBe("false");
  });

  it("Sign out logs out, resets library state, and redirects home", async () => {
    render(AccountMenu);
    openMenu();
    const signOut = screen.getByRole("menuitem", { name: "Sign out" });
    await fireEvent.click(signOut);
    expect(h.logout).toHaveBeenCalledOnce();
    expect(h.reset).toHaveBeenCalledOnce();
    expect(h.goto).toHaveBeenCalledWith("/");
  });

  it("Sign out is the last item, after the routine navigation", async () => {
    render(AccountMenu);
    openMenu();
    const items = screen.getAllByRole("menuitem").map((el) => el.textContent?.trim());
    expect(items[items.length - 1]).toBe("Sign out");
  });
});
