/**
 * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
 */

import { ClassicTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js';
import { VirtualTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor.js';
import {
	ViewDowncastWriter,
	ViewUpcastWriter,
	ViewDocument,
	StylesProcessor,
	_setModelData,
	_parseView,
	_stringifyView
} from '@ckeditor/ckeditor5-engine';

import { Table } from '@ckeditor/ckeditor5-table';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';

import { Image } from '../../src/image.js';
import { ImageEditing } from '../../src/image/imageediting.js';
import { ImageBlockEditing } from '../../src/image/imageblockediting.js';
import { ImageInlineEditing } from '../../src/image/imageinlineediting.js';
import { ImageUtils } from '../../src/imageutils.js';

import {
	getImgViewElementMatcher,
	createBlockImageViewElement,
	createInlineImageViewElement,
	determineImageTypeForInsertionAtSelection
} from '../../src/image/utils.js';

describe( 'image utils', () => {
	let editor, imageUtils, element, image, writer, viewDocument;

	beforeEach( async () => {
		editor = await VirtualTestEditor.create( {
			plugins: [ ImageUtils ]
		} );

		imageUtils = editor.plugins.get( 'ImageUtils' );

		viewDocument = new ViewDocument( new StylesProcessor() );
		writer = new ViewDowncastWriter( viewDocument );
		image = writer.createContainerElement( 'img' );
		element = writer.createContainerElement( 'figure' );
		writer.insert( writer.createPositionAt( element, 0 ), image );
		imageUtils.toImageWidget( element, writer, 'image widget' );
	} );

	afterEach( async () => {
		return editor.destroy();
	} );

	describe( 'determineImageTypeForInsertionAtSelection()', () => {
		let editor, model, schema;

		beforeEach( async () => {
			editor = await VirtualTestEditor.create( {
				plugins: [ ImageUtils, ImageBlockEditing, ImageInlineEditing, Paragraph ]
			} );

			imageUtils = editor.plugins.get( 'ImageUtils' );
			model = editor.model;
			schema = model.schema;
			schema.register( 'block', {
				inheritAllFrom: '$block'
			} );
			schema.register( 'blockWidget', {
				isObject: true,
				allowIn: '$root'
			} );
			schema.register( 'inlineWidget', {
				isObject: true,
				allowIn: [ '$block' ]
			} );
			schema.register( 'listItem', {
				inheritAllFrom: '$block'
			} );

			schema.extend( '$text', { allowIn: [ 'block', '$root' ] } );

			editor.conversion.for( 'downcast' ).elementToElement( { model: 'block', view: 'block' } );
			editor.conversion.for( 'downcast' ).elementToElement( { model: 'listItem', view: 'li' } );
			editor.conversion.for( 'downcast' ).elementToElement( { model: 'blockWidget', view: 'blockWidget' } );
			editor.conversion.for( 'downcast' ).elementToElement( { model: 'inlineWidget', view: 'inlineWidget' } );
		} );

		afterEach( async () => {
			return editor.destroy();
		} );

		it( 'should return "image" when there is no selected block in the selection', () => {
			_setModelData( model, 'f[]oo' );

			expect( determineImageTypeForInsertionAtSelection( schema, model.document.selection ) ).to.equal( 'imageBlock' );
		} );

		it( 'should return "image" when the selected block in the selection is empty', () => {
			_setModelData( model, '<block>[]</block>' );

			expect( determineImageTypeForInsertionAtSelection( schema, model.document.selection ) ).to.equal( 'imageBlock' );
		} );

		it( 'should return "imageInline" when the selected listItem in the selection is empty', () => {
			_setModelData( model, '<listItem>[]</listItem>' );

			expect( determineImageTypeForInsertionAtSelection( schema, model.document.selection ) ).to.equal( 'imageInline' );
		} );

		it( 'should return "image" when the selected block is an object (a widget)', () => {
			_setModelData( model, '[<blockWidget></blockWidget>]' );

			expect( determineImageTypeForInsertionAtSelection( schema, model.document.selection ) ).to.equal( 'imageBlock' );
		} );

		it( 'should return "imageInline" when selected block in the selection has some content', () => {
			_setModelData( model, '<block>[]a</block>' );

			expect( determineImageTypeForInsertionAtSelection( schema, model.document.selection ) ).to.equal( 'imageInline' );
		} );

		it( 'should return "imageInline" when an inline widget is selected', () => {
			_setModelData( model, '<block>[<inlineWidget></inlineWidget>]</block>' );

			expect( determineImageTypeForInsertionAtSelection( schema, model.document.selection ) ).to.equal( 'imageInline' );
		} );
	} );

	describe( 'getImgViewElementMatcher()', () => {
		describe( 'when one of the image editing plugins is not loaded', () => {
			let editor;

			beforeEach( async () => {
				editor = await VirtualTestEditor.create( {
					plugins: [ ImageUtils, ImageEditing ]
				} );

				imageUtils = editor.plugins.get( 'ImageUtils' );

				writer = new ViewUpcastWriter( editor.editing.view.document );
			} );

			afterEach( async () => {
				editor.destroy();
			} );

			it( 'should return a matcher pattern for an img element if ImageBlockEditing plugin is not loaded', () => {
				sinon.stub( editor.plugins, 'has' ).callsFake( pluginName => pluginName !== 'ImageBlockEditing' );

				element = writer.createElement( 'img', { src: 'assets/sample.png' } );
				writer.appendChild( element, writer.createElement( 'figure', { class: 'image' } ) );

				expect( getImgViewElementMatcher( editor, 'imageBlock' )( element ) ).to.deep.equal( {
					name: true,
					attributes: [ 'src' ]
				} );

				expect( getImgViewElementMatcher( editor, 'imageInline' )( element ) ).to.deep.equal( {
					name: true,
					attributes: [ 'src' ]
				} );
			} );

			it( 'should return a matcher pattern for an img element if ImageInlineEditing plugin is not loaded', () => {
				sinon.stub( editor.plugins, 'has' ).callsFake( pluginName => pluginName !== 'ImageInlineEditing' );

				element = writer.createElement( 'img', { src: 'assets/sample.png' } );
				writer.appendChild( element, writer.createElement( 'figure', { class: 'image' } ) );

				expect( getImgViewElementMatcher( editor, 'imageBlock' )( element ) ).to.deep.equal( {
					name: true,
					attributes: [ 'src' ]
				} );

				expect( getImgViewElementMatcher( editor, 'imageInline' )( element ) ).to.deep.equal( {
					name: true,
					attributes: [ 'src' ]
				} );
			} );

			it( 'should not include "src" in the matcher pattern if the image has no "src"', () => {
				sinon.stub( editor.plugins, 'has' ).callsFake( pluginName => pluginName !== 'ImageInlineEditing' );

				element = writer.createElement( 'img' );
				writer.appendChild( element, writer.createElement( 'figure', { class: 'image' } ) );

				expect( getImgViewElementMatcher( editor, 'imageBlock' )( element ) ).to.deep.equal( {
					name: true
				} );

				expect( getImgViewElementMatcher( editor, 'imageInline' )( element ) ).to.deep.equal( {
					name: true
				} );
			} );
		} );

		describe( 'when both image editing plugins are loaded', () => {
			let editor, matcherPattern, editorElement;

			beforeEach( async () => {
				editorElement = document.createElement( 'div' );
				document.body.appendChild( editorElement );

				editor = await ClassicTestEditor.create( editorElement, {
					plugins: [ ImageUtils, Image, Paragraph, Table ]
				} );

				imageUtils = editor.plugins.get( 'ImageUtils' );

				writer = new ViewUpcastWriter( editor.editing.view.document );
			} );

			afterEach( async () => {
				editorElement.remove();
				await editor.destroy();
			} );

			describe( 'the returned matcherPattern function', () => {
				describe( 'for the "imageBlock" type requested', () => {
					beforeEach( () => {
						matcherPattern = getImgViewElementMatcher( editor, 'imageBlock' );
					} );

					it( 'should return a function', () => {
						expect( matcherPattern ).to.be.a( 'function' );
					} );

					it( 'should return null if the element is not an image', () => {
						element = writer.createElement( 'media', { src: 'assets/sample.png' } );

						expect( matcherPattern( element ) ).to.be.null;
					} );

					it( 'should return null if the element has no src property', () => {
						element = writer.createElement( 'img' );

						expect( matcherPattern( element ) ).to.be.null;
					} );

					it( 'should return null if the element is an "imageInline"', () => {
						element = writer.createElement( 'img', { src: 'assets/sample.png' } );

						expect( matcherPattern( element ) ).to.be.null;
					} );

					it( 'should return null if the element is an "imageInline" in a table', () => {
						const fragment = _parseView(
							'<figure><table><tbody><tr><td>' +
								'[<img src="assets/sample.png"></img>]' +
							'</td></tr></tbody></table></figure>'
						);

						expect( matcherPattern( fragment.selection.getSelectedElement() ) ).to.be.null;
					} );

					it( 'should return a matcherPattern object if the element is an "image"', () => {
						element = writer.createElement( 'img', { src: 'assets/sample.png' } );
						writer.appendChild( element, writer.createElement( 'figure', { class: 'image' } ) );

						expect( matcherPattern( element ) ).to.deep.equal( {
							name: true,
							attributes: [ 'src' ]
						} );
					} );

					it( 'should return a matcherPattern object if the element has `display:block` style', () => {
						element = writer.createElement( 'img', { src: 'assets/sample.png', style: 'display:block' } );

						expect( matcherPattern( element ) ).to.deep.equal( {
							name: true,
							attributes: [ 'src' ]
						} );
					} );

					it( 'should not include "src" in the matcher pattern if the image has no "src"', () => {
						element = writer.createElement( 'img' );
						writer.appendChild( element, writer.createElement( 'figure', { class: 'image' } ) );

						expect( matcherPattern( element ) ).to.deep.equal( {
							name: true
						} );
					} );
				} );

				describe( 'for the "imageInline" type requested', () => {
					beforeEach( () => {
						matcherPattern = getImgViewElementMatcher( editor, 'imageInline' );
					} );

					it( 'should return a function', () => {
						expect( matcherPattern ).to.be.a( 'function' );
					} );

					it( 'should return null if the element is not an "image"', () => {
						expect( matcherPattern( element ) ).to.be.null;
					} );

					it( 'should return null if the element has no src property', () => {
						element = writer.createElement( 'media', { src: 'assets/sample.png' } );

						expect( matcherPattern( element ) ).to.be.null;
					} );

					it( 'should return null if the element is an "image"', () => {
						element = writer.createElement( 'img', { src: 'assets/sample.png' } );
						writer.appendChild( element, writer.createElement( 'figure', { class: 'image' } ) );

						expect( matcherPattern( element ) ).to.be.null;
					} );

					it( 'should return a matcherPattern object if the element is an "imageInline"', () => {
						element = writer.createElement( 'img', { src: 'assets/sample.png' } );

						expect( matcherPattern( element ) ).to.deep.equal( {
							name: true,
							attributes: [ 'src' ]
						} );
					} );

					it( 'should return a matcherPattern object if the element is an "imageInline" in a table', () => {
						const fragment = _parseView(
							'<figure><table><tbody><tr><td>' +
								'[<img src="assets/sample.png"></img>]' +
							'</td></tr></tbody></table></figure>'
						);

						expect( matcherPattern( fragment.selection.getSelectedElement() ) ).to.deep.equal( {
							name: true,
							attributes: [ 'src' ]
						} );
					} );

					it( 'should not include "src" in the matcher pattern if the image has no "src"', () => {
						element = writer.createElement( 'img' );

						expect( matcherPattern( element ) ).to.deep.equal( {
							name: true
						} );
					} );
				} );
			} );
		} );
	} );

	describe( 'createBlockImageViewElement()', () => {
		let writer;

		beforeEach( () => {
			const document = new ViewDocument( new StylesProcessor() );
			writer = new ViewDowncastWriter( document );
		} );

		it( 'should create a figure element for "image" type', () => {
			sinon.stub( writer, 'createSlot' ).callsFake( function createSlot() {
				return writer.createEmptyElement( '$slot' );
			} );

			const element = createBlockImageViewElement( writer );

			expect( element.is( 'element', 'figure' ) ).to.be.true;
			expect( element.hasClass( 'image' ) ).to.be.true;
			expect( element.childCount ).to.equal( 2 );
			expect( element.getChild( 0 ).is( 'emptyElement', 'img' ) ).to.be.true;
			expect( element.getChild( 1 ).is( 'emptyElement', '$slot' ) ).to.be.true;
		} );
	} );

	describe( 'createInlineImageViewElement()', () => {
		let writer;

		beforeEach( () => {
			const document = new ViewDocument( new StylesProcessor() );
			writer = new ViewDowncastWriter( document );
		} );

		it( 'should create a span element for "imageInline" type', () => {
			const element = createInlineImageViewElement( writer );

			expect( element.is( 'element', 'span' ) ).to.be.true;
			expect( element.hasClass( 'image-inline' ) ).to.be.true;
			expect( element.childCount ).to.equal( 1 );
			expect( element.getChild( 0 ).is( 'emptyElement', 'img' ) ).to.be.true;
		} );

		it( 'should create a span element for "imageInline" type that does not break the parent attribute element', () => {
			const paragraph = writer.createContainerElement( 'p' );
			const imageElement = createInlineImageViewElement( writer );
			const attributeElement = writer.createAttributeElement( 'a', { foo: 'bar' } );

			writer.insert( writer.createPositionAt( paragraph, 0 ), imageElement );
			writer.insert( writer.createPositionAt( paragraph, 0 ), writer.createText( 'foo' ) );
			writer.wrap( writer.createRangeIn( paragraph ), attributeElement );

			expect( _stringifyView( paragraph ) ).to.equal(
				'<p><a foo="bar">foo<span class="image-inline"><img></img></span></a></p>'
			);
		} );
	} );
} );
