import { describe, it, expect, beforeEach, vi } from 'vitest';
import '../../core/element.js';
import './nav.js';
import '../nav-group/nav-group.js';
import '../nav-item/nav-item.js';

const tick = () => new Promise((r) => queueMicrotask(r));

function mount(html) {
  const wrap = document.createElement('div');
  wrap.innerHTML = html;
  document.body.appendChild(wrap);
  return wrap.firstElementChild;
}

describe('nav-ui', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('registers nav-ui, nav-group-ui, and nav-item-ui as custom elements', () => {
    expect(customElements.get('nav-ui')).toBeDefined();
    expect(customElements.get('nav-group-ui')).toBeDefined();
    expect(customElements.get('nav-item-ui')).toBeDefined();
  });

  it('defaults to variant="primary" with role=navigation', () => {
    const nav = mount('<nav-ui></nav-ui>');
    expect(nav.variant).toBe('primary');
    expect(nav.getAttribute('role')).toBe('navigation');
  });

  it('reflects [variant="section"] from the property', () => {
    const nav = mount('<nav-ui variant="section"></nav-ui>');
    expect(nav.variant).toBe('section');
    expect(nav.getAttribute('variant')).toBe('section');
  });

  it('uses [heading] as aria-label and removes it when cleared', async () => {
    const nav = mount('<nav-ui heading="On this page"></nav-ui>');
    expect(nav.getAttribute('aria-label')).toBe('On this page');
    nav.heading = '';
    await tick();
    expect(nav.hasAttribute('aria-label')).toBe(false);
  });

  it('section variant skips the ResizeObserver wiring', () => {
    const primary = mount('<nav-ui></nav-ui>');
    const section = mount('<nav-ui variant="section"></nav-ui>');
    // The internal ResizeObserver is private (`#ro`); verify by the
    // observable surface — toggle() is a no-op on section, but the
    // primary nav can be flipped collapsed/uncollapsed.
    section.toggle();
    expect(section.collapsed).toBe(false);
    primary.toggle();
    expect(primary.collapsed).toBe(true);
  });

  it('select(item) sets [selected], clears prior selection, and bubbles nav-select', () => {
    const nav = mount(`
      <nav-ui>
        <nav-item-ui id="a" text="Alpha" value="/a"></nav-item-ui>
        <nav-item-ui id="b" text="Bravo" value="/b" selected></nav-item-ui>
      </nav-ui>
    `);
    const a = nav.querySelector('#a');
    const b = nav.querySelector('#b');

    const events = [];
    nav.addEventListener('nav-select', (e) => events.push(e.detail));

    nav.select(a);

    expect(a.hasAttribute('selected')).toBe(true);
    expect(b.hasAttribute('selected')).toBe(false);
    expect(nav.selectedItem).toBe(a);
    expect(events).toHaveLength(1);
    expect(events[0]).toMatchObject({ item: a, text: 'Alpha', value: '/a' });
  });

  it('selectedItem getter returns null when no item is selected', () => {
    const nav = mount(`
      <nav-ui>
        <nav-item-ui text="Alpha" value="/a"></nav-item-ui>
      </nav-ui>
    `);
    expect(nav.selectedItem).toBeNull();
  });

  it('toggle() is a no-op on variant="section"', () => {
    const nav = mount('<nav-ui variant="section"></nav-ui>');
    nav.toggle();
    expect(nav.collapsed).toBe(false);
    nav.toggle();
    expect(nav.collapsed).toBe(false);
  });

  it('survives disconnect without throwing', () => {
    const nav = mount(`
      <nav-ui>
        <nav-item-ui text="Alpha"></nav-item-ui>
      </nav-ui>
    `);
    expect(() => nav.remove()).not.toThrow();
  });
});

describe('nav-item-ui', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('sets role=link and tabindex=0 by default', () => {
    const nav = mount(`
      <nav-ui>
        <nav-item-ui text="Alpha"></nav-item-ui>
      </nav-ui>
    `);
    const item = nav.querySelector('nav-item-ui');
    expect(item.getAttribute('role')).toBe('link');
    expect(item.getAttribute('tabindex')).toBe('0');
  });

  it('switches tabindex to -1 when [disabled]', async () => {
    const nav = mount(`
      <nav-ui>
        <nav-item-ui text="Alpha"></nav-item-ui>
      </nav-ui>
    `);
    const item = nav.querySelector('nav-item-ui');
    item.disabled = true;
    await tick();
    expect(item.getAttribute('tabindex')).toBe('-1');
  });

  it('click on the item calls parent nav-ui.select(this)', () => {
    const nav = mount(`
      <nav-ui>
        <nav-item-ui id="a" text="Alpha" value="/a"></nav-item-ui>
      </nav-ui>
    `);
    const item = nav.querySelector('#a');
    const spy = vi.spyOn(nav, 'select');

    item.click();

    expect(spy).toHaveBeenCalledWith(item);
  });

  it('Enter key activates a nav-item-ui (same as click)', () => {
    const nav = mount(`
      <nav-ui>
        <nav-item-ui id="a" text="Alpha" value="/a"></nav-item-ui>
      </nav-ui>
    `);
    const item = nav.querySelector('#a');
    const spy = vi.spyOn(nav, 'select');

    item.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));

    expect(spy).toHaveBeenCalledWith(item);
  });

  it('Space key activates a nav-item-ui', () => {
    const nav = mount(`
      <nav-ui>
        <nav-item-ui id="a" text="Alpha"></nav-item-ui>
      </nav-ui>
    `);
    const item = nav.querySelector('#a');
    const spy = vi.spyOn(nav, 'select');

    item.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));

    expect(spy).toHaveBeenCalledWith(item);
  });

  it('disabled nav-item-ui does not activate on click or keyboard', () => {
    const nav = mount(`
      <nav-ui>
        <nav-item-ui id="a" text="Alpha" disabled></nav-item-ui>
      </nav-ui>
    `);
    const item = nav.querySelector('#a');
    const spy = vi.spyOn(nav, 'select');

    item.click();
    item.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));

    expect(spy).not.toHaveBeenCalled();
  });

  it('selected item carries aria-current="page"', async () => {
    const nav = mount(`
      <nav-ui>
        <nav-item-ui id="a" text="Alpha"></nav-item-ui>
      </nav-ui>
    `);
    const item = nav.querySelector('#a');
    nav.select(item);
    await tick();
    expect(item.getAttribute('aria-current')).toBe('page');
  });

  it('clearing selected removes aria-current', async () => {
    const nav = mount(`
      <nav-ui>
        <nav-item-ui id="a" text="Alpha"></nav-item-ui>
        <nav-item-ui id="b" text="Bravo"></nav-item-ui>
      </nav-ui>
    `);
    const a = nav.querySelector('#a');
    const b = nav.querySelector('#b');
    nav.select(a);
    await tick();
    expect(a.getAttribute('aria-current')).toBe('page');
    nav.select(b);
    await tick();
    expect(a.hasAttribute('aria-current')).toBe(false);
    expect(b.getAttribute('aria-current')).toBe('page');
  });
});

describe('nav-group-ui', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('auto-mints a header div with icon/text/caret slots', () => {
    const nav = mount(`
      <nav-ui>
        <nav-group-ui text="Settings" icon="gear">
          <nav-item-ui text="General"></nav-item-ui>
        </nav-group-ui>
      </nav-ui>
    `);
    const group = nav.querySelector('nav-group-ui');
    const header = group.querySelector(':scope > [slot="header"]');
    expect(header).not.toBeNull();
    expect(header.querySelector('[slot="text"]').textContent).toBe('Settings');
    expect(header.querySelector('icon-ui[slot="caret"]')).not.toBeNull();
  });

  it('renders [badge] in the header when set', () => {
    const nav = mount(`
      <nav-ui>
        <nav-group-ui text="Settings" badge="3"></nav-group-ui>
      </nav-ui>
    `);
    const group = nav.querySelector('nav-group-ui');
    const badge = group.querySelector('[slot="header"] [slot="badge"]');
    expect(badge).not.toBeNull();
    expect(badge.textContent).toBe('3');
  });

  it('header click toggles [open] when not collapsed (delegated through nav-ui)', async () => {
    const nav = mount(`
      <nav-ui>
        <nav-group-ui text="Settings"></nav-group-ui>
      </nav-ui>
    `);
    // happy-dom returns 0 for getBoundingClientRect width, which makes the
    // nav-ui treat itself as collapsed and route clicks to popover instead
    // of inline expand. Stub the rect so the inline-expand path fires.
    nav.getBoundingClientRect = () => ({ width: 240, height: 600, top: 0, left: 0, right: 240, bottom: 600, x: 0, y: 0 });

    const group = nav.querySelector('nav-group-ui');
    const header = group.querySelector(':scope > [slot="header"]');
    expect(group.open).toBe(false);

    header.click();
    await tick();

    expect(group.open).toBe(true);
  });

  it('keydown Enter on header is ignored when collapsible=false', () => {
    const nav = mount(`
      <nav-ui>
        <nav-group-ui text="Settings" open></nav-group-ui>
      </nav-ui>
    `);
    const group = nav.querySelector('nav-group-ui');
    group.collapsible = false;

    const header = group.querySelector(':scope > [slot="header"]');
    const events = [];
    group.addEventListener('group-toggle', (e) => events.push(e.detail));

    header.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));

    expect(events).toHaveLength(0);
    expect(group.open).toBe(true);
  });

  it('aria-expanded reflects the open state', async () => {
    const nav = mount(`
      <nav-ui>
        <nav-group-ui text="Settings"></nav-group-ui>
      </nav-ui>
    `);
    const group = nav.querySelector('nav-group-ui');
    expect(group.getAttribute('aria-expanded')).toBe('false');
    group.open = true;
    await tick();
    expect(group.getAttribute('aria-expanded')).toBe('true');
  });

  it('Enter on the header dispatches group-toggle when collapsible', () => {
    const nav = mount(`
      <nav-ui>
        <nav-group-ui text="Settings"></nav-group-ui>
      </nav-ui>
    `);
    const group = nav.querySelector('nav-group-ui');
    const header = group.querySelector(':scope > [slot="header"]');

    const events = [];
    group.addEventListener('group-toggle', (e) => events.push(e.detail));

    header.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));

    expect(events).toHaveLength(1);
    expect(events[0]).toMatchObject({ text: 'Settings', open: true });
  });

  it('respects an existing [slot="header"] instead of auto-minting one', () => {
    const nav = mount(`
      <nav-ui>
        <nav-group-ui text="Settings">
          <div slot="header" data-custom>my header</div>
        </nav-group-ui>
      </nav-ui>
    `);
    const group = nav.querySelector('nav-group-ui');
    const headers = group.querySelectorAll(':scope > [slot="header"]');
    expect(headers.length).toBe(1);
    expect(headers[0].dataset.custom).toBe('');
  });

  it('survives disconnect without throwing', () => {
    const nav = mount(`
      <nav-ui>
        <nav-group-ui text="Settings">
          <nav-item-ui text="General"></nav-item-ui>
        </nav-group-ui>
      </nav-ui>
    `);
    expect(() => nav.remove()).not.toThrow();
  });

  it('section-variant subnav stamps groups + items per the docs site pattern', () => {
    // Mirrors site/site.js subnav stamping for `subnav: true` sections:
    // dividers in the sitemap become nav-group-ui kickers; following items
    // become nav-item-ui children of the most recent group.
    const nav = mount(`
      <nav-ui variant="section" aria-label="Authentication">
        <nav-group-ui text="Sign in">
          <nav-item-ui text="Sign In" value="/auth/sign-in"></nav-item-ui>
          <nav-item-ui text="Password" value="/auth/sign-in/password"></nav-item-ui>
        </nav-group-ui>
        <nav-group-ui text="Sign up">
          <nav-item-ui text="Sign Up" value="/auth/sign-up"></nav-item-ui>
        </nav-group-ui>
      </nav-ui>
    `);
    const groups = nav.querySelectorAll(':scope > nav-group-ui');
    expect(groups.length).toBe(2);
    // Each group has its expected children attached (not stripped at mount).
    expect(groups[0].querySelectorAll('nav-item-ui').length).toBe(2);
    expect(groups[1].querySelectorAll('nav-item-ui').length).toBe(1);
    // Group headers are auto-minted with the kicker text.
    expect(groups[0].querySelector('[slot="header"] [slot="text"]').textContent).toBe('Sign in');
    expect(groups[1].querySelector('[slot="header"] [slot="text"]').textContent).toBe('Sign up');
  });
});

describe('section variant — explicit + cascade', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('nav-group-ui[variant="section"] reflects the property', () => {
    const group = mount('<nav-group-ui variant="section" text="Settings"></nav-group-ui>');
    expect(group.variant).toBe('section');
    expect(group.getAttribute('variant')).toBe('section');
  });

  it('nav-item-ui[variant="section"] reflects the property', () => {
    const item = mount('<nav-item-ui variant="section" text="General"></nav-item-ui>');
    expect(item.variant).toBe('section');
    expect(item.getAttribute('variant')).toBe('section');
  });

  it('nav-group-ui defaults to variant="" (empty) when not set', () => {
    const group = mount('<nav-group-ui text="Settings"></nav-group-ui>');
    expect(group.variant).toBe('');
  });

  it('nav-item-ui defaults to variant="" (empty) when not set', () => {
    const item = mount('<nav-item-ui text="General"></nav-item-ui>');
    expect(item.variant).toBe('');
  });

  it('cascade: nav-ui[variant="section"] does NOT mutate the child variant prop', () => {
    // The cascade is CSS-only — children keep their own (empty) variant.
    // CSS rules target `nav-ui[variant="section"] > x:not([variant])` so
    // the parent's variant only governs styling, not the JS state.
    const nav = mount(`
      <nav-ui variant="section">
        <nav-group-ui text="Settings">
          <nav-item-ui text="General"></nav-item-ui>
        </nav-group-ui>
      </nav-ui>
    `);
    const group = nav.querySelector('nav-group-ui');
    const item = nav.querySelector('nav-item-ui');
    expect(group.variant).toBe('');
    expect(item.variant).toBe('');
  });

  it('explicit variant on a child wins over the parent cascade', () => {
    // A child can opt out of the section cascade by setting its own
    // variant. The CSS rules use `:not([variant])` for the cascade path,
    // so an explicit non-section variant on the child suppresses kicker
    // styling and the child renders primary-style.
    const nav = mount(`
      <nav-ui variant="section">
        <nav-group-ui text="Override" variant="primary"></nav-group-ui>
      </nav-ui>
    `);
    const group = nav.querySelector('nav-group-ui');
    expect(group.variant).toBe('primary');
    expect(group.matches('[variant]')).toBe(true);
  });
});
