import { describe, it, expect, beforeEach, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
import * as React from 'react';

import { DataTable } from '../DataTable.js';
import type { ColumnDef } from '@tanstack/react-table';

interface Person {
  id: string;
  name: string;
  age: number;
}

const DATA: Person[] = [
  { id: '1', name: 'Ada', age: 36 },
  { id: '2', name: 'Bob', age: 28 },
  { id: '3', name: 'Cleo', age: 41 },
];

const COLUMNS: ColumnDef<Person, unknown>[] = [
  { id: 'name', accessorKey: 'name', header: 'Name' },
  { id: 'age', accessorKey: 'age', header: 'Age' },
];

const getRowId = (row: Person) => row.id;

beforeEach(() => {
  cleanup();
});

describe('DataTable — rendering', () => {
  it('renders one row per datum plus a header row', () => {
    render(<DataTable columns={COLUMNS} data={DATA} getRowId={getRowId} enableFilter={false} />);
    expect(screen.getByRole('table')).toBeTruthy();
    expect(screen.getByText('Ada')).toBeTruthy();
    expect(screen.getByText('Cleo')).toBeTruthy();
  });

  it('shows the empty state when there are no rows', () => {
    render(<DataTable columns={COLUMNS} data={[]} getRowId={getRowId} emptyState="Nothing here" />);
    expect(screen.getByText('Nothing here')).toBeTruthy();
  });

  it('applies compact density padding classes', () => {
    render(
      <DataTable
        columns={COLUMNS}
        data={DATA}
        getRowId={getRowId}
        density="compact"
        enableFilter={false}
      />
    );
    const cell = screen.getByText('Ada');
    expect(cell.closest('td')?.className).toContain('text-xs');
  });
});

describe('DataTable — sorting', () => {
  it('sorts rows when a header is clicked and sets aria-sort', () => {
    render(<DataTable columns={COLUMNS} data={DATA} getRowId={getRowId} enableFilter={false} />);
    const ageHeaderBtn = screen.getByRole('button', { name: 'Age' });
    fireEvent.click(ageHeaderBtn);
    const headerCell = ageHeaderBtn.closest('th')!;
    // TanStack sorts numeric columns descending-first by default.
    expect(headerCell.getAttribute('aria-sort')).toBe('descending');

    const bodyRows = screen.getAllByRole('row').slice(1); // drop header
    const firstDataCells = within(bodyRows[0]!).getAllByRole('cell');
    expect(firstDataCells[0]!.textContent).toBe('Cleo'); // oldest first (age 41)

    // A second click flips to ascending.
    fireEvent.click(ageHeaderBtn);
    expect(headerCell.getAttribute('aria-sort')).toBe('ascending');
  });
});

describe('DataTable — global filter', () => {
  it('narrows rows by the global filter input', () => {
    render(<DataTable columns={COLUMNS} data={DATA} getRowId={getRowId} />);
    fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'cle' } });
    expect(screen.getByText('Cleo')).toBeTruthy();
    expect(screen.queryByText('Ada')).toBeNull();
  });
});

describe('DataTable — pagination', () => {
  const many: Person[] = Array.from({ length: 25 }, (_, i) => ({
    id: String(i),
    name: `P${i}`,
    age: 20 + i,
  }));

  it('paginates and advances pages', () => {
    render(
      <DataTable
        columns={COLUMNS}
        data={many}
        getRowId={getRowId}
        pageSize={10}
        enableFilter={false}
      />
    );
    expect(screen.getByText('P0')).toBeTruthy();
    expect(screen.queryByText('P10')).toBeNull();
    fireEvent.click(screen.getByRole('button', { name: 'Next' }));
    expect(screen.getByText('P10')).toBeTruthy();
    expect(screen.queryByText('P0')).toBeNull();
  });
});

describe('DataTable — row selection + bulk actions', () => {
  it('selects rows and renders a bulk-action bar', () => {
    const onBulk = vi.fn();
    render(
      <DataTable
        columns={COLUMNS}
        data={DATA}
        getRowId={getRowId}
        enableRowSelection
        enableFilter={false}
        renderBulkActions={(selected, clear) => (
          <button
            type="button"
            onClick={() => {
              onBulk(selected.length);
              clear();
            }}
          >
            Delete
          </button>
        )}
      />
    );
    expect(screen.queryByTestId('bl-bulk-actions')).toBeNull();
    fireEvent.click(screen.getByLabelText('Select row 1'));
    const bar = screen.getByTestId('bl-bulk-actions');
    expect(within(bar).getByText('1 selected')).toBeTruthy();
    fireEvent.click(within(bar).getByText('Delete'));
    expect(onBulk).toHaveBeenCalledWith(1);
    expect(screen.queryByTestId('bl-bulk-actions')).toBeNull(); // cleared
  });

  it('select-all toggles every row', () => {
    render(
      <DataTable
        columns={COLUMNS}
        data={DATA}
        getRowId={getRowId}
        enableRowSelection
        enableFilter={false}
      />
    );
    fireEvent.click(screen.getByLabelText('Select all rows'));
    expect(within(screen.getByTestId('bl-bulk-actions')).getByText('3 selected')).toBeTruthy();
  });
});

describe('DataTable — column resizing', () => {
  it('exposes a resize separator that updates column width on drag', () => {
    render(
      <DataTable
        columns={COLUMNS}
        data={DATA}
        getRowId={getRowId}
        enableColumnResizing
        enableFilter={false}
      />
    );
    const nameHeader = screen.getByRole('button', { name: 'Name' }).closest('th') as HTMLElement;
    const startWidth = nameHeader.style.width;
    const handle = screen.getByLabelText('Resize name');
    fireEvent.mouseDown(handle, { clientX: 100 });
    fireEvent.mouseMove(document, { clientX: 180 });
    fireEvent.mouseUp(document);
    expect(nameHeader.style.width).not.toBe(startWidth);
  });
});

describe('DataTable — column reordering', () => {
  it('reorders columns via header drag-and-drop', () => {
    render(
      <DataTable
        columns={COLUMNS}
        data={DATA}
        getRowId={getRowId}
        enableColumnReorder
        enableFilter={false}
      />
    );
    const headerRow = screen.getAllByRole('row')[0]!;
    const headerText = () =>
      within(headerRow)
        .getAllByRole('columnheader')
        .map(th => th.textContent);
    expect(headerText()[0]).toContain('Name');

    const nameTh = screen.getByRole('button', { name: 'Name' }).closest('th')!;
    const ageTh = screen.getByRole('button', { name: 'Age' }).closest('th')!;
    fireEvent.dragStart(nameTh);
    fireEvent.dragOver(ageTh);
    fireEvent.drop(ageTh);
    expect(headerText()[0]).toContain('Age');
  });
});

describe('DataTable — column pinning', () => {
  it('pins a column to the left via the header toggle', () => {
    render(
      <DataTable
        columns={COLUMNS}
        data={DATA}
        getRowId={getRowId}
        enableColumnPinning
        enableFilter={false}
      />
    );
    const pinBtn = screen.getByRole('button', { name: 'Pin name left' });
    fireEvent.click(pinBtn);
    expect(screen.getByRole('button', { name: 'Unpin name' })).toBeTruthy();
  });
});

describe('DataTable — virtualised', () => {
  it('renders far fewer than 10k DOM rows', () => {
    // happy-dom performs no layout, so give the scroll container a measured
    // size + a ResizeObserver so TanStack Virtual can compute visible rows.
    class RO {
      observe() {}
      unobserve() {}
      disconnect() {}
    }
    vi.stubGlobal('ResizeObserver', RO);
    vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
      width: 600,
      height: 400,
      top: 0,
      left: 0,
      right: 600,
      bottom: 400,
      x: 0,
      y: 0,
      toJSON() {},
    } as DOMRect);

    const big: Person[] = Array.from({ length: 10000 }, (_, i) => ({
      id: String(i),
      name: `Row ${i}`,
      age: i,
    }));
    render(
      <DataTable
        columns={COLUMNS}
        data={big}
        getRowId={getRowId}
        virtualized
        enableFilter={false}
        maxBodyHeight={400}
        rowHeight={40}
      />
    );
    // Virtualisation engaged: the body reserves a full-height spacer
    // (10000 × 40px) instead of laying out every row.
    const tbody = document.querySelector('tbody') as HTMLElement;
    expect(tbody.style.height).toBe(`${10000 * 40}px`);
    // And the DOM holds far fewer than 10k rows.
    const bodyRows = document.querySelectorAll('tbody tr[data-index]');
    expect(bodyRows.length).toBeLessThan(200);
  });
});
