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

/**
 * Internal dependencies
 */
import _FontSizePicker from '../';
import type { FontSize } from '../types';
/**
 * WordPress dependencies
 */
import { useState } from '@wordpress/element';

const FontSizePicker = (
	props: React.ComponentProps< typeof _FontSizePicker >
) => <_FontSizePicker __next40pxDefaultSize { ...props } />;

const ControlledFontSizePicker = ( {
	onChange,
	...props
}: React.ComponentProps< typeof FontSizePicker > ) => {
	const [ fontSize, setFontSize ] =
		useState< typeof props.value >( undefined );

	return (
		<FontSizePicker
			{ ...props }
			value={ fontSize }
			onChange={ ( newFontSize ) => {
				setFontSize( newFontSize );
				onChange?.( newFontSize );
			} }
		/>
	);
};

describe( 'FontSizePicker', () => {
	test.each( [
		// Use units when initial value uses units.
		{ value: '12px', expectedValue: '80px' },
		// Don't use units when initial value does not use units.
		{ value: 12, expectedValue: 80 },
	] )(
		'should call onChange( $expectedValue ) after user types 80 when value is $value',
		async ( { value, expectedValue } ) => {
			const user = userEvent.setup();
			const onChange = jest.fn();
			await render(
				<FontSizePicker value={ value } onChange={ onChange } />
			);
			const input = screen.getByRole( 'spinbutton', {
				name: 'Font size',
			} );
			await user.clear( input );
			await user.type( input, '80' );
			expect( onChange ).toHaveBeenCalledTimes( 3 ); // Once for the clear, then once per keystroke.
			expect( onChange ).toHaveBeenCalledWith( expectedValue );
		}
	);

	test.each( [
		// Use units when first font size uses units.
		{ firstFontSize: '12px', expectedValue: '80px' },
		// Don't use units when first font size does not use units.
		{ firstFontSize: 12, expectedValue: 80 },
	] )(
		'should call onChange( $expectedValue ) after user types 80 when first font size is $firstFontSize',
		async ( { firstFontSize, expectedValue } ) => {
			const user = userEvent.setup();
			const onChange = jest.fn();
			await render(
				<FontSizePicker
					fontSizes={ [ { slug: 'slug', size: firstFontSize } ] }
					onChange={ onChange }
				/>
			);
			await user.click(
				screen.getByRole( 'button', { name: 'Set custom size' } )
			);
			const input = screen.getByRole( 'spinbutton', {
				name: 'Font size',
			} );
			await user.type( input, '80' );
			expect( onChange ).toHaveBeenCalledTimes( 2 ); // Once per keystroke.
			expect( onChange ).toHaveBeenCalledWith( expectedValue );
		}
	);

	describe( 'with > 5 homogeneous font sizes', () => {
		const fontSizes = [
			{
				slug: 'tiny',
				name: 'Tiny',
				size: '8px',
			},
			{
				slug: 'small',
				name: 'Small',
				size: '12px',
			},
			{
				slug: 'medium',
				name: 'Medium',
				size: '16px',
			},
			{
				slug: 'large',
				name: 'Large',
				size: '20px',
			},
			{
				slug: 'x-large',
				name: 'Extra Large',
				size: '30px',
			},
			{
				slug: 'xx-large',
				// no name
				size: '40px',
			},
		];

		it( 'displays a select control', async () => {
			const user = userEvent.setup();
			await render( <FontSizePicker fontSizes={ fontSizes } /> );
			await user.click(
				screen.getByRole( 'combobox', { name: 'Font size' } )
			);
			const options = screen.getAllByRole( 'option' );
			expect( options ).toHaveLength( 7 );
			expect( options[ 0 ] ).toHaveAccessibleName( 'Default' );
			expect( options[ 1 ] ).toHaveAccessibleName( 'Tiny 8px' );
			expect( options[ 2 ] ).toHaveAccessibleName( 'Small 12px' );
			expect( options[ 3 ] ).toHaveAccessibleName( 'Medium 16px' );
			expect( options[ 4 ] ).toHaveAccessibleName( 'Large 20px' );
			expect( options[ 5 ] ).toHaveAccessibleName( 'Extra Large 30px' );
			expect( options[ 6 ] ).toHaveAccessibleName( 'xx-large 40px' );
		} );

		test.each( [
			{
				option: 'Default',
				value: '8px',
				expectedArguments: [ undefined, undefined ],
			},
			{
				option: 'Tiny 8px',
				value: undefined,
				expectedArguments: [ '8px', fontSizes[ 0 ] ],
			},
		] )(
			'calls onChange( $expectedArguments ) when $option is selected',
			async ( { option, value, expectedArguments } ) => {
				const user = userEvent.setup();
				const onChange = jest.fn();
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value={ value }
						onChange={ onChange }
					/>
				);
				await user.click(
					screen.getByRole( 'combobox', { name: 'Font size' } )
				);
				await user.click(
					screen.getByRole( 'option', { name: option } )
				);
				expect( onChange ).toHaveBeenCalledTimes( 1 );
				expect( onChange ).toHaveBeenCalledWith( ...expectedArguments );
			}
		);

		commonTests( fontSizes );
	} );

	describe( 'with > 5 heterogeneous font sizes', () => {
		const fontSizes = [
			{
				slug: 'tiny',
				name: 'Tiny',
				size: '8px',
			},
			{
				slug: 'small',
				name: 'Small',
				size: '1em',
			},
			{
				slug: 'medium',
				name: 'Medium',
				size: '2rem',
			},
			{
				slug: 'large',
				name: 'Large',
				size: 'clamp(1.75rem, 3vw, 2.25rem)',
			},
			{
				slug: 'x-large',
				name: 'Extra Large',
				size: '30px',
			},
			{
				slug: 'xx-large',
				// no name
				size: '40px',
			},
		];

		it( 'displays a select control', async () => {
			const user = userEvent.setup();
			await render( <FontSizePicker fontSizes={ fontSizes } /> );
			await user.click(
				screen.getByRole( 'combobox', { name: 'Font size' } )
			);
			const options = screen.getAllByRole( 'option' );
			expect( options ).toHaveLength( 7 );
			expect( options[ 0 ] ).toHaveAccessibleName( 'Default' );
			expect( options[ 1 ] ).toHaveAccessibleName( 'Tiny 8px' );
			expect( options[ 2 ] ).toHaveAccessibleName( 'Small 1em' );
			expect( options[ 3 ] ).toHaveAccessibleName( 'Medium 2rem' );
			expect( options[ 4 ] ).toHaveAccessibleName( 'Large' );
			expect( options[ 5 ] ).toHaveAccessibleName( 'Extra Large 30px' );
			expect( options[ 6 ] ).toHaveAccessibleName( 'xx-large 40px' );
		} );

		test.each( [
			{ value: undefined, option: 'Default' },
			{ value: '', option: 'Default' },
			{ value: '8px', option: 'Tiny' },
		] )(
			'defaults to $option when value is $value',
			async ( { value, option } ) => {
				await render(
					<FontSizePicker fontSizes={ fontSizes } value={ value } />
				);
				expect(
					screen.getByRole( 'combobox', { name: 'Font size' } )
				).toHaveTextContent( option );
			}
		);

		test.each( [
			{
				option: 'Default',
				value: '8px',
				expectedArguments: [ undefined, undefined ],
			},
			{
				option: 'Tiny 8px',
				value: undefined,
				expectedArguments: [ '8px', fontSizes[ 0 ] ],
			},
			{
				option: 'Small 1em',
				value: '8px',
				expectedArguments: [ '1em', fontSizes[ 1 ] ],
			},
			{
				option: 'Medium 2rem',
				value: '8px',
				expectedArguments: [ '2rem', fontSizes[ 2 ] ],
			},
			{
				option: 'Large',
				value: '8px',
				expectedArguments: [
					'clamp(1.75rem, 3vw, 2.25rem)',
					fontSizes[ 3 ],
				],
			},
		] )(
			'calls onChange( $expectedValue ) when $option is selected',
			async ( { option, value, expectedArguments } ) => {
				const user = userEvent.setup();
				const onChange = jest.fn();
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value={ value }
						onChange={ onChange }
					/>
				);
				await user.click(
					screen.getByRole( 'combobox', { name: 'Font size' } )
				);
				await user.click(
					screen.getByRole( 'option', { name: option } )
				);
				expect( onChange ).toHaveBeenCalledTimes( 1 );
				expect( onChange ).toHaveBeenCalledWith( ...expectedArguments );
			}
		);

		commonTests( fontSizes );
	} );

	describe( 'with ≤ 5 homogeneous font sizes', () => {
		const fontSizes = [
			{
				slug: 'small',
				// no name
				size: '12px',
			},
			{
				slug: 'medium',
				name: 'Medium',
				size: '16px',
			},
			{
				slug: 'large',
				name: 'Large',
				size: '22px',
			},
			{
				slug: 'huge',
				name: 'Huge',
				size: '30px',
			},
			{
				slug: 'gigantosaurus',
				name: 'Gigantosaurus',
				size: '40px',
			},
		];

		it( 'displays a toggle group control with t-shirt sizes', async () => {
			await render( <FontSizePicker fontSizes={ fontSizes } /> );
			const options = screen.getAllByRole( 'radio' );
			expect( options ).toHaveLength( 5 );
			expect( options[ 0 ] ).toHaveTextContent( 'S' );
			expect( options[ 0 ] ).toHaveAccessibleName( 'Small' );
			expect( options[ 1 ] ).toHaveTextContent( 'M' );
			expect( options[ 1 ] ).toHaveAccessibleName( 'Medium' );
			expect( options[ 2 ] ).toHaveTextContent( 'L' );
			expect( options[ 2 ] ).toHaveAccessibleName( 'Large' );
			expect( options[ 3 ] ).toHaveTextContent( 'XL' );
			expect( options[ 3 ] ).toHaveAccessibleName( 'Huge' );
			expect( options[ 4 ] ).toHaveTextContent( 'XXL' );
			expect( options[ 4 ] ).toHaveAccessibleName( 'Gigantosaurus' );
		} );

		it( 'calls onChange when a font size is selected', async () => {
			const user = userEvent.setup();
			const onChange = jest.fn();
			await render(
				<FontSizePicker fontSizes={ fontSizes } onChange={ onChange } />
			);
			await user.click( screen.getByRole( 'radio', { name: 'Medium' } ) );
			expect( onChange ).toHaveBeenCalledTimes( 1 );
			expect( onChange ).toHaveBeenCalledWith( '16px', fontSizes[ 1 ] );
		} );

		commonToggleGroupTests( fontSizes );
		commonTests( fontSizes );

		describe( 'valueMode prop for toggle group', () => {
			it( 'should find font size by size value when valueMode is literal', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value="16px"
						valueMode="literal"
					/>
				);
				// Should select the medium option (16px)
				expect(
					screen.getByRole( 'radio', { checked: true } )
				).toHaveAccessibleName( 'Medium' );
			} );

			it( 'should find font size by slug when valueMode is slug', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value="medium"
						valueMode="slug"
					/>
				);
				// Should select the medium option
				expect(
					screen.getByRole( 'radio', { checked: true } )
				).toHaveAccessibleName( 'Medium' );
			} );

			it( 'should handle multiple font sizes with same value in literal mode', async () => {
				const fontSizesWithDuplicates = [
					{
						slug: 'small-1',
						name: 'Small 1',
						size: '12px',
					},
					{
						slug: 'small-2',
						name: 'Small 2',
						size: '12px',
					},
					{
						slug: 'medium',
						name: 'Medium',
						size: '16px',
					},
				];
				await render(
					<FontSizePicker
						fontSizes={ fontSizesWithDuplicates }
						value="12px"
						valueMode="literal"
					/>
				);
				// Should have no selection when there are multiple matches
				expect(
					screen.queryByRole( 'radio', { checked: true } )
				).not.toBeInTheDocument();
			} );

			it( 'should handle multiple font sizes with same value in slug mode', async () => {
				const fontSizesWithDuplicates = [
					{
						slug: 'small-1',
						name: 'Small 1',
						size: '12px',
					},
					{
						slug: 'small-2',
						name: 'Small 2',
						size: '12px',
					},
					{
						slug: 'medium',
						name: 'Medium',
						size: '16px',
					},
				];
				await render(
					<FontSizePicker
						fontSizes={ fontSizesWithDuplicates }
						value="small-1"
						valueMode="slug"
					/>
				);
				// Should select the specific font size by slug
				expect(
					screen.getByRole( 'radio', { checked: true } )
				).toHaveAccessibleName( 'Small 1' );
			} );
		} );
	} );

	describe( 'with ≤ 5 heterogeneous font sizes', () => {
		const fontSizes = [
			{
				slug: 'small',
				name: 'Small',
				size: '12px',
			},
			{
				slug: 'medium',
				name: 'Medium',
				size: '1em',
			},
			{
				slug: 'large',
				name: 'Large',
				size: '2rem',
			},
			{
				slug: 'x-large',
				name: 'Extra Large',
				size: 'clamp(1.75rem, 3vw, 2.25rem)',
			},
		];

		it( 'displays a toggle group control with t-shirt sizes', async () => {
			await render( <FontSizePicker fontSizes={ fontSizes } /> );
			const options = screen.getAllByRole( 'radio' );
			expect( options ).toHaveLength( 4 );
			expect( options[ 0 ] ).toHaveTextContent( 'S' );
			expect( options[ 0 ] ).toHaveAccessibleName( 'Small' );
			expect( options[ 1 ] ).toHaveTextContent( 'M' );
			expect( options[ 1 ] ).toHaveAccessibleName( 'Medium' );
			expect( options[ 2 ] ).toHaveTextContent( 'L' );
			expect( options[ 2 ] ).toHaveAccessibleName( 'Large' );
			expect( options[ 3 ] ).toHaveTextContent( 'XL' );
			expect( options[ 3 ] ).toHaveAccessibleName( 'Extra Large' );
		} );

		test.each( [
			{ radio: 'Small', expectedArguments: [ '12px', fontSizes[ 0 ] ] },
			{ radio: 'Medium', expectedArguments: [ '1em', fontSizes[ 1 ] ] },
			{ radio: 'Large', expectedArguments: [ '2rem', fontSizes[ 2 ] ] },
			{
				radio: 'Extra Large',
				expectedArguments: [
					'clamp(1.75rem, 3vw, 2.25rem)',
					fontSizes[ 3 ],
				],
			},
		] )(
			'calls onChange( $expectedArguments ) when $radio is selected',
			async ( { radio, expectedArguments } ) => {
				const user = userEvent.setup();
				const onChange = jest.fn();
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						onChange={ onChange }
					/>
				);
				await user.click(
					screen.getByRole( 'radio', { name: radio } )
				);
				expect( onChange ).toHaveBeenCalledTimes( 1 );
				expect( onChange ).toHaveBeenCalledWith( ...expectedArguments );
			}
		);

		commonToggleGroupTests( fontSizes );
		commonTests( fontSizes );

		describe( 'valueMode prop for heterogeneous toggle group', () => {
			it( 'should find font size by size value when valueMode is literal', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value="1em"
						valueMode="literal"
					/>
				);
				// Should select the medium option (1em)
				expect(
					screen.getByRole( 'radio', { checked: true } )
				).toHaveAccessibleName( 'Medium' );
			} );

			it( 'should find font size by slug when valueMode is slug', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value="medium"
						valueMode="slug"
					/>
				);
				// Should select the medium option
				expect(
					screen.getByRole( 'radio', { checked: true } )
				).toHaveAccessibleName( 'Medium' );
			} );

			it( 'should handle complex font size values in literal mode', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value="clamp(1.75rem, 3vw, 2.25rem)"
						valueMode="literal"
					/>
				);
				// Should select the extra large option
				expect(
					screen.getByRole( 'radio', { checked: true } )
				).toHaveAccessibleName( 'Extra Large' );
			} );

			it( 'should handle complex font size values in slug mode', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value="x-large"
						valueMode="slug"
					/>
				);
				// Should select the extra large option
				expect(
					screen.getByRole( 'radio', { checked: true } )
				).toHaveAccessibleName( 'Extra Large' );
			} );
		} );
	} );

	function commonToggleGroupTests( fontSizes: FontSize[] ) {
		it( 'defaults to M when value is 16px', async () => {
			await render(
				<FontSizePicker
					fontSizes={ fontSizes }
					value={ fontSizes[ 0 ].size }
				/>
			);
			expect(
				screen.getByRole( 'radio', { checked: true } )
			).toHaveTextContent( 'S' );
		} );

		test.each( [ undefined, '' ] )(
			'has no selection when value is %p',
			async ( value ) => {
				await render(
					<FontSizePicker fontSizes={ fontSizes } value={ value } />
				);
				expect( screen.getByRole( 'radiogroup' ) ).toBeVisible();
				expect(
					screen.queryByRole( 'radio', { checked: true } )
				).not.toBeInTheDocument();
			}
		);
	}

	function commonTests( fontSizes: FontSize[] ) {
		it( 'shows custom input when value is unknown', async () => {
			await render(
				<FontSizePicker fontSizes={ fontSizes } value="3px" />
			);
			expect(
				screen.getByRole( 'spinbutton', { name: 'Font size' } )
			).toBeVisible();
		} );

		it( 'hides custom input when disableCustomFontSizes is set to `true` with a custom font size', async () => {
			const { rerender } = await render(
				<FontSizePicker fontSizes={ fontSizes } value="3px" />
			);
			expect(
				screen.getByRole( 'spinbutton', { name: 'Font size' } )
			).toBeVisible();

			rerender(
				<FontSizePicker
					disableCustomFontSizes
					fontSizes={ fontSizes }
					value="3px"
				/>
			);
			expect(
				screen.queryByLabelText( 'Custom' )
			).not.toBeInTheDocument();
		} );

		it( "doesn't hide custom input when the selected font size is a predef", async () => {
			const { rerender } = await render(
				<FontSizePicker fontSizes={ fontSizes } value="3px" />
			);
			expect(
				screen.getByRole( 'spinbutton', { name: 'Font size' } )
			).toBeVisible();

			rerender(
				<FontSizePicker
					fontSizes={ fontSizes }
					value={ fontSizes[ 0 ].size }
				/>
			);
			expect(
				screen.getByRole( 'spinbutton', { name: 'Font size' } )
			).toBeVisible();
		} );

		it( 'allows custom values by default', async () => {
			const user = userEvent.setup();
			const onChange = jest.fn();
			await render(
				<FontSizePicker fontSizes={ fontSizes } onChange={ onChange } />
			);
			await user.click(
				screen.getByRole( 'button', { name: 'Set custom size' } )
			);
			await user.type(
				screen.getByRole( 'spinbutton', { name: 'Font size' } ),
				'80'
			);
			expect( onChange ).toHaveBeenCalledTimes( 2 ); // Once per keystroke.
			expect( onChange ).toHaveBeenCalledWith( '80px' );
		} );

		it( 'allows switching between custom and predef inputs when pressing the dedicated toggle', async () => {
			const user = userEvent.setup();

			await render(
				<ControlledFontSizePicker fontSizes={ fontSizes } />
			);

			await user.click(
				screen.getByRole( 'button', { name: 'Set custom size' } )
			);

			await user.type(
				screen.getByRole( 'spinbutton', { name: 'Font size' } ),
				'80'
			);

			await user.click(
				screen.getByRole( 'button', { name: 'Use size preset' } )
			);

			expect(
				screen.queryByLabelText( 'Custom' )
			).not.toBeInTheDocument();
		} );

		it( 'does not allow custom values when disableCustomFontSizes is set', async () => {
			await render(
				<FontSizePicker
					fontSizes={ fontSizes }
					disableCustomFontSizes
				/>
			);
			expect(
				screen.queryByRole( 'button', { name: 'Set custom size' } )
			).not.toBeInTheDocument();
		} );

		it( 'does not display a slider by default', async () => {
			const user = userEvent.setup();
			await render( <FontSizePicker fontSizes={ fontSizes } /> );
			await user.click(
				screen.getByRole( 'button', { name: 'Set custom size' } )
			);
			expect(
				screen.queryByLabelText( 'Custom Size' )
			).not.toBeInTheDocument();
		} );

		it( 'allows a slider to be used when withSlider is set', async () => {
			const user = userEvent.setup();
			const onChange = jest.fn();
			await render(
				<FontSizePicker
					fontSizes={ fontSizes }
					withSlider
					onChange={ onChange }
				/>
			);
			await user.click(
				screen.getByRole( 'button', { name: 'Set custom size' } )
			);
			const sliderInput = screen.getByRole( 'slider', {
				name: 'Font size',
			} );
			fireEvent.change( sliderInput, {
				target: { value: 80 },
			} );
			expect( onChange ).toHaveBeenCalledTimes( 1 );
			expect( onChange ).toHaveBeenCalledWith( '80px' );
		} );

		it( 'allows reset by default', async () => {
			const user = userEvent.setup();
			const onChange = jest.fn();
			await render(
				<FontSizePicker
					fontSizes={ fontSizes }
					value={ fontSizes[ 0 ].size }
					onChange={ onChange }
				/>
			);
			await user.click(
				screen.getByRole( 'button', { name: 'Set custom size' } )
			);
			await user.click( screen.getByRole( 'button', { name: 'Reset' } ) );
			expect( onChange ).toHaveBeenCalledTimes( 1 );
			expect( onChange ).toHaveBeenCalledWith( undefined );
		} );

		it( 'does not allow reset when withReset is false', async () => {
			const user = userEvent.setup();
			await render(
				<FontSizePicker
					fontSizes={ fontSizes }
					value={ fontSizes[ 0 ].size }
					withReset={ false }
				/>
			);
			await user.click(
				screen.getByRole( 'button', { name: 'Set custom size' } )
			);
			expect(
				screen.queryByRole( 'button', { name: 'Reset' } )
			).not.toBeInTheDocument();
		} );

		it( 'applies custom units to custom font size control', async () => {
			const user = userEvent.setup();

			await render(
				<FontSizePicker
					fontSizes={ fontSizes }
					value={ fontSizes[ 0 ].size }
					units={ [ 'px', 'ch', 'ex' ] }
				/>
			);

			await user.click(
				screen.getByRole( 'button', { name: 'Set custom size' } )
			);

			const units = screen.getAllByRole( 'option' );
			expect( units ).toHaveLength( 3 );
			expect( units[ 0 ] ).toHaveAccessibleName( 'px' );
			expect( units[ 1 ] ).toHaveAccessibleName( 'ch' );
			expect( units[ 2 ] ).toHaveAccessibleName( 'ex' );
		} );
	}

	describe( 'valueMode prop', () => {
		// Use 6 font sizes to trigger select control (> 5)
		const fontSizes = [
			{
				slug: 'small',
				name: 'Small',
				size: '12px',
			},
			{
				slug: 'medium',
				name: 'Medium',
				size: '16px',
			},
			{
				slug: 'large',
				name: 'Large',
				size: '20px',
			},
			{
				slug: 'x-large',
				name: 'Extra Large',
				size: '24px',
			},
			{
				slug: 'xx-large',
				name: 'XX Large',
				size: '28px',
			},
			{
				slug: 'huge',
				name: 'Huge',
				size: '32px',
			},
		];

		describe( 'valueMode="literal" (default)', () => {
			it( 'should find font size by size value when valueMode is literal', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value="16px"
						valueMode="literal"
					/>
				);
				// Should select the medium option (16px)
				expect(
					screen.getByRole( 'combobox', { name: 'Font size' } )
				).toHaveTextContent( 'Medium' );
			} );

			it( 'should call onChange with size value and FontSize object when valueMode is literal', async () => {
				const user = userEvent.setup();
				const onChange = jest.fn();
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						onChange={ onChange }
						valueMode="literal"
					/>
				);
				await user.click(
					screen.getByRole( 'combobox', { name: 'Font size' } )
				);
				await user.click(
					screen.getByRole( 'option', { name: 'Medium 16px' } )
				);
				expect( onChange ).toHaveBeenCalledWith(
					'16px',
					fontSizes[ 1 ]
				);
			} );
		} );

		describe( 'valueMode="slug"', () => {
			it( 'should find font size by slug when valueMode is slug', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value="medium"
						valueMode="slug"
					/>
				);
				// Should select the medium option
				expect(
					screen.getByRole( 'combobox', { name: 'Font size' } )
				).toHaveTextContent( 'Medium' );
			} );

			it( 'should call onChange with size value and FontSize object when valueMode is slug', async () => {
				const user = userEvent.setup();
				const onChange = jest.fn();
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						onChange={ onChange }
						valueMode="slug"
					/>
				);
				await user.click(
					screen.getByRole( 'combobox', { name: 'Font size' } )
				);
				await user.click(
					screen.getByRole( 'option', { name: 'Large 20px' } )
				);
				expect( onChange ).toHaveBeenCalledWith(
					'20px',
					fontSizes[ 2 ]
				);
			} );

			it( 'should handle undefined value when valueMode is slug', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value={ undefined }
						valueMode="slug"
					/>
				);
				// Should show default option
				expect(
					screen.getByRole( 'combobox', { name: 'Font size' } )
				).toHaveTextContent( 'Default' );
			} );

			it( 'should handle empty string value when valueMode is slug', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizes }
						value=""
						valueMode="slug"
					/>
				);
				// Should show default option
				expect(
					screen.getByRole( 'combobox', { name: 'Font size' } )
				).toHaveTextContent( 'Default' );
			} );
		} );

		describe( 'edge cases with valueMode', () => {
			// Use 6 font sizes to trigger select control (> 5)
			const fontSizesWithDuplicates = [
				{
					slug: 'small-1',
					name: 'Small 1',
					size: '12px',
				},
				{
					slug: 'small-2',
					name: 'Small 2',
					size: '12px',
				},
				{
					slug: 'medium',
					name: 'Medium',
					size: '16px',
				},
				{
					slug: 'large',
					name: 'Large',
					size: '20px',
				},
				{
					slug: 'x-large',
					name: 'Extra Large',
					size: '24px',
				},
				{
					slug: 'huge',
					name: 'Huge',
					size: '28px',
				},
			];

			it( 'should handle multiple font sizes with same value in literal mode', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizesWithDuplicates }
						value="12px"
						valueMode="literal"
					/>
				);
				// Should show the first matching font size when there are multiple matches
				expect(
					screen.getByRole( 'combobox', { name: 'Font size' } )
				).toHaveTextContent( 'Small 1' );
			} );

			it( 'should handle multiple font sizes with same value in slug mode', async () => {
				await render(
					<FontSizePicker
						fontSizes={ fontSizesWithDuplicates }
						value="small-1"
						valueMode="slug"
					/>
				);
				// Should select the specific font size by slug
				expect(
					screen.getByRole( 'combobox', { name: 'Font size' } )
				).toHaveTextContent( 'Small 1' );
			} );
		} );
	} );

	describe( 'onChange callback signature', () => {
		// Use 6 font sizes to trigger select control (> 5)
		const fontSizes = [
			{
				slug: 'small',
				name: 'Small',
				size: '12px',
			},
			{
				slug: 'medium',
				name: 'Medium',
				size: '16px',
			},
			{
				slug: 'large',
				name: 'Large',
				size: '20px',
			},
			{
				slug: 'x-large',
				name: 'Extra Large',
				size: '24px',
			},
			{
				slug: 'xx-large',
				name: 'XX Large',
				size: '28px',
			},
			{
				slug: 'huge',
				name: 'Huge',
				size: '32px',
			},
		];

		it( 'should call onChange with FontSize object as second parameter for select control', async () => {
			const user = userEvent.setup();
			const onChange = jest.fn();
			await render(
				<FontSizePicker fontSizes={ fontSizes } onChange={ onChange } />
			);
			await user.click(
				screen.getByRole( 'combobox', { name: 'Font size' } )
			);
			await user.click(
				screen.getByRole( 'option', { name: 'Small 12px' } )
			);
			expect( onChange ).toHaveBeenCalledWith( '12px', fontSizes[ 0 ] );
		} );

		it( 'should call onChange with undefined as second parameter for default option', async () => {
			const user = userEvent.setup();
			const onChange = jest.fn();
			await render(
				<FontSizePicker
					fontSizes={ fontSizes }
					value="16px" // Start with a selected value
					onChange={ onChange }
				/>
			);
			await user.click(
				screen.getByRole( 'combobox', { name: 'Font size' } )
			);
			await user.click(
				screen.getByRole( 'option', { name: 'Default' } )
			);
			expect( onChange ).toHaveBeenCalledWith( undefined, undefined );
		} );

		it( 'should call onChange with FontSize object as second parameter for toggle group control', async () => {
			const user = userEvent.setup();
			const onChange = jest.fn();
			// Use fewer font sizes to trigger toggle group (≤ 5)
			const toggleGroupFontSizes = [
				{
					slug: 'small',
					name: 'Small',
					size: '12px',
				},
				{
					slug: 'medium',
					name: 'Medium',
					size: '16px',
				},
				{
					slug: 'large',
					name: 'Large',
					size: '20px',
				},
			];
			await render(
				<FontSizePicker
					fontSizes={ toggleGroupFontSizes }
					onChange={ onChange }
				/>
			);
			await user.click( screen.getByRole( 'radio', { name: 'Small' } ) );
			expect( onChange ).toHaveBeenCalledWith(
				'12px',
				toggleGroupFontSizes[ 0 ]
			);
		} );
	} );
} );
