import { fireEvent, render, within } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { describe, expect, test, vi } from 'vitest';
import { h, ref, nextTick } from 'vue';
import type { ColumnDef } from '@aleph-alpha/ui-library';
import UiDataTable from '../UiDataTable.vue';
import UiDataTableColumnFilter from '../UiDataTableColumnFilter.vue';
import UiDataTableColumnHeader from '../UiDataTableColumnHeader.vue';
import { UiCheckbox } from '@/components/UiCheckbox';

interface TestData {
  id: string;
  name: string;
  amount: number;
}

const testData: TestData[] = [
  { id: '1', name: 'Alice', amount: 100 },
  { id: '2', name: 'Bob', amount: 200 },
  { id: '3', name: 'Charlie', amount: 150 },
];

const basicColumns: ColumnDef<TestData>[] = [
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'amount', header: 'Amount' },
];

const sortableColumns: ColumnDef<TestData>[] = [
  {
    accessorKey: 'name',
    header: ({ column }) =>
      h(UiDataTableColumnHeader, { column, title: 'Name' }),
  },
  {
    accessorKey: 'amount',
    header: ({ column }) =>
      h(UiDataTableColumnHeader, { column, title: 'Amount' }),
  },
];

const sizedColumns: ColumnDef<TestData>[] = [
  { accessorKey: 'name', header: 'Name', size: 200 },
  { accessorKey: 'amount', header: 'Amount' },
];

const selectableColumns: ColumnDef<TestData>[] = [
  {
    id: 'select',
    header: ({ table }) =>
      h(UiCheckbox, {
        modelValue:
          table.getIsAllPageRowsSelected() ||
          (table.getIsSomePageRowsSelected() && 'indeterminate'),
        'onUpdate:modelValue': (value: boolean) =>
          table.toggleAllPageRowsSelected(!!value),
        'aria-label': 'Select all',
        name: 'select-all',
      }),
    cell: ({ row }) =>
      h(UiCheckbox, {
        modelValue: row.getIsSelected(),
        'onUpdate:modelValue': (value: boolean) => row.toggleSelected(!!value),
        'aria-label': `Select row ${row.original.name}`,
        name: `select-${row.id}`,
      }),
    enableSorting: false,
    enableHiding: false,
    size: 40,
  },
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'amount', header: 'Amount' },
];

const expandableColumns: ColumnDef<TestData>[] = [
  {
    id: 'expand',
    header: 'Expand',
    cell: ({ row }) =>
      h(
        'button',
        {
          type: 'button',
          'aria-label': `Toggle ${row.original.name}`,
          onClick: () => row.toggleExpanded(),
        },
        'Toggle',
      ),
  },
  ...basicColumns,
];

describe('UiDataTable', () => {
  describe('Rendering', () => {
    test('renders table with headers and data', () => {
      const { getByRole, getAllByRole } = render(UiDataTable, {
        props: { columns: basicColumns, data: testData },
      });

      expect(getByRole('table')).toBeInTheDocument();
      expect(getByRole('columnheader', { name: 'Name' })).toBeInTheDocument();
      expect(getByRole('columnheader', { name: 'Amount' })).toBeInTheDocument();

      const rows = getAllByRole('row');
      // 1 header row + 3 data rows
      expect(rows).toHaveLength(4);

      // Verify data is rendered
      const table = getByRole('table');
      expect(within(table).getByText('Alice')).toBeInTheDocument();
      expect(within(table).getByText('100')).toBeInTheDocument();
    });

    test('renders default empty state when data is empty', () => {
      const { getByText } = render(UiDataTable, {
        props: { columns: basicColumns, data: [] },
      });

      expect(getByText('No results.')).toBeInTheDocument();
    });

    test('renders custom emptyText when provided', () => {
      const { getByText, queryByText } = render(UiDataTable, {
        props: { columns: basicColumns, data: [], emptyText: 'No items found' },
      });

      expect(getByText('No items found')).toBeInTheDocument();
      expect(queryByText('No results.')).not.toBeInTheDocument();
    });

    test('renders custom empty state via #empty slot', () => {
      const { getByText } = render(UiDataTable, {
        props: { columns: basicColumns, data: [] },
        slots: { empty: '<div>Custom empty content</div>' },
      });

      expect(getByText('Custom empty content')).toBeInTheDocument();
    });

    test('uses muted-subtle row hover fill without a table background', () => {
      const { container, getAllByRole } = render(UiDataTable, {
        props: { columns: basicColumns, data: testData },
      });

      const wrapper = container.firstElementChild;
      const firstDataRow = getAllByRole('row')[1];

      expect(wrapper).not.toHaveClass('bg-background-surface-muted-subtle');
      expect(firstDataRow).toHaveClass(
        'hover:bg-background-surface-muted-subtle',
      );
    });

    test('applies tableMinHeight style when provided', () => {
      const { container } = render(UiDataTable, {
        props: {
          columns: basicColumns,
          data: testData,
          tableMinHeight: '400px',
        },
      });

      const wrapper = container.firstElementChild;
      expect(wrapper).toHaveStyle({ minHeight: '400px' });
    });
  });

  describe('Column Sizing', () => {
    test('applies custom width to columns with size defined', () => {
      const { container } = render(UiDataTable, {
        props: { columns: sizedColumns, data: testData },
      });

      const headers = container.querySelectorAll('th');
      expect(headers[0]).toHaveStyle({ width: '200px' });
      // Only verify that the sized column applies the expected width
    });
  });

  describe('Column Dragging', () => {
    test('reorders columns when a header is dragged onto another header', async () => {
      const onColumnOrderChange = vi.fn();
      const { container, getAllByRole } = render(UiDataTable, {
        props: {
          columns: basicColumns,
          data: testData,
          draggableColumns: true,
          onColumnOrderChange,
        },
      });

      let headers = getAllByRole('columnheader');
      const dataTransfer = {
        effectAllowed: '',
        dropEffect: '',
        setData: vi.fn(),
        getData: vi.fn(() => 'name'),
        setDragImage: vi.fn(),
      };

      await fireEvent.dragStart(headers[0], { dataTransfer });
      expect(headers[0]).not.toHaveAttribute('aria-grabbed');
      expect(headers[0]).toHaveClass('border-border-surface-focus-ring');
      expect(headers[0]).not.toHaveClass('bg-background-accent-soft');
      expect(dataTransfer.setDragImage).toHaveBeenCalledWith(
        expect.objectContaining({
          className: expect.stringContaining(
            'border-border-surface-focus-ring',
          ),
        }),
        expect.any(Number),
        expect.any(Number),
      );
      expect(dataTransfer.setDragImage).toHaveBeenCalledWith(
        expect.objectContaining({
          className: expect.stringContaining('bg-background-accent-soft/10'),
        }),
        expect.any(Number),
        expect.any(Number),
      );

      await fireEvent.dragOver(headers[1], {
        dataTransfer,
        clientX: 9999,
      });
      await nextTick();

      expect(
        container.querySelector('[data-column-drop-indicator]'),
      ).toBeInTheDocument();
      expect(getAllByRole('cell')[0]).not.toHaveClass(
        'bg-background-accent-soft',
      );
      expect(
        container.querySelector('[data-column-drop-indicator]'),
      ).toHaveClass('bg-accent-default');
      expect(
        container.querySelector('[data-column-drop-indicator]'),
      ).not.toHaveClass('transition-[left,top,height,opacity,transform]');

      await fireEvent.drop(headers[1], {
        dataTransfer,
        clientX: 9999,
      });
      await nextTick();

      headers = getAllByRole('columnheader');
      expect(headers.map((header) => header.textContent)).toEqual([
        'Amount',
        'Name',
      ]);
      expect(onColumnOrderChange).toHaveBeenCalledWith(['amount', 'name']);
    });

    test('reorders columns with arrow keys when a draggable header is focused', async () => {
      const user = userEvent.setup();
      const onColumnOrderChange = vi.fn();
      const { getAllByRole } = render(UiDataTable, {
        props: {
          columns: basicColumns,
          data: testData,
          draggableColumns: true,
          onColumnOrderChange,
        },
      });

      let headers = getAllByRole('columnheader');
      headers[0].focus();

      await user.keyboard('{ArrowRight}');
      await nextTick();

      headers = getAllByRole('columnheader');
      expect(headers.map((header) => header.textContent)).toEqual([
        'Amount',
        'Name',
      ]);
      expect(onColumnOrderChange).toHaveBeenCalledWith(['amount', 'name']);
    });

    test('keeps keyboard reordering working after moving right once', async () => {
      const user = userEvent.setup();
      const { getAllByRole } = render(UiDataTable, {
        props: {
          columns: basicColumns,
          data: testData,
          draggableColumns: true,
        },
      });

      let headers = getAllByRole('columnheader');
      headers[0].focus();

      await user.keyboard('{ArrowRight}');
      await nextTick();

      headers = getAllByRole('columnheader');
      expect(headers.map((header) => header.textContent)).toEqual([
        'Amount',
        'Name',
      ]);

      await user.keyboard('{ArrowLeft}');
      await nextTick();

      headers = getAllByRole('columnheader');
      expect(headers.map((header) => header.textContent)).toEqual([
        'Name',
        'Amount',
      ]);
    });

    test('shows a full-column focus indicator when tabbing to a header', async () => {
      const user = userEvent.setup();
      const { container } = render(UiDataTable, {
        props: {
          columns: basicColumns,
          data: testData,
          draggableColumns: true,
        },
      });

      await user.tab();

      expect(
        container.querySelector('[data-column-focus-indicator]'),
      ).toBeInTheDocument();
    });

    test('clears the full-column focus indicator on pointer actions', async () => {
      const user = userEvent.setup();
      const { container, getAllByRole } = render(UiDataTable, {
        props: {
          columns: basicColumns,
          data: testData,
          draggableColumns: true,
        },
      });

      await user.tab();

      expect(
        container.querySelector('[data-column-focus-indicator]'),
      ).toBeInTheDocument();

      await user.click(getAllByRole('cell')[0]);

      expect(
        container.querySelector('[data-column-focus-indicator]'),
      ).not.toBeInTheDocument();
    });

    test('starts column dragging from body cells', async () => {
      const { container, getAllByRole } = render(UiDataTable, {
        props: {
          columns: basicColumns,
          data: testData,
          draggableColumns: true,
        },
      });
      const cell = getAllByRole('cell')[0] as HTMLElement;
      const tableElement = container.querySelector(
        '[data-slot="table"]',
      ) as HTMLElement;
      cell.getBoundingClientRect = vi.fn(
        () =>
          ({
            left: 10,
            top: 20,
            width: 100,
            height: 30,
            right: 110,
            bottom: 50,
            x: 10,
            y: 20,
            toJSON: vi.fn(),
          }) as DOMRect,
      );
      tableElement.getBoundingClientRect = vi.fn(
        () =>
          ({
            left: 0,
            top: 5,
            width: 200,
            height: 120,
            right: 200,
            bottom: 125,
            x: 0,
            y: 5,
            toJSON: vi.fn(),
          }) as DOMRect,
      );

      const dataTransfer = {
        effectAllowed: '',
        dropEffect: '',
        setData: vi.fn(),
        getData: vi.fn(() => 'name'),
        setDragImage: vi.fn(),
      };
      const dragStartEvent = new Event('dragstart', {
        bubbles: true,
        cancelable: true,
      });
      Object.defineProperty(dragStartEvent, 'dataTransfer', {
        value: dataTransfer,
      });
      Object.defineProperty(dragStartEvent, 'clientX', { value: 22 });
      Object.defineProperty(dragStartEvent, 'clientY', { value: 29 });

      await fireEvent(cell, dragStartEvent);

      expect(dataTransfer.setData).toHaveBeenCalledWith('text/plain', 'name');
      expect(dataTransfer.setDragImage.mock.calls[0]?.[0]).toHaveClass(
        'bg-background-accent-soft/10',
      );
      expect(dataTransfer.setDragImage).toHaveBeenCalledWith(
        expect.any(HTMLElement),
        12,
        24,
      );
    });
  });

  describe('Column Visibility', () => {
    test('toggles column visibility from column filter menu', async () => {
      const user = userEvent.setup();
      const tableRef = ref<InstanceType<typeof UiDataTable>>();

      const { getByRole, queryByRole } = render({
        components: { UiDataTable, UiDataTableColumnFilter },
        template: `
          <div>
            <UiDataTableColumnFilter v-if="tableRef?.table" :table="tableRef.table" />
            <UiDataTable ref="tableRef" :columns="columns" :data="data" />
          </div>
        `,
        setup() {
          return { tableRef, columns: basicColumns, data: testData };
        },
      });

      await nextTick();
      await user.click(getByRole('button', { name: 'Toggle columns' }));
      await user.click(getByRole('menuitemcheckbox', { name: 'Amount' }));
      await nextTick();

      expect(
        queryByRole('columnheader', { name: 'Amount' }),
      ).not.toBeInTheDocument();
      expect(getByRole('columnheader', { name: 'Name' })).toBeInTheDocument();
    });
  });

  describe('Sorting', () => {
    test('sorts ascending on first click', async () => {
      const user = userEvent.setup();
      const { getByRole, getAllByRole } = render(UiDataTable, {
        props: { columns: sortableColumns, data: testData },
      });

      // Click the Name column header button to sort ascending
      await user.click(getByRole('button', { name: /Name/i }));

      const cells = getAllByRole('cell');
      // First data row should be Alice (alphabetically first)
      expect(cells[0]).toHaveTextContent('Alice');
    });

    test('sorts descending on second click', async () => {
      const user = userEvent.setup();
      const { getByRole, getAllByRole } = render(UiDataTable, {
        props: { columns: sortableColumns, data: testData },
      });

      // First click: ascending
      await user.click(getByRole('button', { name: /Name/i }));
      // Second click: descending
      await user.click(getByRole('button', { name: /Name/i }));

      const cells = getAllByRole('cell');
      // First data row should be Charlie (alphabetically last)
      expect(cells[0]).toHaveTextContent('Charlie');
    });

    test('clears sorting on third click', async () => {
      const user = userEvent.setup();
      const { getByRole, getAllByRole } = render(UiDataTable, {
        props: { columns: sortableColumns, data: testData },
      });

      const initialCells = getAllByRole('cell');
      const initialFirst = initialCells[0].textContent;

      const headerButton = getByRole('button', { name: /Name/i });
      // First click: ascending
      await user.click(headerButton);
      // Second click: descending
      await user.click(headerButton);
      // Third click: clear sorting (back to original order)
      await user.click(headerButton);

      const finalCells = getAllByRole('cell');
      expect(finalCells[0]).toHaveTextContent(initialFirst ?? '');
    });
  });

  describe('Row Click', () => {
    test('calls onRowClick with row data when a row is clicked', async () => {
      const onRowClick = vi.fn();
      const { getByText } = render(UiDataTable, {
        props: { columns: basicColumns, data: testData, onRowClick },
      });

      // Click cell content - handler is on <td>, event bubbles from text
      await fireEvent.click(getByText('Alice'));

      expect(onRowClick).toHaveBeenCalledWith(testData[0]);
    });

    test('adds cursor-pointer class when onRowClick is provided', () => {
      const { container } = render(UiDataTable, {
        props: {
          columns: basicColumns,
          data: testData,
          onRowClick: () => undefined,
        },
      });

      const dataRows = container.querySelectorAll('tbody tr');
      expect(dataRows[0]).toHaveClass('cursor-pointer');
    });

    test('does not add cursor-pointer when onRowClick is not provided', () => {
      const { container } = render(UiDataTable, {
        props: { columns: basicColumns, data: testData },
      });

      const dataRows = container.querySelectorAll('tbody tr');
      expect(dataRows[0]).not.toHaveClass('cursor-pointer');
    });

    test('does not fire onRowClick when clicking interactive elements inside a row', async () => {
      const user = userEvent.setup();
      const onRowClick = vi.fn();
      const { getByLabelText } = render({
        components: { UiDataTable },
        template:
          '<UiDataTable :columns="columns" :data="data" :on-row-click="onRowClick" />',
        setup() {
          return { columns: selectableColumns, data: testData, onRowClick };
        },
      });

      await nextTick();

      // Click the checkbox (interactive element) - should NOT trigger onRowClick
      await user.click(getByLabelText('Select row Alice'));
      expect(onRowClick).not.toHaveBeenCalled();
    });

    test('activates onRowClick via keyboard on cell', async () => {
      const onRowClick = vi.fn();
      const { getByText } = render(UiDataTable, {
        props: { columns: basicColumns, data: testData, onRowClick },
      });

      const aliceCell = getByText('Alice').closest('td');
      if (!aliceCell) {
        throw new Error('Expected Alice cell to be rendered');
      }

      await fireEvent.keyDown(aliceCell, {
        key: 'Enter',
      });

      expect(onRowClick).toHaveBeenCalledWith(testData[0]);
    });
  });

  describe('Expandable Rows', () => {
    test('renders expanded content in a separate table row', async () => {
      const user = userEvent.setup();
      const { container, getAllByRole, getByLabelText, getByText } = render({
        components: { UiDataTable },
        template: `
          <UiDataTable
            :columns="columns"
            :data="data"
            :get-row-can-expand="() => true"
          >
            <template #expanded-row="{ row }">
              <div>Details for {{ row.original.name }}</div>
            </template>
          </UiDataTable>
        `,
        setup() {
          return { columns: expandableColumns, data: testData };
        },
      });

      expect(getAllByRole('row')).toHaveLength(4);

      await user.click(getByLabelText('Toggle Alice'));
      await nextTick();

      expect(getAllByRole('row')).toHaveLength(5);
      expect(getByText('Details for Alice')).toBeInTheDocument();

      const bodyRows = container.querySelectorAll('tbody tr');
      expect(bodyRows[1]).toHaveTextContent('Details for Alice');
      expect(bodyRows[1].querySelector('td')).toHaveAttribute('colspan', '3');
    });
  });

  describe('Filtering', () => {
    test('filters rows when global filter is applied via table instance', async () => {
      const tableRef = ref<InstanceType<typeof UiDataTable>>();

      const { getAllByRole } = render({
        components: { UiDataTable },
        template:
          '<UiDataTable ref="tableRef" :columns="columns" :data="data" />',
        setup() {
          return { tableRef, columns: basicColumns, data: testData };
        },
      });

      // Wait for component to mount
      await nextTick();

      // Apply global filter via exposed table instance
      tableRef.value?.table.setGlobalFilter('Alice');
      await nextTick();

      const rows = getAllByRole('row');
      // 1 header + 1 filtered data row
      expect(rows).toHaveLength(2);
    });

    test('shows empty state when filter matches no rows', async () => {
      const tableRef = ref<InstanceType<typeof UiDataTable>>();

      const { getByText } = render({
        components: { UiDataTable },
        template:
          '<UiDataTable ref="tableRef" :columns="columns" :data="data" />',
        setup() {
          return { tableRef, columns: basicColumns, data: testData };
        },
      });

      await nextTick();
      tableRef.value?.table.setGlobalFilter('NonExistent');
      await nextTick();

      expect(getByText('No results.')).toBeInTheDocument();
    });
  });

  describe('Row Selection', () => {
    test('toggles individual row selection when checkbox is clicked', async () => {
      const user = userEvent.setup();
      const tableRef = ref<InstanceType<typeof UiDataTable>>();

      const { getByLabelText } = render({
        components: { UiDataTable },
        template:
          '<UiDataTable ref="tableRef" :columns="columns" :data="data" />',
        setup() {
          return { tableRef, columns: selectableColumns, data: testData };
        },
      });

      await nextTick();

      // Select row
      const checkbox = getByLabelText('Select row Alice');
      await user.click(checkbox);
      await nextTick();

      let selectedRows = tableRef.value?.table.getSelectedRowModel().rows;
      expect(selectedRows).toHaveLength(1);
      expect(selectedRows?.[0].original.name).toBe('Alice');

      // Deselect row
      await user.click(checkbox);
      await nextTick();

      selectedRows = tableRef.value?.table.getSelectedRowModel().rows;
      expect(selectedRows).toHaveLength(0);
    });

    test('selects multiple rows independently', async () => {
      const user = userEvent.setup();
      const tableRef = ref<InstanceType<typeof UiDataTable>>();

      const { getByLabelText } = render({
        components: { UiDataTable },
        template:
          '<UiDataTable ref="tableRef" :columns="columns" :data="data" />',
        setup() {
          return { tableRef, columns: selectableColumns, data: testData };
        },
      });

      await nextTick();

      // Select Alice and Charlie
      await user.click(getByLabelText('Select row Alice'));
      await user.click(getByLabelText('Select row Charlie'));
      await nextTick();

      const selectedRows = tableRef.value?.table.getSelectedRowModel().rows;
      expect(selectedRows).toHaveLength(2);
      expect(selectedRows?.map((r) => r.original.name)).toEqual([
        'Alice',
        'Charlie',
      ]);
    });

    test('toggles all rows when select all checkbox is clicked', async () => {
      const user = userEvent.setup();
      const tableRef = ref<InstanceType<typeof UiDataTable>>();

      const { getByLabelText } = render({
        components: { UiDataTable },
        template:
          '<UiDataTable ref="tableRef" :columns="columns" :data="data" />',
        setup() {
          return { tableRef, columns: selectableColumns, data: testData };
        },
      });

      await nextTick();

      const selectAll = getByLabelText('Select all');

      // Select all
      await user.click(selectAll);
      await nextTick();

      let selectedRows = tableRef.value?.table.getSelectedRowModel().rows;
      expect(selectedRows).toHaveLength(3);
      expect(tableRef.value?.table.getIsAllPageRowsSelected()).toBe(true);

      // Deselect all
      await user.click(selectAll);
      await nextTick();

      selectedRows = tableRef.value?.table.getSelectedRowModel().rows;
      expect(selectedRows).toHaveLength(0);
      expect(tableRef.value?.table.getIsAllPageRowsSelected()).toBe(false);
    });

    test('shows indeterminate state when some rows selected', async () => {
      const user = userEvent.setup();
      const tableRef = ref<InstanceType<typeof UiDataTable>>();

      const { getByLabelText } = render({
        components: { UiDataTable },
        template:
          '<UiDataTable ref="tableRef" :columns="columns" :data="data" />',
        setup() {
          return { tableRef, columns: selectableColumns, data: testData };
        },
      });

      await nextTick();

      // Select only one row
      await user.click(getByLabelText('Select row Alice'));
      await nextTick();

      expect(tableRef.value?.table.getIsSomePageRowsSelected()).toBe(true);
      expect(tableRef.value?.table.getIsAllPageRowsSelected()).toBe(false);
    });
  });

  describe('Reactivity', () => {
    test('updates table when data prop changes', async () => {
      const data = ref(testData);

      const { getAllByRole, getByText } = render({
        components: { UiDataTable },
        template: '<UiDataTable :columns="columns" :data="data" />',
        setup() {
          return { columns: basicColumns, data };
        },
      });

      expect(getAllByRole('row')).toHaveLength(4);

      data.value = [...testData, { id: '4', name: 'Diana', amount: 300 }];
      await nextTick();

      expect(getAllByRole('row')).toHaveLength(5);
      expect(getByText('Diana')).toBeInTheDocument();
    });

    test('uses getRowId for stable row identification', async () => {
      const tableRef = ref<InstanceType<typeof UiDataTable>>();

      render({
        components: { UiDataTable },
        template: `
          <UiDataTable 
            ref="tableRef" 
            :columns="columns" 
            :data="data" 
            :get-row-id="(row) => row.id"
          />
        `,
        setup() {
          return { tableRef, columns: basicColumns, data: testData };
        },
      });

      await nextTick();

      const rows = tableRef.value?.table.getRowModel().rows;
      expect(rows?.[0].id).toBe('1');
      expect(rows?.[1].id).toBe('2');
    });
  });
});
