/**
 * External dependencies
 */
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

/**
 * WordPress dependencies
 */
import { useMemo, useState } from '@wordpress/element';

/**
 * Internal dependencies
 */
import DataViews from '../index';
import {
	LAYOUT_ACTIVITY,
	LAYOUT_GRID,
	LAYOUT_LIST,
	LAYOUT_TABLE,
} from '../../constants';
import type { Action, SupportedLayouts, View } from '../../types';
import filterSortAndPaginate from '../../utils/filter-sort-and-paginate';

type Data = {
	id: number;
	title: string;
	author?: number;
	order?: number;
};

const DEFAULT_VIEW = {
	type: 'table' as const,
	search: '',
	page: 1,
	perPage: 10,
	layout: {},
	filters: [],
};

const defaultLayouts: SupportedLayouts = {
	[ LAYOUT_TABLE ]: true,
	[ LAYOUT_GRID ]: true,
	[ LAYOUT_LIST ]: true,
	[ LAYOUT_ACTIVITY ]: true,
};

const fields = [
	{
		id: 'title',
		label: 'Title',
		type: 'text' as const,
	},
	{
		id: 'order',
		label: 'Order',
		type: 'integer' as const,
	},
	{
		id: 'author',
		label: 'Author',
		type: 'integer' as const,
		elements: [
			{ value: 1, label: 'Jane' },
			{ value: 2, label: 'John' },
		],
	},
	{
		label: 'Image',
		id: 'image',
		render: ( { item }: { item: Data } ) => {
			return (
				<svg
					width="400"
					height="180"
					data-testid={ 'image-field-' + item.id }
				>
					<rect
						x="50"
						y="20"
						rx="20"
						ry="20"
						width="150"
						height="150"
						style={ { fill: 'red', opacity: 0.5 } }
					/>
				</svg>
			);
		},
		enableSorting: false,
	},
];

const actions: Action< Data >[] = [
	{
		id: 'delete',
		label: 'Delete',
		supportsBulk: true,
		RenderModal: () => <div>Modal Content</div>,
	},
];

const data: Data[] = [
	{
		id: 1,
		title: 'Hello World',
		author: 1,
		order: 1,
	},
	{
		id: 2,
		title: 'Homepage',
		author: 2,
		order: 1,
	},
	{
		id: 3,
		title: 'Posts',
		author: 2,
		order: 1,
	},
];

function DataViewWrapper( {
	view: additionalView,
	...props
}: Partial< Parameters< typeof DataViews< Data > >[ 0 ] > ) {
	const [ view, setView ] = useState< View >( {
		...DEFAULT_VIEW,
		fields: [ 'title', 'order', 'author' ],
		...additionalView,
	} );

	const { data: shownData, paginationInfo } = useMemo( () => {
		return filterSortAndPaginate( data, view, props.fields || fields );
	}, [ view, props.fields ] );

	const dataViewProps = {
		getItemId: ( item: Data ) => item.id.toString(),
		paginationInfo,
		data: shownData,
		view,
		fields,
		onChangeView: setView,
		actions: [],
		defaultLayouts,
		...props,
	};

	return <DataViews { ...dataViewProps } />;
}

// jest.useFakeTimers();

// Tests run against a DataView which is 500px wide.
const mockUseViewportMatch = jest.fn(
	// eslint-disable-next-line @typescript-eslint/no-unused-vars
	( _viewport: string, _operator: string ) => false
);
jest.mock( '@wordpress/compose', () => {
	return {
		...jest.requireActual( '@wordpress/compose' ),
		useResizeObserver: jest.fn( ( callback ) => {
			setTimeout( () => {
				callback( [
					{
						borderBoxSize: [ { inlineSize: 500 } ],
					},
				] );
			}, 0 );
			return () => {};
		} ),
		useViewportMatch: ( viewport: string, operator: string ): boolean =>
			mockUseViewportMatch( viewport, operator ),
	};
} );

describe( 'DataViews component', () => {
	it( 'should show "No results" if data is empty', () => {
		render( <DataViewWrapper data={ [] } /> );
		expect( screen.getByText( 'No results' ) ).toBeInTheDocument();
	} );

	it( 'should filter results by "search" text, if field has enableGlobalSearch set to true', async () => {
		const fieldsWithSearch = [
			{
				...fields[ 0 ],
				enableGlobalSearch: true,
			},
			fields[ 1 ],
		];
		render(
			<DataViewWrapper
				fields={ fieldsWithSearch }
				view={ { ...DEFAULT_VIEW, search: 'Hello' } }
			/>
		);
		// Row count includes header.
		expect( screen.getAllByRole( 'row' ).length ).toEqual( 2 );
		expect( screen.getByText( 'Hello World' ) ).toBeInTheDocument();
	} );

	it( 'should display matched element label if field contains elements list', () => {
		render(
			<DataViewWrapper
				data={ [ { id: 1, author: 3, title: 'Hello World' } ] }
				fields={ [
					{
						id: 'author',
						label: 'Author',
						type: 'integer' as const,
						elements: [
							{ value: 1, label: 'Jane' },
							{ value: 2, label: 'John' },
							{ value: 3, label: 'Tim' },
						],
					},
				] }
			/>
		);
		expect( screen.getByText( 'Tim' ) ).toBeInTheDocument();
	} );

	it( 'should render custom render function if defined in field definition', () => {
		render(
			<DataViewWrapper
				data={ [ { id: 1, title: 'Test Title' } ] }
				fields={ [
					{
						id: 'title',
						label: 'Title',
						type: 'text' as const,
						render: ( { item }: { item: Data } ) => {
							return item.title?.toUpperCase();
						},
					},
				] }
			/>
		);
		expect( screen.getByText( 'TEST TITLE' ) ).toBeInTheDocument();
	} );

	it( 'should trigger infinite scroll when the layout container scrolls', async () => {
		const onChangeView = jest.fn();

		if ( typeof global.IntersectionObserver === 'undefined' ) {
			( global as any ).IntersectionObserver = jest.fn( () => ( {
				observe: jest.fn(),
				unobserve: jest.fn(),
				disconnect: jest.fn(),
			} ) );
		}

		const { container } = render(
			<DataViewWrapper
				view={ {
					type: LAYOUT_GRID,
					infiniteScrollEnabled: true,
					perPage: 1,
				} }
				onChangeView={ onChangeView }
			/>
		);
		// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
		const layoutContainer = container.querySelector(
			'.dataviews-layout__container'
		) as HTMLDivElement;

		Object.defineProperties( layoutContainer, {
			scrollTop: {
				configurable: true,
				value: 500,
			},
			scrollHeight: {
				configurable: true,
				value: 1000,
			},
			clientHeight: {
				configurable: true,
				value: 500,
			},
		} );

		fireEvent.scroll( layoutContainer );

		await waitFor( () => {
			expect( onChangeView ).toHaveBeenCalledWith(
				expect.objectContaining( {
					infiniteScrollEnabled: true,
					startPosition: 2,
				} )
			);
		} );
	} );

	describe( 'in table view', () => {
		it( 'should display columns for each field', () => {
			render( <DataViewWrapper /> );
			const displayedColumnFields = fields.filter( ( field ) =>
				[ 'title', 'order', 'author' ].includes( field.id )
			);
			for ( const field of displayedColumnFields ) {
				expect(
					screen.getByRole( 'button', { name: field.label } )
				).toBeInTheDocument();
			}
		} );

		it( 'should display the passed in data', () => {
			render( <DataViewWrapper /> );
			for ( const item of data ) {
				expect(
					screen.getAllByText( item.title )[ 0 ]
				).toBeInTheDocument();
			}
		} );

		it( 'should display title column if defined using titleField', () => {
			render(
				<DataViewWrapper
					view={ {
						...DEFAULT_VIEW,
						fields: [ 'order', 'author' ],
						titleField: 'title',
					} }
				/>
			);
			for ( const item of data ) {
				expect(
					screen.getAllByText( item.title )[ 0 ]
				).toBeInTheDocument();
			}
		} );

		it( 'should render actions column if actions are supported and passed in', () => {
			render( <DataViewWrapper actions={ actions } /> );
			expect( screen.getByText( 'Actions' ) ).toBeInTheDocument();
		} );

		it( 'should trigger the onClickItem callback if isItemClickable returns true and title field is clicked', async () => {
			const onClickItemCallback = jest.fn();

			render(
				<DataViewWrapper
					view={ {
						...DEFAULT_VIEW,
						fields: [ 'author' ],
						titleField: 'title',
					} }
					actions={ actions }
					isItemClickable={ () => true }
					renderItemLink={ ( { item, ...props } ) => (
						<button
							// @ts-expect-error
							onClick={ ( event ) => {
								event.preventDefault();
								onClickItemCallback( item );
							} }
							{ ...props }
						/>
					) }
				/>
			);
			const titleField = screen.getByText( data[ 0 ].title );
			const user = userEvent.setup();
			await user.click( titleField );
			expect( onClickItemCallback ).toHaveBeenCalledWith( data[ 0 ] );
		} );

		it( 'accepts ctrl/cmd key and click for non-consecutive multi-selection', async () => {
			render(
				<DataViewWrapper
					view={ {
						...DEFAULT_VIEW,
						fields: [ 'author' ],
						titleField: 'title',
					} }
					// A bulk action is required for the dataview to be multi-selectable.
					actions={ actions }
				/>
			);
			const firstItemElement = screen.getByText( data[ 0 ].title );
			const thirdItemElement = screen.getByText( data[ 2 ].title );
			const user = userEvent.setup();
			await user.keyboard( '{Control>}' );
			await user.click( firstItemElement );

			// First item should be selected.
			expect(
				screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
			).toBeChecked();
			await user.click( thirdItemElement );

			// Both items should be selected.
			expect(
				screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
			).toBeChecked();
			expect(
				screen.getByRole( 'checkbox', { name: data[ 2 ].title } )
			).toBeChecked();

			// Don't keep the modifier pressed down, that's just mean.
			await user.keyboard( '{/Control}' );
		} );
	} );

	describe( 'in grid view', () => {
		it( 'should display the passed in data', () => {
			render(
				<DataViewWrapper
					view={ {
						type: 'grid',
					} }
				/>
			);
			for ( const item of data ) {
				expect(
					screen.getAllByText( item.title )[ 0 ]
				).toBeInTheDocument();
			}
		} );

		it( 'should render mediaField if defined', () => {
			render(
				<DataViewWrapper
					view={ {
						type: 'grid',
						mediaField: 'image',
					} }
				/>
			);
			for ( const item of data ) {
				expect(
					screen.getByTestId( 'image-field-' + item.id )
				).toBeInTheDocument();
			}
		} );

		it( 'should render actions dropdown if actions are supported and passed in for each grid item', () => {
			render(
				<DataViewWrapper
					view={ {
						type: 'grid',
					} }
					actions={ actions }
				/>
			);
			expect(
				screen.getAllByRole( 'button', { name: 'Actions' } ).length
			).toEqual( 3 );
		} );

		it( 'should trigger the onClickItem callback if isItemClickable returns true and a media field is clicked', async () => {
			const mediaClickItemCallback = jest.fn();

			render(
				<DataViewWrapper
					view={ {
						type: 'grid',
						mediaField: 'image',
					} }
					actions={ actions }
					isItemClickable={ () => true }
					renderItemLink={ ( { item, ...props } ) => (
						<button
							// @ts-expect-error
							onClick={ ( event ) => {
								event.preventDefault();
								mediaClickItemCallback( item );
							} }
							{ ...props }
						/>
					) }
				/>
			);
			const imageField = screen.getByTestId(
				'image-field-' + data[ 0 ].id
			);
			const user = userEvent.setup();
			await user.click( imageField );
			expect( mediaClickItemCallback ).toHaveBeenCalledWith( data[ 0 ] );
		} );

		it( 'accepts checkbox click for selection', async () => {
			render(
				<DataViewWrapper
					view={ {
						...DEFAULT_VIEW,
						type: 'grid',
						fields: [ 'author' ],
						titleField: 'title',
						mediaField: 'image',
					} }
					// A bulk action is required for the dataview to be multi-selectable.
					actions={ actions }
				/>
			);
			const firstCheckbox = screen.getByRole( 'checkbox', {
				name: data[ 0 ].title,
			} );
			const thirdCheckbox = screen.getByRole( 'checkbox', {
				name: data[ 2 ].title,
			} );
			const user = userEvent.setup();
			await user.click( firstCheckbox );

			// First item should be selected.
			expect( firstCheckbox ).toBeChecked();
			await user.click( thirdCheckbox );

			// Both items should be selected (checkboxes toggle independently).
			expect( firstCheckbox ).toBeChecked();
			expect( thirdCheckbox ).toBeChecked();
		} );

		it( 'accepts ctrl/cmd key and click for multi-selection', async () => {
			render(
				<DataViewWrapper
					view={ {
						...DEFAULT_VIEW,
						type: 'grid',
						fields: [ 'author' ],
						titleField: 'title',
						mediaField: 'image',
					} }
					// A bulk action is required for the dataview to be multi-selectable.
					actions={ actions }
					isItemClickable={ () => false }
				/>
			);
			// Click on the gridcell directly (not wrapped in ItemClickWrapper)
			const firstItemCard = screen.getByRole( 'gridcell', {
				name: /Hello World/,
			} );
			const user = userEvent.setup();
			await user.keyboard( '{Control>}' );
			await user.click( firstItemCard );

			// First item should be selected.
			expect(
				screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
			).toBeChecked();

			await user.keyboard( '{/Control}' );
		} );

		it( 'supports tabbing to selection and actions when title is visible', async () => {
			render(
				<DataViewWrapper
					view={ {
						...DEFAULT_VIEW,
						type: 'grid',
						fields: [],
						mediaField: 'image',
						titleField: 'title',
					} }
					isItemClickable={ () => true }
					actions={ actions }
				/>
			);

			// Double check that the title is being rendered.
			expect( screen.getByText( data[ 0 ].title ) ).toBeInTheDocument();

			const viewOptionsButton = screen.getByRole( 'button', {
				name: 'View options',
			} );

			const user = userEvent.setup();

			// Double click to open and then close view options. This is performed
			// instead of a direct .focus() so that effects have time to complete.
			await user.click( viewOptionsButton );
			await user.click( viewOptionsButton );

			await user.tab();
			await user.tab();

			expect(
				screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
			).toHaveFocus();

			await user.tab();

			expect(
				screen.getAllByRole( 'button', { name: 'Actions' } )[ 0 ]
			).toHaveFocus();
		} );

		it( 'supports tabbing to selection and actions when title is not visible', async () => {
			render(
				<DataViewWrapper
					view={ {
						...DEFAULT_VIEW,
						type: 'grid',
						fields: [],
						mediaField: 'image',
						titleField: 'title',
						showTitle: false,
					} }
					isItemClickable={ () => true }
					actions={ actions }
				/>
			);

			// Double check that the title is not being rendered.
			expect(
				screen.queryByText( data[ 0 ].title )
			).not.toBeInTheDocument();

			const viewOptionsButton = screen.getByRole( 'button', {
				name: 'View options',
			} );

			const user = userEvent.setup();

			// Double click to open and then close view options. This is performed
			// instead of a direct .focus() so that effects have time to complete.
			await user.click( viewOptionsButton );
			await user.click( viewOptionsButton );
			await user.tab();
			await user.tab();

			expect(
				screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
			).toHaveFocus();

			await user.tab();

			expect(
				screen.getAllByRole( 'button', { name: 'Actions' } )[ 0 ]
			).toHaveFocus();
		} );

		it( 'accepts an invalid previewSize and the preview size picker falls back to another size', async () => {
			render(
				<DataViewWrapper
					view={ {
						type: 'grid',
						mediaField: 'image',
						layout: { previewSize: 13 },
					} }
				/>
			);
			const user = userEvent.setup();
			await user.click(
				screen.getByRole( 'button', { name: 'View options' } )
			);
			const previewSizeSlider = screen.getByRole( 'slider', {
				name: 'Preview size',
			} );
			expect( previewSizeSlider ).toBeInTheDocument();
			expect( previewSizeSlider ).toHaveValue( '0' ); // Falls back to the smallest size, which is the first one.
		} );
	} );

	describe( 'in list view', () => {
		it( 'should display the passed in data', () => {
			render(
				<DataViewWrapper
					view={ {
						type: 'list',
					} }
				/>
			);
			for ( const item of data ) {
				expect(
					screen.getAllByText( item.title )[ 0 ]
				).toBeInTheDocument();
			}
		} );

		it( 'should render actions dropdown if actions are supported and passed in for each list item', () => {
			render(
				<DataViewWrapper
					view={ {
						type: 'list',
					} }
					actions={ actions }
				/>
			);
			expect(
				screen.getAllByRole( 'button', { name: 'Actions' } ).length
			).toEqual( 3 );
		} );
	} );

	describe( 'actions on mobile viewport', () => {
		const testActions: Action< Data >[] = [
			{
				id: 'edit',
				label: 'Edit',
				isPrimary: true,
				callback: () => {},
			},
		];

		beforeEach( () => {
			// Simulate mobile viewport
			mockUseViewportMatch.mockImplementation(
				( viewport: string, operator: string ) =>
					viewport === 'medium' && operator === '<'
			);
		} );

		afterEach( () => {
			mockUseViewportMatch.mockImplementation( () => false );
		} );

		it( 'should show actions dropdown on mobile even when there is only one action in table layout', () => {
			render(
				<DataViewWrapper
					view={ {
						type: LAYOUT_TABLE,
					} }
					actions={ testActions }
				/>
			);
			// On mobile, the dropdown should be visible even with only primary actions
			expect(
				screen.getAllByRole( 'button', { name: 'Actions' } ).length
			).toEqual( 3 );
		} );

		it( 'should show actions dropdown on mobile even when there is only one action in activity layout', () => {
			render(
				<DataViewWrapper
					view={ {
						type: LAYOUT_ACTIVITY,
					} }
					actions={ testActions }
				/>
			);
			// On mobile, the dropdown should be visible even with only primary actions
			expect(
				screen.getAllByRole( 'button', { name: 'Actions' } ).length
			).toEqual( 3 );
		} );
	} );
	describe( 'Default layouts', () => {
		/**
		 * A minimal wrapper that intentionally omits the `defaultLayouts` prop so
		 * DataViews falls back to its internal DEFAULT_LAYOUTS constant
		 * ({ table: true, grid: true, list: true }).
		 */
		function DataViewWrapperWithoutDefaultLayouts() {
			const [ view, setView ] = useState< View >( {
				...DEFAULT_VIEW,
				fields: [ 'title', 'order', 'author' ],
			} );

			const { data: shownData, paginationInfo } = useMemo( () => {
				return filterSortAndPaginate( data, view, fields );
			}, [ view ] );

			return (
				<DataViews
					getItemId={ ( item: Data ) => item.id.toString() }
					paginationInfo={ paginationInfo }
					data={ shownData }
					view={ view }
					fields={ fields }
					onChangeView={ setView }
					// No `defaultLayouts` prop — falls back to DEFAULT_LAYOUTS
				/>
			);
		}

		it( 'renders Table, Grid, and List layout options when defaultLayouts is not provided', async () => {
			render( <DataViewWrapperWithoutDefaultLayouts /> );

			const user = userEvent.setup();

			// All three default layouts are available, so the Layout switcher
			// button (rendered by ViewTypeMenu) must be present.
			const layoutButton = screen.getByRole( 'button', {
				name: 'Layout',
			} );
			expect( layoutButton ).toBeInTheDocument();

			// Open the layout menu.
			await user.click( layoutButton );

			// Table, Grid, and List options must all appear.
			expect(
				screen.getByRole( 'menuitemradio', { name: 'Table' } )
			).toBeInTheDocument();
			expect(
				screen.getByRole( 'menuitemradio', { name: 'Grid' } )
			).toBeInTheDocument();
			expect(
				screen.getByRole( 'menuitemradio', { name: 'List' } )
			).toBeInTheDocument();

			// Table is the default active layout.
			expect(
				screen.getByRole( 'menuitemradio', { name: 'Table' } )
			).toBeChecked();
			expect(
				screen.getByRole( 'menuitemradio', { name: 'Grid' } )
			).not.toBeChecked();
			expect(
				screen.getByRole( 'menuitemradio', { name: 'List' } )
			).not.toBeChecked();
		} );
	} );
} );
