/**
 * <tree-ui> behavioral tests.
 *
 * FB-46 (v0.5.19): tree-select detail now forwards ctrlKey/metaKey/shiftKey
 * from the originating MouseEvent or KeyboardEvent. Programmatic select()
 * calls (no event) surface all three as false.
 */

import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import { html, stamp } from '../../core/template.js';

beforeAll(async () => {
  await import('./tree.js');
  await import('../button/button.js');
});

describe('<tree-ui> tree-select forwards modifier keys (FB-46)', () => {
  let host;
  let tree;

  beforeEach(async () => {
    host = document.createElement('div');
    document.body.appendChild(host);

    tree = document.createElement('tree-ui');
    const a = document.createElement('tree-item-ui');
    a.setAttribute('text', 'Alpha');
    a.setAttribute('value', 'a');
    const b = document.createElement('tree-item-ui');
    b.setAttribute('text', 'Beta');
    b.setAttribute('value', 'b');
    tree.appendChild(a);
    tree.appendChild(b);
    host.appendChild(tree);

    // Let connectedCallback + #stamp() settle.
    await new Promise((r) => setTimeout(r, 30));
  });

  afterEach(() => {
    host.remove();
  });

  it('programmatic select() emits ctrlKey/metaKey/shiftKey = false', () => {
    let received = null;
    tree.addEventListener('tree-select', (e) => { received = e.detail; });

    const alpha = tree.querySelector('tree-item-ui[value="a"]');
    tree.select(alpha);

    expect(received).not.toBeNull();
    expect(received.item).toBe(alpha);
    expect(received.text).toBe('Alpha');
    expect(received.value).toBe('a');
    expect(received.ctrlKey).toBe(false);
    expect(received.metaKey).toBe(false);
    expect(received.shiftKey).toBe(false);
  });

  it('select(item, event) forwards Mouse/KeyboardEvent modifier flags into detail', () => {
    let received = null;
    tree.addEventListener('tree-select', (e) => { received = e.detail; });

    const alpha = tree.querySelector('tree-item-ui[value="a"]');
    // Synthesize an event with all three modifiers held.
    const fakeEvent = new MouseEvent('click', {
      ctrlKey: true,
      metaKey: true,
      shiftKey: true,
    });
    tree.select(alpha, fakeEvent);

    expect(received.ctrlKey).toBe(true);
    expect(received.metaKey).toBe(true);
    expect(received.shiftKey).toBe(true);
  });

  it('detail shape stays backward-compatible (existing fields unchanged)', () => {
    let received = null;
    tree.addEventListener('tree-select', (e) => { received = e.detail; });

    const beta = tree.querySelector('tree-item-ui[value="b"]');
    tree.select(beta);

    // Existing consumers destructure { item, text, value } — those keys
    // MUST remain present + carry the expected values.
    const { item, text, value } = received;
    expect(item).toBe(beta);
    expect(text).toBe('Beta');
    expect(value).toBe('b');
  });

  it('partial modifier state passes through (single key held)', () => {
    let received = null;
    tree.addEventListener('tree-select', (e) => { received = e.detail; });

    const alpha = tree.querySelector('tree-item-ui[value="a"]');
    const onlyMeta = new MouseEvent('click', { metaKey: true });
    tree.select(alpha, onlyMeta);

    expect(received.ctrlKey).toBe(false);
    expect(received.metaKey).toBe(true);
    expect(received.shiftKey).toBe(false);
  });
});

describe('<tree-item-ui> caret slot + actions adoption (caret convention + FB-89)', () => {
  let host;
  const settle = () => new Promise((r) => setTimeout(r, 30));

  beforeEach(() => {
    host = document.createElement('div');
    document.body.appendChild(host);
  });
  afterEach(() => host.remove());

  it('auto-stamps a slot="caret" icon (renamed from slot="chevron")', async () => {
    const item = document.createElement('tree-item-ui');
    item.setAttribute('text', 'Colors');
    host.appendChild(item);
    await settle();
    const row = item.querySelector(':scope > [slot="row"]');
    expect(row.querySelector('[slot="caret"]')).toBeTruthy();
    expect(row.querySelector('[slot="chevron"]')).toBeNull();
  });

  it('adopts a declarative slot="actions" child into the stamped row (FB-89)', async () => {
    const item = document.createElement('tree-item-ui');
    item.setAttribute('text', 'Colors');
    const btn = document.createElement('button-ui');
    btn.setAttribute('slot', 'actions');
    btn.setAttribute('icon', 'plus');
    item.appendChild(btn);
    host.appendChild(item);
    await settle();
    const row = item.querySelector(':scope > [slot="row"]');
    // The action button is now INSIDE the row, not a sibling left below it.
    expect(btn.parentElement).toBe(row);
    expect(item.querySelector(':scope > [slot="actions"]')).toBeNull();
  });

  it('adopts a declarative slot="caret" child instead of stamping a default', async () => {
    const item = document.createElement('tree-item-ui');
    item.setAttribute('text', 'Colors');
    const customCaret = document.createElement('icon-ui');
    customCaret.setAttribute('slot', 'caret');
    customCaret.setAttribute('name', 'folder');
    item.appendChild(customCaret);
    host.appendChild(item);
    await settle();
    const row = item.querySelector(':scope > [slot="row"]');
    const carets = row.querySelectorAll('[slot="caret"]');
    expect(carets.length).toBe(1);                    // no double-stamp
    expect(carets[0]).toBe(customCaret);              // consumer's caret used
    expect(carets[0].getAttribute('name')).toBe('folder');
  });
});

describe('<tree-item-ui> ARIA tree containment (FEEDBACK-91)', () => {
  let host;
  const settle = () => new Promise((r) => setTimeout(r, 30));

  beforeEach(() => {
    host = document.createElement('div');
    document.body.appendChild(host);
  });
  afterEach(() => host.remove());

  // Mirrors axe-core's `aria-required-parent` check for `treeitem`: walking up
  // from the treeitem, the first ancestor carrying a *non-generic* role must be
  // `group` or `tree`. Roleless / presentation / display:contents wrapper spans
  // are transparent pass-throughs (exactly the consumer's `.map()` case).
  function requiredParentSatisfied(treeitem, root) {
    let p = treeitem.parentElement;
    while (p && p !== root.parentElement) {
      const role = p.getAttribute('role');
      if (role === 'group' || role === 'tree') return true;
      if (role && role !== 'presentation' && role !== 'none') return false;
      p = p.parentElement;
    }
    return false;
  }

  function buildStaticTree() {
    const tree = document.createElement('tree-ui');
    const parent = document.createElement('tree-item-ui');
    parent.setAttribute('text', 'Colors');
    parent.setAttribute('open', '');
    for (const t of ['Neutral', 'Brand']) {
      const c = document.createElement('tree-item-ui');
      c.setAttribute('text', t);
      parent.appendChild(c);
    }
    tree.appendChild(parent);
    host.appendChild(tree);
    return tree;
  }

  it('roles the host as group and the [slot="row"] as treeitem', async () => {
    const tree = buildStaticTree();
    await settle();
    expect(tree.getAttribute('role')).toBe('tree');
    for (const item of tree.querySelectorAll('tree-item-ui')) {
      expect(item.getAttribute('role')).toBe('group');
      const row = item.querySelector(':scope > [slot="row"]');
      expect(row.getAttribute('role')).toBe('treeitem');
      expect(row.getAttribute('tabindex')).toBe('0');
    }
  });

  it('every treeitem row has a group/tree required parent (static markup)', async () => {
    const tree = buildStaticTree();
    await settle();
    const rows = tree.querySelectorAll('[role="treeitem"]');
    expect(rows.length).toBe(3); // parent + 2 children — was the axe-critical case
    for (const row of rows) expect(requiredParentSatisfied(row, tree)).toBe(true);
  });

  it('nested treeitems satisfy required-parent when rendered via the template engine .map() (consumer repro)', async () => {
    const ITEMS = [{ id: 'neutral', text: 'Neutral' }, { id: 'brand', text: 'Brand' }];
    stamp(html`
      <tree-ui>
        <tree-item-ui text="Colors" open>
          ${ITEMS.map((i) => html`<tree-item-ui .text=${i.text} .value=${i.id}></tree-item-ui>`)}
        </tree-item-ui>
      </tree-ui>
    `, host);
    await settle();

    const tree = host.querySelector('tree-ui');
    const rows = tree.querySelectorAll('[role="treeitem"]');
    expect(rows.length).toBe(3);
    for (const row of rows) expect(requiredParentSatisfied(row, tree)).toBe(true);

    // The nested items sit BEHIND the template engine's display:contents wrapper
    // span — proving the fix works without re-homing engine-owned nodes.
    const child = tree.querySelector('tree-item-ui[value="neutral"]');
    expect(child.parentElement.style.display).toBe('contents');
    expect(child.getAttribute('role')).toBe('group');
  });

  it('aria-expanded lives on the row (treeitem), not the host group', async () => {
    const tree = buildStaticTree();
    await settle();
    const parent = tree.querySelector('tree-item-ui');
    const parentRow = parent.querySelector(':scope > [slot="row"]');
    expect(parentRow.getAttribute('aria-expanded')).toBe('true');
    expect(parent.hasAttribute('aria-expanded')).toBe(false); // not stranded on the host

    const leaf = tree.querySelectorAll('tree-item-ui')[1]; // a child leaf
    const leafRow = leaf.querySelector(':scope > [slot="row"]');
    expect(leafRow.hasAttribute('aria-expanded')).toBe(false); // leaves carry none
  });

  it('aria-selected reflects onto the selected row only', async () => {
    const tree = buildStaticTree();
    await settle();
    const [parent, neutral] = tree.querySelectorAll('tree-item-ui');
    tree.select(neutral);
    await settle();
    expect(neutral.querySelector(':scope > [slot="row"]').getAttribute('aria-selected')).toBe('true');
    expect(parent.querySelector(':scope > [slot="row"]').hasAttribute('aria-selected')).toBe(false);
  });
});

describe('<tree-item-ui> adopts interpolated [slot="actions"] into the row (FEEDBACK-96)', () => {
  let host;
  const settle = () => new Promise((r) => setTimeout(r, 40));
  beforeEach(() => { host = document.createElement('div'); document.body.appendChild(host); });
  afterEach(() => host.remove());

  it('moves a `.map()`-rendered actions button into the treeitem row (not a stray child of the group)', async () => {
    const PALS = [{ id: 'n', name: 'Neutral' }, { id: 'b', name: 'Brand' }];
    stamp(html`
      <tree-ui>
        <tree-item-ui text="Colors" value="colors" open>
          ${html`<button-ui slot="actions" icon="plus" title="Add"></button-ui>`}
          ${PALS.map((p) => html`<tree-item-ui text="${p.name}" value="${p.id}"></tree-item-ui>`)}
        </tree-item-ui>
      </tree-ui>
    `, host);
    await settle();

    const colors = host.querySelector('tree-item-ui[value="colors"]');
    const row = colors.querySelector(':scope > [slot="row"]');
    const btn = colors.querySelector('button-ui[slot="actions"]');
    expect(btn).not.toBeNull();
    // The fix: the interpolated button (wrapped in a display:contents span) is
    // adopted INTO the row. Pre-fix, #stamp's `:scope >` query missed it (the
    // button arrives after #stamp, inside a wrapper span) and it stayed a bare
    // child of the group/tree → axe aria-required-children.
    expect(btn.parentElement).toBe(row);
    // Auto-stamped empty placeholder dropped — exactly one actions slot in the row.
    expect(row.querySelectorAll(':scope > [slot="actions"]').length).toBe(1);
    // Nested items NOT mis-adopted as actions (the tree-item-ui boundary holds).
    expect(host.querySelectorAll('tree-item-ui').length).toBe(3);
  });

  it('is idempotent — no double-adopt across settles', async () => {
    stamp(html`
      <tree-ui>
        <tree-item-ui text="Colors" value="colors" open>
          ${html`<button-ui slot="actions" icon="plus"></button-ui>`}
        </tree-item-ui>
      </tree-ui>
    `, host);
    await settle();
    await settle();
    const row = host.querySelector('tree-item-ui[value="colors"] > [slot="row"]');
    expect(row.querySelectorAll(':scope > [slot="actions"]').length).toBe(1);
  });
});
