import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import type { ReactNode } from "react";

// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------
// usePathname is the only knob that drives "active state" + the page-title
// derivation in MobileHeader. We swap it per test to verify each NAV_ITEM
// gets selected when its route matches.
const pathnameMock = vi.fn<() => string>(() => "/");
vi.mock("next/navigation", () => ({
  usePathname: () => pathnameMock(),
}));

// next-themes pulls window APIs that don't exist under the SSR-only test
// environment; the ThemeToggle component is rendered inside Sidebar so we
// stub it to a deterministic placeholder we can grep for.
vi.mock("@/components/theme-toggle", () => ({
  ThemeToggle: () => <div data-testid="theme-toggle">theme-toggle</div>,
}));

// Make the Sheet primitives transparent so the drawer's contents render
// inline and we can grep them. The real components defer rendering until
// the trigger is clicked (Radix portal); for a contract test we just want
// to confirm what's INSIDE the drawer.
vi.mock("@/components/ui/sheet", () => {
  const passthrough = ({ children }: { children: ReactNode }) => <>{children}</>;
  return {
    Sheet: passthrough,
    SheetTrigger: ({ children }: { children: ReactNode; asChild?: boolean }) => (
      <>{children}</>
    ),
    SheetContent: ({ children }: { children: ReactNode }) => (
      <div data-testid="sheet-content">{children}</div>
    ),
    SheetHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
    SheetTitle: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
    SheetDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
    SheetClose: passthrough,
  };
});

import { AppShell } from "./app-shell";
import { NAV_ITEMS } from "./nav-config";

// ---------------------------------------------------------------------------
// MobileHeader contract
// ---------------------------------------------------------------------------
describe("AppShell — mobile header (SSR)", () => {
  beforeEach(() => {
    pathnameMock.mockReset();
    pathnameMock.mockReturnValue("/");
  });

  it("renders a fixed top header that hides on sm+ breakpoints", () => {
    const html = renderToStaticMarkup(
      <AppShell>
        <main>page</main>
      </AppShell>,
    );
    // A `<header>` exists with both `fixed top-0` and `sm:hidden` so it
    // covers the mobile viewport edge but yields to the sidebar on desktop.
    expect(html).toMatch(/<header[^>]*class="[^"]*fixed top-0[^"]*sm:hidden/);
  });

  it("brand mark on the left links to /", () => {
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    // The "cm" brand pill is an <a href="/"> with aria-label "Go home"
    // so screen readers announce its purpose.
    expect(html).toMatch(/aria-label="Go home"[^>]*href="\/"|href="\/"[^>]*aria-label="Go home"/);
    // Brand text is the literal "cm" inside the pill (not the page title).
    expect(html).toContain(">cm<");
  });

  it("page title in the centre reflects the active route", () => {
    const cases: Array<{ path: string; expected: string }> = [
      { path: "/", expected: "Dashboard" },
      { path: "/accounts", expected: "Accounts" },
      { path: "/accounts/abc-123", expected: "Accounts" }, // sub-routes still match
      { path: "/reminders", expected: "Reminders" },
      { path: "/reminders/new", expected: "Reminders" },
      { path: "/activity", expected: "Activity" },
      { path: "/settings", expected: "Settings" },
    ];
    for (const c of cases) {
      pathnameMock.mockReturnValue(c.path);
      const html = renderToStaticMarkup(
        <AppShell>
          <div />
        </AppShell>,
      );
      // The mobile header has a span with the title; the desktop sidebar
      // doesn't include this title element. Check the title appears at
      // least once (mobile header) AND specifically in the expected form.
      expect(html).toContain(c.expected);
    }
  });

  it("falls back to 'WhatsApp Bot' when the path doesn't match any nav item", () => {
    pathnameMock.mockReturnValue("/unknown-route");
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    // Should fall back to the generic title in the centre (and also be
    // present in the desktop sidebar header).
    expect(html).toContain("WhatsApp Bot");
  });

  it("menu button on the right uses aria-label='Open menu'", () => {
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    expect(html).toMatch(/aria-label="Open menu"/);
  });
});

// ---------------------------------------------------------------------------
// Menu drawer (Sheet) contents
// ---------------------------------------------------------------------------
describe("AppShell — menu drawer contents (SSR via transparent Sheet mock)", () => {
  beforeEach(() => {
    pathnameMock.mockReset();
    pathnameMock.mockReturnValue("/");
  });

  it("renders one nav link per NAV_ITEM, in order", () => {
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    // Find the substring inside the sheet wrapper to scope our assertions
    // to the drawer (avoids matching the desktop sidebar).
    const sheetSlice = extractSheet(html);
    for (const item of NAV_ITEMS) {
      expect(sheetSlice).toContain(`href="${item.href}"`);
      expect(sheetSlice).toContain(item.label);
    }
    // Order check: each label appears in the drawer in NAV_ITEMS order.
    let cursor = 0;
    for (const item of NAV_ITEMS) {
      const idx = sheetSlice.indexOf(item.label, cursor);
      expect(idx).toBeGreaterThan(-1);
      cursor = idx + item.label.length;
    }
  });

  it("marks the active route's link with aria-current='page'", () => {
    pathnameMock.mockReturnValue("/reminders");
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    const sheetSlice = extractSheet(html);
    // The reminders link should carry aria-current; the others should not.
    expect(sheetSlice).toMatch(/href="\/reminders"[^>]*aria-current="page"|aria-current="page"[^>]*href="\/reminders"/);
    expect(sheetSlice).not.toMatch(/href="\/accounts"[^>]*aria-current="page"/);
    expect(sheetSlice).not.toMatch(/href="\/activity"[^>]*aria-current="page"/);
  });

  it("Dashboard ('/') matches exactly, not as a prefix of every route", () => {
    // Regression guard: NAV_ITEMS contains '/' as the dashboard href. A
    // naïve `pathname.startsWith(href)` would mark Dashboard active on
    // every page. The header uses an exact-match check for "/".
    pathnameMock.mockReturnValue("/accounts");
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    const sheetSlice = extractSheet(html);
    expect(sheetSlice).not.toMatch(/href="\/"[^>]*aria-current="page"/);
    expect(sheetSlice).toMatch(/href="\/accounts"[^>]*aria-current="page"|aria-current="page"[^>]*href="\/accounts"/);
  });

  it("does NOT include a theme toggle in the mobile drawer (per request)", () => {
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    const sheetSlice = extractSheet(html);
    expect(sheetSlice).not.toContain("theme-toggle");
  });

  it("drawer header carries the brand wording and a screen-reader description", () => {
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    const sheetSlice = extractSheet(html);
    // Visible title carries the brand wording.
    expect(sheetSlice).toContain("WhatsApp Bot");
    // Description text is present (the actual sr-only class lives on the
    // shadcn primitive, which the mock here doesn't reproduce — so we
    // just assert the text is rendered, leaving a11y class testing to
    // the primitive's own coverage).
    expect(sheetSlice).toContain("Primary navigation menu");
  });
});

// ---------------------------------------------------------------------------
// Desktop sidebar contract
// ---------------------------------------------------------------------------
describe("AppShell — desktop sidebar (SSR)", () => {
  beforeEach(() => {
    pathnameMock.mockReset();
    pathnameMock.mockReturnValue("/");
  });

  it("renders the sidebar nav with every NAV_ITEM", () => {
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    // Desktop sidebar starts with `hidden sm:flex` so it's invisible on mobile.
    expect(html).toMatch(/<aside[^>]*class="[^"]*hidden sm:flex/);
    for (const item of NAV_ITEMS) {
      expect(html).toContain(item.label);
    }
  });

  it("keeps the theme toggle in the sidebar footer", () => {
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    // The mocked ThemeToggle prints `data-testid="theme-toggle"`; it must
    // appear in the sidebar (we removed it from the mobile drawer).
    expect(html).toContain('data-testid="theme-toggle"');
  });

  it("sidebar brand header is a link to / with a 'Go to dashboard' aria-label", () => {
    pathnameMock.mockReturnValue("/accounts");
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    // Scope to the sidebar: it's the <aside> element. Pull just the
    // <aside>...</aside> slice so this assertion can't accidentally
    // match the mobile-header brand link (which has aria-label="Go home").
    const sidebarSlice = extractSidebar(html);
    expect(sidebarSlice).toMatch(
      /<a\b[^>]*href="\/"[^>]*aria-label="Go to dashboard"|<a\b[^>]*aria-label="Go to dashboard"[^>]*href="\/"/,
    );
  });

  it("mobile header brand link uses 'Go home' (separate copy from sidebar)", () => {
    // Make sure the two brand-link aria-labels stay distinct so screen-
    // reader users on a wide-window split-screen don't hear two
    // identical announcements when both are visible.
    const html = renderToStaticMarkup(
      <AppShell>
        <div />
      </AppShell>,
    );
    expect(html).toContain('aria-label="Go home"');
    expect(html).toContain('aria-label="Go to dashboard"');
  });
});

// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
/**
 * Slice off everything from the SheetContent marker onward. The
 * AppShell renders <Sidebar /> first and <MobileHeader /> (which owns
 * the Sheet) second, so anything after the marker belongs to the
 * mobile drawer + its surrounding JSX (the closing tags). This avoids
 * matching the desktop sidebar's nav links, which would otherwise
 * trigger false positives.
 *
 * We can't reliably scope to "just the SheetContent div" without an
 * HTML parser — the slice includes a few closing tags from outer
 * elements, but those don't introduce false matches for our
 * assertions (they have no href / aria-current attributes).
 */
function extractSheet(html: string): string {
  const open = html.indexOf('data-testid="sheet-content"');
  if (open === -1) return "";
  return html.slice(open);
}

/**
 * Pull just the desktop <aside>...</aside> slice. The shell renders
 * the sidebar first, then the mobile header, so the closing
 * </aside> tag cleanly separates the two brand markup blocks.
 */
function extractSidebar(html: string): string {
  const open = html.indexOf("<aside");
  if (open === -1) return "";
  const close = html.indexOf("</aside>", open);
  return html.slice(open, close === -1 ? html.length : close + "</aside>".length);
}
