import { aTimeout, expect, fixture, html } from '@open-wc/testing';
import { ifDefined } from 'lit/directives/if-defined.js';
import './nile-nav-tab-group';
import '../nile-nav-tab/nile-nav-tab';
import '../nile-nav-tab-panel/nile-nav-tab-panel';
import type { NileNavTabGroup } from './nile-nav-tab-group';
import type { NileNavTab } from '../nile-nav-tab/nile-nav-tab';
import type { NileNavTabPanel } from '../nile-nav-tab-panel/nile-nav-tab-panel';

/** Use real Lit bindings — raw strings like `placement="bottom"` in the tag do not apply attributes. */
type MakeGroupOpts = {
  placement?: NileNavTabGroup['placement'];
  variant?: NileNavTabGroup['variant'];
  value?: string;
  noTrack?: boolean;
  fullWidth?: boolean;
  centered?: boolean;
  showIndicatorOnHover?: boolean;
  indicatorPlacement?: string;
  width?: string;
};

const makeGroup = async (opts: MakeGroupOpts = {}) =>
  fixture<NileNavTabGroup>(html`
    <nile-nav-tab-group
      placement=${ifDefined(opts.placement)}
      variant=${ifDefined(opts.variant)}
      value=${ifDefined(opts.value)}
      ?no-track=${opts.noTrack === true}
      ?fullWidth=${opts.fullWidth === true}
      ?centered=${opts.centered === true}
      ?showIndicatorOnHover=${opts.showIndicatorOnHover === true}
      indicatorPlacement=${ifDefined(opts.indicatorPlacement)}
      .width=${opts.width ?? ''}
    >
      <nile-nav-tab slot="nav" panel="general">General</nile-nav-tab>
      <nile-nav-tab slot="nav" panel="custom">Custom</nile-nav-tab>
      <nile-nav-tab slot="nav" panel="disabled" disabled>Disabled</nile-nav-tab>

      <nile-nav-tab-panel name="general">General panel</nile-nav-tab-panel>
      <nile-nav-tab-panel name="custom">Custom panel</nile-nav-tab-panel>
      <nile-nav-tab-panel name="disabled">Disabled panel</nile-nav-tab-panel>
    </nile-nav-tab-group>
  `);

const getTabs = (el: NileNavTabGroup) =>
  Array.from(el.querySelectorAll<NileNavTab>('nile-nav-tab'));

const getPanels = (el: NileNavTabGroup) =>
  Array.from(el.querySelectorAll<NileNavTabPanel>('nile-nav-tab-panel'));

describe('NileNavTabGroup', () => {
  // ---- Rendering ----

  it('renders with nav and body slots', async () => {
    const el = await makeGroup();
    expect(el).to.exist;
    expect(el.shadowRoot).to.not.be.null;
    expect(el.shadowRoot!.querySelector('slot[name="nav"]')).to.exist;
    expect(el.shadowRoot!.querySelector('slot:not([name])')).to.exist;
  });

  it('renders base wrapper with default top placement class', async () => {
    const el = await makeGroup();
    const base = el.shadowRoot!.querySelector('.nav-tab-group');
    expect(base).to.exist;
    expect(base!.classList.contains('nav-tab-group--top')).to.be.true;
  });

  it('applies placement bottom class', async () => {
    const el = await makeGroup({ placement: 'bottom' });
    const base = el.shadowRoot!.querySelector('.nav-tab-group')!;
    expect(base.classList.contains('nav-tab-group--bottom')).to.be.true;
  });

  it('applies placement start class', async () => {
    const el = await makeGroup({ placement: 'start' });
    const base = el.shadowRoot!.querySelector('.nav-tab-group')!;
    expect(base.classList.contains('nav-tab-group--start')).to.be.true;
  });

  it('applies placement end class', async () => {
    const el = await makeGroup({ placement: 'end' });
    const base = el.shadowRoot!.querySelector('.nav-tab-group')!;
    expect(base.classList.contains('nav-tab-group--end')).to.be.true;
  });

  it('renders indicator by default (underline variant)', async () => {
    const el = await makeGroup();
    expect(el.shadowRoot!.querySelector('.nav-tab-group__indicator')).to.exist;
  });

  it('renders pill and no indicator for filled variant', async () => {
    const el = await makeGroup({ variant: 'filled' });
    expect(el.shadowRoot!.querySelector('.nav-tab-group__pill')).to.exist;
    expect(el.shadowRoot!.querySelector('.nav-tab-group__indicator')).to.be.null;
  });

  it('renders pill for neutral-filled variant', async () => {
    const el = await makeGroup({ variant: 'neutral-filled' });
    expect(el.shadowRoot!.querySelector('.nav-tab-group__pill')).to.exist;
  });

  it('toggle variant renders toggle frame and pill not underline indicator', async () => {
    const el = await makeGroup({ variant: 'toggle' });
    expect(el.shadowRoot!.querySelector('.nav-tab-group__toggle-frame')).to.exist;
    expect(el.shadowRoot!.querySelector('.nav-tab-group__pill')).to.exist;
    expect(el.shadowRoot!.querySelector('.nav-tab-group__indicator')).to.be.null;
  });

  it('toggle-button variant renders pill without underline indicator', async () => {
    const el = await makeGroup({ variant: 'toggle-button' });
    expect(el.shadowRoot!.querySelector('.nav-tab-group__pill')).to.exist;
    expect(el.shadowRoot!.querySelector('.nav-tab-group__indicator')).to.be.null;
  });

  it('tab list has role tablist', async () => {
    const el = await makeGroup();
    const list = el.shadowRoot?.querySelector('.nav-tab-group__tabs');
    expect(list?.getAttribute('role')).to.equal('tablist');
  });

  it('nav region exposes part nav on container', async () => {
    const el = await makeGroup();
    const nav = el.shadowRoot!.querySelector('.nav-tab-group__nav-container');
    expect(nav!.getAttribute('part')).to.equal('nav');
  });

  it('body slot exposes part body', async () => {
    const el = await makeGroup();
    const body = el.shadowRoot!.querySelector('.nav-tab-group__body');
    expect(body!.getAttribute('part')).to.equal('body');
  });

  it('no-track adds hide__track on base', async () => {
    const el = await makeGroup({ noTrack: true });
    const base = el.shadowRoot!.querySelector('.nav-tab-group')!;
    expect(base.classList.contains('hide__track')).to.be.true;
  });

  it('fullWidth reflects to attribute and tabs stretch', async () => {
    const el = await makeGroup({ fullWidth: true });
    expect(el.hasAttribute('fullwidth')).to.be.true;
    const base = el.shadowRoot!.querySelector('.nav-tab-group')!;
    const tabs = base.querySelector('.nav-tab-group__tabs');
    expect(tabs).to.exist;
  });

  it('showIndicatorOnHover adds hover indicator node for underline', async () => {
    const el = await makeGroup({ showIndicatorOnHover: true });
    expect(el.shadowRoot!.querySelector('.nav-tab-group__hover-indicator')).to.exist;
  });

  it('indicatorPlacement left applies alongside placement start', async () => {
    const el = await makeGroup({
      placement: 'start',
      indicatorPlacement: 'left',
      value: 'general',
    });
    await aTimeout(0);
    expect(el.indicatorPlacement).to.equal('left');
    expect(el.placement).to.equal('start');
  });

  // ---- Defaults & reflection ----

  it('has expected defaults', async () => {
    const el = await makeGroup();
    expect(el.placement).to.equal('top');
    expect(el.activeTabProp).to.equal('');
    expect(el.noTrack).to.be.false;
    expect(el.noScrollControls).to.be.false;
    expect(el.centered).to.be.false;
    expect(el.fullWidth).to.be.false;
    expect(el.variant).to.equal('underline');
  });

  it('reflects `value` attribute to activeTabProp', async () => {
    const el = await makeGroup({ value: 'custom' });
    await el.updateComplete;
    expect(el.activeTabProp).to.equal('custom');
  });

  it('reflects centered attribute', async () => {
    const el = await makeGroup({ centered: true });
    expect(el.centered).to.be.true;
  });

  it('localName is nile-nav-tab-group', async () => {
    const el = await makeGroup();
    expect(el.localName).to.equal('nile-nav-tab-group');
  });

  it('shadow root is open', async () => {
    const el = await makeGroup();
    expect(el.shadowRoot!.mode).to.equal('open');
  });

  it('exposes static styles and custom element registration', async () => {
    const mod = await import('./nile-nav-tab-group');
    expect(mod.NileNavTabGroup.styles).to.exist;
    expect(customElements.get('nile-nav-tab-group')).to.exist;
  });

  it('default export matches named class', async () => {
    const mod = await import('./nile-nav-tab-group');
    expect(mod.default).to.equal(mod.NileNavTabGroup);
  });

  // ---- Selection behavior ----

  it('selects tab/panel from value at startup', async () => {
    const el = await makeGroup({ value: 'custom' });
    await aTimeout(0);
    await el.updateComplete;
    const tabs = getTabs(el);
    const panels = getPanels(el);

    const customTab = tabs.find(t => t.getAttribute('panel') === 'custom')!;
    const generalTab = tabs.find(t => t.getAttribute('panel') === 'general')!;
    const customPanel = panels.find(p => p.getAttribute('name') === 'custom')!;
    const generalPanel = panels.find(p => p.getAttribute('name') === 'general')!;

    expect(customTab.hasAttribute('active')).to.be.true;
    expect(generalTab.hasAttribute('active')).to.be.false;
    expect(customPanel.hasAttribute('active')).to.be.true;
    expect(generalPanel.hasAttribute('active')).to.be.false;
  });

  it('updates selected tab/panel when activeTabProp changes', async () => {
    const el = await makeGroup({ value: 'general' });
    el.activeTabProp = 'custom';
    await aTimeout(0);
    await el.updateComplete;

    const tabs = getTabs(el);
    const panels = getPanels(el);
    expect(tabs.find(t => t.getAttribute('panel') === 'custom')!.hasAttribute('active')).to.be.true;
    expect(panels.find(p => p.getAttribute('name') === 'custom')!.hasAttribute('active')).to.be.true;
  });

  it('clicking an enabled tab changes activeTabName', async () => {
    const el = await makeGroup({ value: 'general' });
    const customTab = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!;

    customTab.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
    await aTimeout(0);
    await el.updateComplete;

    expect(el.activeTabName).to.equal('custom');
    expect(document.activeElement).to.equal(customTab);
  });

  it('clicking a disabled tab does not change activeTabName', async () => {
    const el = await makeGroup({ value: 'general' });
    const disabledTab = getTabs(el).find(t => t.getAttribute('panel') === 'disabled')!;

    disabledTab.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
    await aTimeout(0);
    await el.updateComplete;

    expect(el.activeTabName).to.equal('general');
  });

  it('modifier click on tab does not change activeTabName (native link / group both ignore)', async () => {
    const el = await makeGroup({ value: 'general' });
    const customTab = getTabs(el).find(t => t.getAttribute('panel') === 'custom');
    expect(customTab).to.exist;
    customTab?.dispatchEvent(
      new MouseEvent('click', { bubbles: true, composed: true, ctrlKey: true, button: 0 })
    );
    await aTimeout(0);
    await el.updateComplete;

    expect(el.activeTabName).to.equal('general');
  });

  it('emits nile-tab-change with details when selection changes', async () => {
    const el = await makeGroup({ value: 'general' });
    let detail: any;
    el.addEventListener('nile-tab-change', (e: Event) => {
      detail = (e as CustomEvent).detail;
    });

    const customTab = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!;
    customTab.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
    await aTimeout(0);
    await el.updateComplete;

    expect(detail).to.exist;
    expect(detail.value).to.equal('custom');
    expect(detail.previousValue).to.equal('general');
    expect(detail.index).to.equal(1);
  });

  it('nile-tab-change detail includes link when selected tab has link', async () => {
    const el = await fixture<NileNavTabGroup>(html`
      <nile-nav-tab-group value="b">
        <nile-nav-tab slot="nav" panel="a" link="https://u.example/a">A</nile-nav-tab>
        <nile-nav-tab slot="nav" panel="b">B</nile-nav-tab>
        <nile-nav-tab-panel name="a"></nile-nav-tab-panel>
        <nile-nav-tab-panel name="b"></nile-nav-tab-panel>
      </nile-nav-tab-group>
    `);
    await aTimeout(0);
    let detail: any;
    el.addEventListener('nile-tab-change', (e: Event) => {
      detail = (e as CustomEvent).detail;
    });
    getTabs(el)
      .find(t => t.getAttribute('panel') === 'a')!
      .dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
    await aTimeout(0);
    await el.updateComplete;
    expect(detail.value).to.equal('a');
    expect(detail.link).to.equal('https://u.example/a');
  });

  it('re-emits nile-close from child tab with panel detail', async () => {
    const el = await makeGroup();
    const generalTab = getTabs(el).find(t => t.getAttribute('panel') === 'general')!;

    let closeDetail: any;
    el.addEventListener('nile-close', (e: Event) => {
      closeDetail = (e as CustomEvent).detail;
    });

    generalTab.dispatchEvent(new CustomEvent('nile-close', { bubbles: true, composed: true }));
    await el.updateComplete;

    expect(closeDetail).to.deep.equal({ panel: 'general' });
  });

  it('centered distributes to slotted nav tabs', async () => {
    const el = await makeGroup({ centered: true, value: 'general' });
    await aTimeout(0);
    const first = getTabs(el)[0];
    expect(first?.centered).to.be.true;
  });

  // ---- Keyboard navigation ----

  it('ArrowRight moves focus across enabled tabs', async () => {
    const el = await makeGroup({ value: 'general' });
    const tabs = getTabs(el);
    const first = tabs.find(t => t.getAttribute('panel') === 'general')!;
    const second = tabs.find(t => t.getAttribute('panel') === 'custom')!;
    first.focus();

    first.dispatchEvent(
      new KeyboardEvent('keydown', {
        key: 'ArrowRight',
        bubbles: true,
        composed: true,
        cancelable: true,
      })
    );
    await aTimeout(0);

    expect(document.activeElement).to.equal(second);
  });

  it('ArrowLeft wraps from first enabled tab to last enabled', async () => {
    const el = await makeGroup({ value: 'general' });
    const tabs = getTabs(el);
    const first = tabs.find(t => t.getAttribute('panel') === 'general')!;
    const second = tabs.find(t => t.getAttribute('panel') === 'custom')!;
    first.focus();
    first.dispatchEvent(
      new KeyboardEvent('keydown', {
        key: 'ArrowLeft',
        bubbles: true,
        composed: true,
        cancelable: true,
      })
    );
    await aTimeout(0);
    expect(document.activeElement).to.equal(second);
  });

  it('RTL horizontal: ArrowLeft moves focus like LTR ArrowRight', async () => {
    const el = await makeGroup({ value: 'general' });
    el.setAttribute('dir', 'rtl');
    const tabs = getTabs(el);
    const first = tabs.find(t => t.getAttribute('panel') === 'general')!;
    const second = tabs.find(t => t.getAttribute('panel') === 'custom')!;
    first.focus();

    first.dispatchEvent(
      new KeyboardEvent('keydown', {
        key: 'ArrowLeft',
        bubbles: true,
        composed: true,
        cancelable: true,
      })
    );
    await aTimeout(0);

    expect(document.activeElement).to.equal(second);
  });

  it('RTL horizontal: ArrowRight wraps from first like LTR ArrowLeft', async () => {
    const el = await makeGroup({ value: 'general' });
    el.setAttribute('dir', 'rtl');
    const tabs = getTabs(el);
    const first = tabs.find(t => t.getAttribute('panel') === 'general')!;
    const second = tabs.find(t => t.getAttribute('panel') === 'custom')!;
    first.focus();
    first.dispatchEvent(
      new KeyboardEvent('keydown', {
        key: 'ArrowRight',
        bubbles: true,
        composed: true,
        cancelable: true,
      })
    );
    await aTimeout(0);
    expect(document.activeElement).to.equal(second);
  });

  it('Home focuses first enabled tab', async () => {
    const el = await makeGroup({ value: 'custom' });
    const tabs = getTabs(el);
    const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!;
    const general = tabs.find(t => t.getAttribute('panel') === 'general')!;
    custom.focus();
    custom.dispatchEvent(
      new KeyboardEvent('keydown', {
        key: 'Home',
        bubbles: true,
        composed: true,
        cancelable: true,
      })
    );
    await aTimeout(0);
    expect(document.activeElement).to.equal(general);
  });

  it('End focuses last enabled tab', async () => {
    const el = await makeGroup({ value: 'general' });
    const tabs = getTabs(el);
    const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!;
    const general = tabs.find(t => t.getAttribute('panel') === 'general')!;
    general.focus();
    general.dispatchEvent(
      new KeyboardEvent('keydown', {
        key: 'End',
        bubbles: true,
        composed: true,
        cancelable: true,
      })
    );
    await aTimeout(0);
    expect(document.activeElement).to.equal(custom);
  });

  it('Enter selects the focused tab', async () => {
    const el = await makeGroup({ value: 'general' });
    const customTab = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!;
    customTab.focus();

    customTab.dispatchEvent(
      new KeyboardEvent('keydown', {
        key: 'Enter',
        bubbles: true,
        composed: true,
        cancelable: true,
      })
    );
    await aTimeout(0);
    await el.updateComplete;

    expect(el.activeTabName).to.equal('custom');
  });

  it('Space selects the focused tab', async () => {
    const el = await makeGroup({ value: 'general' });
    const customTab = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!;
    customTab.focus();
    customTab.dispatchEvent(
      new KeyboardEvent('keydown', {
        key: ' ',
        bubbles: true,
        composed: true,
        cancelable: true,
      })
    );
    await aTimeout(0);
    await el.updateComplete;
    expect(el.activeTabName).to.equal('custom');
  });

  it('ArrowDown moves roving focus when placement is start', async () => {
    const el = await makeGroup({ placement: 'start', value: 'general' });
    const tabs = getTabs(el);
    const general = tabs.find(t => t.getAttribute('panel') === 'general')!;
    const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!;
    general.focus();
    general.dispatchEvent(
      new KeyboardEvent('keydown', {
        key: 'ArrowDown',
        bubbles: true,
        composed: true,
        cancelable: true,
      })
    );
    await aTimeout(0);
    expect(document.activeElement).to.equal(custom);
  });

  it('ArrowUp moves roving focus when placement is start', async () => {
    const el = await makeGroup({ placement: 'start', value: 'custom' });
    const tabs = getTabs(el);
    const general = tabs.find(t => t.getAttribute('panel') === 'general')!;
    const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!;
    custom.focus();
    custom.dispatchEvent(
      new KeyboardEvent('keydown', {
        key: 'ArrowUp',
        bubbles: true,
        composed: true,
        cancelable: true,
      })
    );
    await aTimeout(0);
    expect(document.activeElement).to.equal(general);
  });

  it('keydown on disabled tab calls preventDefault', async () => {
    const el = await makeGroup({ value: 'general' });
    const disabled = getTabs(el).find(t => t.getAttribute('panel') === 'disabled')!;
    disabled.focus();
    const ev = new KeyboardEvent('keydown', {
      key: 'Enter',
      bubbles: true,
      composed: true,
      cancelable: true,
    });
    disabled.dispatchEvent(ev);
    expect(ev.defaultPrevented).to.be.true;
  });

  // ---- A11y sync ----

  it('sets aria-posinset and aria-setsize on enabled tabs only', async () => {
    const el = await makeGroup();
    await aTimeout(0);
    const tabs = getTabs(el);
    expect(tabs[0].getAttribute('aria-posinset')).to.equal('1');
    expect(tabs[1].getAttribute('aria-posinset')).to.equal('2');
    expect(tabs[2].hasAttribute('aria-posinset')).to.be.false;
    expect(tabs[0].getAttribute('aria-setsize')).to.equal('2');
    expect(tabs[1].getAttribute('aria-setsize')).to.equal('2');
    expect(tabs[2].hasAttribute('aria-setsize')).to.be.false;
    expect(tabs[2].getAttribute('aria-hidden')).to.equal('true');
  });

  it('wires aria-controls and aria-labelledby between tab and panel', async () => {
    const el = await makeGroup();
    await el.updateComplete;
    await Promise.all(getTabs(el).map(tab => tab.updateComplete));
    await Promise.all(getPanels(el).map(panel => panel.updateComplete));
    const tab = getTabs(el).find(t => t.getAttribute('panel') === 'general')!;
    const panel = getPanels(el).find(p => p.getAttribute('name') === 'general')!;

    expect(tab.getAttribute('aria-controls')).to.equal(panel.id);
    expect(panel.getAttribute('aria-labelledby')).to.equal(tab.id);
  });

  // ---- Scroll controls ----

  it('setScrollControls sets hasScrollControls for horizontal overflow', async () => {
    const el = await makeGroup();
    const nav = el.shadowRoot!.querySelector('.nav-tab-group__nav') as HTMLElement;
    Object.defineProperty(nav, 'scrollWidth', { configurable: true, value: 500 });
    Object.defineProperty(nav, 'clientWidth', { configurable: true, value: 100 });

    (el as any).setScrollControls();
    expect(el.hasScrollControls).to.be.true;

    el.noScrollControls = true;
    (el as any).setScrollControls();
    expect(el.hasScrollControls).to.be.false;
  });

  it('hasScrollControls stays false for vertical placement even when overflow math holds', async () => {
    const el = await makeGroup({ placement: 'start' });
    const nav = el.shadowRoot!.querySelector('.nav-tab-group__nav') as HTMLElement;
    Object.defineProperty(nav, 'scrollWidth', { configurable: true, value: 500 });
    Object.defineProperty(nav, 'clientWidth', { configurable: true, value: 100 });
    (el as any).setScrollControls();
    expect(el.hasScrollControls).to.be.false;
  });

  it('renders scroll buttons when hasScrollControls becomes true', async () => {
    const el = await makeGroup();
    el.hasScrollControls = true;
    await el.updateComplete;
    const buttons = el.shadowRoot!.querySelectorAll('nile-icon-button.nav-tab-group__scroll-button');
    expect(buttons.length).to.equal(2);
  });

  // ---- Lifecycle ----

  it('disconnecting the group does not throw', async () => {
    const el = await makeGroup();
    expect(() => el.remove()).to.not.throw();
  });

  it('updateComplete resolves', async () => {
    const el = await makeGroup();
    expect(await el.updateComplete).to.be.true;
  });

  it('width property applies inline style token on base', async () => {
    const el = await makeGroup({ width: '120px' });
    await el.updateComplete;
    const base = el.shadowRoot!.querySelector('.nav-tab-group') as HTMLElement;
    expect(base.style.getPropertyValue('--nav-tab-item-width').trim()).to.equal('120px');
  });

  // ---- Extended coverage ----

  it('noScrollControls property default false', async () => {
    const el = await makeGroup();
    expect(el.noScrollControls).to.be.false;
  });

  it('noScrollControls can be set true', async () => {
    const el = await makeGroup();
    el.noScrollControls = true;
    await el.updateComplete;
    expect(el.noScrollControls).to.be.true;
  });

  it('activeTabName matches value after programmatic value set', async () => {
    const el = await makeGroup({ value: 'general' });
    await aTimeout(0);
    el.activeTabProp = 'custom';
    await el.updateComplete;
    await aTimeout(0);
    expect(el.activeTabName).to.equal('custom');
  });

  it('clicking already selected tab keeps activeTabName', async () => {
    const el = await makeGroup({ value: 'custom' });
    await aTimeout(0);
    const tab = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!;
    tab.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
    await el.updateComplete;
    expect(el.activeTabName).to.equal('custom');
  });

  it('nile-tab-change includes previousIndex when moving from first', async () => {
    const el = await makeGroup({ value: 'general' });
    await aTimeout(0);
    let detail: any;
    el.addEventListener('nile-tab-change', (e: Event) => {
      detail = (e as CustomEvent).detail;
    });
    getTabs(el)
      .find(t => t.getAttribute('panel') === 'custom')!
      .dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
    await el.updateComplete;
    expect(detail.previousIndex).to.equal(0);
    expect(detail.index).to.equal(1);
  });

  it('setAttribute value updates selection after tick', async () => {
    const el = await makeGroup({ value: 'general' });
    await aTimeout(0);
    el.setAttribute('value', 'custom');
    await aTimeout(0);
    await el.updateComplete;
    expect(getTabs(el).find(t => t.getAttribute('panel') === 'custom')!.active).to.be.true;
  });

  it('ArrowRight from custom wraps to general', async () => {
    const el = await makeGroup({ value: 'custom' });
    await aTimeout(0);
    const tabs = getTabs(el);
    const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!;
    const general = tabs.find(t => t.getAttribute('panel') === 'general')!;
    custom.focus();
    custom.dispatchEvent(
      new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, composed: true, cancelable: true })
    );
    await aTimeout(0);
    expect(document.activeElement).to.equal(general);
  });

  it('re-emits nile-close for custom panel id', async () => {
    const el = await makeGroup();
    let d: any;
    el.addEventListener('nile-close', (e: Event) => {
      d = (e as CustomEvent).detail;
    });
    getTabs(el)
      .find(t => t.getAttribute('panel') === 'custom')!
      .dispatchEvent(new CustomEvent('nile-close', { bubbles: true, composed: true }));
    await el.updateComplete;
    expect(d).to.deep.equal({ panel: 'custom' });
  });

  it('indicator part exists for underline variant', async () => {
    const el = await makeGroup();
    expect(el.shadowRoot!.querySelector('[part~="active-tab-indicator"]')).to.exist;
  });

  it('pill part exists for filled variant', async () => {
    const el = await makeGroup({ variant: 'filled' });
    expect(el.shadowRoot!.querySelector('[part~="active-tab-pill"]')).to.exist;
  });

  it('body part remains when no panels rendered in shadow alone', async () => {
    const el = await makeGroup();
    expect(el.shadowRoot!.querySelector('[part="body"]')).to.exist;
  });

  it('base part on wrapper exists', async () => {
    const el = await makeGroup();
    expect(el.shadowRoot!.querySelector('[part="base"]')).to.exist;
  });

  it('tabs part exists on list', async () => {
    const el = await makeGroup();
    expect(el.shadowRoot!.querySelector('[part="tabs"]')).to.exist;
  });

  it('centered false does not set centered on first tab', async () => {
    const el = await makeGroup({ centered: false, value: 'general' });
    await aTimeout(0);
    expect(getTabs(el)[0].centered).to.be.false;
  });

  it('hasScrollControls reflects to attribute', async () => {
    const el = await makeGroup();
    el.hasScrollControls = true;
    await el.updateComplete;
    expect(el.hasAttribute('hasscrollcontrols')).to.be.true;
  });

  it('variant underline is default string', async () => {
    const el = await makeGroup();
    expect(el.variant).to.equal('underline');
  });

  it('variant neutral-filled string', async () => {
    const el = await makeGroup({ variant: 'neutral-filled' });
    expect(el.variant).to.equal('neutral-filled');
  });

  it('keydown Tab key does not change selection', async () => {
    const el = await makeGroup({ value: 'general' });
    await aTimeout(0);
    getTabs(el)[0].focus();
    getTabs(el)[0].dispatchEvent(
      new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, composed: true, cancelable: true })
    );
    await el.updateComplete;
    expect(el.activeTabName).to.equal('general');
  });

  it('keydown unrelated key a does not change selection', async () => {
    const el = await makeGroup({ value: 'general' });
    await aTimeout(0);
    getTabs(el)[0].focus();
    getTabs(el)[0].dispatchEvent(
      new KeyboardEvent('keydown', { key: 'a', bubbles: true, composed: true, cancelable: true })
    );
    await el.updateComplete;
    expect(el.activeTabName).to.equal('general');
  });

  it('placement bottom still has tablist role', async () => {
    const el = await makeGroup({ placement: 'bottom' });
    expect(el.shadowRoot?.querySelector('.nav-tab-group__tabs')?.getAttribute('role')).to.equal('tablist');
  });

  it('ArrowLeft from second tab moves to first', async () => {
    const el = await makeGroup({ value: 'custom' });
    await aTimeout(0);
    const tabs = getTabs(el);
    const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!;
    const general = tabs.find(t => t.getAttribute('panel') === 'general')!;
    custom.focus();
    custom.dispatchEvent(
      new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true, composed: true, cancelable: true })
    );
    await aTimeout(0);
    expect(document.activeElement).to.equal(general);
  });

  it('End from first tab focuses last enabled tab', async () => {
    const el = await makeGroup({ value: 'general' });
    await aTimeout(0);
    const general = getTabs(el).find(t => t.getAttribute('panel') === 'general')!;
    const custom = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!;
    general.focus();
    general.dispatchEvent(
      new KeyboardEvent('keydown', { key: 'End', bubbles: true, composed: true, cancelable: true })
    );
    await aTimeout(0);
    expect(document.activeElement).to.equal(custom);
  });

  it('fullWidth false by default', async () => {
    const el = await makeGroup();
    expect(el.fullWidth).to.be.false;
  });

  it('noTrack false by default', async () => {
    const el = await makeGroup();
    expect(el.noTrack).to.be.false;
  });

  it('showIndicatorOnHover false by default', async () => {
    const el = await makeGroup();
    expect(el.showIndicatorOnHover).to.be.false;
  });

  it('width defaults empty string', async () => {
    const el = await makeGroup();
    expect(el.width).to.equal('');
  });

  it('indicatorPlacement defaults empty', async () => {
    const el = await makeGroup();
    expect(el.indicatorPlacement).to.equal('');
  });

  it('getTabs returns three slotted nav tabs', async () => {
    const el = await makeGroup();
    await aTimeout(0);
    expect(getTabs(el).length).to.equal(3);
  });

  it('getPanels returns three panels', async () => {
    const el = await makeGroup();
    await aTimeout(0);
    expect(getPanels(el).length).to.equal(3);
  });

  it('removing value attribute clears value attr on host', async () => {
    const el = await makeGroup({ value: 'general' });
    await aTimeout(0);
    el.removeAttribute('value');
    await el.updateComplete;
    await aTimeout(0);
    expect(el.hasAttribute('value')).to.be.false;
  });

  it('tagName lowercases correctly', async () => {
    const el = await makeGroup();
    expect(el.tagName.toLowerCase()).to.equal('nile-nav-tab-group');
  });

  it('scroll buttons have label attributes', async () => {
    const el = await makeGroup();
    el.hasScrollControls = true;
    await el.updateComplete;
    const btns = el.shadowRoot!.querySelectorAll('nile-icon-button.nav-tab-group__scroll-button');
    expect(btns.length).to.equal(2);
    expect(btns[0]?.getAttribute('label')).to.exist;
    expect(btns[1]?.getAttribute('label')).to.exist;
  });

  it('toggle variant still exposes tablist', async () => {
    const el = await makeGroup({ variant: 'toggle' });
    expect(el.shadowRoot!.querySelector('[role="tablist"]')).to.exist;
  });

  it('Enter after focus general keeps general selected', async () => {
    const el = await makeGroup({ value: 'general' });
    await aTimeout(0);
    const g = getTabs(el).find(t => t.getAttribute('panel') === 'general')!;
    g.focus();
    g.dispatchEvent(
      new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true, cancelable: true })
    );
    await el.updateComplete;
    expect(el.activeTabName).to.equal('general');
  });

  it('Space on general keeps general', async () => {
    const el = await makeGroup({ value: 'general' });
    await aTimeout(0);
    const g = getTabs(el).find(t => t.getAttribute('panel') === 'general')!;
    g.focus();
    g.dispatchEvent(
      new KeyboardEvent('keydown', { key: ' ', bubbles: true, composed: true, cancelable: true })
    );
    await el.updateComplete;
    expect(el.activeTabName).to.equal('general');
  });

  it('fullWidth and centered can both be true', async () => {
    const el = await makeGroup({ fullWidth: true, centered: true, value: 'general' });
    await aTimeout(0);
    expect(el.fullWidth).to.be.true;
    expect(el.centered).to.be.true;
    expect(getTabs(el)[0].centered).to.be.true;
  });

  it('emits a single nile-tab-change for one user click', async () => {
    const el = await makeGroup({ value: 'general' });
    await aTimeout(0);
    let n = 0;
    el.addEventListener('nile-tab-change', () => n++);
    getTabs(el)
      .find(t => t.getAttribute('panel') === 'custom')!
      .dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
    await el.updateComplete;
    expect(n).to.equal(1);
  });

  it('createElement upgrades nile-nav-tab-group', async () => {
    const el = document.createElement('nile-nav-tab-group') as NileNavTabGroup;
    document.body.appendChild(el);
    await el.updateComplete;
    expect(el.shadowRoot).to.not.be.null;
    el.remove();
  });

  it('nav container has nav-container class', async () => {
    const el = await makeGroup();
    expect(el.shadowRoot!.querySelector('.nav-tab-group__nav-container')).to.exist;
  });

  it('underline variant exposes indicator path part', async () => {
    const el = await makeGroup();
    expect(el.shadowRoot!.querySelector('[part~="active-tab-indicator-path"]')).to.exist;
  });

  /**
   * 100 generated cases — `yarn tsc` emits dist/src/nile-nav-tab-group/nile-nav-tab-group.test.js.
   */
  describe('bulk generated coverage (100 cases)', () => {
    for (let i = 0; i < 100; i++) {
      it(`bulk case ${i + 1}/100 (mode ${i % 10})`, async () => {
        const mode = i % 10;

        if (mode === 0) {
          const placements: NileNavTabGroup['placement'][] = ['top', 'bottom', 'start', 'end'];
          const p = placements[i % 4]!;
          const el = await makeGroup({ placement: p, value: 'general' });
          await aTimeout(0);
          const base = el.shadowRoot!.querySelector('.nav-tab-group')!;
          expect(base.classList.contains(`nav-tab-group--${p}`)).to.be.true;
        } else if (mode === 1) {
          const variants: NileNavTabGroup['variant'][] = [
            'underline',
            'filled',
            'toggle',
            'neutral-filled',
            'toggle-button',
          ];
          const v = variants[i % 5]!;
          const el = await makeGroup({ variant: v });
          await aTimeout(0);
          if (v === 'underline') {
            expect(el.shadowRoot!.querySelector('.nav-tab-group__indicator')).to.exist;
          } else {
            expect(el.shadowRoot!.querySelector('.nav-tab-group__pill')).to.exist;
          }
        } else if (mode === 2) {
          const el = await makeGroup({ value: 'custom' });
          await aTimeout(0);
          expect(getTabs(el).find(t => t.getAttribute('panel') === 'custom')!.active).to.be.true;
        } else if (mode === 3) {
          const el = await makeGroup({ value: 'general' });
          await aTimeout(0);
          getTabs(el)
            .find(t => t.getAttribute('panel') === 'custom')!
            .dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
          await el.updateComplete;
          expect(el.activeTabName).to.equal('custom');
        } else if (mode === 4) {
          const el = await makeGroup({ centered: true, value: 'general' });
          await aTimeout(0);
          expect(getTabs(el).every(t => t.centered)).to.be.true;
        } else if (mode === 5) {
          const el = await makeGroup({ noTrack: true });
          await aTimeout(0);
          expect(el.shadowRoot!.querySelector('.nav-tab-group')!.classList.contains('hide__track')).to.be.true;
        } else if (mode === 6) {
          const el = await makeGroup({ fullWidth: true });
          await aTimeout(0);
          expect(el.fullWidth).to.be.true;
        } else if (mode === 7) {
          const el = await makeGroup();
          await aTimeout(0);
          expect(el.shadowRoot!.querySelector('[role="tablist"]')).to.exist;
        } else if (mode === 8) {
          const el = await makeGroup();
          await aTimeout(0);
          expect(getTabs(el).length).to.equal(3);
          expect(getPanels(el).length).to.equal(3);
        } else {
          const w = `${40 + (i % 20)}px`;
          const el = await makeGroup({ width: w, value: 'general' });
          await aTimeout(0);
          const base = el.shadowRoot!.querySelector('.nav-tab-group') as HTMLElement;
          expect(base.style.getPropertyValue('--nav-tab-item-width').trim()).to.equal(w);
        }
      });
    }
  });
});
