import { Y } from '@wordpress/sync';
import { select } from '@wordpress/data';
import {
	areSelectionsStatesEqual,
	getSelectionState,
	SelectionType,
} from '../crdt-user-selections';
import { CRDT_RECORD_MAP_KEY } from '../../sync';
jest.mock( '@wordpress/data', () => ( {
	select: jest.fn(),
	// Needed because @wordpress/rich-text initialises its store at import time.
	combineReducers: jest.fn( () => jest.fn( () => ( {} ) ) ),
	createReduxStore: jest.fn( () => ( {} ) ),
	register: jest.fn(),
	createSelector: ( selector: Function ) => selector,
} ) );
jest.mock( '@wordpress/block-editor', () => ( {
	store: 'core/block-editor',
} ) );
import type {
	CursorPosition,
	SelectionNone,
	SelectionCursor,
	SelectionInOneBlock,
	SelectionInMultipleBlocks,
	SelectionWholeBlock,
	SelectionState,
	WPBlockSelection,
} from '../../types';

// Shared Y.Doc and Y.Map for creating Y.Text instances
const yDoc = new Y.Doc();
const yMap = yDoc.getMap( 'test-map' );
let textCounter = 0;
let blockCounter = 0;

/**
 * Helper to create a Y.Text instance attached to a Y.Doc.
 * Y.Text objects must be attached to a Y.Doc to create relative positions.
 *
 * @param yTextValue - The value of the Y.Text instance.
 * @param yTextKey   - The key of the Y.Text instance.
 * @return The Y.Text instance, attached to the Y.Doc.
 */
function createYText( yTextValue: string, yTextKey?: string ): Y.Text {
	textCounter++;
	const key = yTextKey ?? `test-text-${ textCounter }`;
	const yText = new Y.Text( yTextValue );
	yMap.set( key, yText );
	return yText;
}

/**
 * Helper to create a Y.RelativePosition pointing to an index in a Y.Array.
 *
 * @param index - The index in the Y.Array.
 * @return The Y.RelativePosition.
 */
function createBlockPosition( index: number ): Y.RelativePosition {
	blockCounter++;
	const yArray = new Y.Array();
	yMap.set( `test-array-${ blockCounter }`, yArray );

	for ( let i = 0; i <= index; i++ ) {
		yArray.push( [ new Y.Map() ] );
	}

	return Y.createRelativePositionFromTypeIndex( yArray, index );
}

/**
 * Helper to create a CursorPosition with a relative position.
 *
 * @param text   - The text of the Y.Text instance.
 * @param offset - The offset of the Y.Text instance.
 * @return The CursorPosition.
 */
function createCursorPosition( text: string, offset: number ): CursorPosition {
	const yText = createYText( text );
	const relativePosition = Y.createRelativePositionFromTypeIndex(
		yText,
		offset
	);

	return {
		relativePosition,
		absoluteOffset: offset,
	};
}

describe( 'areSelectionsStatesEqual', () => {
	describe( 'different selection types', () => {
		test( 'returns false when comparing different selection types', () => {
			const selection1: SelectionNone = { type: SelectionType.None };
			const selection2: SelectionCursor = {
				type: SelectionType.Cursor,
				cursorPosition: createCursorPosition( 'test', 0 ),
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				false
			);
		} );
	} );

	describe( 'SelectionType.None', () => {
		test( 'returns true when both selections are None', () => {
			const selection1: SelectionNone = { type: SelectionType.None };
			const selection2: SelectionNone = { type: SelectionType.None };

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				true
			);
		} );
	} );

	describe( 'SelectionType.Cursor', () => {
		test( 'returns true when cursor selections are identical', () => {
			const cursorPosition = createCursorPosition( 'test text', 5 );
			const selection1: SelectionCursor = {
				type: SelectionType.Cursor,
				cursorPosition,
			};
			const selection2: SelectionCursor = {
				type: SelectionType.Cursor,
				cursorPosition,
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				true
			);
		} );

		test( 'returns false when relative position differs', () => {
			const selection1: SelectionCursor = {
				type: SelectionType.Cursor,
				cursorPosition: createCursorPosition( 'test text', 5 ),
			};
			const selection2: SelectionCursor = {
				type: SelectionType.Cursor,
				cursorPosition: createCursorPosition( 'test text', 3 ),
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				false
			);
		} );

		test( 'returns false when absolute offset differs', () => {
			const yText = createYText( 'test text' );
			const relativePosition = Y.createRelativePositionFromTypeIndex(
				yText,
				5
			);

			const selection1: SelectionCursor = {
				type: SelectionType.Cursor,
				cursorPosition: {
					relativePosition,
					absoluteOffset: 5,
				},
			};
			const selection2: SelectionCursor = {
				type: SelectionType.Cursor,
				cursorPosition: {
					relativePosition,
					absoluteOffset: 6,
				},
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				false
			);
		} );
	} );

	describe( 'SelectionType.SelectionInOneBlock', () => {
		test( 'returns true when selections in one block are identical', () => {
			const cursorStartPosition = createCursorPosition( 'test text', 0 );
			const cursorEndPosition = createCursorPosition( 'test text', 4 );
			const selection1: SelectionInOneBlock = {
				type: SelectionType.SelectionInOneBlock,
				cursorStartPosition,
				cursorEndPosition,
			};
			const selection2: SelectionInOneBlock = {
				type: SelectionType.SelectionInOneBlock,
				cursorStartPosition,
				cursorEndPosition,
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				true
			);
		} );

		test( 'returns false when start position differs', () => {
			const selection1: SelectionInOneBlock = {
				type: SelectionType.SelectionInOneBlock,
				cursorStartPosition: createCursorPosition( 'test text', 0 ),
				cursorEndPosition: createCursorPosition( 'test text', 4 ),
			};
			const selection2: SelectionInOneBlock = {
				type: SelectionType.SelectionInOneBlock,
				cursorStartPosition: createCursorPosition( 'test text', 1 ),
				cursorEndPosition: createCursorPosition( 'test text', 4 ),
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				false
			);
		} );

		test( 'returns false when end position differs', () => {
			const selection1: SelectionInOneBlock = {
				type: SelectionType.SelectionInOneBlock,
				cursorStartPosition: createCursorPosition( 'test text', 0 ),
				cursorEndPosition: createCursorPosition( 'test text', 4 ),
			};
			const selection2: SelectionInOneBlock = {
				type: SelectionType.SelectionInOneBlock,
				cursorStartPosition: createCursorPosition( 'test text', 0 ),
				cursorEndPosition: createCursorPosition( 'test text', 5 ),
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				false
			);
		} );

		test( 'returns false when absolute offset differs in start position', () => {
			const yText = createYText( 'test text' );
			const relativePosition = Y.createRelativePositionFromTypeIndex(
				yText,
				0
			);
			const cursorEndPosition = createCursorPosition( 'test text', 4 );

			const selection1: SelectionInOneBlock = {
				type: SelectionType.SelectionInOneBlock,
				cursorStartPosition: {
					relativePosition,
					absoluteOffset: 0,
				},
				cursorEndPosition,
			};
			const selection2: SelectionInOneBlock = {
				type: SelectionType.SelectionInOneBlock,
				cursorStartPosition: {
					relativePosition,
					absoluteOffset: 1,
				},
				cursorEndPosition,
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				false
			);
		} );
	} );

	describe( 'SelectionType.SelectionInMultipleBlocks', () => {
		test( 'returns true when both endpoints are identical cursor endpoints', () => {
			const startEndpoint: SelectionCursor = {
				type: SelectionType.Cursor,
				cursorPosition: createCursorPosition( 'first block', 5 ),
			};
			const endEndpoint: SelectionCursor = {
				type: SelectionType.Cursor,
				cursorPosition: createCursorPosition( 'second block', 3 ),
			};
			const selection1: SelectionInMultipleBlocks = {
				type: SelectionType.SelectionInMultipleBlocks,
				startEndpoint,
				endEndpoint,
			};
			const selection2: SelectionInMultipleBlocks = {
				type: SelectionType.SelectionInMultipleBlocks,
				startEndpoint,
				endEndpoint,
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				true
			);
		} );

		test( 'returns true when both endpoints are identical whole-block endpoints', () => {
			const startEndpoint: SelectionWholeBlock = {
				type: SelectionType.WholeBlock,
				blockPosition: createBlockPosition( 0 ),
			};
			const endEndpoint: SelectionWholeBlock = {
				type: SelectionType.WholeBlock,
				blockPosition: createBlockPosition( 1 ),
			};
			const selection1: SelectionInMultipleBlocks = {
				type: SelectionType.SelectionInMultipleBlocks,
				startEndpoint,
				endEndpoint,
			};
			const selection2: SelectionInMultipleBlocks = {
				type: SelectionType.SelectionInMultipleBlocks,
				startEndpoint,
				endEndpoint,
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				true
			);
		} );

		test( 'returns false when start cursor endpoint position differs', () => {
			const selection1: SelectionInMultipleBlocks = {
				type: SelectionType.SelectionInMultipleBlocks,
				startEndpoint: {
					type: SelectionType.Cursor,
					cursorPosition: createCursorPosition( 'first block', 5 ),
				},
				endEndpoint: {
					type: SelectionType.Cursor,
					cursorPosition: createCursorPosition( 'second block', 3 ),
				},
			};
			const selection2: SelectionInMultipleBlocks = {
				type: SelectionType.SelectionInMultipleBlocks,
				startEndpoint: {
					type: SelectionType.Cursor,
					cursorPosition: createCursorPosition( 'first block', 6 ),
				},
				endEndpoint: {
					type: SelectionType.Cursor,
					cursorPosition: createCursorPosition( 'second block', 3 ),
				},
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				false
			);
		} );

		test( 'returns false when end cursor endpoint position differs', () => {
			const selection1: SelectionInMultipleBlocks = {
				type: SelectionType.SelectionInMultipleBlocks,
				startEndpoint: {
					type: SelectionType.Cursor,
					cursorPosition: createCursorPosition( 'first block', 5 ),
				},
				endEndpoint: {
					type: SelectionType.Cursor,
					cursorPosition: createCursorPosition( 'second block', 3 ),
				},
			};
			const selection2: SelectionInMultipleBlocks = {
				type: SelectionType.SelectionInMultipleBlocks,
				startEndpoint: {
					type: SelectionType.Cursor,
					cursorPosition: createCursorPosition( 'first block', 5 ),
				},
				endEndpoint: {
					type: SelectionType.Cursor,
					cursorPosition: createCursorPosition( 'second block', 4 ),
				},
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				false
			);
		} );

		test( 'returns false when endpoint types differ (text vs block)', () => {
			const selection1: SelectionInMultipleBlocks = {
				type: SelectionType.SelectionInMultipleBlocks,
				startEndpoint: {
					type: SelectionType.Cursor,
					cursorPosition: createCursorPosition( 'first block', 5 ),
				},
				endEndpoint: {
					type: SelectionType.Cursor,
					cursorPosition: createCursorPosition( 'second block', 3 ),
				},
			};
			const selection2: SelectionInMultipleBlocks = {
				type: SelectionType.SelectionInMultipleBlocks,
				startEndpoint: {
					type: SelectionType.WholeBlock,
					blockPosition: createBlockPosition( 0 ),
				},
				endEndpoint: {
					type: SelectionType.Cursor,
					cursorPosition: createCursorPosition( 'second block', 3 ),
				},
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				false
			);
		} );
	} );

	describe( 'SelectionType.WholeBlock', () => {
		test( 'returns true when whole block selections are identical', () => {
			const blockPosition = createBlockPosition( 0 );
			const selection1: SelectionWholeBlock = {
				type: SelectionType.WholeBlock,
				blockPosition,
			};
			const selection2: SelectionWholeBlock = {
				type: SelectionType.WholeBlock,
				blockPosition,
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				true
			);
		} );

		test( 'returns false when blockPosition differs', () => {
			const selection1: SelectionWholeBlock = {
				type: SelectionType.WholeBlock,
				blockPosition: createBlockPosition( 0 ),
			};
			const selection2: SelectionWholeBlock = {
				type: SelectionType.WholeBlock,
				blockPosition: createBlockPosition( 1 ),
			};

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				false
			);
		} );
	} );

	describe( 'unknown selection type', () => {
		test( 'returns false for unknown selection type', () => {
			const selection1: SelectionState = {
				type: 'unknown-type' as SelectionType,
			} as SelectionState;
			const selection2: SelectionNone = { type: SelectionType.None };

			expect( areSelectionsStatesEqual( selection1, selection2 ) ).toBe(
				false
			);
		} );
	} );
} );

/**
 * Helper function to create a Y.Doc with blocks for testing getSelectionState.
 */
function createTestDocWithBlocks() {
	const testDoc = new Y.Doc();
	const documentMap = testDoc.getMap( CRDT_RECORD_MAP_KEY );
	const blocks = new Y.Array();
	documentMap.set( 'blocks', blocks );

	// Create block 1 with a content attribute
	const block1 = new Y.Map();
	block1.set( 'clientId', 'block-1' );
	const block1Attrs = new Y.Map();
	block1Attrs.set( 'content', new Y.Text( 'Hello world' ) );
	block1.set( 'attributes', block1Attrs );
	block1.set( 'innerBlocks', new Y.Array() );
	blocks.push( [ block1 ] );

	// Create block 2 with a content attribute
	const block2 = new Y.Map();
	block2.set( 'clientId', 'block-2' );
	const block2Attrs = new Y.Map();
	block2Attrs.set( 'content', new Y.Text( 'Second block content' ) );
	const body = new Y.Array();
	const row = new Y.Map();
	const cells = new Y.Array();
	const cell = new Y.Map();
	cell.set( 'content', new Y.Text( 'Cell text' ) );
	cells.push( [ cell ] );
	row.set( 'cells', cells );
	body.push( [ row ] );
	block2Attrs.set( 'body', body );
	block2.set( 'attributes', block2Attrs );
	block2.set( 'innerBlocks', new Y.Array() );
	blocks.push( [ block2 ] );

	// Create block 3 with nested inner blocks
	const block3 = new Y.Map();
	block3.set( 'clientId', 'block-3' );
	const block3Attrs = new Y.Map();
	block3Attrs.set( 'content', new Y.Text( 'Parent block' ) );
	block3.set( 'attributes', block3Attrs );

	// Add inner block to block 3
	const innerBlocks = new Y.Array();
	const innerBlock = new Y.Map();
	innerBlock.set( 'clientId', 'inner-block-1' );
	const innerBlockAttrs = new Y.Map();
	innerBlockAttrs.set( 'content', new Y.Text( 'Inner block content' ) );
	innerBlock.set( 'attributes', innerBlockAttrs );
	innerBlock.set( 'innerBlocks', new Y.Array() );
	innerBlocks.push( [ innerBlock ] );

	block3.set( 'innerBlocks', innerBlocks );
	blocks.push( [ block3 ] );

	return testDoc;
}

describe( 'getSelectionState', () => {
	let testDoc: Y.Doc;

	const blockIndexMap: Record< string, number > = {
		'block-1': 0,
		'block-2': 1,
		'block-3': 2,
		'inner-block-1': 0,
	};

	const blockParentMap: Record< string, string > = {
		'block-1': '',
		'block-2': '',
		'block-3': '',
		'inner-block-1': 'block-3',
	};

	beforeEach( () => {
		testDoc = createTestDocWithBlocks();

		( select as jest.Mock ).mockReturnValue( {
			getBlockIndex: jest
				.fn()
				.mockImplementation(
					( clientId: string ) => blockIndexMap[ clientId ] ?? -1
				),
			getBlockRootClientId: jest
				.fn()
				.mockImplementation(
					( clientId: string ) => blockParentMap[ clientId ] ?? ''
				),
			getBlockName: jest.fn().mockReturnValue( 'core/paragraph' ),
		} );
	} );

	afterEach( () => {
		testDoc.destroy();
	} );

	describe( 'SelectionType.None', () => {
		test( 'returns None when selectionStart is empty object', () => {
			const selectionStart = {} as WPBlockSelection;
			const selectionEnd = {} as WPBlockSelection;

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.None );
		} );
	} );

	describe( 'SelectionType.WholeBlock', () => {
		test( 'returns WholeBlock when same clientId and no offsets', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: undefined,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: undefined,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.WholeBlock );
			expect(
				( result as SelectionWholeBlock ).blockPosition
			).toBeDefined();
		} );
	} );

	describe( 'SelectionType.Cursor', () => {
		test( 'returns Cursor when same clientId and same offset', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 5,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 5,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.Cursor );
			expect(
				( result as SelectionCursor ).cursorPosition
			).toBeDefined();
			expect(
				( result as SelectionCursor ).cursorPosition.absoluteOffset
			).toBe( 5 );
			expect(
				( result as SelectionCursor ).cursorPosition.attributeKey
			).toBe( 'content' );
		} );

		test( 'returns Cursor at start of block (offset 0)', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 0,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 0,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.Cursor );
			expect(
				( result as SelectionCursor ).cursorPosition.absoluteOffset
			).toBe( 0 );
		} );

		test( 'returns Cursor for a nested rich-text attribute path', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'block-2',
				attributeKey: 'body.0.cells.0.content',
				offset: 4,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-2',
				attributeKey: 'body.0.cells.0.content',
				offset: 4,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.Cursor );
			expect(
				( result as SelectionCursor ).cursorPosition.absoluteOffset
			).toBe( 4 );
			expect(
				( result as SelectionCursor ).cursorPosition.attributeKey
			).toBe( 'body.0.cells.0.content' );
		} );

		test( 'returns None when block does not exist', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'non-existent-block',
				attributeKey: 'content',
				offset: 5,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'non-existent-block',
				attributeKey: 'content',
				offset: 5,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.None );
		} );

		test( 'returns None when attributeKey does not exist on block', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'non-existent-attribute',
				offset: 5,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'non-existent-attribute',
				offset: 5,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.None );
		} );

		test( 'returns None when attributeKey is missing', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: undefined as unknown as string,
				offset: 5,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: undefined as unknown as string,
				offset: 5,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.None );
		} );
	} );

	describe( 'SelectionType.SelectionInOneBlock', () => {
		test( 'returns SelectionInOneBlock when same clientId but different offsets', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 0,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 5,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.SelectionInOneBlock );
			expect(
				( result as SelectionInOneBlock ).cursorStartPosition
					.absoluteOffset
			).toBe( 0 );
			expect(
				( result as SelectionInOneBlock ).cursorEndPosition
					.absoluteOffset
			).toBe( 5 );
		} );

		test( 'returns None when start block does not exist', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'non-existent-block',
				attributeKey: 'content',
				offset: 0,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'non-existent-block',
				attributeKey: 'content',
				offset: 5,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.None );
		} );
	} );

	describe( 'SelectionType.SelectionInMultipleBlocks', () => {
		test( 'returns SelectionInMultipleBlocks when different clientIds', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 5,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-2',
				attributeKey: 'content',
				offset: 3,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe(
				SelectionType.SelectionInMultipleBlocks
			);
			const multiBlock = result as SelectionInMultipleBlocks;
			expect( multiBlock.startEndpoint.type ).toBe(
				SelectionType.Cursor
			);
			expect( multiBlock.endEndpoint.type ).toBe( SelectionType.Cursor );
			expect(
				( multiBlock.startEndpoint as SelectionCursor ).cursorPosition
					.absoluteOffset
			).toBe( 5 );
			expect(
				( multiBlock.endEndpoint as SelectionCursor ).cursorPosition
					.absoluteOffset
			).toBe( 3 );
		} );

		test( 'returns None when start block does not exist in multi-block selection', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'non-existent-block',
				attributeKey: 'content',
				offset: 5,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-2',
				attributeKey: 'content',
				offset: 3,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.None );
		} );

		test( 'returns None when end block does not exist in multi-block selection', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 5,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'non-existent-block',
				attributeKey: 'content',
				offset: 3,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.None );
		} );
	} );

	describe( 'inner blocks', () => {
		test( 'returns Cursor for selection in inner block', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'inner-block-1',
				attributeKey: 'content',
				offset: 3,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'inner-block-1',
				attributeKey: 'content',
				offset: 3,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.Cursor );
		} );

		test( 'returns SelectionInMultipleBlocks spanning parent and inner block', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'block-3',
				attributeKey: 'content',
				offset: 2,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'inner-block-1',
				attributeKey: 'content',
				offset: 5,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe(
				SelectionType.SelectionInMultipleBlocks
			);
		} );
	} );

	describe( 'relative position accuracy', () => {
		test( 'cursor position survives text insertion before it', () => {
			const selectionStart: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 5,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 5,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				testDoc
			);

			expect( result.type ).toBe( SelectionType.Cursor );
			const cursorResult = result as SelectionCursor;

			// Insert text before the cursor position
			const documentMap = testDoc.getMap( CRDT_RECORD_MAP_KEY );
			const blocks = documentMap.get( 'blocks' ) as Y.Array<
				Y.Map< unknown >
			>;
			const block1 = blocks.get( 0 );
			const attrs = block1.get( 'attributes' ) as Y.Map< Y.Text >;
			const ytext = attrs.get( 'content' );

			if ( ! ytext ) {
				throw new Error( 'Y.Text not found' );
			}

			ytext.insert( 0, 'PREFIX ' ); // Insert at beginning

			// Convert relative position back to absolute
			const absolutePosition =
				Y.createAbsolutePositionFromRelativePosition(
					cursorResult.cursorPosition.relativePosition,
					testDoc
				);

			// The absolute index should have shifted by 7 (length of 'PREFIX ')
			expect( absolutePosition?.index ).toBe( 12 ); // 5 + 7
		} );
	} );

	describe( 'edge cases', () => {
		test( 'handles empty blocks array', () => {
			const emptyDoc = new Y.Doc();
			const documentMap = emptyDoc.getMap( CRDT_RECORD_MAP_KEY );
			documentMap.set( 'blocks', new Y.Array() );

			const selectionStart: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 5,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 5,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				emptyDoc
			);

			expect( result.type ).toBe( SelectionType.None );

			emptyDoc.destroy();
		} );

		test( 'handles missing blocks in document', () => {
			const docWithoutBlocks = new Y.Doc();
			docWithoutBlocks.getMap( CRDT_RECORD_MAP_KEY );
			// Note: not setting 'blocks' at all

			const selectionStart: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 5,
			};
			const selectionEnd: WPBlockSelection = {
				clientId: 'block-1',
				attributeKey: 'content',
				offset: 5,
			};

			const result = getSelectionState(
				selectionStart,
				selectionEnd,
				docWithoutBlocks
			);

			expect( result.type ).toBe( SelectionType.None );

			docWithoutBlocks.destroy();
		} );
	} );
} );
