import { describe, it, expect } from "vitest";
import { Hono } from "hono";
import { BaseLayout } from "../base.js";
import type { WebSession } from "../../lib/session.js";
import { FALLBACK_THEME } from "../../lib/theme-resolution.js";

const app = new Hono().get("/", (c) =>
  c.html(
    <BaseLayout title="Test Page" resolvedTheme={FALLBACK_THEME}>
      Page content
    </BaseLayout>
  )
);

describe("BaseLayout", () => {
  it("injects neobrutal tokens as :root CSS custom properties", async () => {
    const res = await app.request("/");
    const html = await res.text();
    // css-tree generates compact CSS (no space before brace)
    expect(html).toContain(":root{");
    expect(html).toContain("--color-bg:");
    expect(html).toContain("--color-primary:");
  });

  it("loads reset.css and theme.css stylesheets", async () => {
    const res = await app.request("/");
    const html = await res.text();
    expect(html).toContain('href="/static/css/reset.css"');
    expect(html).toContain('href="/static/css/theme.css"');
  });

  it("loads Space Grotesk from Google Fonts", async () => {
    const res = await app.request("/");
    const html = await res.text();
    expect(html).toContain("fonts.googleapis.com");
    expect(html).toContain("Space+Grotesk");
  });

  it("renders semantic site-header, content-container, and site-footer", async () => {
    const res = await app.request("/");
    const html = await res.text();
    expect(html).toContain('class="site-header"');
    expect(html).toContain('class="content-container"');
    expect(html).toContain('class="site-footer"');
  });

  it("renders provided page title", async () => {
    const res = await app.request("/");
    const html = await res.text();
    expect(html).toContain("<title>Test Page</title>");
  });

  it("falls back to default title when none provided", async () => {
    const defaultApp = new Hono().get("/", (c) =>
      c.html(
        <BaseLayout resolvedTheme={FALLBACK_THEME}>content</BaseLayout>
      )
    );
    const res = await defaultApp.request("/");
    const html = await res.text();
    expect(html).toContain("<title>atBB Forum</title>");
  });

  it("renders children inside content-container", async () => {
    const res = await app.request("/");
    const html = await res.text();
    expect(html).toContain("Page content");
  });

  it("renders header title link pointing to /", async () => {
    const res = await app.request("/");
    const html = await res.text();
    expect(html).toContain('href="/"');
    expect(html).toContain('class="site-header__title"');
  });

  it("includes Accept-CH meta tag for color scheme hint", async () => {
    const res = await app.request("/");
    const html = await res.text();
    expect(html).toContain('http-equiv="Accept-CH"');
    expect(html).toContain('content="Sec-CH-Prefers-Color-Scheme"');
  });

  it("renders cssOverrides in a style tag when non-null", async () => {
    const themeWithOverrides = {
      ...FALLBACK_THEME,
      cssOverrides: ".card { border: 2px solid black; }",
    };
    const overridesApp = new Hono().get("/", (c) =>
      c.html(
        <BaseLayout resolvedTheme={themeWithOverrides}>content</BaseLayout>
      )
    );
    const res = await overridesApp.request("/");
    const html = await res.text();
    // css-tree generates compact CSS — check for key selectors and properties
    expect(html).toContain(".card{");
    expect(html).toContain("border:2px solid black");
  });

  it("does not render Google Fonts preconnect tags when fontUrls is null", async () => {
    const themeNoFonts = { ...FALLBACK_THEME, fontUrls: null };
    const noFontsApp = new Hono().get("/", (c) =>
      c.html(
        <BaseLayout resolvedTheme={themeNoFonts}>content</BaseLayout>
      )
    );
    const res = await noFontsApp.request("/");
    const html = await res.text();
    expect(html).not.toContain("fonts.googleapis.com");
  });

  it("filters out non-https font URLs and does not render them", async () => {
    const themeWithUnsafeFontUrl = {
      ...FALLBACK_THEME,
      fontUrls: ["http://evil.com/style.css", "https://fonts.example.com/safe.css"],
    };
    const unsafeFontApp = new Hono().get("/", (c) =>
      c.html(
        <BaseLayout resolvedTheme={themeWithUnsafeFontUrl}>content</BaseLayout>
      )
    );
    const res = await unsafeFontApp.request("/");
    const html = await res.text();
    expect(html).not.toContain("http://evil.com/style.css");
    expect(html).toContain("https://fonts.example.com/safe.css");
  });

  it("does not render cssOverrides style tag when cssOverrides is null", async () => {
    const themeNoOverrides = { ...FALLBACK_THEME, cssOverrides: null };
    const noOverridesApp = new Hono().get("/", (c) =>
      c.html(
        <BaseLayout resolvedTheme={themeNoOverrides}>content</BaseLayout>
      )
    );
    const res = await noOverridesApp.request("/");
    const html = await res.text();
    // The only <style> tag should be the :root block — no second style tag for overrides
    const styleTagMatches = html.match(/<style/g);
    expect(styleTagMatches).toHaveLength(1);
    // css-tree generates compact CSS (no space before brace)
    expect(html).toContain(":root{");
  });

  describe("auth-aware navigation", () => {
    it("shows Log in link when auth is not provided (default unauthenticated)", async () => {
      const unauthApp = new Hono().get("/", (c) =>
        c.html(
          <BaseLayout resolvedTheme={FALLBACK_THEME}>content</BaseLayout>
        )
      );
      const res = await unauthApp.request("/");
      const html = await res.text();
      expect(html).toContain('href="/login"');
      expect(html).toContain("Log in");
    });

    it("shows Log in link when auth is explicitly unauthenticated", async () => {
      const auth: WebSession = { authenticated: false };
      const unauthApp = new Hono().get("/", (c) =>
        c.html(
          <BaseLayout auth={auth} resolvedTheme={FALLBACK_THEME}>
            content
          </BaseLayout>
        )
      );
      const res = await unauthApp.request("/");
      const html = await res.text();
      expect(html).toContain('href="/login"');
      expect(html).toContain("Log in");
      expect(html).not.toContain("Log out");
    });

    it("shows handle and Log out button when authenticated", async () => {
      const auth: WebSession = {
        authenticated: true,
        did: "did:plc:abc123",
        handle: "alice.bsky.social",
      };
      const authApp = new Hono().get("/", (c) =>
        c.html(
          <BaseLayout auth={auth} resolvedTheme={FALLBACK_THEME}>
            content
          </BaseLayout>
        )
      );
      const res = await authApp.request("/");
      const html = await res.text();
      expect(html).toContain("alice.bsky.social");
      expect(html).toContain("Log out");
      expect(html).not.toContain('href="/login"');
    });

    it("renders logout as a form POST (not a link)", async () => {
      const auth: WebSession = {
        authenticated: true,
        did: "did:plc:abc123",
        handle: "alice.bsky.social",
      };
      const authApp = new Hono().get("/", (c) =>
        c.html(
          <BaseLayout auth={auth} resolvedTheme={FALLBACK_THEME}>
            content
          </BaseLayout>
        )
      );
      const res = await authApp.request("/");
      const html = await res.text();
      // Logout must be a form POST for CSRF protection, not a plain link
      expect(html).toContain('action="/logout"');
      expect(html).toContain('method="post"');
      expect(html).toContain("Log out");
    });

    it("shows Settings link when authenticated", async () => {
      const auth: WebSession = {
        authenticated: true,
        did: "did:plc:abc123",
        handle: "alice.bsky.social",
      };
      const authApp = new Hono().get("/", (c) =>
        c.html(
          <BaseLayout auth={auth} resolvedTheme={FALLBACK_THEME}>
            content
          </BaseLayout>
        )
      );
      const res = await authApp.request("/");
      const html = await res.text();
      expect(html).toContain('href="/settings"');
      expect(html).toContain("Settings");
    });

    it("does not show Settings link when unauthenticated", async () => {
      const auth: WebSession = { authenticated: false };
      const unauthApp = new Hono().get("/", (c) =>
        c.html(
          <BaseLayout auth={auth} resolvedTheme={FALLBACK_THEME}>
            content
          </BaseLayout>
        )
      );
      const res = await unauthApp.request("/");
      const html = await res.text();
      expect(html).not.toContain('href="/settings"');
      expect(html).not.toContain("Settings");
    });
  });

  describe("accessibility", () => {
    it("renders skip-to-content link before the site header", async () => {
      const res = await app.request("/");
      const html = await res.text();
      expect(html).toContain('class="skip-link"');
      expect(html).toContain('href="#main-content"');
      expect(html).toContain("Skip to main content");
      // Skip link must come before header in DOM order
      const skipLinkPos = html.indexOf("skip-link");
      const headerPos = html.indexOf("site-header");
      expect(skipLinkPos).toBeLessThan(headerPos);
    });

    it("renders main element with id for skip link target", async () => {
      const res = await app.request("/");
      const html = await res.text();
      expect(html).toContain('id="main-content"');
    });

    it("desktop nav has aria-label for Main navigation", async () => {
      const res = await app.request("/");
      const html = await res.text();
      expect(html).toContain('aria-label="Main navigation"');
    });

    it("mobile nav has distinct aria-label", async () => {
      const res = await app.request("/");
      const html = await res.text();
      expect(html).toContain('aria-label="Mobile navigation"');
    });
  });

  describe("color scheme toggle", () => {
    it("renders toggle button in site header when color scheme is light", async () => {
      const res = await app.request("/");
      const html = await res.text();
      expect(html).toContain("color-scheme-toggle");
      expect(html).toContain('aria-label="Switch to dark mode"');
    });

    it("renders toggle button with aria-label 'Switch to light mode' when dark theme", async () => {
      const darkTheme = { ...FALLBACK_THEME, colorScheme: "dark" as const };
      const darkApp = new Hono().get("/", (c) =>
        c.html(<BaseLayout resolvedTheme={darkTheme}>content</BaseLayout>)
      );
      const res = await darkApp.request("/");
      const html = await res.text();
      expect(html).toContain("color-scheme-toggle");
      expect(html).toContain('aria-label="Switch to light mode"');
    });

    it("toggle button appears in both desktop and mobile nav", async () => {
      const res = await app.request("/");
      const html = await res.text();
      const toggleMatches = html.match(/color-scheme-toggle/g);
      expect(toggleMatches).toHaveLength(2);
    });

    it("toggle button calls toggleColorScheme on click", async () => {
      const res = await app.request("/");
      const html = await res.text();
      expect(html).toContain("toggleColorScheme()");
    });

    it("page includes toggleColorScheme script that sets cookie and reloads", async () => {
      const res = await app.request("/");
      const html = await res.text();
      expect(html).toContain("atbb-color-scheme");
      expect(html).toContain("location.reload");
      expect(html).toContain("max-age=31536000");
      expect(html).toContain("SameSite=Lax");
      expect(html).toContain("path=/");
    });

    it("toggleColorScheme script defaults to 'light' when no cookie present (first toggle must produce 'dark')", async () => {
      // Regression: the old script used `m&&m[1]==='light'` which evaluates to `null`
      // (not `false`) when no cookie exists, causing the first toggle to always produce
      // 'light' instead of 'dark'. The fix introduces a `current` variable that defaults
      // to 'light', ensuring `next` is always the opposite of the current scheme.
      const res = await app.request("/");
      const html = await res.text();
      // Verify the corrected pattern is present: `current` defaults to 'light' when no cookie
      expect(html).toContain("var current=m?m[1]:'light'");
      // Verify `next` is derived from `current`, not from the raw regex match
      expect(html).toContain("current==='light'?'dark':'light'");
    });
  });

  describe("favicon", () => {
    it("includes favicon link in head", async () => {
      const res = await app.request("/");
      const html = await res.text();
      expect(html).toContain('rel="icon"');
      expect(html).toContain("favicon.svg");
    });
  });

  describe("mobile navigation", () => {
    it("renders details/summary hamburger menu for mobile", async () => {
      const res = await app.request("/");
      const html = await res.text();
      expect(html).toContain("mobile-nav");
      expect(html).toContain("mobile-nav__toggle");
    });

    it("renders desktop nav separately from mobile nav", async () => {
      const res = await app.request("/");
      const html = await res.text();
      expect(html).toContain("desktop-nav");
    });

    it("hamburger has aria-label for accessibility", async () => {
      const res = await app.request("/");
      const html = await res.text();
      expect(html).toContain('aria-label="Menu"');
    });

    it("mobile nav contains login link when not authenticated", async () => {
      const res = await app.request("/");
      const html = await res.text();
      // Both mobile and desktop nav should have "Log in"
      const loginMatches = html.match(/Log in/g);
      expect(loginMatches!.length).toBe(2);
    });

    it("mobile nav contains auth state when logged in", async () => {
      const auth: WebSession = {
        authenticated: true,
        did: "did:plc:abc123",
        handle: "alice.bsky.social",
      };
      const authApp = new Hono().get("/", (c) =>
        c.html(
          <BaseLayout auth={auth} resolvedTheme={FALLBACK_THEME}>
            content
          </BaseLayout>
        )
      );
      const res = await authApp.request("/");
      const html = await res.text();
      // Both mobile and desktop nav should have "Log out"
      const logoutMatches = html.match(/Log out/g);
      expect(logoutMatches!.length).toBe(2);
    });

    it("renders Settings link in both desktop and mobile nav when authenticated", async () => {
      const auth: WebSession = {
        authenticated: true,
        did: "did:plc:abc123",
        handle: "alice.bsky.social",
      };
      const authApp = new Hono().get("/", (c) =>
        c.html(
          <BaseLayout auth={auth} resolvedTheme={FALLBACK_THEME}>
            content
          </BaseLayout>
        )
      );
      const res = await authApp.request("/");
      const html = await res.text();
      // NavContent is rendered twice (desktop + mobile), so the link appears twice
      const settingsMatches = [...html.matchAll(/href="\/settings"/g)];
      expect(settingsMatches).toHaveLength(2);
    });
  });
});
