import { describe, it, expect, beforeEach } from 'vitest';
import { arrowGridNav } from './arrow-grid-nav.js';
import { mountHost, connectTrait, spyEvent, resetDOM } from '../test-helpers.js';

/**
 * Build a row-flat grid with `total` cells laid out in `cols` columns.
 * Returns the host element pre-populated with <button> children.
 */
function flatGrid(cols, total) {
  const host = mountHost('div', { 'data-grid-columns': String(cols) });
  for (let i = 0; i < total; i++) {
    const btn = document.createElement('button');
    btn.textContent = `cell-${i}`;
    btn.setAttribute('data-cell-index', String(i));
    host.appendChild(btn);
  }
  return host;
}

/**
 * Build a row-nested grid: each direct child is a row, each row's children
 * are cells. `rowSpec` is an array of column counts per row.
 */
function nestedGrid(rowSpec) {
  const host = mountHost('div', { 'data-grid-mode': 'row-nested' });
  rowSpec.forEach((cols, rowIdx) => {
    const row = document.createElement('div');
    for (let c = 0; c < cols; c++) {
      const btn = document.createElement('button');
      btn.textContent = `r${rowIdx}c${c}`;
      row.appendChild(btn);
    }
    host.appendChild(row);
  });
  return host;
}

function key(host, k, modifiers = {}) {
  host.dispatchEvent(new KeyboardEvent('keydown', {
    key: k,
    bubbles: true,
    ...modifiers,
  }));
}

describe('arrow-grid-nav', () => {
  beforeEach(resetDOM);

  it('paints active row + col on connect at (0, 0)', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    expect(host.getAttribute('data-grid-active-row')).toBe('0');
    expect(host.getAttribute('data-grid-active-col')).toBe('0');
    const first = host.children[0];
    expect(first.hasAttribute('data-grid-active')).toBe(true);
    expect(first.getAttribute('tabindex')).toBe('0');
  });

  it('sets host tabindex to 0 when not pre-set', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    expect(host.getAttribute('tabindex')).toBe('0');
  });

  it('preserves a pre-set host tabindex', () => {
    const host = flatGrid(7, 42);
    host.setAttribute('tabindex', '5');
    connectTrait(arrowGridNav, host);
    expect(host.getAttribute('tabindex')).toBe('5');
  });

  it('non-active cells receive tabindex="-1"', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    expect(host.children[1].getAttribute('tabindex')).toBe('-1');
    expect(host.children[10].getAttribute('tabindex')).toBe('-1');
  });

  it('ArrowRight increments col within the row', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    key(host, 'ArrowRight');
    expect(host.getAttribute('data-grid-active-col')).toBe('1');
    expect(host.getAttribute('data-grid-active-row')).toBe('0');
    expect(host.children[1].hasAttribute('data-grid-active')).toBe(true);
    expect(host.children[0].hasAttribute('data-grid-active')).toBe(false);
  });

  it('ArrowLeft decrements col', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    key(host, 'ArrowRight');
    key(host, 'ArrowRight');
    key(host, 'ArrowLeft');
    expect(host.getAttribute('data-grid-active-col')).toBe('1');
  });

  it('ArrowDown increments row, keeps col', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    key(host, 'ArrowRight');
    key(host, 'ArrowRight');
    key(host, 'ArrowRight');
    key(host, 'ArrowDown');
    expect(host.getAttribute('data-grid-active-row')).toBe('1');
    expect(host.getAttribute('data-grid-active-col')).toBe('3');
    // index 3 + 7 = 10
    expect(host.children[10].hasAttribute('data-grid-active')).toBe(true);
  });

  it('ArrowUp decrements row', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    key(host, 'ArrowDown');
    key(host, 'ArrowDown');
    key(host, 'ArrowUp');
    expect(host.getAttribute('data-grid-active-row')).toBe('1');
  });

  it('Home moves to col 0 within current row', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    key(host, 'ArrowDown');
    key(host, 'ArrowRight');
    key(host, 'ArrowRight');
    expect(host.getAttribute('data-grid-active-col')).toBe('2');
    key(host, 'Home');
    expect(host.getAttribute('data-grid-active-row')).toBe('1');
    expect(host.getAttribute('data-grid-active-col')).toBe('0');
  });

  it('Ctrl+Home moves to (0, 0)', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    key(host, 'ArrowDown');
    key(host, 'ArrowDown');
    key(host, 'ArrowRight');
    key(host, 'Home', { ctrlKey: true });
    expect(host.getAttribute('data-grid-active-row')).toBe('0');
    expect(host.getAttribute('data-grid-active-col')).toBe('0');
  });

  it('End moves to last col in current row', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    key(host, 'End');
    expect(host.getAttribute('data-grid-active-col')).toBe('6');
    expect(host.getAttribute('data-grid-active-row')).toBe('0');
  });

  it('Ctrl+End moves to (lastRow, lastCol)', () => {
    const host = flatGrid(7, 42); // 6 rows × 7 cols
    connectTrait(arrowGridNav, host);
    key(host, 'End', { ctrlKey: true });
    expect(host.getAttribute('data-grid-active-row')).toBe('5');
    expect(host.getAttribute('data-grid-active-col')).toBe('6');
  });

  it('Meta+End is treated as Ctrl+End on Mac', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    key(host, 'End', { metaKey: true });
    expect(host.getAttribute('data-grid-active-row')).toBe('5');
    expect(host.getAttribute('data-grid-active-col')).toBe('6');
  });

  it('Enter dispatches grid-activate with row, col, element', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    const spy = spyEvent(host, 'grid-activate');
    key(host, 'ArrowRight');
    key(host, 'ArrowDown');
    key(host, 'Enter');
    expect(spy.count).toBe(1);
    expect(spy.last).toMatchObject({ row: 1, col: 1 });
    expect(spy.last.element).toBe(host.children[8]); // row 1 col 1 = index 8
  });

  it('Space dispatches grid-activate', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    const spy = spyEvent(host, 'grid-activate');
    key(host, ' ');
    expect(spy.count).toBe(1);
    expect(spy.last).toMatchObject({ row: 0, col: 0 });
  });

  it('emits grid-edge "left" at column 0', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    const spy = spyEvent(host, 'grid-edge');
    key(host, 'ArrowLeft');
    expect(spy.count).toBe(1);
    expect(spy.last.edge).toBe('left');
  });

  it('emits grid-edge "right" at last column', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    key(host, 'End');
    const spy = spyEvent(host, 'grid-edge');
    key(host, 'ArrowRight');
    expect(spy.count).toBe(1);
    expect(spy.last.edge).toBe('right');
  });

  it('emits grid-edge "top" at row 0', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    const spy = spyEvent(host, 'grid-edge');
    key(host, 'ArrowUp');
    expect(spy.count).toBe(1);
    expect(spy.last.edge).toBe('top');
  });

  it('emits grid-edge "bottom" at last row', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    key(host, 'End', { ctrlKey: true });
    const spy = spyEvent(host, 'grid-edge');
    key(host, 'ArrowDown');
    expect(spy.count).toBe(1);
    expect(spy.last.edge).toBe('bottom');
  });

  it('grid-cell-change fires when active changes', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    const spy = spyEvent(host, 'grid-cell-change');
    key(host, 'ArrowRight');
    key(host, 'ArrowRight');
    expect(spy.count).toBe(2);
    expect(spy.last).toMatchObject({ row: 0, col: 2 });
  });

  it('grid-cell-change does NOT fire when no movement happens at edge', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    const spy = spyEvent(host, 'grid-cell-change');
    key(host, 'ArrowLeft'); // already at col 0
    expect(spy.count).toBe(0);
  });

  it('row-nested mode infers rows from container children', () => {
    const host = nestedGrid([3, 3, 3]); // 3x3 grid
    connectTrait(arrowGridNav, host);
    expect(host.getAttribute('data-grid-active-row')).toBe('0');
    expect(host.getAttribute('data-grid-active-col')).toBe('0');
    key(host, 'ArrowDown');
    expect(host.getAttribute('data-grid-active-row')).toBe('1');
    const row1 = host.children[1];
    expect(row1.children[0].hasAttribute('data-grid-active')).toBe(true);
  });

  it('row-nested mode handles ragged rows by clamping col on row change', () => {
    const host = nestedGrid([5, 2, 4]); // row 0 has 5 cols, row 1 has 2, row 2 has 4
    connectTrait(arrowGridNav, host);
    key(host, 'End'); // col 4 in row 0
    expect(host.getAttribute('data-grid-active-col')).toBe('4');
    key(host, 'ArrowDown'); // row 1 only has 2 cols → clamp to 1
    expect(host.getAttribute('data-grid-active-row')).toBe('1');
    expect(host.getAttribute('data-grid-active-col')).toBe('1');
  });

  it('disabled cells are skipped in row-flat mode', () => {
    const host = flatGrid(7, 42);
    host.children[1].setAttribute('disabled', '');
    connectTrait(arrowGridNav, host);
    key(host, 'ArrowRight');
    // Disabled child #1 is filtered out → row-flat splits the remaining
    // 41 cells; index 1 in the post-filter list is original cell #2.
    expect(host.children[2].hasAttribute('data-grid-active')).toBe(true);
  });

  it('does not steal arrow keys from <input> within a cell', () => {
    const host = mountHost('div', { 'data-grid-columns': '3' });
    for (let i = 0; i < 3; i++) {
      const cell = document.createElement('div');
      const inp = document.createElement('input');
      cell.appendChild(inp);
      host.appendChild(cell);
    }
    connectTrait(arrowGridNav, host);
    const inp = host.querySelector('input');
    inp.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
    // No movement — col remains 0.
    expect(host.getAttribute('data-grid-active-col')).toBe('0');
  });

  it('does not respond to non-handled keys', () => {
    const host = flatGrid(7, 42);
    connectTrait(arrowGridNav, host);
    const spy = spyEvent(host, 'grid-cell-change');
    key(host, 'a');
    key(host, 'Tab');
    key(host, 'Escape');
    expect(spy.count).toBe(0);
  });

  it('MutationObserver re-paints when children are added', async () => {
    const host = flatGrid(7, 7); // 1 row of 7
    connectTrait(arrowGridNav, host);
    key(host, 'End'); // col 6
    expect(host.getAttribute('data-grid-active-col')).toBe('6');
    // Add 7 more children → row 1 exists.
    for (let i = 0; i < 7; i++) {
      const btn = document.createElement('button');
      btn.textContent = `new-${i}`;
      host.appendChild(btn);
    }
    await new Promise((r) => setTimeout(r, 10));
    // Active should still be at (0, 6).
    expect(host.getAttribute('data-grid-active-row')).toBe('0');
    // Now arrow-down should reach the new row.
    key(host, 'ArrowDown');
    expect(host.getAttribute('data-grid-active-row')).toBe('1');
  });

  it('disconnect removes all managed attrs from host and active cell', () => {
    const host = flatGrid(7, 42);
    const inst = connectTrait(arrowGridNav, host);
    key(host, 'ArrowRight');
    inst.disconnect(host);
    expect(host.hasAttribute('data-arrow-grid-nav-active')).toBe(false);
    expect(host.hasAttribute('data-grid-active-row')).toBe(false);
    expect(host.hasAttribute('data-grid-active-col')).toBe(false);
    for (const child of host.children) {
      expect(child.hasAttribute('data-grid-active')).toBe(false);
    }
  });

  it('disconnect removes the keydown listener', () => {
    const host = flatGrid(7, 42);
    const inst = connectTrait(arrowGridNav, host);
    inst.disconnect(host);
    const spy = spyEvent(host, 'grid-cell-change');
    key(host, 'ArrowRight');
    expect(spy.count).toBe(0);
  });

  it('reconnect after disconnect works', () => {
    const host = flatGrid(7, 42);
    const inst1 = connectTrait(arrowGridNav, host);
    inst1.disconnect(host);
    const inst2 = connectTrait(arrowGridNav, host);
    expect(host.getAttribute('data-grid-active-row')).toBe('0');
    expect(host.children[0].hasAttribute('data-grid-active')).toBe(true);
    inst2.disconnect(host);
  });

  it('default mode is row-flat when data-grid-mode missing', () => {
    const host = flatGrid(4, 12);
    connectTrait(arrowGridNav, host);
    key(host, 'ArrowDown');
    expect(host.getAttribute('data-grid-active-row')).toBe('1');
    expect(host.children[4].hasAttribute('data-grid-active')).toBe(true);
  });

  it('missing data-grid-columns falls back to 1 (single column)', () => {
    const host = mountHost('div');
    for (let i = 0; i < 4; i++) {
      const btn = document.createElement('button');
      host.appendChild(btn);
    }
    connectTrait(arrowGridNav, host);
    key(host, 'ArrowDown');
    expect(host.getAttribute('data-grid-active-row')).toBe('1');
    expect(host.getAttribute('data-grid-active-col')).toBe('0');
  });

  it('empty grid does not throw', () => {
    const host = mountHost('div', { 'data-grid-columns': '7' });
    expect(() => connectTrait(arrowGridNav, host)).not.toThrow();
  });
});
