/**
 * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

import Model from '../../src/model/model';
import Writer from '../../src/model/writer';
import Batch from '../../src/model/batch';
import InsertOperation from '../../src/model/operation/insertoperation';

import DocumentFragment from '../../src/model/documentfragment';
import Element from '../../src/model/element';
import Text from '../../src/model/text';
import Position from '../../src/model/position';
import Range from '../../src/model/range';

import count from '@ckeditor/ckeditor5-utils/src/count';

import { getNodesAndText } from '../../tests/model/_utils/utils';
import DocumentSelection from '../../src/model/documentselection';
import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';

describe( 'Writer', () => {
	let model, doc, batch;

	beforeEach( () => {
		model = new Model();
		batch = new Batch();

		doc = model.document;
	} );

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

		beforeEach( () => {
			writer = new Writer( model, batch );
		} );

		it( 'should have model instance', () => {
			expect( writer.model ).to.instanceof( Model );
		} );

		it( 'should have batch instance', () => {
			expect( writer.batch ).to.instanceof( Batch );
		} );
	} );

	describe( 'createText()', () => {
		it( 'should create text node', () => {
			const text = createText( 'foo' );

			expect( text ).to.instanceof( Text );
			expect( text.data ).to.equal( 'foo' );
			expect( Array.from( text.getAttributes() ) ).to.length( 0 );
		} );

		it( 'should create text with attributes', () => {
			const text = createText( 'foo', { foo: 'bar', biz: 'baz' } );

			expect( Array.from( text.getAttributes() ) ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'biz', 'baz' ] ] );
		} );
	} );

	describe( 'createElement()', () => {
		it( 'should create element', () => {
			const element = createElement( 'foo' );

			expect( element ).to.instanceof( Element );
			expect( element.name ).to.equal( 'foo' );
			expect( Array.from( element.getAttributes() ) ).to.length( 0 );
		} );

		it( 'should create element with attributes', () => {
			const element = createText( 'foo', { foo: 'bar', biz: 'baz' } );

			expect( Array.from( element.getAttributes() ) ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'biz', 'baz' ] ] );
		} );
	} );

	describe( 'createDocumentFragment()', () => {
		it( 'should create element', () => {
			const element = createDocumentFragment();

			expect( element ).to.instanceof( DocumentFragment );
		} );
	} );

	describe( 'insert()', () => {
		it( 'should insert node at given position', () => {
			const parent = createDocumentFragment();
			const child = createElement( 'child' );
			const textChild = createText( 'textChild' );

			insert( child, new Position( parent, [ 0 ] ) );
			insert( textChild, new Position( parent, [ 1 ] ) );

			expect( Array.from( parent ) ).to.deep.equal( [ child, textChild ] );
		} );

		it( 'should insert node at the beginning of given element', () => {
			const parent = createDocumentFragment();
			const child1 = createElement( 'child' );
			const child2 = createElement( 'child' );

			insert( child1, parent );
			insert( child2, parent );

			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child2, child1 ] );
		} );

		it( 'should insert node at the end of given element', () => {
			const parent = createDocumentFragment();
			const child1 = createElement( 'child' );
			const child2 = createElement( 'child' );

			insert( child1, parent );
			insert( child2, parent, 'end' );

			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2 ] );
		} );

		it( 'should insert node at the given offset of given element', () => {
			const parent = createDocumentFragment();
			const child1 = createElement( 'child' );
			const child2 = createElement( 'child' );
			const child3 = createElement( 'child' );

			insert( child3, parent );
			insert( child1, parent );
			insert( child2, parent, 1 );

			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] );
		} );

		it( 'should insert node before the given node', () => {
			const parent = createDocumentFragment();
			const child1 = createElement( 'child' );
			const child2 = createElement( 'child' );
			const child3 = createElement( 'child' );

			insert( child3, parent );
			insert( child1, parent );
			insert( child2, child3, 'before' );

			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] );
		} );

		it( 'should insert node after the given node', () => {
			const parent = createDocumentFragment();
			const child1 = createElement( 'child' );
			const child2 = createElement( 'child' );
			const child3 = createElement( 'child' );

			insert( child3, parent );
			insert( child1, parent );
			insert( child2, child1, 'after' );

			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] );
		} );

		it( 'should do nothing if empty text node is being inserted', () => {
			const parent = createDocumentFragment();

			model.enqueueChange( batch, writer => {
				const text = writer.createText( '' );

				writer.insert( text, parent );
			} );

			expect( parent.childCount ).to.equal( 0 );
		} );

		it( 'should create proper operation for inserting element', () => {
			const parent = createDocumentFragment();
			const element = createElement( 'child' );

			const spy = sinon.spy( model, 'applyOperation' );

			insert( element, parent );

			sinon.assert.calledOnce( spy );

			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
			expect( spy.lastCall.args[ 0 ] ).to.instanceof( InsertOperation );
			expect( spy.lastCall.args[ 0 ].shouldReceiveAttributes ).to.be.false;
			expect( spy.lastCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should create proper operation for inserting text', () => {
			const parent = createDocumentFragment();
			const text = createText( 'child' );

			const spy = sinon.spy( model, 'applyOperation' );

			insert( text, parent );

			sinon.assert.calledOnce( spy );
			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
			expect( spy.lastCall.args[ 0 ] ).to.instanceof( InsertOperation );
			expect( spy.lastCall.args[ 0 ].shouldReceiveAttributes ).to.be.true;
			expect( spy.lastCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should move element from one parent to the other within the same document (rootA -> rootA)', () => {
			const root = doc.createRoot();
			const parent1 = createElement( 'parent' );
			const parent2 = createElement( 'parent' );
			const node = createText( 'foo' );

			insert( node, parent1 );
			insert( parent1, root );
			insert( parent2, root );

			const spy = sinon.spy( model, 'applyOperation' );

			insert( node, parent2 );

			// Verify result.
			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );

			// Verify operations.
			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
			expect( spy.firstCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should move element from one parent to the other within the same document (rootA -> rootB)', () => {
			const rootA = doc.createRoot( '$root', 'A' );
			const rootB = doc.createRoot( '$root', 'B' );
			const node = createText( 'foo' );

			insert( node, rootA );

			const spy = sinon.spy( model, 'applyOperation' );

			insert( node, rootB );

			// Verify result.
			expect( Array.from( rootA.getChildren() ) ).to.deep.equal( [] );
			expect( Array.from( rootB.getChildren() ) ).to.deep.equal( [ node ] );

			// Verify operations.
			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
			expect( spy.firstCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should move element from one parent to the other within the same document (docFragA -> docFragA)', () => {
			const docFragA = createDocumentFragment();
			const parent1 = createElement( 'parent' );
			const parent2 = createElement( 'parent' );
			const node = createText( 'foo' );

			insert( node, parent1 );
			insert( parent1, docFragA );
			insert( parent2, docFragA );

			const spy = sinon.spy( model, 'applyOperation' );

			insert( node, parent2 );

			// Verify result.
			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );

			// Verify operations.
			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
			expect( spy.firstCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should move element from one parent to the other within different document (docFragA -> docFragB)', () => {
			const docFragA = createDocumentFragment();
			const docFragB = createDocumentFragment();
			const node = createText( 'foo' );

			insert( node, docFragA );

			const spy = sinon.spy( model, 'applyOperation' );

			insert( node, docFragB );

			// Verify result.
			expect( Array.from( docFragA ) ).to.deep.equal( [] );
			expect( Array.from( docFragB ) ).to.deep.equal( [ node ] );

			// Verify operations.
			sinon.assert.calledTwice( spy );
			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'detach' );
			expect( spy.firstCall.args[ 0 ].batch ).to.equal( batch );
			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
			expect( spy.secondCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should throw when moving element from document to document fragment', () => {
			const root = doc.createRoot();
			const docFrag = createDocumentFragment();
			const node = createText( 'foo' );

			insert( node, root );

			expectToThrowCKEditorError( () => {
				insert( node, docFrag );
			}, /^model-writer-insert-forbidden-move/, model );
		} );

		it( 'should transfer markers from given DocumentFragment', () => {
			const root = doc.createRoot();
			const docFrag = createDocumentFragment();

			appendText( 'abcd', root );

			appendElement( 'p', docFrag );
			insertText( 'foo bar', new Position( docFrag, [ 0, 0 ] ) );

			const marker = new Range( new Position( docFrag, [ 0, 1 ] ), new Position( docFrag, [ 0, 5 ] ) );

			docFrag.markers.set( 'marker', marker );

			insert( docFrag, new Position( root, [ 2 ] ) );

			expect( Array.from( model.markers ).length ).to.equal( 1 );

			const modelMarker = model.markers.get( 'marker' );
			const range = modelMarker.getRange();
			expect( range.root ).to.equal( root );
			expect( range.start.path ).to.deep.equal( [ 2, 1 ] );
			expect( range.end.path ).to.deep.equal( [ 2, 5 ] );
			expect( modelMarker.managedUsingOperations ).to.be.true;
			expect( modelMarker.affectsData ).to.be.true;
		} );

		// https://github.com/ckeditor/ckeditor5-engine/issues/1721.
		it( 'should update a marker if DocumentFragment has a marker that is already in the model (markers have the same name)', () => {
			const root = doc.createRoot();
			const docFrag = createDocumentFragment();

			// <root><p></p><p>[ab]cd</p></root>.
			appendText( 'abcd', root );

			// <docFrag><p>f[oo b]ar</p></docFrag>.
			appendElement( 'p', docFrag );
			insertText( 'foo bar', new Position( docFrag, [ 0, 0 ] ) );

			model.change( writer => {
				const range = new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 2 ] ) );

				writer.addMarker( 'marker', { range, usingOperation: true } );
			} );

			docFrag.markers.set( 'marker', new Range( new Position( docFrag, [ 0, 1 ] ), new Position( docFrag, [ 0, 5 ] ) ) );

			insert( docFrag, new Position( root, [ 2 ] ) );

			expect( Array.from( model.markers ).length ).to.equal( 1 );

			const modelMarker = model.markers.get( 'marker' );
			const range = modelMarker.getRange();
			expect( range.root ).to.equal( root );
			expect( range.start.path ).to.deep.equal( [ 2, 1 ] );
			expect( range.end.path ).to.deep.equal( [ 2, 5 ] );
			expect( modelMarker.managedUsingOperations ).to.be.true;
			expect( modelMarker.affectsData ).to.be.true;
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );
			const root = doc.createRoot();
			const node = createText( 'foo' );

			expectToThrowCKEditorError( () => {
				writer.insert( node, root );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'insertText()', () => {
		it( 'should create and insert text node with attributes at given position', () => {
			const parent = createDocumentFragment();

			insertText( 'foo', { bar: 'biz' }, new Position( parent, [ 0 ] ) );

			expect( parent.childCount ).to.equal( 1 );
			expect( parent.getChild( 0 ) ).to.instanceof( Text );
			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
		} );

		it( 'should create and insert text node with no attributes at given position', () => {
			const parent = createDocumentFragment();

			insertText( 'foo', null, new Position( parent, [ 0 ] ) );

			expect( parent.childCount ).to.equal( 1 );
			expect( parent.getChild( 0 ) ).to.instanceof( Text );
			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
		} );

		it( 'should create and insert text node omitting attributes param', () => {
			const parent = createDocumentFragment();

			insertText( 'foo', new Position( parent, [ 0 ] ) );

			expect( parent.childCount ).to.equal( 1 );
			expect( parent.getChild( 0 ) ).to.instanceof( Text );
			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
		} );

		it( 'should create and insert text node at the beginning of given element', () => {
			const parent = createDocumentFragment();

			insert( createElement( 'child' ), parent );

			insertText( 'foo', parent );

			expect( parent.childCount ).to.equal( 2 );
			expect( parent.getChild( 0 ) ).to.instanceof( Text );
			expect( parent.getChild( 1 ) ).to.instanceof( Element );
		} );

		it( 'should create and insert text node at the end of given element', () => {
			const parent = createDocumentFragment();

			insert( createElement( 'child' ), parent );
			insertText( 'foo', parent, 'end' );

			expect( parent.childCount ).to.equal( 2 );
			expect( parent.getChild( 0 ) ).to.instanceof( Element );
			expect( parent.getChild( 1 ) ).to.instanceof( Text );
		} );

		it( 'should create and insert text node at the given offset of given element', () => {
			const parent = createDocumentFragment();

			insert( createElement( 'child' ), parent );
			insert( createElement( 'child' ), parent );

			insertText( 'foo', parent, 1 );

			expect( parent.childCount ).to.equal( 3 );
			expect( parent.getChild( 0 ) ).to.instanceof( Element );
			expect( parent.getChild( 1 ) ).to.instanceof( Text );
			expect( parent.getChild( 2 ) ).to.instanceof( Element );
		} );

		it( 'should create and insert text node before the given node', () => {
			const parent = createDocumentFragment();
			const child1 = createElement( 'child' );
			const child2 = createElement( 'child' );

			insert( child1, parent );
			insert( child2, parent, 'end' );

			insertText( 'foo', child2, 'before' );

			expect( parent.childCount ).to.equal( 3 );
			expect( parent.getChild( 0 ) ).to.instanceof( Element );
			expect( parent.getChild( 1 ) ).to.instanceof( Text );
			expect( parent.getChild( 2 ) ).to.instanceof( Element );
		} );

		it( 'should create and insert text node after the given node', () => {
			const parent = createDocumentFragment();
			const child1 = createElement( 'child' );
			const child2 = createElement( 'child' );

			insert( child1, parent );
			insert( child2, parent, 'end' );

			insertText( 'foo', child1, 'after' );

			expect( parent.childCount ).to.equal( 3 );
			expect( parent.getChild( 0 ) ).to.instanceof( Element );
			expect( parent.getChild( 1 ) ).to.instanceof( Text );
			expect( parent.getChild( 2 ) ).to.instanceof( Element );
		} );

		it( 'should create proper operation', () => {
			const parent = createDocumentFragment();
			const spy = sinon.spy( model, 'applyOperation' );

			insertText( 'foo', parent );

			sinon.assert.calledOnce( spy );
			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
			expect( spy.lastCall.args[ 0 ] ).to.instanceof( InsertOperation );
			expect( spy.lastCall.args[ 0 ].shouldReceiveAttributes ).to.be.true;
			expect( spy.lastCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );
			const parent = createDocumentFragment();

			expectToThrowCKEditorError( () => {
				writer.insertText( 'foo', parent );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'insertElement()', () => {
		it( 'should create and insert element with attributes at given position', () => {
			const parent = createDocumentFragment();

			insertElement( 'foo', { bar: 'biz' }, new Position( parent, [ 0 ] ) );

			expect( parent.childCount ).to.equal( 1 );
			expect( parent.getChild( 0 ) ).to.instanceof( Element );
			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
		} );

		it( 'should create and insert element with no attributes at given position', () => {
			const parent = createDocumentFragment();
			insertElement( 'foo', null, new Position( parent, [ 0 ] ) );

			expect( parent.childCount ).to.equal( 1 );
			expect( parent.getChild( 0 ) ).to.instanceof( Element );
			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
		} );

		it( 'should create and insert element with no attributes omitting attributes param', () => {
			const parent = createDocumentFragment();
			insertElement( 'foo', new Position( parent, [ 0 ] ) );

			expect( parent.childCount ).to.equal( 1 );
			expect( parent.getChild( 0 ) ).to.instanceof( Element );
			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
		} );

		it( 'should create and insert element at the beginning of given element', () => {
			const parent = createDocumentFragment();
			insert( createElement( 'child' ), parent );

			insertElement( 'foo', parent );

			expect( parent.childCount ).to.equal( 2 );
			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
			expect( parent.getChild( 1 ).name ).to.equal( 'child' );
		} );

		it( 'should create and insert element at the end of given element', () => {
			const parent = createDocumentFragment();
			insert( createElement( 'child' ), parent );

			insertElement( 'foo', parent, 'end' );

			expect( parent.childCount ).to.equal( 2 );
			expect( parent.getChild( 0 ).name ).to.equal( 'child' );
			expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
		} );

		it( 'should create and insert element at the given offset of given element', () => {
			const parent = createDocumentFragment();
			insert( createElement( 'child1' ), parent );
			insert( createElement( 'child2' ), parent, 'end' );

			insertElement( 'foo', parent, 1 );

			expect( parent.childCount ).to.equal( 3 );
			expect( parent.getChild( 0 ).name ).to.equal( 'child1' );
			expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
			expect( parent.getChild( 2 ).name ).to.equal( 'child2' );
		} );

		it( 'should create and insert element before the given node', () => {
			const parent = createDocumentFragment();
			const child1 = createElement( 'child1' );
			const child2 = createElement( 'child2' );

			insert( child1, parent );
			insert( child2, parent, 'end' );

			insertElement( 'foo', child2, 'before' );

			expect( parent.childCount ).to.equal( 3 );
			expect( parent.getChild( 0 ).name ).to.equal( 'child1' );
			expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
			expect( parent.getChild( 2 ).name ).to.equal( 'child2' );
		} );

		it( 'should create and insert element after the given node', () => {
			const parent = createDocumentFragment();
			const child1 = createElement( 'child1' );
			const child2 = createElement( 'child2' );

			insert( child1, parent );
			insert( child2, parent, 'end' );

			insertElement( 'foo', child1, 'after' );

			expect( parent.childCount ).to.equal( 3 );
			expect( parent.getChild( 0 ).name ).to.equal( 'child1' );
			expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
			expect( parent.getChild( 2 ).name ).to.equal( 'child2' );
		} );

		it( 'should create proper operation', () => {
			const parent = createDocumentFragment();
			const spy = sinon.spy( model, 'applyOperation' );

			insertElement( 'foo', parent );

			sinon.assert.calledOnce( spy );
			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
			expect( spy.lastCall.args[ 0 ] ).to.instanceof( InsertOperation );
			expect( spy.lastCall.args[ 0 ].shouldReceiveAttributes ).to.be.false;
			expect( spy.lastCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );
			const child = createElement( 'child' );

			expectToThrowCKEditorError( () => {
				writer.insertElement( 'foo', child, 'after' );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'append()', () => {
		it( 'should insert element at the end of the parent', () => {
			const parent = createDocumentFragment();
			const childText = createText( 'foo' );
			const childElement = createElement( 'foo' );

			append( childText, parent );
			append( childElement, parent );

			expect( Array.from( parent ) ).to.deep.equal( [ childText, childElement ] );
		} );

		it( 'should create proper operation', () => {
			const parent = createDocumentFragment();
			const text = createText( 'foo' );
			const spy = sinon.spy( model, 'applyOperation' );

			append( text, parent );

			sinon.assert.calledOnce( spy );
			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
			expect( spy.lastCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should move element from one parent to the other within the same root (rootA -> rootA)', () => {
			const rootA = doc.createRoot();

			const parent1 = createElement( 'parent' );
			const parent2 = createElement( 'parent' );
			const node = createText( 'foo' );

			insert( node, parent1 );
			insert( parent1, rootA );
			insert( parent2, rootA );

			const spy = sinon.spy( model, 'applyOperation' );

			append( node, parent2 );

			// Verify result.
			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );

			// Verify operations.
			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
			expect( spy.firstCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should move element from one parent to the other within the same document (rootA -> rootB)', () => {
			const rootA = doc.createRoot( '$root', 'A' );
			const rootB = doc.createRoot( '$root', 'B' );
			const node = createText( 'foo' );

			insert( node, rootA );

			const spy = sinon.spy( model, 'applyOperation' );

			append( node, rootB );

			// Verify result.
			expect( Array.from( rootA.getChildren() ) ).to.deep.equal( [] );
			expect( Array.from( rootB.getChildren() ) ).to.deep.equal( [ node ] );

			// Verify operations.
			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
			expect( spy.firstCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should move element from one parent to the other within the same document fragment (docFragA -> docFragA)', () => {
			const docFragA = createDocumentFragment();
			const parent1 = createElement( 'parent' );
			const parent2 = createElement( 'parent' );
			const node = createText( 'foo' );

			insert( node, parent1 );
			insert( parent1, docFragA );
			insert( parent2, docFragA );

			const spy = sinon.spy( model, 'applyOperation' );

			append( node, parent2 );

			// Verify result.
			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );

			// Verify operations.
			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
			expect( spy.firstCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should move element from one parent to the other within different document fragments (docFragA -> docFragB)', () => {
			const docFragA = createDocumentFragment();
			const docFragB = createDocumentFragment();
			const node = createText( 'foo' );

			insert( node, docFragA );

			const spy = sinon.spy( model, 'applyOperation' );

			append( node, docFragB );

			// Verify result.
			expect( Array.from( docFragA ) ).to.deep.equal( [] );
			expect( Array.from( docFragB ) ).to.deep.equal( [ node ] );

			// Verify operations.
			sinon.assert.calledTwice( spy );
			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'detach' );
			expect( spy.firstCall.args[ 0 ].batch ).to.equal( batch );
			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
			expect( spy.secondCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should throw when moving element from document to document fragment', () => {
			const root = doc.createRoot();
			const docFrag = createDocumentFragment();
			const node = createText( 'foo' );

			insert( node, root );

			expectToThrowCKEditorError( () => {
				append( node, docFrag );
			}, /^model-writer-insert-forbidden-move/, model );
		} );
	} );

	describe( 'appendText()', () => {
		it( 'should create and insert text node with attributes at the end of the parent', () => {
			const parent = createDocumentFragment();
			appendText( 'foo', { bar: 'biz' }, parent );
			appendText( 'bar', { biz: 'bar' }, parent );

			expect( parent.childCount ).to.equal( 2 );
			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
			expect( parent.getChild( 1 ).data ).to.equal( 'bar' );
			expect( Array.from( parent.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'biz', 'bar' ] ] );
		} );

		it( 'should create and insert text node with no attributes at the end of the parent', () => {
			const parent = createDocumentFragment();
			appendText( 'foo', null, parent );

			expect( parent.childCount ).to.equal( 1 );
			expect( parent.getChild( 0 ) ).to.instanceof( Text );
			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
		} );

		it( 'should create and insert text node with no attributes omitting attributes param', () => {
			const parent = createDocumentFragment();
			appendText( 'foo', parent );

			expect( parent.childCount ).to.equal( 1 );
			expect( parent.getChild( 0 ) ).to.instanceof( Text );
			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
		} );

		it( 'should create proper operations', () => {
			const parent = createDocumentFragment();
			const spy = sinon.spy( model, 'applyOperation' );

			appendText( 'foo', parent );

			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'insert' );
			expect( spy.firstCall.args[ 0 ] ).to.instanceof( InsertOperation );
			expect( spy.firstCall.args[ 0 ].shouldReceiveAttributes ).to.be.true;
			expect( spy.firstCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );
			const parent = createDocumentFragment();

			expectToThrowCKEditorError( () => {
				writer.appendText( 'foo', parent );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'appendElement()', () => {
		it( 'should create and insert element with attributes at the end of the parent', () => {
			const parent = createDocumentFragment();
			appendElement( 'foo', { bar: 'biz' }, parent );
			appendElement( 'bar', { biz: 'bar' }, parent );

			expect( parent.childCount ).to.equal( 2 );
			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
			expect( parent.getChild( 1 ).name ).to.equal( 'bar' );
			expect( Array.from( parent.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'biz', 'bar' ] ] );
		} );

		it( 'should create and insert element with no attributes at the end of the parent', () => {
			const parent = createDocumentFragment();
			appendElement( 'foo', null, parent );

			expect( parent.childCount ).to.equal( 1 );
			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
		} );

		it( 'should create and insert element with no attributes omitting attributes param', () => {
			const parent = createDocumentFragment();
			appendElement( 'foo', parent );

			expect( parent.childCount ).to.equal( 1 );
			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
		} );

		it( 'should create proper operation', () => {
			const parent = createDocumentFragment();
			const spy = sinon.spy( model, 'applyOperation' );

			appendElement( 'foo', parent );

			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'insert' );
			expect( spy.firstCall.args[ 0 ] ).to.be.instanceof( InsertOperation );
			expect( spy.firstCall.args[ 0 ].shouldReceiveAttributes ).to.be.false;
			expect( spy.firstCall.args[ 0 ].batch ).to.equal( batch );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );
			const parent = createDocumentFragment();

			expectToThrowCKEditorError( () => {
				writer.appendElement( 'foo', parent );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'setAttribute() / removeAttribute()', () => {
		let root, spy;

		beforeEach( () => {
			root = doc.createRoot();
		} );

		describe( 'change attribute on node', () => {
			let node, text;

			beforeEach( () => {
				node = createElement( 'p', { a: 1 } );
				text = createText( 'c', { a: 1 } );

				append( node, root );
				append( text, root );

				spy = sinon.spy( model, 'applyOperation' );
			} );

			describe( 'setAttribute', () => {
				it( 'should create the attribute on element', () => {
					setAttribute( 'b', 2, node );
					expect( spy.callCount ).to.equal( 1 );
					expect( node.getAttribute( 'b' ) ).to.equal( 2 );
				} );

				it( 'should change the attribute of element', () => {
					setAttribute( 'a', 2, node );
					expect( spy.callCount ).to.equal( 1 );
					expect( node.getAttribute( 'a' ) ).to.equal( 2 );
				} );

				it( 'should create the attribute on text node', () => {
					setAttribute( 'b', 2, text );
					expect( spy.callCount ).to.equal( 1 );
					expect( root.getChild( 1 ).getAttribute( 'b' ) ).to.equal( 2 );
				} );

				it( 'should change the attribute of text node', () => {
					setAttribute( 'a', 2, text );
					expect( spy.callCount ).to.equal( 1 );
					expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.equal( 2 );
				} );

				it( 'should do nothing if the attribute value is the same', () => {
					setAttribute( 'a', 1, node );
					expect( spy.callCount ).to.equal( 0 );
					expect( node.getAttribute( 'a' ) ).to.equal( 1 );
				} );

				it( 'should throw when trying to use detached writer', () => {
					const writer = new Writer( model, batch );

					expectToThrowCKEditorError( () => {
						writer.setAttribute( 'a', 1, node );
					}, /^writer-incorrect-use/, model );
				} );
			} );

			describe( 'removeAttribute', () => {
				it( 'should remove the attribute from element', () => {
					removeAttribute( 'a', node );
					expect( spy.callCount ).to.equal( 1 );
					expect( node.getAttribute( 'a' ) ).to.be.undefined;
				} );

				it( 'should remove the attribute from character', () => {
					removeAttribute( 'a', text );
					expect( spy.callCount ).to.equal( 1 );
					expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.be.undefined;
				} );

				it( 'should do nothing if the attribute is not set', () => {
					removeAttribute( 'b', node );
					expect( spy.callCount ).to.equal( 0 );
				} );

				it( 'should throw when trying to use detached writer', () => {
					const writer = new Writer( model, batch );

					expectToThrowCKEditorError( () => {
						writer.removeAttribute( 'b', node );
					}, /^writer-incorrect-use/, model );
				} );
			} );
		} );

		describe( 'change attribute on range', () => {
			beforeEach( () => {
				const element = createElement( 'e', { a: 2 } );

				appendText( 'xxx', { a: 1 }, root );
				appendText( 'xxx', root );
				appendText( 'xxx', { a: 1 }, root );
				appendText( 'xxx', { a: 2 }, root );
				appendText( 'xxx', root );
				appendText( 'xxx', { a: 1 }, root );
				appendText( 'xxx', element );
				append( element, root );
				appendText( 'xxx', root );

				spy = sinon.spy( model, 'applyOperation' );
			} );

			function getRange( startIndex, endIndex ) {
				return new Range(
					Position._createAt( root, startIndex ),
					Position._createAt( root, endIndex )
				);
			}

			function getChangesAttrsCount() {
				let totalNumber = 0;

				for ( const operation of batch.operations ) {
					if ( operation.range ) {
						totalNumber += count( operation.range.getItems( { singleCharacters: true } ) );
					}
				}

				return totalNumber;
			}

			function getCompressedAttrs() {
				// default: 111---111222---1112------
				const range = Range._createIn( root );

				return Array.from( range.getItems( { singleCharacters: true } ) )
					.map( item => item.getAttribute( 'a' ) || '-' )
					.join( '' );
			}

			describe( 'setAttribute', () => {
				it( 'should set the attribute on the range', () => {
					setAttribute( 'a', 3, getRange( 3, 6 ) );
					expect( spy.callCount ).to.equal( 1 );
					expect( getChangesAttrsCount() ).to.equal( 3 );
					expect( getCompressedAttrs() ).to.equal( '111333111222---1112------' );
				} );

				it( 'should split the operations if parts of the range have different attributes', () => {
					setAttribute( 'a', 3, getRange( 4, 14 ) );
					expect( spy.callCount ).to.equal( 4 );
					expect( getChangesAttrsCount() ).to.equal( 10 );
					expect( getCompressedAttrs() ).to.equal( '111-3333333333-1112------' );
				} );

				it( 'should split the operations if parts of the part of the range have the attribute', () => {
					setAttribute( 'a', 2, getRange( 4, 14 ) );
					expect( spy.callCount ).to.equal( 3 );
					expect( getChangesAttrsCount() ).to.equal( 7 );
					expect( getCompressedAttrs() ).to.equal( '111-2222222222-1112------' );
				} );

				it( 'should strip the range if the beginning have the attribute', () => {
					setAttribute( 'a', 1, getRange( 1, 5 ) );
					expect( spy.callCount ).to.equal( 1 );
					expect( getChangesAttrsCount() ).to.equal( 2 );
					expect( getCompressedAttrs() ).to.equal( '11111-111222---1112------' );
				} );

				it( 'should strip the range if the ending have the attribute', () => {
					setAttribute( 'a', 1, getRange( 13, 17 ) );
					expect( spy.callCount ).to.equal( 1 );
					expect( getChangesAttrsCount() ).to.equal( 2 );
					expect( getCompressedAttrs() ).to.equal( '111---111222-111112------' );
				} );

				it( 'should do nothing if the range has attribute', () => {
					setAttribute( 'a', 1, getRange( 0, 3 ) );
					expect( spy.callCount ).to.equal( 0 );
					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
				} );

				it( 'should not check range\'s start position node when creating operations', () => {
					const range = new Range(
						new Position( root, [ 18, 1 ] ),
						new Position( root, [ 19 ] )
					);

					setAttribute( 'a', 1, range );
					expect( spy.callCount ).to.equal( 1 );
					expect( getChangesAttrsCount() ).to.equal( 2 );
					expect( getCompressedAttrs() ).to.equal( '111---111222---1112-11---' );
				} );

				it( 'should not change elements attribute if range contains closing tag', () => {
					const range = new Range(
						new Position( root, [ 18, 1 ] ),
						new Position( root, [ 21 ] )
					);

					setAttribute( 'a', 1, range );
					expect( spy.callCount ).to.equal( 2 );
					expect( getChangesAttrsCount() ).to.equal( 4 );
					expect( getCompressedAttrs() ).to.equal( '111---111222---1112-1111-' );
				} );

				it( 'should not create an operation if the range contains only closing tag', () => {
					const range = new Range(
						new Position( root, [ 18, 3 ] ),
						new Position( root, [ 19 ] )
					);

					setAttribute( 'a', 3, range );
					expect( spy.callCount ).to.equal( 0 );
					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
				} );

				it( 'should not create an operation if is collapsed', () => {
					setAttribute( 'a', 1, getRange( 3, 3 ) );
					expect( spy.callCount ).to.equal( 0 );
					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
				} );

				it( 'should not change children of items in the range', () => {
					setAttribute( 'a', 1, getRange( 0, 20 ) );
					expect( spy.callCount ).to.equal( 5 );
					expect( getChangesAttrsCount() ).to.equal( 14 );
					expect( getCompressedAttrs() ).to.equal( '1111111111111111111---1--' );
				} );

				it( 'should throw when trying to use detached writer', () => {
					const writer = new Writer( model, batch );

					expectToThrowCKEditorError( () => {
						writer.setAttribute( 'a', 1, getRange( 0, 20 ) );
					}, /^writer-incorrect-use/, model );
				} );
			} );

			describe( 'removeAttribute', () => {
				it( 'should remove the attribute on the range', () => {
					removeAttribute( 'a', getRange( 0, 2 ) );
					expect( spy.callCount ).to.equal( 1 );
					expect( getChangesAttrsCount() ).to.equal( 2 );
					expect( getCompressedAttrs() ).to.equal( '--1---111222---1112------' );
				} );

				it( 'should split the operations if parts of the range have different attributes', () => {
					removeAttribute( 'a', getRange( 7, 11 ) );
					expect( spy.callCount ).to.equal( 2 );
					expect( getChangesAttrsCount() ).to.equal( 4 );
					expect( getCompressedAttrs() ).to.equal( '111---1----2---1112------' );
				} );

				it( 'should split the operations if parts of the part of the range have no attribute', () => {
					removeAttribute( 'a', getRange( 1, 7 ) );
					expect( spy.callCount ).to.equal( 2 );
					expect( getChangesAttrsCount() ).to.equal( 3 );
					expect( getCompressedAttrs() ).to.equal( '1------11222---1112------' );
				} );

				it( 'should strip the range if the beginning have no attribute', () => {
					removeAttribute( 'a', getRange( 4, 12 ) );
					expect( spy.callCount ).to.equal( 2 );
					expect( getChangesAttrsCount() ).to.equal( 6 );
					expect( getCompressedAttrs() ).to.equal( '111------------1112------' );
				} );

				it( 'should strip the range if the ending have no attribute', () => {
					removeAttribute( 'a', getRange( 7, 15 ) );
					expect( spy.callCount ).to.equal( 2 );
					expect( getChangesAttrsCount() ).to.equal( 5 );
					expect( getCompressedAttrs() ).to.equal( '111---1--------1112------' );
				} );

				it( 'should do nothing if the range has no attribute', () => {
					removeAttribute( 'a', getRange( 4, 5 ) );
					expect( spy.callCount ).to.equal( 0 );
					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
				} );

				it( 'should not check range\'s start position node when creating operations', () => {
					const range = new Range(
						new Position( root, [ 18, 3 ] ),
						new Position( root, [ 19 ] )
					);

					removeAttribute( 'a', range );
					expect( spy.callCount ).to.equal( 0 );
					expect( getChangesAttrsCount() ).to.equal( 0 );
					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
				} );

				it( 'should not create an operation if range is collapsed', () => {
					removeAttribute( 'a', getRange( 3, 3 ) );
					expect( spy.callCount ).to.equal( 0 );
					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
				} );

				it( 'should create a proper operations for the mixed range', () => {
					removeAttribute( 'a', getRange( 3, 15 ) );
					expect( spy.callCount ).to.equal( 2 );
					expect( getChangesAttrsCount() ).to.equal( 6 );
					expect( getCompressedAttrs() ).to.equal( '111------------1112------' );
				} );

				it( 'should throw when trying to use detached writer', () => {
					const writer = new Writer( model, batch );

					expectToThrowCKEditorError( () => {
						writer.removeAttribute( 'a', getRange( 3, 15 ) );
					}, /^writer-incorrect-use/, model );
				} );
			} );
		} );

		describe( 'change attribute on root element', () => {
			let p;

			beforeEach( () => {
				p = createElement( 'p', { a: 3 } );
				spy = sinon.spy( model, 'applyOperation' );
			} );

			describe( 'setAttribute', () => {
				it( 'should create the attribute on root', () => {
					setAttribute( 'b', 2, root );
					expect( spy.callCount ).to.equal( 1 );
					expect( root.getAttribute( 'b' ) ).to.equal( 2 );
				} );

				it( 'should create the attribute on detached root', () => {
					setAttribute( 'b', 2, p );
					expect( spy.callCount ).to.equal( 1 );
					expect( p.getAttribute( 'b' ) ).to.equal( 2 );
				} );

				it( 'should change the attribute of root', () => {
					setAttribute( 'a', 2, root );
					expect( spy.callCount ).to.equal( 1 );
					expect( root.getAttribute( 'a' ) ).to.equal( 2 );
				} );

				it( 'should change the attribute of detached root', () => {
					setAttribute( 'a', 2, p );
					expect( spy.callCount ).to.equal( 1 );
					expect( p.getAttribute( 'a' ) ).to.equal( 2 );
				} );

				it( 'should do nothing if the attribute value is the same', () => {
					setAttribute( 'a', 1, root );
					expect( spy.callCount ).to.equal( 1 );
					setAttribute( 'a', 1, root );
					expect( spy.callCount ).to.equal( 1 );
					expect( root.getAttribute( 'a' ) ).to.equal( 1 );
				} );

				it( 'should do nothing if the attribute value is the same on detached root', () => {
					setAttribute( 'a', 1, p );
					expect( spy.callCount ).to.equal( 1 );
					setAttribute( 'a', 1, p );
					expect( spy.callCount ).to.equal( 1 );
					expect( p.getAttribute( 'a' ) ).to.equal( 1 );
				} );

				it( 'should throw when trying to use detached writer', () => {
					const writer = new Writer( model, batch );

					expectToThrowCKEditorError( () => {
						writer.setAttribute( 'a', 1, p );
					}, /^writer-incorrect-use/, model );
				} );
			} );

			describe( 'removeAttribute', () => {
				it( 'should remove the attribute from root', () => {
					setAttribute( 'a', 1, root );
					removeAttribute( 'a', root );

					expect( spy.callCount ).to.equal( 2 );
					expect( root.getAttribute( 'a' ) ).to.be.undefined;
				} );

				it( 'should do nothing if the attribute is not set', () => {
					removeAttribute( 'b', root );
					expect( spy.callCount ).to.equal( 0 );
				} );

				it( 'should throw when trying to use detached writer', () => {
					const writer = new Writer( model, batch );

					expectToThrowCKEditorError( () => {
						writer.removeAttribute( 'b', root );
					}, /^writer-incorrect-use/, model );
				} );
			} );

			describe( 'clearAttributes', () => {
				it( 'should clear attributes from range', () => {
					appendText( 'xxx', { a: 1, b: 2, c: 3 }, root );
					appendText( 'xxx', root );
					appendText( 'xxx', { a: 1 }, root );
					appendText( 'xxx', { b: 2 }, root );
					appendText( 'xxx', root );
					appendElement( 'e', { a: 1 }, root );
					appendText( 'xxx', root );

					const range = Range._createIn( root );

					clearAttributes( range );

					let itemsCount = 0;

					for ( const item of range.getItems() ) {
						itemsCount++;
						expect( Array.from( item.getAttributeKeys() ).length ).to.equal( 0 );
					}

					expect( itemsCount ).to.equal( 3 );
				} );

				it( 'should clear attributes on element', () => {
					const element = createElement( 'x', { a: 1, b: 2, c: 3 }, root );

					expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 3 );

					clearAttributes( element );

					expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 0 );
				} );

				it( 'should clear attributes on root element', () => {
					setAttributes( { a: 1, b: 2, c: 3 }, root );

					expect( Array.from( root.getAttributeKeys() ).length ).to.equal( 3 );

					clearAttributes( root );

					expect( Array.from( root.getAttributeKeys() ).length ).to.equal( 0 );
				} );

				it( 'should do nothing if there are no attributes', () => {
					const element = createElement( 'x' );

					expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 0 );

					clearAttributes( element );

					expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 0 );
				} );

				it( 'should throw when trying to use detached writer', () => {
					const writer = new Writer( model, batch );
					const element = createElement( 'x' );

					expectToThrowCKEditorError( () => {
						writer.clearAttributes( element );
					}, /^writer-incorrect-use/, model );
				} );
			} );
		} );
	} );

	describe( 'setAttributes()', () => {
		let frag, item;

		beforeEach( () => {
			frag = createDocumentFragment();
			item = createText( 'xxx', { b: 2, c: 3 } );

			appendText( 'xxx', { a: 1 }, frag );
			append( item, frag );
		} );

		it( 'should set attributes one by one on range', () => {
			const range = Range._createIn( frag );
			let spy;

			model.change( writer => {
				// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
				// such a big amount of the same tests, so let's use a spy here.
				spy = sinon.spy( writer, 'setAttribute' );

				writer.setAttributes( { a: 3, c: null }, range );
			} );

			// Verify result.
			expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] );
			expect( Array.from( frag.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );

			// Verify operations
			sinon.assert.calledTwice( spy );
			sinon.assert.calledWith( spy.firstCall, 'a', 3, range );
			sinon.assert.calledWith( spy.secondCall, 'c', null, range );
		} );

		it( 'should set attributes one by one on range for map as attributes list', () => {
			const range = Range._createIn( frag );
			let spy;

			model.change( writer => {
				// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
				// such a big amount of the same tests, so let's use a spy here.
				spy = sinon.spy( writer, 'setAttribute' );

				writer.setAttributes( new Map( [ [ 'a', 3 ], [ 'c', null ] ] ), range );
			} );

			// Verify result.
			expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] );
			expect( Array.from( frag.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );

			// Verify operations
			sinon.assert.calledTwice( spy );
			sinon.assert.calledWith( spy.firstCall, 'a', 3, range );
			sinon.assert.calledWith( spy.secondCall, 'c', null, range );
		} );

		it( 'should set attributes one by one on item', () => {
			let spy;

			model.change( writer => {
				// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
				// such a big amount of the same tests, so let's use a spy here.
				spy = sinon.spy( writer, 'setAttribute' );

				writer.setAttributes( { a: 3, c: null }, item );
			} );

			// Verify result.
			expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );

			// Verify operations
			sinon.assert.calledTwice( spy );
			sinon.assert.calledWith( spy.firstCall, 'a', 3, item );
			sinon.assert.calledWith( spy.secondCall, 'c', null, item );
		} );

		it( 'should set attributes one by one on item for maps as attributes list', () => {
			let spy;

			model.change( writer => {
				// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
				// such a big amount of the same tests, so let's use a spy here.
				spy = sinon.spy( writer, 'setAttribute' );

				writer.setAttributes( new Map( [ [ 'a', 3 ], [ 'c', null ] ] ), item );
			} );

			// Verify result.
			expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );

			// Verify operations
			sinon.assert.calledTwice( spy );
			sinon.assert.calledWith( spy.firstCall, 'a', 3, item );
			sinon.assert.calledWith( spy.secondCall, 'c', null, item );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );

			expectToThrowCKEditorError( () => {
				writer.setAttributes( new Map( [ [ 'a', 3 ], [ 'c', null ] ] ), item );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'merge()', () => {
		let root, p1, p2;

		beforeEach( () => {
			root = doc.createRoot();

			p1 = new Element( 'p', { key1: 'value1' }, new Text( 'foo' ) );
			p2 = new Element( 'p', { key2: 'value2' }, new Text( 'bar' ) );

			root._insertChild( 0, [ p1, p2 ] );
		} );

		it( 'should merge foo and bar into foobar', () => {
			merge( new Position( root, [ 1 ] ) );

			expect( root.maxOffset ).to.equal( 1 );
			expect( root.getChild( 0 ).name ).to.equal( 'p' );
			expect( root.getChild( 0 ).maxOffset ).to.equal( 6 );
			expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
			expect( root.getChild( 0 ).getAttribute( 'key1' ) ).to.equal( 'value1' );
			expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' );
		} );

		it( 'should correctly merge in document fragment', () => {
			const docFrag = new DocumentFragment( [
				new Element( 'p', null, 'foo' ),
				new Element( 'p', null, 'bar' )
			] );

			merge( new Position( docFrag, [ 1 ] ) );

			expect( docFrag.getChild( 0 ).name ).to.equal( 'p' );
			expect( docFrag.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' );
		} );

		describe( 'should create a marker operation if a marker was affected', () => {
			it( '<p>Foo[</p><p>Bar]</p>', () => {
				testMerge( p1, 'end', p2, 0 );
			} );

			it( '<p>[Foo</p><p>]Bar</p>', () => {
				testMerge( p1, 0, p2, 0 );
			} );

			it( '<p>[Foo</p>]<p>Bar</p>', () => {
				testMerge( p1, 0, root, 1 );
			} );

			it( '<p>Foo</p>[<p>Bar]</p>', () => {
				testMerge( root, 1, p2, 'end' );
			} );

			function testMerge( startElement, startOffset, endElement, endOffset ) {
				const markerRange = new Range(
					Position._createAt( startElement, startOffset ),
					Position._createAt( endElement, endOffset )
				);

				addMarker( 'name', {
					range: markerRange,
					usingOperation: true
				} );

				const documentVersion = model.document.version;

				merge( Position._createAfter( p1 ) );

				const history = model.document.history;

				const lastOperation = history._operations[ history._operations.length - 1 ];
				const secondLastOperation = history._operations[ history._operations.length - 2 ];

				expect( secondLastOperation.type ).to.equal( 'marker' );
				expect( secondLastOperation.oldRange.isEqual( markerRange ) );
				expect( secondLastOperation.newRange.isEqual( markerRange ) );

				expect( lastOperation.type ).to.equal( 'merge' );
				expect( model.document.version ).to.equal( documentVersion + 2 );
			}
		} );

		it( 'should not create a marker operation if affected marker was not using operations', () => {
			const markerRange = new Range( Position._createAt( p2, 0 ), Position._createAt( p2, 2 ) );

			addMarker( 'name', {
				range: markerRange,
				usingOperation: false
			} );

			const documentVersion = model.document.version;

			merge( Position._createAfter( p1 ) );

			const history = model.document.history;

			const lastOperation = history._operations[ history._operations.length - 1 ];

			expect( lastOperation.type ).to.equal( 'merge' );
			expect( model.document.version ).to.equal( documentVersion + 1 );
		} );

		it( 'should throw if there is no element after', () => {
			expectToThrowCKEditorError( () => {
				merge( new Position( root, [ 2 ] ) );
			}, /^writer-merge-no-element-after/, model );
		} );

		it( 'should throw if there is no element before', () => {
			expectToThrowCKEditorError( () => {
				merge( new Position( root, [ 0, 2 ] ) );
			}, /^writer-merge-no-element-before/, model );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );

			expectToThrowCKEditorError( () => {
				writer.merge( new Position( root, [ 1 ] ) );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'move()', () => {
		let root, range, div, p;

		beforeEach( () => {
			root = doc.createRoot();

			div = new Element( 'div', [], new Text( 'foobar' ) );
			p = new Element( 'p', [], new Text( 'abcxyz' ) );

			div._insertChild( 0, [ new Element( 'p', [], new Text( 'gggg' ) ) ] );
			div._insertChild( 2, [ new Element( 'p', [], new Text( 'hhhh' ) ) ] );

			root._insertChild( 0, [ div, p ] );

			range = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 7 ] ) );
		} );

		it( 'should move flat range of nodes', () => {
			move( range, new Position( root, [ 1, 3 ] ) );

			expect( getNodesAndText( Range._createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggggPfoPhhhhP' );
			expect( getNodesAndText( Range._createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcobarxyz' );
		} );

		it( 'should create a marker operation if a marker was affected', () => {
			const markerRange = new Range( Position._createAt( p, 1 ), Position._createAt( p, 4 ) );

			addMarker( 'name', {
				range: markerRange,
				usingOperation: true
			} );

			const documentVersion = model.document.version;

			move( new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) ), Position._createAt( div, 0 ) );

			const history = model.document.history;

			const lastOperation = history._operations[ history._operations.length - 1 ];
			const secondLastOperation = history._operations[ history._operations.length - 2 ];

			expect( secondLastOperation.type ).to.equal( 'marker' );
			expect( secondLastOperation.oldRange.isEqual( markerRange ) );
			expect( secondLastOperation.newRange.isEqual( markerRange ) );

			expect( lastOperation.type ).to.equal( 'move' );
			expect( model.document.version ).to.equal( documentVersion + 2 );
		} );

		it( 'should not create a marker operation if affected marker was not using operations', () => {
			const markerRange = new Range( Position._createAt( p, 1 ), Position._createAt( p, 4 ) );

			addMarker( 'name', {
				range: markerRange,
				usingOperation: false
			} );

			const documentVersion = model.document.version;

			move( new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) ), Position._createAt( div, 0 ) );

			const history = model.document.history;

			const lastOperation = history._operations[ history._operations.length - 1 ];

			expect( lastOperation.type ).to.equal( 'move' );
			expect( model.document.version ).to.equal( documentVersion + 1 );
		} );

		it( 'should throw if object to move is not a range', () => {
			expectToThrowCKEditorError( () => {
				move( root.getChild( 0 ), new Position( root, [ 1, 3 ] ) );
			}, /^writer-move-invalid-range/, model );
		} );

		it( 'should throw if given range is not flat', () => {
			const notFlatRange = new Range( new Position( root, [ 0, 2, 2 ] ), new Position( root, [ 0, 6 ] ) );

			expectToThrowCKEditorError( () => {
				move( notFlatRange, new Position( root, [ 1, 3 ] ) );
			}, /^writer-move-range-not-flat/, model );
		} );

		it( 'should throw if range is going to be moved to the other document', () => {
			const docFrag = createDocumentFragment();

			expectToThrowCKEditorError( () => {
				move( range, docFrag, 0 );
			}, /^writer-move-different-document/, model );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );

			expectToThrowCKEditorError( () => {
				writer.move( range, new Position( root, [ 1, 3 ] ) );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'remove()', () => {
		let div, p, range;

		beforeEach( () => {
			div = createElement( 'div' );
			appendText( 'foobar', div );

			p = createElement( 'p' );
			appendText( 'abcxyz', p );

			insertElement( 'p', div );
			appendElement( 'p', div );

			insertText( 'gggg', new Position( div, [ 0, 0 ] ) );
			insertText( 'hhhh', new Position( div, [ 7, 0 ] ) );
		} );

		describe( 'remove from document', () => {
			let root;

			beforeEach( () => {
				root = doc.createRoot();

				append( div, root );
				append( p, root );

				// Range starts in ROOT > DIV > P > gg|gg.
				// Range ends in ROOT > DIV > ...|ar.
				range = new Range( new Position( root, [ 0, 0, 2 ] ), new Position( root, [ 0, 5 ] ) );
			} );

			it( 'should remove specified node', () => {
				remove( div );

				expect( root.maxOffset ).to.equal( 1 );
				expect( root.childCount ).to.equal( 1 );
				expect( getNodesAndText( Range._createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' );
			} );

			it( 'should remove specified text node', () => {
				remove( p.getChild( 0 ) );

				expect( getNodesAndText( Range._createOn( p ) ) ).to.equal( 'PP' );
			} );

			it( 'should remove any range of nodes', () => {
				remove( range );

				expect( getNodesAndText( Range._createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggParPhhhhP' );
				expect( getNodesAndText( Range._createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcxyz' );
			} );

			it( 'should create minimal number of remove operations, each with only one operation', () => {
				batch = new Batch();
				remove( range );

				expect( batch.operations.length ).to.equal( 2 );
			} );

			it( 'should use MoveOperation to graveyard', () => {
				batch = new Batch();
				remove( div );

				expect( batch.operations[ 0 ].type ).to.equal( 'remove' );
			} );

			it( 'should create a marker operation if a marker was affected', () => {
				const markerRange = new Range( Position._createAt( p, 1 ), Position._createAt( p, 4 ) );

				addMarker( 'name', {
					range: markerRange,
					usingOperation: true
				} );

				const documentVersion = model.document.version;

				remove( new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) ) );

				const history = model.document.history;

				const lastOperation = history._operations[ history._operations.length - 1 ];
				const secondLastOperation = history._operations[ history._operations.length - 2 ];

				expect( secondLastOperation.type ).to.equal( 'marker' );
				expect( secondLastOperation.oldRange.isEqual( markerRange ) );
				expect( secondLastOperation.newRange.isEqual( markerRange ) );

				expect( lastOperation.type ).to.equal( 'remove' );

				expect( model.document.version ).to.equal( documentVersion + 2 );
			} );

			it( 'should not create a marker operation if affected marker was not using operations', () => {
				const markerRange = new Range( Position._createAt( p, 1 ), Position._createAt( p, 4 ) );

				addMarker( 'name', {
					range: markerRange,
					usingOperation: false
				} );

				const documentVersion = model.document.version;

				remove( new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) ) );

				const history = model.document.history;

				const lastOperation = history._operations[ history._operations.length - 1 ];

				expect( lastOperation.type ).to.equal( 'remove' );
				expect( model.document.version ).to.equal( documentVersion + 1 );
			} );

			it( 'should throw when trying to use detached writer', () => {
				const writer = new Writer( model, batch );

				expectToThrowCKEditorError( () => {
					writer.remove( range );
				}, /^writer-incorrect-use/, model );
			} );
		} );

		describe( 'remove from document fragment', () => {
			let frag;

			beforeEach( () => {
				frag = createDocumentFragment();
				append( div, frag );
				append( p, frag );

				// Range starts in FRAG > DIV > P > gg|gg.
				// Range ends in FRAG > DIV > ...|ar.
				range = new Range( new Position( frag, [ 0, 0, 2 ] ), new Position( frag, [ 0, 5 ] ) );
			} );

			it( 'should remove specified node', () => {
				remove( div );

				expect( frag.maxOffset ).to.equal( 1 );
				expect( frag.childCount ).to.equal( 1 );
				expect( getNodesAndText( Range._createIn( frag.getChild( 0 ) ) ) ).to.equal( 'abcxyz' );
			} );

			it( 'should remove specified text node', () => {
				remove( p.getChild( 0 ) );

				expect( getNodesAndText( Range._createOn( p ) ) ).to.equal( 'PP' );
			} );

			it( 'should remove any range of nodes', () => {
				remove( range );

				expect( getNodesAndText( Range._createIn( frag.getChild( 0 ) ) ) ).to.equal( 'PggParPhhhhP' );
				expect( getNodesAndText( Range._createIn( frag.getChild( 1 ) ) ) ).to.equal( 'abcxyz' );
			} );

			it( 'should create minimal number of remove operations, each with only one operation', () => {
				batch = new Batch();
				remove( range );

				expect( batch.operations.length ).to.equal( 2 );
			} );

			it( 'should use DetachOperation', () => {
				batch = new Batch();
				remove( div );

				expect( batch.operations[ 0 ].type ).to.equal( 'detach' );
			} );

			it( 'should throw when trying to use detached writer', () => {
				const writer = new Writer( model, batch );

				expectToThrowCKEditorError( () => {
					writer.remove( range );
				}, /^writer-incorrect-use/, model );
			} );
		} );
	} );

	describe( 'rename()', () => {
		it( 'should rename given element', () => {
			const root = doc.createRoot();
			const p = new Element( 'p', null, new Text( 'abc' ) );

			root._appendChild( p );

			rename( p, 'h' );

			expect( root.maxOffset ).to.equal( 1 );
			expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' );
		} );

		it( 'should rename in document fragment', () => {
			const docFrag = new DocumentFragment();
			const p = new Element( 'p' );

			docFrag._appendChild( p );

			rename( p, 'h' );

			expect( docFrag.maxOffset ).to.equal( 1 );
			expect( docFrag.getChild( 0 ) ).to.have.property( 'name', 'h' );
		} );

		it( 'should throw if not an Element instance is passed', () => {
			expectToThrowCKEditorError( () => {
				rename( new Text( 'abc' ), 'h' );
			}, /^writer-rename-not-element-instance/, model );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );
			const p = new Element( 'p', null, new Text( 'abc' ) );

			expectToThrowCKEditorError( () => {
				writer.rename( p, 'h' );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'split()', () => {
		let root, p;

		beforeEach( () => {
			root = doc.createRoot();

			p = new Element( 'p', { key: 'value' }, new Text( 'foobar' ) );

			root._insertChild( 0, p );
		} );

		it( 'should split foobar to foo and bar', () => {
			split( new Position( root, [ 0, 3 ] ) );

			expect( root.maxOffset ).to.equal( 2 );

			expect( root.getChild( 0 ).name ).to.equal( 'p' );
			expect( root.getChild( 0 ).maxOffset ).to.equal( 3 );
			expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
			expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
			expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );

			expect( root.getChild( 1 ).name ).to.equal( 'p' );
			expect( root.getChild( 1 ).maxOffset ).to.equal( 3 );
			expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 );
			expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
			expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'bar' );
		} );

		it( 'should split inside document fragment', () => {
			const docFrag = new DocumentFragment();
			docFrag._appendChild( new Element( 'p', null, new Text( 'foobar' ) ) );

			split( new Position( docFrag, [ 0, 3 ] ) );

			expect( docFrag.maxOffset ).to.equal( 2 );

			expect( docFrag.getChild( 0 ).name ).to.equal( 'p' );
			expect( docFrag.getChild( 0 ).maxOffset ).to.equal( 3 );
			expect( docFrag.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );

			expect( docFrag.getChild( 1 ).name ).to.equal( 'p' );
			expect( docFrag.getChild( 1 ).maxOffset ).to.equal( 3 );
			expect( docFrag.getChild( 1 ).getChild( 0 ).data ).to.equal( 'bar' );
		} );

		it( 'should create an empty paragraph if we split at the end', () => {
			split( new Position( root, [ 0, 6 ] ) );

			expect( root.maxOffset ).to.equal( 2 );

			expect( root.getChild( 0 ).name ).to.equal( 'p' );
			expect( root.getChild( 0 ).maxOffset ).to.equal( 6 );
			expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
			expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
			expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' );

			expect( root.getChild( 1 ).name ).to.equal( 'p' );
			expect( root.getChild( 1 ).maxOffset ).to.equal( 0 );
			expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 );
			expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
		} );

		it( 'should throw if we try to split a root', () => {
			expectToThrowCKEditorError( () => {
				split( new Position( root, [ 0 ] ) );
			}, /^writer-split-element-no-parent/, model );
		} );

		it( 'should throw if we try to split an element with no parent', () => {
			expectToThrowCKEditorError( () => {
				const element = createElement( 'p' );

				split( new Position( element, [ 0 ] ) );
			}, /^writer-split-element-no-parent/, model );
		} );

		it( 'should throw if we try to split a document fragment', () => {
			expectToThrowCKEditorError( () => {
				const documentFragment = createDocumentFragment();

				split( new Position( documentFragment, [ 0 ] ) );
			}, /^writer-split-element-no-parent/, model );
		} );

		it( 'should split elements to limitElement', () => {
			const div = new Element( 'div', null, p );
			const section = new Element( 'section', null, div );

			root._insertChild( 0, section );

			split( new Position( p, [ 3 ] ), section );

			expect( root.maxOffset ).to.equal( 1 );
			expect( section.maxOffset ).to.equal( 2 );

			expect( section.getChild( 0 ).name ).to.equal( 'div' );
			expect( section.getChild( 0 ).getChild( 0 ).name ).to.equal( 'p' );
			expect( section.getChild( 0 ).getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
			expect( count( section.getChild( 0 ).getChild( 0 ).getAttributes() ) ).to.equal( 1 );
			expect( section.getChild( 0 ).getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );

			expect( section.getChild( 1 ).name ).to.equal( 'div' );
			expect( section.getChild( 1 ).getChild( 0 ).name ).to.equal( 'p' );
			expect( section.getChild( 1 ).getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
			expect( count( section.getChild( 1 ).getChild( 0 ).getAttributes() ) ).to.equal( 1 );
			expect( section.getChild( 1 ).getChild( 0 ).getChild( 0 ).data ).to.equal( 'bar' );
		} );

		it( 'should throw when limitElement is not a position ancestor', () => {
			const div = new Element( 'div', null, p );
			const section = new Element( 'section', null, div );

			root._insertChild( 0, div );
			root._insertChild( 1, section );

			expectToThrowCKEditorError( () => {
				split( new Position( p, [ 3 ] ), section );
			}, /^writer-split-invalid-limit-element/, model );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );

			expectToThrowCKEditorError( () => {
				writer.split( new Position( root, [ 0, 3 ] ) );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'wrap()', () => {
		let root, range;

		beforeEach( () => {
			root = doc.createRoot();

			root._insertChild( 0, new Text( 'foobar' ) );

			range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) );
		} );

		it( 'should wrap flat range with given element', () => {
			const p = new Element( 'p' );
			wrap( range, p );

			expect( root.maxOffset ).to.equal( 5 );
			expect( root.getChild( 0 ).data ).to.equal( 'fo' );
			expect( root.getChild( 1 ).name ).to.equal( 'p' );
			expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'ob' );
			expect( root.getChild( 2 ).data ).to.equal( 'ar' );
		} );

		it( 'should wrap flat range with an element of given name', () => {
			wrap( range, 'p' );

			expect( root.maxOffset ).to.equal( 5 );
			expect( root.getChild( 0 ).data ).to.equal( 'fo' );
			expect( root.getChild( 1 ).name ).to.equal( 'p' );
			expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'ob' );
			expect( root.getChild( 2 ).data ).to.equal( 'ar' );
		} );

		it( 'should wrap inside document fragment', () => {
			const docFrag = new DocumentFragment( new Text( 'foo' ) );

			wrap( Range._createIn( docFrag ), 'p' );

			expect( docFrag.maxOffset ).to.equal( 1 );
			expect( docFrag.getChild( 0 ).name ).to.equal( 'p' );
			expect( docFrag.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
		} );

		it( 'should throw if range to wrap is not flat', () => {
			root._insertChild( 1, [ new Element( 'p', [], new Text( 'xyz' ) ) ] );
			const notFlatRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 6, 2 ] ) );

			expectToThrowCKEditorError( () => {
				wrap( notFlatRange, 'p' );
			}, /^writer-wrap-range-not-flat/, model );
		} );

		it( 'should throw if element to wrap with has children #1', () => {
			const p = new Element( 'p', [], new Text( 'a' ) );

			expectToThrowCKEditorError( () => {
				wrap( range, p );
			}, /^writer-wrap-element-not-empty/, model );
		} );

		it( 'should throw if element to wrap with has children #2', () => {
			const p = new Element( 'p' );
			root._insertChild( 0, p );

			expectToThrowCKEditorError( () => {
				wrap( range, p );
			}, /^writer-wrap-element-attached/, model );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );

			expectToThrowCKEditorError( () => {
				writer.wrap( range, 'p' );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'unwrap()', () => {
		let root, p;

		beforeEach( () => {
			root = doc.createRoot();

			p = new Element( 'p', [], new Text( 'xyz' ) );
			root._insertChild( 0, [ new Text( 'a' ), p, new Text( 'b' ) ] );
		} );

		it( 'should unwrap given element', () => {
			unwrap( p );

			expect( root.maxOffset ).to.equal( 5 );
			expect( root.getChild( 0 ).data ).to.equal( 'axyzb' );
		} );

		it( 'should unwrap inside document fragment', () => {
			const docFrag = new DocumentFragment( new Element( 'p', null, new Text( 'foo' ) ) );

			unwrap( docFrag.getChild( 0 ) );

			expect( docFrag.maxOffset ).to.equal( 3 );
			expect( docFrag.getChild( 0 ).data ).to.equal( 'foo' );
		} );

		it( 'should throw if element to unwrap has no parent', () => {
			const element = new Element( 'p' );

			expectToThrowCKEditorError( () => {
				unwrap( element );
			}, /^writer-unwrap-element-no-parent/, model );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );

			expectToThrowCKEditorError( () => {
				writer.unwrap( p );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'addMarker()', () => {
		let root, range;

		beforeEach( () => {
			root = doc.createRoot();
			root._appendChild( new Text( 'foo' ) );
			range = Range._createIn( root );
		} );

		it( 'should throw if options.usingOperation is not defined', () => {
			expectToThrowCKEditorError( () => {
				addMarker( 'name' );
			}, /^writer-addMarker-no-usingOperation/, model );
		} );

		it( 'should throw if name and range is defined but options.usingOperation is not defined', () => {
			expectToThrowCKEditorError( () => {
				addMarker( 'name', { range } );
			}, /^writer-addMarker-no-usingOperation/, model );
		} );

		it( 'should add marker to the document marker collection', () => {
			addMarker( 'name', { range, usingOperation: false } );

			expect( model.markers.get( 'name' ).getRange().isEqual( range ) ).to.be.true;
		} );

		it( 'should return marker if that was set directly', () => {
			const marker = addMarker( 'name', { range, usingOperation: false } );

			expect( model.markers.get( 'name' ) ).to.equal( marker );
		} );

		it( 'should return marker if that was set using operation API', () => {
			const marker = addMarker( 'name', { range, usingOperation: true } );

			expect( model.markers.get( 'name' ) ).to.equal( marker );
		} );

		it( 'should return marker with properly set managedUsingOperations (to true)', () => {
			const marker = addMarker( 'name', { range, usingOperation: true } );

			expect( marker.managedUsingOperations ).to.be.true;
		} );

		it( 'should return marker with properly set managedUsingOperations (to false)', () => {
			const marker = addMarker( 'name', { range, usingOperation: false } );

			expect( marker.managedUsingOperations ).to.be.false;
		} );

		it( 'should return marker with properly set affectsData (default to false)', () => {
			const marker = addMarker( 'name', { range, usingOperation: false } );

			expect( marker.affectsData ).to.be.false;
		} );

		it( 'should return marker with properly set affectsData (to false)', () => {
			const marker = addMarker( 'name', { range, usingOperation: false, affectsData: true } );

			expect( marker.affectsData ).to.be.true;
		} );

		it( 'should throw when trying to update existing marker in the document marker collection', () => {
			addMarker( 'name', { range, usingOperation: false } );

			const range2 = new Range( Position._createAt( root, 0 ), Position._createAt( root, 0 ) );

			expectToThrowCKEditorError( () => {
				addMarker( 'name', { range: range2, usingOperation: false } );
			}, /^writer-addMarker-marker-exists/, model );
		} );

		it( 'should use operations when having set usingOperation to true', () => {
			const spy = sinon.spy();

			model.on( 'applyOperation', spy );

			addMarker( 'name', { range, usingOperation: true } );
			const op = batch.operations[ 0 ];

			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 1 ][ 0 ].type ).to.equal( 'marker' );
			expect( op.oldRange ).to.be.null;
			expect( op.newRange.isEqual( range ) ).to.be.true;
		} );

		it( 'should throw if marker with given name does not exist and range is not passed', () => {
			expectToThrowCKEditorError( () => {
				addMarker( 'name', { usingOperation: true } );
			}, /^writer-addMarker-no-range/, model );
		} );

		it( 'should throw if marker is set directly and range is not passed', () => {
			expectToThrowCKEditorError( () => {
				addMarker( 'name', { usingOperation: false } );
			}, /^writer-addMarker-no-range/, model );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const marker = addMarker( 'name', { range, usingOperation: false } );
			const writer = new Writer( model, batch );

			expectToThrowCKEditorError( () => {
				writer.addMarker( marker, null, { usingOperation: true } );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'updateMarker()', () => {
		let root, range;

		beforeEach( () => {
			root = doc.createRoot();
			root._appendChild( new Text( 'foo' ) );
			range = Range._createIn( root );
		} );

		it( 'should update managed marker\'s range by marker instance using operations', () => {
			const marker = addMarker( 'name', { range, usingOperation: true } );
			const range2 = new Range( Position._createAt( root, 0 ), Position._createAt( root, 0 ) );

			updateMarker( marker, { range: range2 } );

			expect( batch.operations.length ).to.equal( 2 );

			const op = batch.operations[ 1 ];

			expect( marker.getRange().isEqual( range2 ) ).to.be.true;
			expect( op.oldRange.isEqual( range ) ).to.be.true;
			expect( op.newRange.isEqual( range2 ) ).to.be.true;
		} );

		it( 'should update managed marker\'s range by marker name using operations', () => {
			const marker = addMarker( 'name', { range, usingOperation: true } );
			const range2 = new Range( Position._createAt( root, 0 ), Position._createAt( root, 0 ) );

			updateMarker( 'name', { range: range2 } );

			expect( batch.operations.length ).to.equal( 2 );

			const op = batch.operations[ 1 ];

			expect( marker.getRange().isEqual( range2 ) ).to.be.true;
			expect( op.oldRange.isEqual( range ) ).to.be.true;
			expect( op.newRange.isEqual( range2 ) ).to.be.true;
		} );

		it( 'should update managed marker\'s range by marker instance using operations and usingOperation explicitly passed', () => {
			const marker = addMarker( 'name', { range, usingOperation: true } );
			const range2 = new Range( Position._createAt( root, 0 ), Position._createAt( root, 0 ) );

			updateMarker( marker, { range: range2, usingOperation: true } );

			expect( batch.operations.length ).to.equal( 2 );

			const op = batch.operations[ 1 ];

			expect( marker.getRange().isEqual( range2 ) ).to.be.true;
			expect( op.oldRange.isEqual( range ) ).to.be.true;
			expect( op.newRange.isEqual( range2 ) ).to.be.true;
		} );

		it( 'should update managed marker\'s range by marker name using operations and usingOperation explicitly passed', () => {
			const marker = addMarker( 'name', { range, usingOperation: true } );
			const range2 = new Range( Position._createAt( root, 0 ), Position._createAt( root, 0 ) );

			updateMarker( 'name', { range: range2, usingOperation: true } );

			expect( batch.operations.length ).to.equal( 2 );

			const op = batch.operations[ 1 ];

			expect( marker.getRange().isEqual( range2 ) ).to.be.true;
			expect( op.oldRange.isEqual( range ) ).to.be.true;
			expect( op.newRange.isEqual( range2 ) ).to.be.true;
		} );

		it( 'should not use operations when updating marker which does not use operations', () => {
			const spy = sinon.spy();
			model.on( 'applyOperation', spy );

			const marker = addMarker( 'name', { range, usingOperation: false } );
			const range2 = new Range( Position._createAt( root, 0 ), Position._createAt( root, 0 ) );

			updateMarker( marker, { range: range2 } );

			sinon.assert.notCalled( spy );
		} );

		it( 'should create additional operation when marker type changes to not managed using operation', () => {
			const spy = sinon.spy();
			model.on( 'applyOperation', spy );

			addMarker( 'name', { range, usingOperation: true } );
			updateMarker( 'name', { usingOperation: false } );

			const marker = model.markers.get( 'name' );

			const op1 = batch.operations[ 0 ];
			const op2 = batch.operations[ 1 ];

			sinon.assert.calledTwice( spy );
			expect( spy.firstCall.args[ 1 ][ 0 ].type ).to.equal( 'marker' );
			expect( spy.secondCall.args[ 1 ][ 0 ].type ).to.equal( 'marker' );

			expect( op1.oldRange ).to.be.null;
			expect( op1.newRange.isEqual( range ) ).to.be.true;

			expect( op2.oldRange.isEqual( range ) ).to.be.true;
			expect( op2.newRange ).to.be.null;

			expect( marker.managedUsingOperations ).to.be.false;
		} );

		it( 'should create additional operation when marker type changes to not managed using operation and changing its range', () => {
			const spy = sinon.spy();
			model.on( 'applyOperation', spy );
			const range2 = new Range( Position._createAt( root, 0 ), Position._createAt( root, 0 ) );

			addMarker( 'name', { range, usingOperation: true } );
			updateMarker( 'name', { range: range2, usingOperation: false } );

			const marker = model.markers.get( 'name' );

			const op1 = batch.operations[ 0 ];
			const op2 = batch.operations[ 1 ];

			sinon.assert.calledTwice( spy );
			expect( spy.firstCall.args[ 1 ][ 0 ].type ).to.equal( 'marker' );
			expect( spy.secondCall.args[ 1 ][ 0 ].type ).to.equal( 'marker' );

			expect( op1.oldRange ).to.be.null;
			expect( op1.newRange.isEqual( range ) ).to.be.true;

			expect( op2.oldRange.isEqual( range ) ).to.be.true;
			expect( op2.newRange ).to.be.null;

			expect( marker.getRange().isEqual( range2 ) );

			expect( marker.managedUsingOperations ).to.be.false;
		} );

		it( 'should enable changing marker to be managed using operation', () => {
			const spy = sinon.spy();
			model.on( 'applyOperation', spy );

			addMarker( 'name', { range, usingOperation: false } );
			updateMarker( 'name', { usingOperation: true } );

			const marker = model.markers.get( 'name' );

			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 1 ][ 0 ].type ).to.equal( 'marker' );

			expect( marker.managedUsingOperations ).to.be.true;
		} );

		it( 'should enable changing marker to be managed using operation while changing range', () => {
			const spy = sinon.spy();
			model.on( 'applyOperation', spy );
			const range2 = new Range( Position._createAt( root, 0 ), Position._createAt( root, 0 ) );

			addMarker( 'name', { range, usingOperation: false } );
			updateMarker( 'name', { range: range2, usingOperation: true } );

			const marker = model.markers.get( 'name' );

			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 1 ][ 0 ].type ).to.equal( 'marker' );
			expect( marker.getRange().isEqual( range2 ) ).to.be.true;

			expect( marker.managedUsingOperations ).to.be.true;
		} );

		it( 'should allow changing affectsData property not using operations', () => {
			addMarker( 'name', { range, usingOperation: false } );
			updateMarker( 'name', { affectsData: false } );

			const marker = model.markers.get( 'name' );

			expect( marker.affectsData ).to.be.false;
		} );

		it( 'should allow changing affectsData property using operations', () => {
			addMarker( 'name', { range, usingOperation: true } );
			updateMarker( 'name', { affectsData: true } );

			const op1 = batch.operations[ 0 ];
			const op2 = batch.operations[ 1 ];
			const marker = model.markers.get( 'name' );

			expect( op1.affectsData ).to.be.false;
			expect( op2.affectsData ).to.be.true;

			expect( marker.affectsData ).to.be.true;
		} );

		it( 'should not change affectsData property if not provided', () => {
			const range2 = new Range( Position._createAt( root, 0 ), Position._createAt( root, 0 ) );

			addMarker( 'name', { range, affectsData: false, usingOperation: false } );
			updateMarker( 'name', { range: range2 } );

			const marker = model.markers.get( 'name' );

			expect( marker.affectsData ).to.be.false;
		} );

		it( 'should allow changing affectsData and usingOperation', () => {
			addMarker( 'name', { range, usingOperation: true } );
			updateMarker( 'name', { affectsData: true, usingOperation: false } );

			const op1 = batch.operations[ 0 ];
			const op2 = batch.operations[ 1 ];
			const marker = model.markers.get( 'name' );

			expect( op1.affectsData ).to.be.false;
			expect( op2.affectsData ).to.be.true;

			expect( marker.affectsData ).to.be.true;
		} );

		it( 'should allow changing affectsData and usingOperation #2', () => {
			const spy = sinon.spy();
			model.on( 'applyOperation', spy );

			addMarker( 'name', { range, usingOperation: false, affectsData: true } );
			updateMarker( 'name', { usingOperation: true, affectsData: false } );

			const marker = model.markers.get( 'name' );

			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 1 ][ 0 ].type ).to.equal( 'marker' );

			expect( marker.affectsData ).to.be.false;
		} );

		it( 'should throw when range and usingOperations were not provided', () => {
			expectToThrowCKEditorError( () => {
				addMarker( 'name', { range, usingOperation: false } );
				updateMarker( 'name', {} );
			}, /^writer-updateMarker-wrong-options/, model );
		} );

		it( 'should throw when marker provided by name does not exists', () => {
			expectToThrowCKEditorError( () => {
				updateMarker( 'name', { usingOperation: false } );
			}, /^writer-updateMarker-marker-not-exists/, model );
		} );

		it( 'should only refresh the marker when there is no provided options to update', () => {
			const marker = addMarker( 'name', { range, usingOperation: true } );
			const spy = sinon.spy( model.markers, '_refresh' );

			updateMarker( marker );

			sinon.assert.calledOnce( spy );
			sinon.assert.calledWithExactly( spy, marker );

			updateMarker( 'name' );

			sinon.assert.calledTwice( spy );
			sinon.assert.calledWithExactly( spy.secondCall, marker );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const marker = addMarker( 'name', { range, usingOperation: false } );
			const writer = new Writer( model, batch );

			expectToThrowCKEditorError( () => {
				writer.updateMarker( marker, { usingOperation: true } );
			}, /^writer-incorrect-use/, model );
		} );
	} );

	describe( 'removeMarker()', () => {
		let root, range;

		beforeEach( () => {
			root = doc.createRoot();
			root._appendChild( new Text( 'foo' ) );
			range = Range._createIn( root );
		} );

		it( 'should remove marker from the document marker collection', () => {
			addMarker( 'name', { range, usingOperation: false } );
			removeMarker( 'name' );

			expect( model.markers.get( 'name' ) ).to.be.null;
		} );

		it( 'should throw when trying to remove non existing marker', () => {
			expectToThrowCKEditorError( () => {
				removeMarker( 'name' );
			}, /^writer-removeMarker-no-marker/, model );
		} );

		it( 'should throw when trying to use detached writer', () => {
			const writer = new Writer( model, batch );

			expectToThrowCKEditorError( () => {
				writer.removeMarker( 'name' );
			}, /^writer-incorrect-use/, model );
		} );

		it( 'should accept marker instance', () => {
			addMarker( 'name', { range, usingOperation: false } );
			const marker = model.markers.get( 'name' );

			removeMarker( marker );

			expect( model.markers.get( 'name' ) ).to.be.null;
		} );

		it( 'should use MarkerOperation when marker was created using operation', () => {
			addMarker( 'name', { range, usingOperation: true } );

			const marker = model.markers.get( 'name' );
			const spy = sinon.spy();

			model.on( 'applyOperation', spy );

			removeMarker( marker );

			sinon.assert.calledOnce( spy );
			expect( spy.firstCall.args[ 1 ][ 0 ].type ).to.equal( 'marker' );
			expect( model.markers.get( 'name' ) ).to.be.null;
		} );
	} );

	describe( 'setSelection()', () => {
		let root;

		beforeEach( () => {
			model.schema.register( 'p', { inheritAllFrom: '$block' } );
			model.schema.extend( 'p', { allowIn: '$root' } );

			root = doc.createRoot();
			root._appendChild( [
				new Element( 'p' ),
				new Element( 'p' ),
				new Element( 'p', [], new Text( 'foo' ) )
			] );
		} );

		it( 'should use DocumentSelection#_setTo method', () => {
			const firstParagraph = root.getNodeByPath( [ 1 ] );

			const setToSpy = sinon.spy( DocumentSelection.prototype, '_setTo' );
			setSelection( firstParagraph, 0 );

			expect( setToSpy.calledOnce ).to.be.true;
			setToSpy.restore();
		} );

		it( 'should change document selection ranges', () => {
			const range = new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 2, 2 ] ) );

			setSelection( range, { backward: true } );

			expect( model.document.selection._ranges.length ).to.equal( 1 );
			expect( model.document.selection._ranges[ 0 ].start.path ).to.deep.equal( [ 1, 0 ] );
			expect( model.document.selection._ranges[ 0 ].end.path ).to.deep.equal( [ 2, 2 ] );
			expect( model.document.selection.isBackward ).to.be.true;
		} );
	} );

	describe( 'setSelectionFocus()', () => {
		let root;

		beforeEach( () => {
			model.schema.register( 'p', { inheritAllFrom: '$block' } );
			model.schema.extend( 'p', { allowIn: '$root' } );

			root = doc.createRoot();
			root._appendChild( [
				new Element( 'p' ),
				new Element( 'p' ),
				new Element( 'p', [], new Text( 'foo' ) )
			] );
		} );

		it( 'should use DocumentSelection#_setFocus method', () => {
			const firstParagraph = root.getNodeByPath( [ 1 ] );

			const setFocusSpy = sinon.spy( DocumentSelection.prototype, '_setFocus' );
			setSelectionFocus( firstParagraph, 0 );

			expect( setFocusSpy.calledOnce ).to.be.true;
			setFocusSpy.restore();
		} );

		it( 'should change document selection ranges', () => {
			setSelection( new Position( root, [ 0, 0 ] ) );
			setSelectionFocus( new Position( root, [ 2, 2 ] ) );

			expect( model.document.selection._ranges.length ).to.equal( 1 );
			expect( model.document.selection._ranges[ 0 ].start.path ).to.deep.equal( [ 0, 0 ] );
			expect( model.document.selection._ranges[ 0 ].end.path ).to.deep.equal( [ 2, 2 ] );
		} );
	} );

	describe( 'setSelectionAttribute()', () => {
		const fooStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'foo' );
		let root, rangeInEmptyP, emptyP;

		beforeEach( () => {
			model.schema.register( 'p', { inheritAllFrom: '$block' } );
			model.schema.extend( 'p', { allowIn: '$root' } );

			root = doc.createRoot();
			root._appendChild( [
				new Element( 'p', [], [] ),
				new Element( 'p' ),
				new Element( 'p', [], new Text( 'foo' ) )
			] );

			rangeInEmptyP = new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) );
			emptyP = root.getChild( 0 );
		} );

		it( 'should store attribute if the selection is in empty node', () => {
			setSelection( rangeInEmptyP );
			setSelectionAttribute( 'foo', 'bar' );

			expect( model.document.selection.getAttribute( 'foo' ) ).to.equal( 'bar' );

			expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
		} );

		it( 'should be able to store attributes from the given object', () => {
			setSelection( rangeInEmptyP );
			setSelectionAttribute( { key1: 'foo', key2: 'bar' } );

			expect( model.document.selection.getAttribute( 'key1' ) ).to.equal( 'foo' );
			expect( model.document.selection.getAttribute( 'key2' ) ).to.equal( 'bar' );
		} );

		it( 'should be able to store attributes from the given iterable', () => {
			setSelection( rangeInEmptyP );
			setSelectionAttribute( new Map( [ [ 'key1', 'foo' ], [ 'key2', 'bar' ] ] ) );

			expect( model.document.selection.getAttribute( 'key1' ) ).to.equal( 'foo' );
			expect( model.document.selection.getAttribute( 'key2' ) ).to.equal( 'bar' );
		} );
	} );

	describe( 'removeSelectionAttribute()', () => {
		const fooStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'foo' );
		let root, rangeInEmptyP, emptyP;

		beforeEach( () => {
			model.schema.register( 'p', { inheritAllFrom: '$block' } );
			model.schema.extend( 'p', { allowIn: '$root' } );

			root = doc.createRoot();
			root._appendChild( [
				new Element( 'p', [], [] ),
				new Element( 'p' ),
				new Element( 'p', [], new Text( 'foo' ) )
			] );

			rangeInEmptyP = new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) );
			emptyP = root.getChild( 0 );
		} );

		it( 'should remove stored attribute if the selection is in empty node', () => {
			setSelection( rangeInEmptyP );
			setSelectionAttribute( 'foo', 'bar' );
			removeSelectionAttribute( 'foo' );

			expect( model.document.selection.getAttribute( 'foo' ) ).to.be.undefined;

			expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
		} );

		it( 'should remove all attributes from the given iterable', () => {
			setSelection( rangeInEmptyP );
			setSelectionAttribute( 'foo', 'bar' );
			setSelectionAttribute( 'foo2', 'bar2' );
			removeSelectionAttribute( [ 'foo', 'foo2' ] );

			expect( model.document.selection.getAttribute( 'foo' ) ).to.be.undefined;
			expect( model.document.selection.getAttribute( 'foo2' ) ).to.be.undefined;

			expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
		} );

		it( 'should do nothing if attribute does not exist in the selection', () => {
			setSelection( rangeInEmptyP );
			setSelectionAttribute( 'foo', 'bar' );
			setSelectionAttribute( 'foo2', 'bar2' );
			removeSelectionAttribute( [ 'foo', 'baz' ] );

			expect( model.document.selection.getAttribute( 'foo' ) ).to.be.undefined;
			expect( model.document.selection.getAttribute( 'foo2' ) ).to.equal( 'bar2' );

			expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
		} );
	} );

	describe( 'overrideSelectionGravity()', () => {
		it( 'should use DocumentSelection#_overrideGravity', () => {
			const overrideGravitySpy = sinon.spy( DocumentSelection.prototype, '_overrideGravity' );

			overrideSelectionGravity();

			sinon.assert.calledOnce( overrideGravitySpy );
			overrideGravitySpy.restore();
		} );

		it( 'should return the unique id', () => {
			expect( overrideSelectionGravity() ).to.be.a( 'string' );
		} );

		it( 'should not get attributes from the node before the caret when gravity is overridden', () => {
			const root = doc.createRoot();
			root._appendChild( [
				new Text( 'foo', { foo: true } ),
				new Text( 'bar', { foo: true, bar: true } ),
				new Text( 'biz', { foo: true } )
			] );

			setSelection( new Position( root, [ 6 ] ) );

			expect( Array.from( model.document.selection.getAttributeKeys() ) ).to.deep.equal( [ 'foo', 'bar' ] );

			overrideSelectionGravity();

			expect( Array.from( model.document.selection.getAttributeKeys() ) ).to.deep.equal( [ 'foo' ] );
			expect( model.document.selection.isGravityOverridden ).to.true;

			// Moving selection should not restore the gravity.
			setSelection( new Position( root, [ 5 ] ) );

			expect( Array.from( model.document.selection.getAttributeKeys() ) ).to.deep.equal( [ 'foo', 'bar' ] );
			expect( model.document.selection.isGravityOverridden ).to.true;
		} );
	} );

	describe( 'restoreSelectionGravity()', () => {
		it( 'should use DocumentSelection#_restoreGravity', () => {
			const overrideUid = overrideSelectionGravity();
			const restoreGravitySpy = sinon.spy( DocumentSelection.prototype, '_restoreGravity' );

			restoreSelectionGravity( overrideUid );

			sinon.assert.calledOnce( restoreGravitySpy );
			restoreGravitySpy.restore();
		} );

		it( 'should restore overridden gravity to default', () => {
			const root = doc.createRoot();
			root._appendChild( [
				new Text( 'foo', { foo: true } ),
				new Text( 'bar', { foo: true, bar: true } ),
				new Text( 'biz', { foo: true } )
			] );

			setSelection( new Position( root, [ 6 ] ) );

			const overrideUid = overrideSelectionGravity();

			expect( Array.from( model.document.selection.getAttributeKeys() ) ).to.deep.equal( [ 'foo' ] );

			restoreSelectionGravity( overrideUid );

			expect( Array.from( model.document.selection.getAttributeKeys() ) ).to.deep.equal( [ 'foo', 'bar' ] );
		} );
	} );

	describe( 'createPositionFromPath()', () => {
		it( 'should call model.createPositionFromPath()', () => {
			const stub = sinon.stub( model, 'createPositionFromPath' );

			model.change( writer => {
				writer.createPositionFromPath();
			} );

			sinon.assert.calledOnce( stub );
		} );
	} );

	describe( 'createPositionAt()', () => {
		it( 'should call model.createPositionAt()', () => {
			const stub = sinon.stub( model, 'createPositionAt' );

			model.change( writer => {
				writer.createPositionAt();
			} );

			sinon.assert.calledOnce( stub );
		} );
	} );

	describe( 'createPositionAfter()', () => {
		it( 'should call model.createPositionAfter()', () => {
			const stub = sinon.stub( model, 'createPositionAfter' );

			model.change( writer => {
				writer.createPositionAfter();
			} );

			sinon.assert.calledOnce( stub );
		} );
	} );

	describe( 'createPositionBefore()', () => {
		it( 'should call model.createPositionBefore()', () => {
			const stub = sinon.stub( model, 'createPositionBefore' );

			model.change( writer => {
				writer.createPositionBefore();
			} );

			sinon.assert.calledOnce( stub );
		} );
	} );

	describe( 'createRange()', () => {
		it( 'should call model.createRange()', () => {
			const stub = sinon.stub( model, 'createRange' );

			model.change( writer => {
				writer.createRange();
			} );

			sinon.assert.calledOnce( stub );
		} );
	} );

	describe( 'createRangeIn()', () => {
		it( 'should call model.createRangeIn()', () => {
			const stub = sinon.stub( model, 'createRangeIn' );

			model.change( writer => {
				writer.createRangeIn();
			} );

			sinon.assert.calledOnce( stub );
		} );
	} );

	describe( 'createRangeOn()', () => {
		it( 'should call model.createRangeOn()', () => {
			const stub = sinon.stub( model, 'createRangeOn' );

			model.change( writer => {
				writer.createRangeOn();
			} );

			sinon.assert.calledOnce( stub );
		} );
	} );

	describe( 'createSelection()', () => {
		it( 'should call model.createSelection()', () => {
			const stub = sinon.stub( model, 'createSelection' );

			model.change( writer => {
				writer.createSelection();
			} );

			sinon.assert.calledOnce( stub );
		} );
	} );

	function createText( data, attributes ) {
		return model.change( writer => {
			return writer.createText( data, attributes );
		} );
	}

	function createElement( name, attributes ) {
		return model.change( writer => {
			return writer.createElement( name, attributes );
		} );
	}

	function createDocumentFragment() {
		return model.change( writer => {
			return writer.createDocumentFragment();
		} );
	}

	function insert( item, itemOrPosition, offset ) {
		model.enqueueChange( batch, writer => {
			writer.insert( item, itemOrPosition, offset );
		} );
	}

	function insertText( text, attributes, itemOrPosition, offset ) {
		model.enqueueChange( batch, writer => {
			writer.insertText( text, attributes, itemOrPosition, offset );
		} );
	}

	function insertElement( name, attributes, itemOrPosition, offset ) {
		model.enqueueChange( batch, writer => {
			writer.insertElement( name, attributes, itemOrPosition, offset );
		} );
	}

	function append( item, parent ) {
		model.enqueueChange( batch, writer => {
			writer.append( item, parent );
		} );
	}

	function appendText( text, attributes, parent ) {
		model.enqueueChange( batch, writer => {
			writer.appendText( text, attributes, parent );
		} );
	}

	function appendElement( name, attributes, parent ) {
		model.enqueueChange( batch, writer => {
			writer.appendElement( name, attributes, parent );
		} );
	}

	function setAttribute( key, value, itemOrRange ) {
		model.enqueueChange( batch, writer => {
			writer.setAttribute( key, value, itemOrRange );
		} );
	}

	function setAttributes( attributes, itemOrRange ) {
		model.enqueueChange( batch, writer => {
			writer.setAttributes( attributes, itemOrRange );
		} );
	}

	function removeAttribute( key, itemOrRange ) {
		model.enqueueChange( batch, writer => {
			writer.removeAttribute( key, itemOrRange );
		} );
	}

	function clearAttributes( itemOrRange ) {
		model.enqueueChange( batch, writer => {
			writer.clearAttributes( itemOrRange );
		} );
	}

	function move( range, itemOrPosition, offset ) {
		model.enqueueChange( batch, writer => {
			writer.move( range, itemOrPosition, offset );
		} );
	}

	function remove( itemOrRange ) {
		model.enqueueChange( batch, writer => {
			writer.remove( itemOrRange );
		} );
	}

	function merge( position ) {
		model.enqueueChange( batch, writer => {
			writer.merge( position );
		} );
	}

	function rename( element, newName ) {
		model.enqueueChange( batch, writer => {
			writer.rename( element, newName );
		} );
	}

	function split( position, limitElement ) {
		model.enqueueChange( batch, writer => {
			writer.split( position, limitElement );
		} );
	}

	function wrap( range, elementOrString ) {
		model.enqueueChange( batch, writer => {
			writer.wrap( range, elementOrString );
		} );
	}

	function unwrap( element ) {
		model.enqueueChange( batch, writer => {
			writer.unwrap( element );
		} );
	}

	function addMarker( name, options ) {
		let marker;

		model.enqueueChange( batch, writer => {
			marker = writer.addMarker( name, options );
		} );

		return marker;
	}

	function updateMarker( markerOrName, options ) {
		model.enqueueChange( batch, writer => {
			writer.updateMarker( markerOrName, options );
		} );
	}

	function removeMarker( markerOrName, options ) {
		model.enqueueChange( batch, writer => {
			writer.removeMarker( markerOrName, options );
		} );
	}

	function setSelection( selectable, optionsOrPlaceOrOffset, options ) {
		model.enqueueChange( batch, writer => {
			writer.setSelection( selectable, optionsOrPlaceOrOffset, options );
		} );
	}

	function setSelectionFocus( itemOrPosition, offset ) {
		model.enqueueChange( batch, writer => {
			writer.setSelectionFocus( itemOrPosition, offset );
		} );
	}

	function setSelectionAttribute( key, value ) {
		model.enqueueChange( batch, writer => {
			writer.setSelectionAttribute( key, value );
		} );
	}

	function removeSelectionAttribute( key ) {
		model.enqueueChange( batch, writer => {
			writer.removeSelectionAttribute( key );
		} );
	}

	function overrideSelectionGravity() {
		return model.change( writer => {
			return writer.overrideSelectionGravity();
		} );
	}

	function restoreSelectionGravity( overrideUid ) {
		model.change( writer => {
			writer.restoreSelectionGravity( overrideUid );
		} );
	}
} );
