/**
 * External dependencies
 */
import { render } from '@testing-library/react';

/**
 * Internal dependencies
 */
import { createElement, Fragment, Component } from '../react';
import createInterpolateElement from '../create-interpolate-element';
import type {
	ExtractTags,
	InterpolationInput,
	InterpolationString,
} from '../types';

describe( 'createInterpolateElement', () => {
	it( 'throws an error when there is no conversion map', () => {
		const testString = 'This is a string';
		expect( () => createInterpolateElement( testString, {} ) ).toThrow(
			TypeError
		);
	} );
	it( 'returns same string when there are no tokens in the string', () => {
		const testString = 'This is a string';
		const expectedElement = <>{ testString }</>;
		expect(
			createInterpolateElement( testString, { someValue: <em /> } )
		).toEqual( expectedElement );
	} );
	it( 'throws an error when there is an invalid conversion map', () => {
		const testString = 'This is a <someValue/> string';
		expect( () =>
			createInterpolateElement(
				testString,
				// @ts-expect-error - Invalid argument type
				[ 'someValue', { value: 10 } ]
			)
		).toThrow( TypeError );
	} );
	it(
		'throws an error when there is an invalid entry in the conversion ' +
			'map',
		() => {
			const testString = 'This is a <item /> string and <somethingElse/>';
			expect( () =>
				createInterpolateElement( testString, {
					item: <em />,
					// @ts-expect-error - Invalid type for somethingElse
					somethingElse: 10,
				} )
			).toThrow( TypeError );
		}
	);
	it(
		'returns same string when there is an non matching token in the ' +
			'string',
		() => {
			const testString = 'This is a <non_parsed/> string';
			const expectedElement = <>{ testString }</>;
			expect(
				createInterpolateElement( testString, {
					// @ts-expect-error - Unknown tag
					someValue: <strong />,
				} )
			).toEqual( expectedElement );
		}
	);
	it( 'returns same string when there is spaces in the token', () => {
		const testString = 'This is a <spaced token/>string';
		const expectedElement = <>{ testString }</>;
		expect(
			createInterpolateElement( testString, { 'spaced token': <em /> } )
		).toEqual( expectedElement );
	} );
	it( 'returns expected react element for non nested components', () => {
		const testString = 'This is a string with <a>a link</a>.';
		const expectedElement = createElement(
			Fragment,
			null,
			'This is a string with ',
			createElement(
				'a',
				{ href: 'https://github.com', className: 'some_class' },
				'a link'
			),
			'.'
		);
		const component = createInterpolateElement( testString, {
			// eslint-disable-next-line jsx-a11y/anchor-has-content
			a: <a href="https://github.com" className="some_class" />,
		} );
		expect( JSON.stringify( component ) ).toEqual(
			JSON.stringify( expectedElement )
		);
	} );
	it( 'returns expected react element for nested components', () => {
		const testString = 'This is a <a>string that is <em>linked</em></a>.';
		const expectedElement = createElement(
			Fragment,
			{},
			'This is a ',
			createElement(
				'a',
				null,
				'string that is ',
				createElement( 'em', null, 'linked' )
			),
			'.'
		);
		expect(
			JSON.stringify(
				createInterpolateElement( testString, {
					a: createElement( 'a' ),
					em: <em />,
				} )
			)
		).toEqual( JSON.stringify( expectedElement ) );
	} );
	it(
		'returns expected output for a custom component with children ' +
			'replacement',
		() => {
			const TestComponent = ( props ) => {
				return <div { ...props }>{ props.children }</div>;
			};
			const testString =
				'This is a string with a <TestComponent>Custom Component</TestComponent>';
			const expectedElement = createElement(
				Fragment,
				null,
				'This is a string with a ',
				createElement( TestComponent, null, 'Custom Component' )
			);
			expect(
				JSON.stringify(
					createInterpolateElement( testString, {
						TestComponent: <TestComponent />,
					} )
				)
			).toEqual( JSON.stringify( expectedElement ) );
		}
	);
	it( 'returns expected output for self closing custom component', () => {
		const TestComponent = ( props ) => {
			return <div { ...props } />;
		};
		const testString =
			'This is a string with a self closing custom component: <TestComponent/>';
		const expectedElement = createElement(
			Fragment,
			null,
			'This is a string with a self closing custom component: ',
			createElement( TestComponent )
		);
		expect(
			JSON.stringify(
				createInterpolateElement( testString, {
					TestComponent: <TestComponent />,
				} )
			)
		).toEqual( JSON.stringify( expectedElement ) );
	} );
	it( 'throws an error with an invalid element in the conversion map', () => {
		const test = () =>
			createInterpolateElement( 'This is a <invalid /> string', {
				invalid: 10,
			} );
		expect( test ).toThrow( TypeError );
	} );
	it( 'returns expected output for complex replacement', () => {
		class TestComponent extends Component {
			render() {
				return <div { ...this.props } />;
			}
		}
		const testString =
			'This is a complex string with ' +
			'a <a1>nested <em1>emphasized string</em1> link</a1> and value: <TestComponent/>';
		const expectedElement = createElement(
			Fragment,
			null,
			'This is a complex string with a ',
			createElement(
				'a',
				null,
				'nested ',
				createElement( 'em', null, 'emphasized string' ),
				' link'
			),
			' and value: ',
			createElement( TestComponent )
		);
		expect(
			JSON.stringify(
				createInterpolateElement( testString, {
					TestComponent: <TestComponent />,
					em1: <em />,
					a1: createElement( 'a' ),
				} )
			)
		).toEqual( JSON.stringify( expectedElement ) );
	} );
	it( 'renders expected components across renders for keys in use', () => {
		const TestComponent = ( { switchKey } ) => {
			const elementConfig = switchKey
				? { item: <em /> }
				: { item: <strong /> };
			return (
				<div>
					{ createInterpolateElement(
						'This is a <item>string!</item>',
						elementConfig
					) }
				</div>
			);
		};
		const { container, rerender } = render( <TestComponent switchKey /> );

		expect( container ).toContainHTML( '<em>string!</em>' );
		expect( container ).not.toContainHTML( '<strong>' );

		rerender( <TestComponent switchKey={ false } /> );

		expect( container ).toContainHTML( '<strong>string!</strong>' );
		expect( container ).not.toContainHTML( '<em>' );
	} );
	it( 'extracts all tag names from a template literal string', () => {
		// Type-level test: verify ExtractTags extracts all tags correctly.
		type Tags = ExtractTags< '<a>link</a> and <b>bold <c>nested</c></b>' >;
		const tags: Tags[] = [ 'a', 'b', 'c' ];
		expect( tags ).toHaveLength( 3 );
	} );
	it( 'extracts tags from a TransformedText input', () => {
		// Type-level test: verify InterpolationString unwraps TransformedText.
		type Text = InterpolationString<
			string & {
				readonly __transformedText: '<a>link</a> and <em>emphasis</em>';
			}
		>;
		type Tags = ExtractTags< Text >;
		const tags: Tags[] = [ 'a', 'em' ];
		expect( tags ).toHaveLength( 2 );
	} );
	it( 'extracts tags from a sprintf (TransformedText) input', () => {
		// Type-level test: sprintf returns TransformedText, so
		// InterpolationString unwraps it for tag inference.
		type SprintfResult = string & {
			readonly __transformedText: '<Name>%1$s</Name> wrote <Link>%2$s</Link>';
		};
		const _check: InterpolationInput = '' as SprintfResult;
		void _check;

		type Text = InterpolationString< SprintfResult >;
		type Tags = ExtractTags< Text >;
		const tags: Tags[] = [ 'Name', 'Link' ];
		expect( tags ).toHaveLength( 2 );
	} );
	it( 'handles parsing emojii correctly', () => {
		const testString = '👳‍♀️<icon>🚨🤷‍♂️⛈️fully</icon> here';
		const expectedElement = createElement(
			Fragment,
			null,
			'👳‍♀️',
			createElement( 'strong', null, '🚨🤷‍♂️⛈️fully' ),
			' here'
		);
		expect(
			JSON.stringify(
				createInterpolateElement( testString, {
					icon: <strong />,
				} )
			)
		).toEqual( JSON.stringify( expectedElement ) );
	} );
} );
