/**
 * table-ui — focused unit tests for the v0.6.18 loading=skeleton-rows
 * behavior change (FB-12 P2 resolution).
 *
 * Pre-v0.6.18: `loading=true` rendered a `<progress-ui>` spinner overlay
 * inside the body via `[data-loading]`. The yaml description always said
 * "Shows a loading overlay AND skeleton rows" but the impl only did the
 * overlay — yaml-vs-impl drift since the initial table cut.
 *
 * v0.6.18 changes the loading branch to render N ghost skeleton rows
 * (N = paginate if set, else 5) inside the [data-body] rowgroup. Header +
 * columns stay intact so layout is preserved. Sets aria-busy="true" on the
 * host. Old `[data-loading]` overlay element is removed if it lingers.
 *
 * Note: table-ui needs columns + data set imperatively (via the .columns /
 * .data properties on the element) — declarative <col-def> children also
 * work but require the col-def element to be registered first. Tests use
 * the imperative path.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import '../../core/element.js';
import './table.js';
import '../skeleton/skeleton.js';

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

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

const COLS = [
  { key: 'id',    label: 'ID' },
  { key: 'name',  label: 'Name' },
  { key: 'email', label: 'Email' },
];
const ROWS = [
  { id: 1, name: 'Alice', email: 'alice@acme.com' },
  { id: 2, name: 'Bob',   email: 'bob@acme.com' },
];

describe('table-ui — v0.6.18 loading=skeleton-rows (FB-12 P2)', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('renders skeleton rows in [data-body] when [loading] is set', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await tick();
    el.setAttribute('loading', '');
    await tick();
    const body = el.querySelector(':scope > [data-body]');
    expect(body).not.toBeNull();
    const skRows = body.querySelectorAll('[data-skeleton-row]');
    expect(skRows.length).toBeGreaterThan(0);
  });

  it('sets aria-busy="true" on the host when [loading]', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await tick();
    el.setAttribute('loading', '');
    await tick();
    expect(el.getAttribute('aria-busy')).toBe('true');
  });

  it('replaces real body rows with skeleton rows when [loading]', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    // table-ui uses requestAnimationFrame in #requestRender(), not microtasks.
    // queueMicrotask-based tick() won't drain RAF callbacks — must await raf().
    // Loop up to 5 RAF cycles in case the initial mount needs multiple renders
    // to settle (columns set → render → data set → render).
    for (let i = 0; i < 5; i++) {
      await raf();
      const body = el.querySelector(':scope > [data-body]');
      if (body && body.children.length >= 2) break;
    }
    const body = el.querySelector(':scope > [data-body]');
    expect(body).not.toBeNull();
    // Real rows present (2)
    const realRows = body.querySelectorAll(':scope > [role="row"]:not([data-skeleton-row])');
    expect(realRows.length).toBe(2);
    el.setAttribute('loading', '');
    await raf();
    // Real rows gone, skeleton rows present
    expect(body.querySelectorAll(':scope > [role="row"]:not([data-skeleton-row])').length).toBe(0);
    expect(body.querySelectorAll(':scope > [data-skeleton-row]').length).toBeGreaterThan(0);
  });

  it('each skeleton row has cell count matching column count', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;  // 3 columns
    el.data = ROWS;
    await tick();
    el.setAttribute('loading', '');
    await tick();
    const skRow = el.querySelector('[data-skeleton-row]');
    expect(skRow.children.length).toBe(3);
  });

  it('each skeleton cell contains a <skeleton-ui> shimmer element', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await tick();
    el.setAttribute('loading', '');
    await tick();
    const skCells = el.querySelectorAll('[data-skeleton-row] > [role="gridcell"]');
    expect(skCells.length).toBeGreaterThan(0);
    for (const cell of skCells) {
      const sk = cell.querySelector('skeleton-ui');
      expect(sk).not.toBeNull();
      expect(sk.tagName.toLowerCase()).toBe('skeleton-ui');
    }
  });

  it('does NOT render a [data-loading] overlay element (old behavior removed)', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await tick();
    el.setAttribute('loading', '');
    await tick();
    // Old impl created <div data-loading> with <progress-ui> child.
    // v0.6.18 does NOT — skeleton rows ARE the loading affordance.
    expect(el.querySelector(':scope > [data-loading]')).toBeNull();
  });

  it('restores real rows + clears aria-busy when loading toggles off', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await tick();
    el.setAttribute('loading', '');
    await tick();
    el.removeAttribute('loading');
    await tick();
    expect(el.getAttribute('aria-busy')).toBeNull();
    expect(el.querySelectorAll('[data-body] > [data-skeleton-row]').length).toBe(0);
    expect(el.querySelectorAll('[data-body] > [role="row"]').length).toBe(2);
  });

  it('skeleton row count tracks paginate when set, capped at 8', async () => {
    const el = mount('<table-ui paginate="3"></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await tick();
    el.setAttribute('loading', '');
    await tick();
    expect(el.querySelectorAll('[data-body] > [data-skeleton-row]').length).toBe(3);
  });

  it('preserves header row when [loading]', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await tick();
    const beforeHeader = el.querySelector(':scope > [data-header]');
    expect(beforeHeader).not.toBeNull();
    el.setAttribute('loading', '');
    await tick();
    const afterHeader = el.querySelector(':scope > [data-header]');
    expect(afterHeader).not.toBeNull();
    // Header cells unchanged in count
    expect(afterHeader.children.length).toBe(3);
  });
});

/**
 * Body-cell wrap default + opt-in paths (PATCH-class behavior change).
 *
 * Default: cells truncate single-line with ellipsis (the CSS rule sets
 * white-space: nowrap; overflow: hidden; text-overflow: ellipsis). Three
 * opt-in surfaces lift the truncation:
 *   - host [wrap] attribute (reflected from the `wrap` prop)
 *   - col.wrap=true in columns[] → renderer sets [data-wrap] on each
 *     body cell in that column
 *   - [data-wrap] directly on a hand-authored <td>
 *
 * Tests lock the attribute contract; CSS-rule application is covered by
 * the source-grep stylistic invariants of table.css.
 */
describe('table-ui — wrap default + opt-in', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('host [wrap] attribute reflects from the wrap property', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await raf();
    expect(el.hasAttribute('wrap')).toBe(false);
    el.wrap = true;
    await raf();
    expect(el.hasAttribute('wrap')).toBe(true);
    el.wrap = false;
    await raf();
    expect(el.hasAttribute('wrap')).toBe(false);
  });

  it('col.wrap=true marks body cells in that column with [data-wrap]', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = [
      { key: 'id',    label: 'ID' },
      { key: 'name',  label: 'Name', wrap: true },
      { key: 'email', label: 'Email' },
    ];
    el.data = ROWS;
    await raf();
    const bodyRows = el.querySelectorAll(':scope > [data-body] > [role="row"]');
    expect(bodyRows.length).toBe(2);
    for (const row of bodyRows) {
      const cells = row.querySelectorAll('[role="gridcell"]');
      expect(cells.length).toBe(3);
      expect(cells[0].hasAttribute('data-wrap')).toBe(false);
      expect(cells[1].hasAttribute('data-wrap')).toBe(true);
      expect(cells[2].hasAttribute('data-wrap')).toBe(false);
    }
  });

  it('omitting col.wrap leaves all body cells without [data-wrap]', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await raf();
    const cells = el.querySelectorAll(':scope > [data-body] [role="gridcell"]');
    expect(cells.length).toBeGreaterThan(0);
    for (const cell of cells) {
      expect(cell.hasAttribute('data-wrap')).toBe(false);
    }
  });
});

describe('table-ui — FB-47 keyboard: Enter activates focused body cell', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('Enter on a focused body cell fires cell-click with the row data', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await raf();

    // ArrowDown from null initialises #focusedCell to {row:0,col:0} then
    // increments to {row:1,col:0} and calls #updateFocus → [data-focused] on
    // the second body row cell.
    el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
    await tick();

    let fired = null;
    el.addEventListener('cell-click', (e) => { fired = e.detail; });

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

    expect(fired).not.toBeNull();
    expect(fired.row).toMatchObject({ id: 2, name: 'Bob' });  // index 1 = second row
  });

  it('Enter on a focused header cell does NOT fire cell-click (sort path only)', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await raf();

    // ArrowUp from null initialises to {row:0,col:0} then decrements to
    // {row:-1,col:0} → header row.
    el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
    await tick();

    let cellClickFired = false;
    el.addEventListener('cell-click', () => { cellClickFired = true; });

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

    // Header Enter triggers sort (or no-op if not sortable), never cell-click.
    expect(cellClickFired).toBe(false);
  });
});

/**
 * Raw mode: visual-only passthrough — consumer owns the body shape.
 *
 * Pre-v0.6.33 (FB-53 §2), `<table-ui raw>` only applied the CSS chrome
 * reset (background/border/radius transparent) but the data lifecycle
 * still ran. With `.data === []` (the natural state when wrapping a
 * consumer-authored native <table>), `#renderOverlays` injected a
 * [data-empty] "No data" placeholder above the consumer's content.
 *
 * v0.6.33 short-circuits `render()` at the top when `this.raw` is set:
 * no header injection, no body reconciliation, no overlays, no
 * aggregation, no pagination.
 */
describe('table-ui — raw mode passthrough (FB-53 §2)', () => {
  beforeEach(() => { document.body.innerHTML = ''; });

  it('raw mode does NOT inject [data-empty] overlay when .data is empty', async () => {
    const el = mount('<table-ui raw><table><tr><td>foo</td></tr></table></table-ui>');
    // No .data set → empty by default. Without the raw early-return,
    // #renderOverlays would create the [data-empty] placeholder.
    await raf();
    expect(el.querySelector(':scope > [data-empty]')).toBeNull();
    // Consumer's native <table> survives untouched.
    expect(el.querySelector(':scope > table')).not.toBeNull();
  });

  it('raw mode does NOT inject [data-header] or [data-body] rowgroups', async () => {
    const el = mount('<table-ui raw><table><tr><td>x</td></tr></table></table-ui>');
    await raf();
    expect(el.querySelector(':scope > [data-header]')).toBeNull();
    expect(el.querySelector(':scope > [data-body]')).toBeNull();
  });

  it('raw mode preserves consumer markup even when .columns + .data are set', async () => {
    const el = mount('<table-ui raw><table id="consumer"><tr><td>consumer-owned</td></tr></table></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await raf();
    // The consumer's native <table> is still the only table-shaped child.
    expect(el.querySelector('#consumer')).not.toBeNull();
    expect(el.querySelectorAll(':scope > [role="rowgroup"]').length).toBe(0);
  });

  it('non-raw mode (default) still injects header + body rowgroups', async () => {
    const el = mount('<table-ui></table-ui>');
    el.columns = COLS;
    el.data = ROWS;
    await raf();
    expect(el.querySelector(':scope > [data-header]')).not.toBeNull();
    expect(el.querySelector(':scope > [data-body]')).not.toBeNull();
  });
});
