import { describe, it, expect, beforeEach } from 'vitest';
import '../../core/element.js';
import './select.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('select-ui', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  // §FEEDBACK-36 — declarative <option selected> initial value (single-select)
  it('honours the first <option selected> as initial value for single-select', async () => {
    const s = mount(`
      <select-ui>
        <option value="a">Alpha</option>
        <option value="b" selected>Beta</option>
        <option value="c">Gamma</option>
      </select-ui>
    `);
    await tick();
    expect(s.value).toBe('b');
  });

  // §FB-98 — <option> children nested in the template engine's display:contents
  // wrapper spans (.map()/repeat()) must be parsed, not just static literals.
  it('FB-98: parses options nested in display:contents/role=presentation wrappers (present at connect)', async () => {
    const s = mount(`
      <select-ui value="a">
        <span style="display:contents" role="presentation">
          <span style="display:contents" role="presentation"><option value="a">Alpha</option></span>
          <span style="display:contents" role="presentation"><option value="b">Beta</option></span>
          <span style="display:contents" role="presentation"><option value="c">Gamma</option></span>
        </span>
      </select-ui>
    `);
    await tick();
    expect(s.querySelectorAll('[role="option"]').length).toBe(3);
    expect(s.options.map((o) => o.value)).toEqual(['a', 'b', 'c']);
    expect(s.value).toBe('a');
  });

  // §FB-98 — interpolated options that ARRIVE after connect (the template's
  // update() pass) are picked up by the re-parse MutationObserver.
  it('FB-98: re-parses wrapped options that arrive after connect (observer)', async () => {
    const s = mount(`<select-ui></select-ui>`);
    await tick();
    expect(s.querySelectorAll('[role="option"]').length).toBe(0);
    const wrap = document.createElement('span');
    wrap.style.display = 'contents';
    wrap.setAttribute('role', 'presentation');
    wrap.innerHTML = `<option value="x">X</option><option value="y">Y</option>`;
    s.appendChild(wrap);
    await tick(); await tick();   // mutation → observer microtask → re-parse + render
    expect(s.querySelectorAll('[role="option"]').length).toBe(2);
  });

  // §FB-46 — <option selected> must capture ALL values in [multiple] mode
  it('captures all <option selected> values as comma-separated for [multiple]', async () => {
    const s = mount(`
      <select-ui multiple>
        <option value="a" selected>Alpha</option>
        <option value="b">Beta</option>
        <option value="c" selected>Gamma</option>
      </select-ui>
    `);
    await tick();
    // All selected values should be present (order-agnostic)
    const parts = new Set(s.value.split(',').map(v => v.trim()));
    expect(parts.has('a')).toBe(true);
    expect(parts.has('c')).toBe(true);
    expect(parts.has('b')).toBe(false);
  });

  // §FB-46 — .value setter with comma-separated string must sync aria-selected
  it('syncs aria-selected on all comma-separated values when .value is set', async () => {
    const s = mount(`
      <select-ui multiple open>
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
        <option value="c">Gamma</option>
      </select-ui>
    `);
    await tick();
    s.value = 'a,c';
    await tick();

    const opts = s.querySelectorAll('[role="option"]');
    const selected = Array.from(opts)
      .filter(o => o.getAttribute('aria-selected') === 'true')
      .map(o => o.getAttribute('data-value'));

    expect(selected).toContain('a');
    expect(selected).toContain('c');
    expect(selected).not.toContain('b');
  });

  // Regression: single-select aria-selected must still work with the Set path
  it('still sets aria-selected correctly for single-select after §FB-46 changes', async () => {
    const s = mount(`
      <select-ui open>
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
      </select-ui>
    `);
    await tick();
    s.value = 'b';
    await tick();

    const opts = s.querySelectorAll('[role="option"]');
    const selected = Array.from(opts)
      .filter(o => o.getAttribute('aria-selected') === 'true')
      .map(o => o.getAttribute('data-value'));

    expect(selected).toEqual(['b']);
  });

  // ── SPEC-040 (multi-select) ──

  it('multi-select: [multiple] sets data-multi-chips and renders tag-ui chips per selected value', async () => {
    const s = mount(`
      <select-ui multiple value="a,c">
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
        <option value="c">Gamma</option>
      </select-ui>
    `);
    await tick();
    expect(s.hasAttribute('data-multi-chips')).toBe(true);
    const chips = s.querySelectorAll('[slot="trigger"] [data-chips] tag-ui');
    expect(chips.length).toBe(2);
    const chipValues = Array.from(chips).map((c) => c.dataset.chipValue);
    expect(chipValues).toContain('a');
    expect(chipValues).toContain('c');
  });

  it('multi-select: aria-multiselectable=true is set on the listbox', async () => {
    const s = mount(`
      <select-ui multiple open>
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
      </select-ui>
    `);
    await tick();
    const lb = s.querySelector('[slot="listbox"]');
    expect(lb).toBeTruthy();
    expect(lb.getAttribute('aria-multiselectable')).toBe('true');
  });

  it('multi-select: clicking an option toggles selection without closing the popover', async () => {
    const s = mount(`
      <select-ui multiple open>
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
        <option value="c">Gamma</option>
      </select-ui>
    `);
    await tick();
    const opts = s.querySelectorAll('[role="option"]');
    const optA = Array.from(opts).find((o) => o.getAttribute('data-value') === 'a');
    optA.click();
    await tick();
    expect(s.open).toBe(true); // still open
    expect(s.value).toBe('a');

    const optC = Array.from(s.querySelectorAll('[role="option"]')).find((o) => o.getAttribute('data-value') === 'c');
    optC.click();
    await tick();
    expect(s.open).toBe(true);
    const parts = new Set(s.value.split(','));
    expect(parts.has('a')).toBe(true);
    expect(parts.has('c')).toBe(true);
  });

  it('multi-select: Backspace from the trigger removes the last chip', async () => {
    const s = mount(`
      <select-ui multiple value="a,b">
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
      </select-ui>
    `);
    await tick();
    const ev = new KeyboardEvent('keydown', { key: 'Backspace', bubbles: true });
    s.dispatchEvent(ev);
    await tick();
    expect(s.value).toBe('a');
  });

  it('multi-select: toggle(id) is a public API', async () => {
    const s = mount(`
      <select-ui multiple>
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
      </select-ui>
    `);
    await tick();
    s.toggle('a');
    await tick();
    expect(s.value).toBe('a');
    s.toggle('a');
    await tick();
    expect(s.value).toBe('');
  });

  it('multi-select: [max] suppresses further selection and fires invalid', async () => {
    const s = mount(`
      <select-ui multiple max="2" value="a,b">
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
        <option value="c">Gamma</option>
      </select-ui>
    `);
    await tick();
    let invalidFired = null;
    s.addEventListener('invalid', (e) => { invalidFired = e.detail; });
    s.toggle('c');
    await tick();
    expect(invalidFired).toBeTruthy();
    expect(invalidFired.reason).toBe('max');
    expect(s.value).toBe('a,b'); // unchanged
  });

  it('multi-select: clear() empties value and fires change with removed array', async () => {
    const s = mount(`
      <select-ui multiple value="a,b">
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
      </select-ui>
    `);
    await tick();
    let changed = null;
    s.addEventListener('change', (e) => { changed = e.detail; });
    s.clear();
    await tick();
    expect(s.value).toBe('');
    expect(changed.removed).toEqual(['a', 'b']);
  });

  it('multi-select: selectAllOptions() selects every non-disabled option', async () => {
    const s = mount(`
      <select-ui multiple>
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
        <option value="c" disabled>Gamma</option>
      </select-ui>
    `);
    await tick();
    s.selectAllOptions();
    await tick();
    const parts = new Set(s.value.split(','));
    expect(parts.has('a')).toBe(true);
    expect(parts.has('b')).toBe(true);
    expect(parts.has('c')).toBe(false); // disabled
  });

  it('multi-select: [max-chips] shows "+N more" pill when exceeded', async () => {
    const s = mount(`
      <select-ui multiple max-chips="2" value="a,b,c,d">
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
        <option value="c">Gamma</option>
        <option value="d">Delta</option>
      </select-ui>
    `);
    await tick();
    const chips = s.querySelectorAll('[slot="trigger"] [data-chips] tag-ui');
    expect(chips.length).toBe(2);
    const more = s.querySelector('[slot="trigger"] [data-more]');
    expect(more).toBeTruthy();
    expect(more.textContent).toBe('+2 more');
  });

  it('multi-select: [clearable] reveals the clear-all button when chips present', async () => {
    const s = mount(`
      <select-ui multiple clearable value="a">
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
      </select-ui>
    `);
    await tick();
    const clearBtn = s.querySelector('[data-clear-all]');
    expect(clearBtn).toBeTruthy();
    expect(clearBtn.hasAttribute('hidden')).toBe(false);
  });

  it('multi-select: [select-all] renders the bulk-action header', async () => {
    const s = mount(`
      <select-ui multiple select-all open>
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
      </select-ui>
    `);
    await tick();
    const header = s.querySelector('[slot="listbox"] [data-select-all]');
    expect(header).toBeTruthy();
    const btn = header.querySelector('[data-select-all-btn]');
    expect(btn).toBeTruthy();
    expect(btn.textContent).toBe('Select all');
  });

  it('multi-select: chip remove event empties that selection from value', async () => {
    const s = mount(`
      <select-ui multiple value="a,b">
        <option value="a">Alpha</option>
        <option value="b">Beta</option>
      </select-ui>
    `);
    await tick();
    const chipA = s.querySelector('tag-ui[data-chip-value="a"]');
    expect(chipA).toBeTruthy();
    // Simulate tag-ui's remove event (bubbles, with detail.text)
    chipA.dispatchEvent(new CustomEvent('remove', {
      bubbles: true,
      detail: { text: 'Alpha', value: 'Alpha' },
    }));
    await tick();
    expect(s.value).toBe('b');
  });

  // §FB-88 — placeholder-only select derives an accessible name (WCAG 4.1.2),
  // mirroring input-ui, without clobbering a real label.
  describe('accessible name from placeholder (FB-88)', () => {
    it('names a placeholder-only select from its placeholder', async () => {
      const s = mount(`<select-ui placeholder="Status"></select-ui>`);
      await tick();
      expect(s.getAttribute('aria-label')).toBe('Status');
    });

    it('does NOT derive a name from the default "Select..." placeholder', async () => {
      const s = mount(`<select-ui></select-ui>`);
      await tick();
      expect(s.hasAttribute('aria-label')).toBe(false);
    });

    it('preserves an author-supplied aria-label (override wins over placeholder)', async () => {
      const s = mount(`<select-ui aria-label="Filter by status" placeholder="Status"></select-ui>`);
      await tick();
      expect(s.getAttribute('aria-label')).toBe('Filter by status');
    });

    it('does not set aria-label when a visible label is present', async () => {
      const s = mount(`<select-ui label="Status" placeholder="Pick one"></select-ui>`);
      await tick();
      expect(s.hasAttribute('aria-label')).toBe(false);
    });

    it('does not set aria-label when aria-labelledby is present (e.g. field-ui)', async () => {
      const s = mount(`<select-ui aria-labelledby="ext-label" placeholder="Pick one"></select-ui>`);
      await tick();
      expect(s.hasAttribute('aria-label')).toBe(false);
    });
  });
});
