/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { describe, expect, it, vi } from 'vitest';
import { Column, ListSelectMode } from '../List.model';
import { ListRow, ListRowProps, setLocale } from './ListRow';

interface TestExplorerData {
  id: string;
  desc: string;
  title: string;
}

// Possible bug setting this const as an array does not trigger TypeScript error
const mockExplorerData: TestExplorerData = {
  id: '1',
  desc: 'Description 1',
  title: 'Item 1',
};

const mockProps: ListRowProps<TestExplorerData> = {
  columns: [{ propertyName: 'id', size: '1fr', label: 'id' }],
  data: mockExplorerData,
  columnSizes: '1fr',
  columnGap: '5px',
  rowHeight: '50px',
  actionSize: '50px',
  horizontalTextAlign: 'left',
  verticalTextAlign: 'center',
  onItemClicked: vi.fn() as (data: TestExplorerData) => void,
};

describe('ListRow', () => {
  it('renders the component without crashing', () => {
    const { container } = render(<ListRow {...mockProps} />);

    expect(container).toBeTruthy();
  });

  it.each`
    expectedColumns                                                                                                                                                                                   | name
    ${[{ propertyName: 'desc', size: '1fr', label: 'desc' }, { propertyName: 'id', size: '1fr', label: 'id' }, { propertyName: 'title', size: '1fr', label: 'title' }] as Column<TestExplorerData>[]} | ${'all columns from data in changed order'}
    ${[{ propertyName: 'desc', size: '1fr', label: 'desc' }, { propertyName: 'id', size: '1fr', label: 'id' }] as Column<TestExplorerData>[]}                                                         | ${'less columns than in data'}
  `(
    'renders the defined columns in the defined order -> $name',
    ({ expectedColumns }: { expectedColumns: Column<TestExplorerData>[] }) => {
      const { container } = render(
        <ListRow {...mockProps} columns={expectedColumns} />,
      );

      const actualColumns = container.querySelectorAll('.cellWrapper');

      // Check amount of rendered columns
      expect(actualColumns).toHaveLength(expectedColumns.length);

      actualColumns.forEach((column, idx) => {
        const expectedPropertyName = expectedColumns[idx].propertyName!;
        expect(column.textContent).toEqual(
          mockExplorerData[expectedPropertyName],
        );
      });
    },
  );

  it('renders locale strings', () => {
    const expectedColumns: Column<{ date: Date; number: number }>[] = [
      { propertyName: 'date', size: '1fr', label: 'date' },
      { propertyName: 'number', size: '1fr', label: 'number' },
    ];
    const rowData = { date: new Date(0), number: 123456.12 };

    setLocale('en-US');

    const { container } = render(
      <ListRow
        data={rowData}
        columns={expectedColumns}
        columnSizes="1fr"
        columnGap="5px"
        rowHeight="50px"
        actionSize="50px"
        horizontalTextAlign="left"
        verticalTextAlign="center"
      />,
    );

    const actualColumns = container.querySelectorAll('.cellWrapper');
    // Check amount of rendered columns
    expect(actualColumns).toHaveLength(2);

    // see https://stackoverflow.com/questions/23199909/using-tolocalestring-in-node-js/23200062
    expect(actualColumns[0].textContent).toEqual(
      new Intl.DateTimeFormat('en-US', {
        year: 'numeric',
        month: 'numeric',
        day: 'numeric',
        hour: 'numeric',
        minute: 'numeric',
        second: 'numeric',
      }).format(rowData.date),
    );
    expect(actualColumns[1].textContent).toBe('123,456.12');
  });

  it('uses custom renderer', () => {
    const expectedColumns: Column<{ value: string }>[] = [
      {
        propertyName: 'value',
        label: 'value',
        render: (value: unknown) => `Changed: ${value}`,
      },
    ];
    const rowData = { value: 'something' };

    const { container } = render(
      // @ts-expect-error Typings to not match up
      <ListRow {...mockProps} data={rowData} columns={expectedColumns} />,
    );
    const actualColumns = container.querySelectorAll('.cellWrapper');
    expect(actualColumns[0].textContent).toBe('Changed: something');
  });

  it('shows empty field if a value required in a column is not present on the data object', () => {
    const expectedColumns: Column<TestExplorerData>[] = [
      // @ts-expect-error intentional bad property
      { propertyName: "doesn't exist", size: '1fr', label: 'desc' },
    ];

    const { container } = render(
      <ListRow {...mockProps} columns={expectedColumns} />,
    );

    const actualColumns = container.querySelectorAll('.cellWrapper');

    // Check amount of rendered columns
    expect(actualColumns).toHaveLength(1);

    expect(actualColumns[0].textContent).toBe('');
  });

  it('renders a action button by default', () => {
    const { container } = render(
      <ListRow
        {...mockProps}
        columns={[{ propertyName: 'id', size: '1fr', label: 'id' }]}
      />,
    );

    const actionButton = container.querySelector('button');

    expect(actionButton).toBeInTheDocument();
  });

  it('renders no action button if told', () => {
    const { container } = render(
      <ListRow {...mockProps} showActionButton={false} />,
    );

    expect(container.querySelector('button')).not.toBeInTheDocument();
  });

  it('renders a checkmark button', () => {
    const { container } = render(
      <ListRow
        {...mockProps}
        columns={[{ propertyName: 'id', size: '1fr', label: 'id' }]}
        showActionButton={false}
        showCheckMark={true}
      />,
    );

    const checkMark = container.querySelector('.selectionCheckMark');

    expect(checkMark).toBeInTheDocument();
  });

  it(`'actionSize' sets navigation action height and width`, () => {
    const mockActionSize = '10px';
    const { container } = render(
      <ListRow {...mockProps} actionSize={mockActionSize} />,
    );

    const actionButton = container.querySelector('button') as HTMLElement;

    expect(actionButton.style.height).toBe(mockActionSize);
    expect(actionButton.style.width).toBe(mockActionSize);
  });

  it(`'actionSize' sets checkmark action height and width`, () => {
    const mockActionSize = '10px';
    const { container } = render(
      <ListRow
        {...mockProps}
        actionSize={mockActionSize}
        showActionButton={false}
        showCheckMark={true}
      />,
    );

    const actionButton = container.querySelector('button') as HTMLElement;

    expect(actionButton.style.height).toBe(mockActionSize);
    expect(actionButton.style.width).toBe(mockActionSize);
  });

  it(`'actionSize' sets action checkbox height and width`, () => {
    const mockActionSize = '10px';
    const { container } = render(
      <ListRow
        {...mockProps}
        actionSize={mockActionSize}
        showItemCheckbox={true}
      />,
    );

    const checkbox = container.querySelector(
      '.list-checkbox-container',
    ) as HTMLElement;

    expect(checkbox).toBeInTheDocument();
  });

  it('calls the "onItemClicked" event with data', () => {
    const spy = vi.fn();
    const { container } = render(
      <ListRow {...mockProps} onItemClicked={spy} />,
    );
    const content = container.querySelector('.content') as HTMLElement;
    fireEvent.click(content);
    expect(spy).toHaveBeenCalledTimes(1);
    expect(spy).toHaveBeenCalledWith(mockExplorerData);
  });

  it('does not render a checkbox by default', () => {
    const { container } = render(<ListRow {...mockProps} />);

    const checkbox = container.querySelector('.list-checkbox-container');

    expect(checkbox).not.toBeInTheDocument();
  });

  it('renders a checkbox button', () => {
    const { container } = render(
      <ListRow
        {...mockProps}
        columns={[{ propertyName: 'id', size: '1fr', label: 'id' }]}
        showActionButton={false}
        showItemCheckbox={true}
      />,
    );

    const checkbox = container.querySelector('.list-checkbox-container');

    expect(checkbox).toBeInTheDocument();
  });

  it('raises checkBoxHandler', () => {
    const spy = vi.fn().mockImplementation(() => null);
    const { container } = render(
      <ListRow
        {...mockProps}
        onItemSelected={spy}
        showItemCheckbox={true}
        selectionMode={ListSelectMode.Multi}
      />,
    );

    const checkbox = container.querySelector(
      '.list-checkbox-container button',
    ) as HTMLButtonElement;
    fireEvent.click(checkbox);

    expect(spy).toHaveBeenCalledTimes(1);
  });

  it(`does not have the 'selected' class by default`, () => {
    const { container } = render(<ListRow {...mockProps} />);

    const row = container.querySelector('.columnsRoot');

    expect(row?.classList.contains('selected')).toBe(false);
  });

  it(`only has the 'selected' class if both itemSelect and showItemCheckbox props are true`, () => {
    const { container } = render(
      <ListRow {...mockProps} itemSelected={true} showItemCheckbox={true} />,
    );

    const row = container.querySelector('.columnsRoot');

    expect(row?.classList.contains('selected')).toBe(true);
  });

  it(`raises onItemClicked if list selectionMode is set to None(the default mode)`, () => {
    const onItemClickedSpy = vi.fn();
    const onItemSelectedSpy = vi.fn();

    const { container } = render(
      <ListRow
        {...mockProps}
        showItemCheckbox={true}
        onItemClicked={onItemClickedSpy}
        onItemSelected={onItemSelectedSpy}
      />,
    );

    const row = container.querySelector('.content') as HTMLElement;

    fireEvent.click(row);

    expect(onItemClickedSpy).toHaveBeenCalledTimes(1);
    expect(onItemSelectedSpy).not.toHaveBeenCalled();
  });

  it(`raises onItemClicked if list selectionMode is set to Single`, () => {
    const onItemClickedSpy = vi.fn();
    const onItemSelectedSpy = vi.fn();

    const { container } = render(
      <ListRow
        {...mockProps}
        showItemCheckbox={true}
        selectionMode={ListSelectMode.Single}
        onItemClicked={onItemClickedSpy}
        onItemSelected={onItemSelectedSpy}
      />,
    );

    const row = container.querySelector('.content') as HTMLElement;

    fireEvent.click(row);

    expect(onItemClickedSpy).toHaveBeenCalledTimes(1);
    expect(onItemSelectedSpy).not.toHaveBeenCalled();
  });

  it(`raises onItemSelected and toggles item selection if list selectionMode is set to Multi and checkbox is not disabled`, async () => {
    const onItemClickedSpy = vi.fn();
    const onItemSelectedSpy = vi.fn();
    const mockChecked = false;

    const { container } = render(
      <ListRow
        {...mockProps}
        showItemCheckbox={true}
        selectionMode={ListSelectMode.Multi}
        onItemClicked={onItemClickedSpy}
        onItemSelected={onItemSelectedSpy}
      />,
    );

    const row = container.querySelector('.content') as HTMLElement;

    fireEvent.click(row);

    expect(onItemSelectedSpy).toHaveBeenCalledTimes(1);
    expect(onItemSelectedSpy).toHaveBeenCalledWith(!mockChecked);
    expect(onItemClickedSpy).not.toHaveBeenCalled();
  });

  it(`does not raise onItemSelected if the row is disabled`, async () => {
    const onItemSelectedSpy = vi.fn();

    const { container } = render(
      <ListRow
        {...mockProps}
        showItemCheckbox={true}
        selectionMode={ListSelectMode.Multi}
        onItemSelected={onItemSelectedSpy}
        isRowDisabled={true}
      />,
    );

    const row = container.querySelector('.columnsRoot') as HTMLElement;

    fireEvent.click(row);

    expect(onItemSelectedSpy).not.toHaveBeenCalled();
    expect(row.classList.contains('disabled')).toBe(true);
  });

  it(`does not onItemClicked if the row is disabled`, async () => {
    const onItemClickedSpy = vi.fn();

    const { container } = render(
      <ListRow
        {...mockProps}
        showItemCheckbox={true}
        onItemClicked={onItemClickedSpy}
        isRowDisabled={true}
      />,
    );

    const row = container.querySelector('.columnsRoot') as HTMLElement;

    fireEvent.click(row);

    expect(onItemClickedSpy).not.toHaveBeenCalled();
    expect(row.classList.contains('disabled')).toBe(true);
  });

  describe('linking', () => {
    it('does render a link if not in select mode and got a string as "onItemClicked"', () => {
      const { container } = render(
        <BrowserRouter>
          <ListRow
            {...mockProps}
            onItemClicked="http://this.is.a.test.url"
            selectionMode={ListSelectMode.None}
          />
        </BrowserRouter>,
      );

      const link = container.querySelector('a');

      expect(link).toBeInTheDocument();
    });

    it('does not render a link if in select mode and got a string as "onItemClicked"', () => {
      const { container } = render(
        <ListRow
          {...mockProps}
          onItemClicked="http://this.is.a.test.url"
          selectionMode={ListSelectMode.Single}
        />,
      );

      const link = container.querySelector('.link');

      expect(link).not.toBeInTheDocument();
    });

    it('does not render a link if it got a function as "onItemClicked"', () => {
      const { container } = render(
        <ListRow
          {...mockProps}
          onItemClicked={() => {
            // empty
          }}
          selectionMode={ListSelectMode.Single}
        />,
      );

      const link = container.querySelector('.link');

      expect(link).not.toBeInTheDocument();
    });
  });

  describe('tooltip', () => {
    it(`renders a tooltip using the 'title' html attribute by default`, () => {
      const { container } = render(<ListRow {...mockProps} />);
      const cells = container.querySelectorAll('.cell');

      cells.forEach((cell) => {
        expect((cell as HTMLElement).title).toBe(cell.textContent);
      });
    });

    it(`does not render a title attribute if tooltip column property is set to 'false'`, () => {
      const mockColumns = mockProps.columns.map((obj) => ({
        ...obj,
        tooltip: false,
      }));
      const { container } = render(
        <ListRow {...mockProps} columns={mockColumns} />,
      );

      const cells = container.querySelectorAll('.cell');

      cells.forEach((cell) => {
        expect((cell as HTMLElement).title).toBe('');
      });
    });

    it('renders a tooltip when a custom renderer is used and the return value results in a valid string', () => {
      const mockValue = 'mock-value';
      const expectedColumns: Column<{ value: string }>[] = [
        {
          propertyName: 'value',
          label: 'value',
          render: () => mockValue,
        },
      ];
      const rowData = { value: mockValue };

      const { container } = render(
        // @ts-expect-error Typings to not match up
        <ListRow {...mockProps} data={rowData} columns={expectedColumns} />,
      );

      const cell = container.querySelector('.cell') as HTMLElement;

      expect(cell.title).toBe(mockValue);
    });
  });

  describe('column text alignments', () => {
    const alignments: {
      horizontal: 'left' | 'right' | 'center';
      vertical: 'center' | 'start' | 'end';
    }[] = [
      { horizontal: 'center', vertical: 'center' },
      { horizontal: 'left', vertical: 'start' },
      { horizontal: 'right', vertical: 'end' },
    ];

    alignments.forEach(({ horizontal, vertical }) => {
      it(`should apply the correct styles for textAlign: ${horizontal}, alignSelf: ${vertical}`, () => {
        const { container } = render(
          <ListRow
            {...mockProps}
            columns={[{ propertyName: 'id', size: '1fr', label: 'id' }]}
            horizontalTextAlign={horizontal}
            verticalTextAlign={vertical}
          />,
        );

        const wrapperDivs = container.querySelectorAll('.cell');

        wrapperDivs.forEach((node) => {
          const element = node as HTMLElement;

          expect(element.style.textAlign).toBe(horizontal);
          expect(element.style.alignSelf).toBe(vertical);
        });
      });
    });
  });
});
