import {
	Box,
	ExtractShapeByProps,
	GroupShapeUtil,
	TLArrowShape,
	TLGroupShape,
	TLLineShape,
	TLShape,
	TLShapeId,
	TLShapePartial,
	VecLike,
	approximately,
	assert,
	compact,
	createShapeId,
	mockUniqueId,
	sortByIndex,
	toRichText,
} from '@tldraw/editor'
import { getArrowBindings } from '../lib/shapes/arrow/shared'
import { TestEditor } from './TestEditor'

let nextNanoId = 0
mockUniqueId(() => `${nextNanoId++}`)

const ids = {
	boxA: createShapeId('boxA'),
	boxB: createShapeId('boxB'),
	boxC: createShapeId('boxC'),
	boxD: createShapeId('boxD'),
	boxE: createShapeId('boxE'),
	boxF: createShapeId('boxF'),
	boxX: createShapeId('boxX'),
	lineA: createShapeId('lineA'),
	groupA: createShapeId('groupA'),
}

type BoxShape = ExtractShapeByProps<{ fill: string }>

const box = (
	id: TLShapeId,
	x: number,
	y: number,
	w = 10,
	h = 10,
	fill: BoxShape['props']['fill'] = 'solid'
): TLShapePartial<BoxShape> => ({
	type: 'geo',
	id,
	x,
	y,
	// index: bumpIndex(),
	props: {
		w,
		h,
		fill,
	},
})
const arrow = (id: TLShapeId, start: VecLike, end: VecLike): TLShapePartial => ({
	type: 'arrow',
	id,
	// index: bumpIndex(),
	props: {
		start: { x: start.x, y: start.y },
		end: { x: end.x, y: end.y },
	},
})
const randomRotation = () => Math.random() * Math.PI * 2
const randomCoord = () => Math.random() * 100 - 50
const randomSize = () => Math.random() * 99 + 1

let editor: TestEditor

beforeEach(() => {
	editor = new TestEditor()
})
afterEach(() => {
	editor?.dispose()
})

const getAllShapes = () => editor.getCurrentPageShapes()

const onlySelectedId = () => {
	expect(editor.getSelectedShapeIds()).toHaveLength(1)
	return editor.getSelectedShapeIds()[0]
}

const onlySelectedShape = () => {
	const id = onlySelectedId()
	return editor.getShape(id)!
}

const children = (shape: TLShape) => {
	return new Set(
		compact(editor.getSortedChildIdsForParent(shape.id).map((id) => editor.getShape(id)))
	)
}

const isRemoved = (shape: TLShape) => {
	return !editor.getShape(shape.id)
}

describe('creating groups', () => {
	it('works if there are multiple shapes in the selection', () => {
		// 0   10  20  30  40  50
		// ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │
		// └───┘   └───┘   └───┘
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])

		editor.select(ids.boxA, ids.boxB)
		expect(getAllShapes()).toHaveLength(3)
		expect(editor.getSelectedShapeIds().length).toBe(2)

		editor.groupShapes(editor.getSelectedShapeIds())

		expect(getAllShapes()).toHaveLength(4)
		expect(editor.getSelectedShapeIds().length).toBe(1)
		expect(editor.getShape(ids.boxA)).toBeTruthy()
		expect(editor.getShape(ids.boxB)).toBeTruthy()

		const group = onlySelectedShape()
		expect(group.type).toBe(GroupShapeUtil.type)
		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: 0,
			y: 0,
			w: 30,
			h: 10,
		})
		expect(children(group).has(editor.getShape(ids.boxA)!)).toBe(true)
		expect(children(group).has(editor.getShape(ids.boxB)!)).toBe(true)
		expect(children(group).has(editor.getShape(ids.boxC)!)).toBe(false)
	})
	it('does not work if there are zero or one shape in the selection ', () => {
		// 0   10  20  30  40  50
		// ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │
		// └───┘   └───┘   └───┘
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])
		expect(getAllShapes()).toHaveLength(3)
		editor.groupShapes(editor.getSelectedShapeIds())
		expect(getAllShapes()).toHaveLength(3)
		editor.select(ids.boxA)
		editor.groupShapes(editor.getSelectedShapeIds())
		expect(getAllShapes()).toHaveLength(3)
		expect(onlySelectedId()).toBe(ids.boxA)
	})

	it('preserves the page positions and rotations of the grouped shapes', () => {
		for (let i = 0; i < 100; i++) {
			const shapes = [
				{
					...box(ids.boxA, randomCoord(), randomCoord(), randomSize(), randomSize()),
					rotation: randomRotation(),
				},
				{
					...box(ids.boxB, randomCoord(), randomCoord(), randomSize(), randomSize()),
					rotation: randomRotation(),
				},
				{
					...box(ids.boxC, randomCoord(), randomCoord(), randomSize(), randomSize()),
					rotation: randomRotation(),
				},
			]
			editor.createShapes(shapes)

			const initialPageBounds = {
				A: editor.getShapePageBounds(ids.boxA)!.clone(),
				B: editor.getShapePageBounds(ids.boxB)!.clone(),
				C: editor.getShapePageBounds(ids.boxC)!.clone(),
			}

			const initialPageRotations = {
				A: editor.getPageRotationById(ids.boxA),
				B: editor.getPageRotationById(ids.boxB),
				C: editor.getPageRotationById(ids.boxC),
			}

			editor.select(ids.boxA, ids.boxB, ids.boxC)
			editor.groupShapes(editor.getSelectedShapeIds())

			try {
				expect({
					A: editor.getShapePageBounds(ids.boxA)!.clone(),
					B: editor.getShapePageBounds(ids.boxB)!.clone(),
					C: editor.getShapePageBounds(ids.boxC)!.clone(),
				}).toCloselyMatchObject(initialPageBounds)
				expect({
					A: editor.getPageRotationById(ids.boxA),
					B: editor.getPageRotationById(ids.boxB),
					C: editor.getPageRotationById(ids.boxC),
				}).toCloselyMatchObject(initialPageRotations)
			} catch (e) {
				console.error('Failing nodes', JSON.stringify(shapes))
				throw e
			}
		}
	})
	it('works with nested groups', () => {
		// 0   10  20  30  40  50  60  70
		// ┌───┐   ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │   │ D │
		// └───┘   └───┘   └───┘   └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 0),
			box(ids.boxC, 40, 0),
			box(ids.boxD, 60, 0),
		])

		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())

		const groupAId = onlySelectedId()

		editor.select(ids.boxC, ids.boxD)
		editor.groupShapes(editor.getSelectedShapeIds())

		const groupBId = onlySelectedId()

		editor.select(groupAId, groupBId)
		editor.groupShapes(editor.getSelectedShapeIds())

		const uberGroup = onlySelectedShape()
		expect(uberGroup.type).toBe(GroupShapeUtil.type)
		expect(editor.getShapePageBounds(uberGroup.id)!).toCloselyMatchObject({
			x: 0,
			y: 0,
			w: 70,
			h: 10,
		})

		expect(children(uberGroup).size).toBe(2)
		expect(children(uberGroup).has(editor.getShape(groupAId)!)).toBe(true)
		expect(children(uberGroup).has(editor.getShape(groupBId)!)).toBe(true)
	})
	it('works with shapes inside individual nested groups', () => {
		//     0   10  20  30  40  50  60  70  80  90  100 110
		//
		//     ┌───┐           ┌───┐   ┌───┐           ┌───┐
		//     │ A │           │ C │   │ D │           │ F │
		// 10  └───┘           └───┘   └───┘           └───┘
		//
		// 20          ┌───┐                   ┌───┐
		//             │ B │                   │ E │
		// 30          └───┘                   └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 20),
			box(ids.boxC, 40, 0),
			box(ids.boxD, 60, 0),
			box(ids.boxE, 80, 20),
			box(ids.boxF, 100, 0),
		])

		editor.select(ids.boxA, ids.boxB, ids.boxC)
		editor.groupShapes(editor.getSelectedShapeIds())
		const groupA = onlySelectedShape()
		editor.select(ids.boxD, ids.boxE, ids.boxF)
		editor.groupShapes(editor.getSelectedShapeIds())
		const groupB = onlySelectedShape()

		editor.select(ids.boxB, ids.boxE)
		editor.groupShapes(editor.getSelectedShapeIds())
		const groupC = onlySelectedShape()

		expect(children(groupA).size).toBe(2)
		expect(children(groupB).size).toBe(2)
		expect(children(groupC).size).toBe(2)

		expect(groupA.parentId).toBe(editor.getCurrentPageId())
		expect(groupB.parentId).toBe(editor.getCurrentPageId())
		expect(groupC.parentId).toBe(editor.getCurrentPageId())

		expect(editor.getShape(ids.boxA)!.parentId).toBe(groupA.id)
		expect(editor.getShape(ids.boxC)!.parentId).toBe(groupA.id)

		expect(editor.getShape(ids.boxB)!.parentId).toBe(groupC.id)
		expect(editor.getShape(ids.boxE)!.parentId).toBe(groupC.id)

		expect(editor.getShape(ids.boxD)!.parentId).toBe(groupB.id)
		expect(editor.getShape(ids.boxF)!.parentId).toBe(groupB.id)
	})
	it('does not work if the scene is in readonly mode', () => {
		// 0   10  20  30  40  50
		// ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │
		// └───┘   └───┘   └───┘
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])
		editor.updateInstanceState({ isReadonly: true })
		editor.setCurrentTool('hand')
		editor.selectAll()
		expect(editor.getSelectedShapeIds().length).toBe(3)
		editor.groupShapes(editor.getSelectedShapeIds())
		expect(editor.getSelectedShapeIds().length).toBe(3)
	})
	it('keeps order correct simple', () => {
		// 0   10  20  30  40  50  60  70
		// ┌───┐   ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │   │ D │
		// └───┘   └───┘   └───┘   └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 0),
			box(ids.boxC, 40, 0),
			box(ids.boxD, 60, 0),
		])

		editor.select(ids.boxC, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())

		const groupAId = onlySelectedId()
		const sortedGroupChildrenIds = editor
			.getSortedChildIdsForParent(groupAId)
			.map((id) => editor.getShape(id)!)
			.sort(sortByIndex)
			.map((shape) => shape.id)

		const sortedIds = editor.getSortedChildIdsForParent(editor.getCurrentPageId())
		expect(sortedIds.length).toBe(3)
		expect(sortedIds[0]).toBe(ids.boxA)
		expect(sortedIds[1]).toBe(groupAId)
		expect(sortedIds[2]).toBe(ids.boxD)

		expect(sortedGroupChildrenIds.length).toBe(2)
		expect(sortedGroupChildrenIds[0]).toBe(ids.boxB)
		expect(sortedGroupChildrenIds[1]).toBe(ids.boxC)
	})

	it('keeps order correct complex', () => {
		// 0   10  20  30  40  50  60  70
		// ┌───┐   ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │   │ D │
		// └───┘   └───┘   └───┘   └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 0),
			box(ids.boxC, 40, 0),
			box(ids.boxD, 60, 0),
		])

		editor.select(ids.boxC, ids.boxA)
		editor.groupShapes(editor.getSelectedShapeIds())

		const groupAId = onlySelectedId()

		const sortedGroupChildrenIds = editor
			.getSortedChildIdsForParent(groupAId)
			.map((id) => editor.getShape(id)!)
			.sort(sortByIndex)
			.map((shape) => shape.id)

		const sortedIds = editor.getSortedChildIdsForParent(editor.getCurrentPageId())
		expect(sortedIds.length).toBe(3)
		expect(sortedIds[0]).toBe(ids.boxB)
		expect(sortedIds[1]).toBe(groupAId)
		expect(sortedIds[2]).toBe(ids.boxD)

		expect(sortedGroupChildrenIds.length).toBe(2)
		expect(sortedGroupChildrenIds[0]).toBe(ids.boxA)
		expect(sortedGroupChildrenIds[1]).toBe(ids.boxC)
	})
})

describe('ungrouping shapes', () => {
	it('works if there is one selected shape and that shape is a group', () => {
		// 0   10  20  30  40  50
		// ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │
		// └───┘   └───┘   └───┘
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])

		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())

		const groupA = onlySelectedShape()

		editor.ungroupShapes(editor.getSelectedShapeIds())

		expect(isRemoved(groupA)).toBe(true)
		expect(new Set(editor.getSelectedShapeIds())).toEqual(new Set([ids.boxA, ids.boxB]))

		expect(editor.getShapePageBounds(ids.boxA)!).toCloselyMatchObject({
			x: 0,
			y: 0,
			w: 10,
			h: 10,
		})

		expect(editor.getShapePageBounds(ids.boxB)!).toCloselyMatchObject({
			x: 20,
			y: 0,
			w: 10,
			h: 10,
		})
	})
	it('selects the groups children and other non-group shapes on ungroup', () => {
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])
		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())

		const groupA = onlySelectedShape()

		editor.select(groupA.id, ids.boxC)
		editor.ungroupShapes(editor.getSelectedShapeIds())

		expect(new Set(editor.getSelectedShapeIds())).toMatchObject(
			new Set([ids.boxA, ids.boxB, ids.boxC])
		)
	})
	it('preserves the page positions and rotations of the ungrouped shapes', () => {
		for (let i = 0; i < 100; i++) {
			const shapes = [
				{
					...box(ids.boxA, randomCoord(), randomCoord(), randomSize(), randomSize()),
					rotation: randomRotation(),
				},
				{
					...box(ids.boxB, randomCoord(), randomCoord(), randomSize(), randomSize()),
					rotation: randomRotation(),
				},
				{
					...box(ids.boxC, randomCoord(), randomCoord(), randomSize(), randomSize()),
					rotation: randomRotation(),
				},
			]

			editor.createShapes(shapes)

			const initialPageBounds = {
				A: editor.getShapePageBounds(ids.boxA)!.clone(),
				B: editor.getShapePageBounds(ids.boxB)!.clone(),
				C: editor.getShapePageBounds(ids.boxC)!.clone(),
			}

			const initialPageRotations = {
				A: editor.getPageRotationById(ids.boxA),
				B: editor.getPageRotationById(ids.boxB),
				C: editor.getPageRotationById(ids.boxC),
			}

			editor.select(ids.boxA, ids.boxB, ids.boxC)
			editor.groupShapes(editor.getSelectedShapeIds())
			editor.ungroupShapes(editor.getSelectedShapeIds())
			expect(editor.getSelectedShapeIds().length).toBe(3)

			try {
				expect({
					A: editor.getShapePageBounds(ids.boxA)!.clone(),
					B: editor.getShapePageBounds(ids.boxB)!.clone(),
					C: editor.getShapePageBounds(ids.boxC)!.clone(),
				}).toCloselyMatchObject(initialPageBounds)
				expect({
					A: editor.getPageRotationById(ids.boxA),
					B: editor.getPageRotationById(ids.boxB),
					C: editor.getPageRotationById(ids.boxC),
				}).toCloselyMatchObject(initialPageRotations)
			} catch (e) {
				console.error('Failing shapes', JSON.stringify(shapes))
				throw e
			}
		}
	})
	it('does not ungroup nested groups', () => {
		// 0   10  20  30  40  50  60  70
		// ┌───┐   ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │   │ D │
		// └───┘   └───┘   └───┘   └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 0),
			box(ids.boxC, 40, 0),
			box(ids.boxD, 60, 0),
		])

		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())

		const groupAId = onlySelectedId()

		editor.select(ids.boxC, ids.boxD)
		editor.groupShapes(editor.getSelectedShapeIds())

		const groupBId = onlySelectedId()

		editor.select(groupAId, groupBId)
		editor.groupShapes(editor.getSelectedShapeIds())
		expect(editor.getSelectedShapeIds().length).toBe(1)
		editor.ungroupShapes(editor.getSelectedShapeIds())
		expect(editor.getSelectedShapeIds().length).toBe(2)
		expect(editor.getShape(groupAId)).not.toBe(undefined)
		expect(editor.getShape(groupBId)).not.toBe(undefined)
	})
	it('does not work if the scene is in readonly mode', () => {
		// 0   10  20  30  40  50
		// ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │
		// └───┘   └───┘   └───┘
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])
		editor.selectAll()
		expect(editor.getSelectedShapeIds().length).toBe(3)
		editor.groupShapes(editor.getSelectedShapeIds())
		expect(editor.getSelectedShapeIds().length).toBe(1)
		editor.updateInstanceState({ isReadonly: true })
		editor.setCurrentTool('hand')

		editor.ungroupShapes(editor.getSelectedShapeIds())
		expect(editor.getSelectedShapeIds().length).toBe(1)
		expect(onlySelectedShape().type).toBe(GroupShapeUtil.type)
	})
	it('keeps order correct simple', () => {
		// 0   10  20  30  40  50  60  70
		// ┌───┐   ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │   │ D │
		// └───┘   └───┘   └───┘   └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 0),
			box(ids.boxC, 40, 0),
			box(ids.boxD, 60, 0),
		])

		editor.select(ids.boxC, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		editor.ungroupShapes(editor.getSelectedShapeIds())

		const sortedShapesOnCurrentPage = editor
			.getCurrentPageShapes()
			.sort(sortByIndex)
			.map((shape) => shape.id)
		expect(sortedShapesOnCurrentPage.length).toBe(4)
		expect(sortedShapesOnCurrentPage[0]).toBe(ids.boxA)
		expect(sortedShapesOnCurrentPage[1]).toBe(ids.boxB)
		expect(sortedShapesOnCurrentPage[2]).toBe(ids.boxC)
		expect(sortedShapesOnCurrentPage[3]).toBe(ids.boxD)
	})
	it('keeps order correct complex', () => {
		// 0   10  20  30  40  50  60  70
		// ┌───┐   ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │   │ D │
		// └───┘   └───┘   └───┘   └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 0),
			box(ids.boxC, 40, 0),
			box(ids.boxD, 60, 0),
		])

		editor.select(ids.boxC, ids.boxA)
		editor.groupShapes(editor.getSelectedShapeIds())
		editor.ungroupShapes(editor.getSelectedShapeIds())

		const sortedShapesOnCurrentPage = editor
			.getCurrentPageShapes()
			.sort(sortByIndex)
			.map((shape) => shape.id)
		expect(sortedShapesOnCurrentPage.length).toBe(4)
		expect(sortedShapesOnCurrentPage[0]).toBe(ids.boxB)
		expect(sortedShapesOnCurrentPage[1]).toBe(ids.boxA)
		expect(sortedShapesOnCurrentPage[2]).toBe(ids.boxC)
		expect(sortedShapesOnCurrentPage[3]).toBe(ids.boxD)
	})
})

describe('the bounds of a group', () => {
	it('changes when the children rotate', () => {
		editor.createShapes([
			box(ids.boxA, 0, 0, 100, 100),
			{
				id: ids.boxB,
				type: 'geo',
				x: 200,
				y: 200,
				props: {
					geo: 'ellipse',
					w: 100,
					h: 100,
				},
			},
		])

		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		const group = onlySelectedShape()

		expect(editor.getShapePageBounds(group.id)!.minX).toBe(0)

		editor.select(ids.boxA).rotateSelection(Math.PI / 4)

		// pythagoras to the rescue
		const expectedLeftBound = 50 - Math.sqrt(2 * (100 * 100)) / 2
		expect(editor.getShapePageBounds(group.id)!.minX).toBeCloseTo(expectedLeftBound)

		// rotating the circle doesn't move the right edge because it's outline doesn't change
		expect(editor.getShapePageBounds(group.id)!.maxX).toBe(300)
		editor.select(ids.boxB).rotateSelection(Math.PI / 4)
		expect(approximately(editor.getShapePageBounds(group.id)!.maxX, 300, 1)).toBe(true)
	})

	it('changes when shapes translate', () => {
		// 0   10  20  30  40  50
		// ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │
		// └───┘   └───┘   └───┘
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])

		editor.select(ids.boxA, ids.boxB, ids.boxC)
		editor.groupShapes(editor.getSelectedShapeIds())
		const group = onlySelectedShape()

		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: 0,
			y: 0,
			w: 50,
			h: 10,
		})

		// move A to the left
		editor.select(ids.boxA).translateSelection(-10, 0)
		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: -10,
			y: 0,
			w: 60,
			h: 10,
		})
		// move C up and to the right
		editor.select(ids.boxC).translateSelection(10, -10)
		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: -10,
			y: -10,
			w: 70,
			h: 20,
		})
	})

	it('changes when shapes resize', () => {
		// 0   10  20  30  40  50
		// ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │
		// └───┘   └───┘   └───┘
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])

		editor.select(ids.boxA, ids.boxB, ids.boxC)
		editor.groupShapes(editor.getSelectedShapeIds())
		const group = onlySelectedShape()

		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: 0,
			y: 0,
			w: 50,
			h: 10,
		})

		// resize A to the left
		editor.select(ids.boxA).resizeSelection({ scaleX: 2 }, 'left')
		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: -10,
			y: 0,
			w: 60,
			h: 10,
		})
		// resize C up and to the right
		editor.select(ids.boxC).resizeSelection({ scaleY: 2, scaleX: 2 }, 'top_right')
		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: -10,
			y: -10,
			w: 70,
			h: 20,
		})
	})

	it('accounts for label-only geometry', () => {
		const ids = {
			groupId: createShapeId('groupId'),
			textId: createShapeId('textId'),
			geoId: createShapeId('geoId'),
		}
		editor.createShapes([
			{ id: ids.groupId, type: 'group', x: 0, y: 0, props: {} },
			{
				id: ids.textId,
				type: 'text',
				x: 10,
				y: 10,
				parentId: ids.groupId,
				props: { richText: toRichText('Hello') },
			},
			{
				id: ids.geoId,
				type: 'geo',
				x: 0,
				y: 100,
				parentId: ids.groupId,
				props: { w: 100, h: 100 },
			},
		])

		expect(editor.getShapePageBounds(ids.groupId)!).toCloselyMatchObject({
			x: 0,
			y: 10,
			w: 100,
			h: 190,
		})
	})
})

describe('the bounds of a rotated group', () => {
	it('changes when the children rotate', () => {
		editor.createShapes([
			box(ids.boxA, 0, 0, 100, 100),
			{
				id: ids.boxB,
				type: 'geo',
				x: 200,
				y: 200,
				props: {
					geo: 'ellipse',
					w: 100,
					h: 100,
				},
			},
		])

		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		const group = onlySelectedShape()

		editor.rotateSelection(Math.PI / 2)

		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: 0,
			y: 0,
			w: 300,
			h: 300,
		})

		editor.select(ids.boxA).rotateSelection(Math.PI / 4)

		// pythagoras to the rescue
		const expectedTopBound = 50 - Math.sqrt(2 * (100 * 100)) / 2
		expect(editor.getShapePageBounds(group.id)!.minY).toBeCloseTo(expectedTopBound)

		// rotating the circle doesn't move the right edge because it's outline doesn't change
		expect(editor.getShapePageBounds(group.id)!.maxY).toBe(300)
		editor.select(ids.boxB).rotateSelection(Math.PI / 4)
		expect(approximately(editor.getShapePageBounds(group.id)!.maxY, 300, 1)).toBe(true)
	})

	it('changes when shapes translate', () => {
		// 0   10  20  30  40  50
		// ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │
		// └───┘   └───┘   └───┘
		// rotate this all 90 degrees
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])

		editor.select(ids.boxA, ids.boxB, ids.boxC)
		editor.groupShapes(editor.getSelectedShapeIds())
		const group = onlySelectedShape()
		editor.updateShapes([{ id: group.id, type: 'group', rotation: Math.PI / 2, x: 10, y: 0 }])

		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: 0,
			y: 0,
			w: 10,
			h: 50,
		})

		// move A up and to the left
		editor.select(ids.boxA).translateSelection(-10, -10)
		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: -10,
			y: -10,
			w: 20,
			h: 60,
		})
		// move C up and to the right
		editor.select(ids.boxC).translateSelection(10, -10)
		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: -10,
			y: -10,
			w: 30,
			h: 50,
		})
	})

	it('changes when shapes resize', () => {
		// 0   10  20  30  40  50
		// ┌───┐   ┌───┐   ┌───┐
		// │ A │   │ B │   │ C │
		// └───┘   └───┘   └───┘
		// rotate this all 90 degrees
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])

		expect(editor.getShapeGeometry(ids.boxA)!.bounds).toMatchObject({ x: 0, y: 0, w: 10, h: 10 })

		editor.select(ids.boxA, ids.boxB, ids.boxC)
		editor.groupShapes(editor.getSelectedShapeIds())
		const group = onlySelectedShape()
		editor.updateShapes([{ id: group.id, type: 'group', rotation: Math.PI / 2, x: 10, y: 0 }])

		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: 0,
			y: 0,
			w: 10,
			h: 50,
		})

		// resize A to up
		editor.select(ids.boxA).resizeSelection({ scaleX: 2 }, 'left')
		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: 0,
			y: -10,
			w: 10,
			h: 60,
		})
		// resize C up and to the right
		editor.select(ids.boxC).resizeSelection({ scaleY: 2, scaleX: 2 }, 'top_right')
		expect(editor.getShapePageBounds(group.id)!).toCloselyMatchObject({
			x: 0,
			y: -10,
			w: 20,
			h: 70,
		})
	})
})

describe('focus layers', () => {
	let groupAId: TLShapeId
	let groupBId: TLShapeId
	let groupCId: TLShapeId
	beforeEach(() => {
		//  group C
		// ┌─────────────────────────────────────────────────────────┐
		// │ group A                         group B                 │
		// │ ┌────────────────────────┐     ┌──────────────────────┐ │
		// │ │  ┌───┐          ┌───┐  │     │ ┌───┐          ┌───┐ │ │
		// │ │  │ A │          │ B │  │     │ │ C │          │ D │ │ │
		// │ │  └───┘          └───┘  │     │ └───┘          └───┘ │ │
		// │ └────────────────────────┘     └──────────────────────┘ │
		// └─────────────────────────────────────────────────────────┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 0),
			box(ids.boxC, 40, 0),
			box(ids.boxD, 60, 0),
		])
		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupAId = onlySelectedId()
		editor.select(ids.boxC, ids.boxD)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupBId = onlySelectedId()
		editor.select(groupAId, groupBId)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupCId = onlySelectedId()
		editor.selectNone()
	})
	it('should adjust to the parent layer of any selected shape', () => {
		expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
		editor.select(ids.boxA)
		expect(editor.getFocusedGroupId()).toBe(groupAId)
		editor.select(ids.boxB)
		expect(editor.getFocusedGroupId()).toBe(groupAId)
		editor.select(ids.boxC)
		expect(editor.getFocusedGroupId()).toBe(groupBId)
		editor.select(ids.boxD)
		expect(editor.getFocusedGroupId()).toBe(groupBId)
		editor.select(groupAId)
		expect(editor.getFocusedGroupId()).toBe(groupCId)
	})
	it('should adjust to the common ancestor of selected shapes in multiple groups', () => {
		expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
		editor.select(ids.boxA)
		expect(editor.getFocusedGroupId()).toBe(groupAId)
		editor.setSelectedShapes([...editor.getSelectedShapeIds(), ids.boxC])
		expect(editor.getFocusedGroupId()).toBe(groupCId)
		editor.deselect(ids.boxA)
		expect(editor.getFocusedGroupId()).toBe(groupBId)
		editor.setSelectedShapes([...editor.getSelectedShapeIds(), ids.boxB])
		expect(editor.getFocusedGroupId()).toBe(groupCId)
	})
	it('should not adjust the focus layer when clearing the selection', () => {
		expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
		editor.select(ids.boxA)
		expect(editor.getFocusedGroupId()).toBe(groupAId)
		editor.deselect(ids.boxA)
		expect(editor.getFocusedGroupId()).toBe(groupAId)
		editor.select(ids.boxB, ids.boxC)
		expect(editor.getFocusedGroupId()).toBe(groupCId)
		editor.selectNone()
		expect(editor.getFocusedGroupId()).toBe(groupCId)
	})
})

describe('right clicking in detail', () => {
	const groupId = createShapeId('groupA')
	beforeEach(() => {
		// A - fill: none
		// B - fill: solid
		// C - fill: none
		// ┌──────────────────┐
		// │ 0 			 20   │
		// │ ┌───┐		┌───┐ │
		// │ │ A │		│ B │ │
		// │ └───┘		└───┘ │
		// └──────────────────┘
		// ┌───┐
		// | C |
		// └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0, 10, 10, 'none'),
			box(ids.boxB, 20, 0, 10, 10, 'solid'),
			box(ids.boxC, 0, 50, 10, 10, 'none'),
		])
		editor.groupShapes([ids.boxA, ids.boxB], { groupId: groupId })
		editor.selectNone()
	})

	// TODO: fix this
	it('should select the group by right-clicking the margin of a hollow shape', () => {
		editor.pointerMove(4, 4).rightClick(4, 4)
		expect(onlySelectedId()).toBe(groupId)
	})
})

describe('the select tool', () => {
	const groupAId = createShapeId('groupA')
	const groupBId = createShapeId('groupB')
	const groupCId = createShapeId('groupC')
	beforeEach(() => {
		//  group C
		// ┌─────────────────────────────────────────────────────────┐
		// │ group A                         group B                 │
		// │ ┌────────────────────────┐     ┌──────────────────────┐ │
		// │ │  0             20      │     │ 40            60     │ │
		// │ │  ┌───┐          ┌───┐  │     │ ┌───┐          ┌───┐ │ │
		// │ │  │ A │          │ B │  │     │ │ C │          │ D │ │ │
		// │ │  └───┘          └───┘  │     │ └───┘          └───┘ │ │
		// │ └────────────────────────┘     └──────────────────────┘ │
		// └─────────────────────────────────────────────────────────┘
		editor.createShapes([
			box(ids.boxA, 0, 0, 10, 10),
			box(ids.boxB, 30, 0, 10, 10),
			box(ids.boxC, 60, 0, 10, 10),
			box(ids.boxD, 90, 0, 10, 10),
		])
		editor.groupShapes([ids.boxA, ids.boxB], { groupId: groupAId })
		editor.groupShapes([ids.boxC, ids.boxD], { groupId: groupBId })
		editor.groupShapes([groupAId, groupBId], { groupId: groupCId })
		editor.selectNone()
	})

	it('should select the outermost non-selected group when you click on one of the shapes in that group', () => {
		editor.pointerDown(0, 0, ids.boxA).pointerUp(0, 0)
		expect(onlySelectedId()).toBe(groupCId)
		expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
		editor.pointerDown(0, 0, ids.boxA)
		editor.pointerUp(0, 0, ids.boxA)
		expect(onlySelectedId()).toBe(groupAId)
		expect(editor.getFocusedGroupId()).toBe(groupCId)
		editor.pointerDown(0, 0, ids.boxA).pointerUp(0, 0, ids.boxA)
		expect(onlySelectedId()).toBe(ids.boxA)
		expect(editor.getFocusedGroupId()).toBe(groupAId)
	})

	it('should select the outermost non-selected group when you right-click on one of the shapes in that group', () => {
		const boxA = editor.getShape(ids.boxA)

		editor.rightClick(0, 0, { target: 'shape', shape: boxA })
		expect(onlySelectedId()).toBe(groupCId)
		expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
		editor.rightClick(0, 0, { target: 'shape', shape: boxA })
		expect(onlySelectedId()).toBe(groupCId)
		expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
		editor.select(groupAId)
		expect(editor.getFocusedGroupId()).toBe(groupCId)
		editor.rightClick(0, 0, { target: 'shape', shape: boxA })
		expect(onlySelectedId()).toBe(groupAId)
		expect(editor.getFocusedGroupId()).toBe(groupCId)
	})

	it('should allow to shift-select other shapes outside of the current focus layer', () => {
		editor.pointerDown(0, 0, ids.boxA).pointerUp(0, 0)
		editor.pointerDown(0, 0, ids.boxA).pointerUp(0, 0)
		editor.pointerDown(0, 0, ids.boxA).pointerUp(0, 0)
		expect(onlySelectedId()).toBe(ids.boxA)
		expect(editor.getFocusedGroupId()).toBe(groupAId)

		editor
			.pointerDown(60, 0, ids.boxC, { shiftKey: true })
			.pointerUp(0, 0, ids.boxC, { shiftKey: true })
		expect(editor.getSelectedShapeIds().includes(ids.boxA)).toBe(true)
		expect(editor.getSelectedShapeIds().includes(groupBId)).toBe(true)
		expect(editor.getFocusedGroupId()).toBe(groupCId)

		editor.pointerDown(60, 0, ids.boxC, { shiftKey: true }).pointerUp()
		expect(editor.getSelectedShapeIds().includes(ids.boxA)).toBe(true)
		expect(editor.getSelectedShapeIds().includes(groupBId)).toBe(false)
		expect(editor.getSelectedShapeIds().includes(ids.boxC)).toBe(true)
		expect(editor.getFocusedGroupId()).toBe(groupCId)
	})

	it('if a shape inside a focused group is selected and you click outside the group it should clear the selection and focus the page', () => {
		editor.select(ids.boxA)
		expect(editor.getFocusedGroupId()).toBe(groupAId)

		// click outside the focused group, but inside another group
		editor.pointerDown(-235, 5, { target: 'canvas' }).pointerUp(-235, 5)
		expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
		expect(editor.getSelectedShapeIds()).toHaveLength(0)

		editor.select(ids.boxA)
		expect(editor.getFocusedGroupId()).toBe(groupAId)

		// click the empty canvas
		editor.pointerDown(-235, 50, { target: 'canvas' }).pointerUp(-235, 50)
		expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
		expect(editor.getSelectedShapeIds()).toHaveLength(0)
	})

	// ! Removed with hollow shape clicking feature
	// it('if a shape inside a focused group is selected and you click an empty space inside the group it should deselect the shape', () => {
	// 	editor.select(ids.boxA)
	// 	expect(editor.focusedGroupId).toBe(groupAId)
	// 	expect(editor.selectedShapeIds).toEqual([ids.boxA])
	// 	editor.pointerDown(20, 5)
	// 	editor.pointerUp(20, 5)
	// 	expect(editor.focusedGroupId).toBe(groupAId)
	// 	expect(editor.selectedShapeIds).toEqual([])
	// })

	// ! Removed
	// it('if you click inside the empty space of a focused group while there are no selected shapes, it should pop the focus layer and select the group', () => {
	// 	editor.select(ids.boxA)
	// 	editor.pointerDown(15, 5, groupAId).pointerUp(15, 5, groupAId)
	// 	expect(editor.focusedGroupId).toBe(groupAId)
	// 	expect(editor.selectedShapeIds.length).toBe(0)
	// 	editor.pointerDown(15, 5, groupAId).pointerUp(15, 5, groupAId)
	// 	expect(editor.focusedGroupId).toBe(groupCId)
	// 	expect(onlySelectedId()).toBe(groupAId)
	// })

	it('should pop the focus layer when escape is pressed in idle state', () => {
		editor.select(ids.boxA)
		expect(editor.getSelectedShapeIds()).toMatchObject([ids.boxA]) // box1
		expect(editor.getFocusedGroupId()).toBe(groupAId)
		// deselct
		editor.cancel()
		expect(editor.getSelectedShapeIds()).toMatchObject([groupAId]) // groupA
		expect(editor.getFocusedGroupId()).toBe(groupCId)
		// pop focus layer
		editor.cancel()
		expect(editor.getSelectedShapeIds().length).toBe(1) // Group C
		expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
		editor.cancel()
		expect(editor.getSelectedShapeIds().length).toBe(0)
		expect(editor.getFocusedGroupId()).toBe(editor.getCurrentPageId())
	})

	// ! Removed: pointing a group is impossible; you'd be pointing the selection instead.
	// it('should work while focused in a group if you start the drag from within the group', () => {
	// 	editor.select(ids.boxA)
	// 	editor.pointerDown(15, 5, groupAId).pointerMove(25, 9, ids.boxB)
	// 	expect(editor.getPath()).toBe(`select.brushing`)
	// 	expect(editor.selectedShapeIds.includes(ids.boxA)).toBe(false)
	// 	expect(editor.selectedShapeIds.includes(ids.boxB)).toBe(true)

	// 	editor.keyDown('Shift')
	// 	expect(editor.selectedShapeIds.includes(ids.boxA)).toBe(true)
	// 	expect(editor.selectedShapeIds.includes(ids.boxB)).toBe(true)
	// })

	it('should work while focused in a group if you start the drag from outside of the group', () => {
		editor.select(ids.boxA)
		expect(editor.getShapesAtPoint({ x: -305, y: -5 })).toMatchObject([])
		editor.pointerDown(-305, -5, { target: 'canvas' }).pointerMove(35, 9, ids.boxB)

		editor.expectToBeIn(`select.brushing`)
		expect(editor.getSelectedShapeIds().includes(ids.boxA)).toBe(true)
		expect(editor.getSelectedShapeIds().includes(ids.boxB)).toBe(true)

		// this one didn't make sense as written—the forced event on canvas won't work now
		// that we're doing hit testing manually—we'll catch that it was inside a shape

		// editor.keyUp('Shift')
		// vi.advanceTimersByTime(200)

		// expect(editor.selectedShapeIds.includes(ids.boxA)).toBe(false)
		// expect(editor.selectedShapeIds.includes(ids.boxB)).toBe(true)
	})

	it('should not select the group until you hit one of its child shapes', () => {
		//             ┌────┐
		//  group C    │    │
		// ┌───────────┼────┼────────────────────────────────────────┐
		// │ group A   │    │                group B                 │
		// │ ┌─────────┼────┼─────────┐     ┌──────────────────────┐ │
		// │ │  ┌───┐  │    │  ┌───┐  │     │ ┌───┐          ┌───┐ │ │
		// │ │  │ A │  │    │  │ B │  │     │ │ C │          │ D │ │ │
		// │ │  └───┘  │    │  └───┘  │     │ └───┘          └───┘ │ │
		// │ └─────────┼────┼─────────┘     └──────────────────────┘ │
		// └───────────┼────┼────────────────────────────────────────┘
		//             │    │
		//             └────┘
		//                  ▲
		//                  │ mouse selection
		editor.pointerDown(12.5, -15, undefined).pointerMove(17.5, 15, ids.boxB)
		expect(editor.getSelectedShapeIds().length).toBe(0)
		editor.pointerMove(35, 15)
		expect(onlySelectedId()).toBe(groupCId)
	})
})

describe("when a group's children are deleted", () => {
	let groupAId: TLShapeId
	let groupBId: TLShapeId
	let groupCId: TLShapeId
	beforeEach(() => {
		//  group C
		// ┌─────────────────────────────────────────────────────────┐
		// │ group A                         group B                 │
		// │ ┌────────────────────────┐     ┌──────────────────────┐ │
		// │ │  0             20      │     │ 40            60     │ │
		// │ │  ┌───┐          ┌───┐  │     │ ┌───┐          ┌───┐ │ │
		// │ │  │ A │          │ B │  │     │ │ C │          │ D │ │ │
		// │ │  └───┘          └───┘  │     │ └───┘          └───┘ │ │
		// │ └────────────────────────┘     └──────────────────────┘ │
		// └─────────────────────────────────────────────────────────┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 0),
			box(ids.boxC, 40, 0),
			box(ids.boxD, 60, 0),
		])
		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupAId = onlySelectedId()
		editor.select(ids.boxC, ids.boxD)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupBId = onlySelectedId()
		editor.select(groupAId, groupBId)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupCId = onlySelectedId()
		editor.selectNone()
	})

	it('should ungroup if there is only one shape left', () => {
		editor.deleteShapes([ids.boxD])
		expect(editor.getShape(groupBId)).toBeUndefined()
		expect(editor.getShape(ids.boxC)?.parentId).toBe(groupCId)
	})

	it('should remove the group if there are no shapes left', () => {
		editor.deleteShapes([ids.boxC, ids.boxD])
		expect(editor.getShape(groupBId)).toBeUndefined()
		expect(editor.getShape(groupCId)).toBeUndefined()
		expect(editor.getShape(groupAId)).not.toBeUndefined()
	})

	it('preserves the collapsed group z-index when reparenting the last child', () => {
		// Page contains: groupC (lower) then boxX (on top). Deleting boxA collapses
		// groupA inside groupC, so boxB should take groupA's z-index in groupC and
		// stay below groupB instead of jumping to the top of groupC's children.
		editor.createShapes([box(ids.boxX, 80, 0)])

		const pageId = editor.getCurrentPageId()
		expect(editor.getSortedChildIdsForParent(pageId)).toEqual([groupCId, ids.boxX])
		expect(editor.getSortedChildIdsForParent(groupCId)).toEqual([groupAId, groupBId])

		editor.deleteShapes([ids.boxA])

		expect(editor.getShape(groupAId)).toBeUndefined()
		expect(editor.getSortedChildIdsForParent(groupCId)).toEqual([ids.boxB, groupBId])
	})
})

describe('creating new shapes', () => {
	let groupA: TLGroupShape
	beforeEach(() => {
		// group A
		// ┌──────────────────────────────┐
		// │      0   10         90   100 │
		// │      ┌───┐                   │
		// │      │ A │                   │
		// │  10  └───┘                   │
		// │                              │
		// │                              │
		// │                              │
		// │                              │
		// │  90                  ┌───┐   │
		// │                      │ B │   │
		// │ 100                  └───┘   │
		// └──────────────────────────────┘
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 90, 90)])
		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupA = onlySelectedShape() as TLGroupShape
		editor.selectNone()
	})
	describe('boxes', () => {
		it('does not create inside the group if the group is only selected and not focused', () => {
			editor.select(groupA.id)
			editor.setCurrentTool('geo')
			editor.pointerDown(20, 20).pointerMove(80, 80).pointerUp(80, 80)
			const boxC = onlySelectedShape()

			expect(boxC.parentId).toBe(editor.getCurrentPageId())
			expect(editor.getShapePageBounds(boxC.id)).toCloselyMatchObject({
				x: 20,
				y: 20,
				w: 60,
				h: 60,
			})
		})

		it('does create inside the group if the group is focused', () => {
			editor.select(ids.boxA)
			expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

			editor.setCurrentTool('geo')
			editor.pointerDown(20, 20).pointerMove(80, 80).pointerUp(80, 80)
			const boxC = onlySelectedShape()

			expect(boxC.parentId).toBe(groupA.id)
			expect(editor.getShapePageBounds(boxC.id)).toCloselyMatchObject({
				x: 20,
				y: 20,
				w: 60,
				h: 60,
			})
			expect(editor.getFocusedGroupId() === groupA.id).toBe(true)
		})

		it('will reisze the group appropriately if the new shape changes the group bounds', () => {
			editor.select(ids.boxA)
			expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

			editor.setCurrentTool('geo')
			editor.pointerDown(20, 20).pointerMove(-10, -10)

			expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
				x: -10,
				y: -10,
				w: 110,
				h: 110,
			})
			editor.pointerMove(-20, -20).pointerUp(-20, -20)
			expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
				x: -20,
				y: -20,
				w: 120,
				h: 120,
			})
			const boxC = onlySelectedShape()
			expect(editor.getShapePageBounds(boxC.id)).toCloselyMatchObject({
				x: -20,
				y: -20,
				w: 40,
				h: 40,
			})
		})

		it('works if the shape drawing begins outside of the current group bounds', () => {
			editor.select(ids.boxA)
			expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

			editor.setCurrentTool('geo')
			editor.pointerDown(-50, -50).pointerMove(-100, -100).pointerUp()

			expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
				x: -100,
				y: -100,
				w: 200,
				h: 200,
			})

			const boxC = onlySelectedShape()
			expect(editor.getShapePageBounds(boxC.id)).toCloselyMatchObject({
				x: -100,
				y: -100,
				w: 50,
				h: 50,
			})
		})
	})

	describe('pencil', () => {
		it('does not draw inside the group if the group is only selected and not focused', () => {
			editor.select(groupA.id)

			editor.setCurrentTool('draw')
			editor.pointerDown(20, 20).pointerMove(80, 80).pointerUp(80, 80)

			const lineC = onlySelectedShape()
			expect(lineC.parentId).toBe(editor.getCurrentPageId())
		})

		it('does draw inside the group if the group is focused', () => {
			editor.select(ids.boxA)
			expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

			editor.setCurrentTool('draw')
			editor.pointerDown(20, 20).pointerMove(80, 80).pointerUp(80, 80)

			const lineC = onlySelectedShape()
			expect(lineC.parentId).toBe(groupA.id)
		})

		it('will resize the group appropriately if the new shape changes the group bounds', () => {
			editor.select(ids.boxA)
			expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

			editor.setCurrentTool('draw')
			editor.pointerDown(20, 20)
			for (let i = 20; i >= -20; i--) {
				editor.pointerMove(i, i)
			}
			editor.pointerUp()

			const roundToNearestTen = (vals: Box) => {
				return {
					x: Math.round(vals.x / 10) * 10,
					y: Math.round(vals.y / 10) * 10,
					w: Math.round(vals.w / 10) * 10,
					h: Math.round(vals.h / 10) * 10,
				}
			}

			expect(roundToNearestTen(editor.getShapePageBounds(groupA.id)!)).toCloselyMatchObject({
				x: -20,
				y: -20,
				w: 120,
				h: 120,
			})
		})

		it('works if the shape drawing begins outside of the current group bounds', () => {
			editor.select(ids.boxA)
			expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

			editor.setCurrentTool('draw')
			editor.pointerDown(-20, -20)
			for (let i = -20; i >= -100; i--) {
				editor.pointerMove(i, i)
			}
			editor.pointerUp()

			const roundToNearestTen = (vals: Box) => {
				return {
					x: Math.round(vals.x / 10) * 10,
					y: Math.round(vals.y / 10) * 10,
					w: Math.round(vals.w / 10) * 10,
					h: Math.round(vals.h / 10) * 10,
				}
			}

			expect(roundToNearestTen(editor.getShapePageBounds(groupA.id)!)).toCloselyMatchObject({
				x: -100,
				y: -100,
				w: 200,
				h: 200,
			})
		})

		describe('lines', () => {
			it('does not draw inside the group if the group is only selected and not focused', () => {
				editor.select(groupA.id)

				editor.setCurrentTool('line')
				editor.pointerDown(20, 20)
				editor.pointerMove(80, 80)
				editor.pointerUp(80, 80)

				const lineC = onlySelectedShape()
				expect(lineC.type).toBe('line')
				expect(lineC.parentId).toBe(editor.getCurrentPageId())
			})

			it('does draw inside the group if the group is focused', () => {
				editor.select(ids.boxA)
				expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

				editor.setCurrentTool('line')
				editor.pointerDown(20, 20).pointerMove(80, 80).pointerUp(80, 80)

				const lineC = onlySelectedShape() as TLLineShape
				expect(lineC.type).toBe('line')
				expect(lineC.parentId).toBe(groupA.id)
			})

			it('will reisze the group appropriately if the new shape changes the group bounds', () => {
				editor.select(ids.boxA)
				expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

				editor.setCurrentTool('line')
				editor.pointerDown(20, 20).pointerMove(-10, -10)

				expect(editor.getShapePageBounds(groupA.id)).toMatchSnapshot('group with line shape')
				editor.pointerMove(-20, -20).pointerUp(-20, -20)
				expect(editor.getShapePageBounds(groupA.id)!.toJson()).toMatchSnapshot(
					'group shape after second resize'
				)
				const boxC = onlySelectedShape()
				expect(editor.getShapePageBounds(boxC.id)!.toJson()).toMatchSnapshot(
					'box shape after second resize'
				)
			})

			it('works if the shape drawing begins outside of the current group bounds', () => {
				editor.select(ids.boxA)
				expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

				editor.setCurrentTool('line')
				editor.pointerDown(-50, -50).pointerMove(-100, -100).pointerUp()

				expect(editor.getShapePageBounds(groupA.id)!.toJson()).toMatchSnapshot('group with line')

				const boxC = onlySelectedShape()
				expect(editor.getShapePageBounds(boxC.id)!.toJson()).toMatchSnapshot(
					'box shape after resize'
				)
			})
		})

		describe('sticky notes', () => {
			it('does not draw inside the group if the group is only selected and not focused', () => {
				editor.select(groupA.id)
				expect(editor.getFocusedGroupId() === editor.getCurrentPageId()).toBe(true)

				editor.setCurrentTool('note')
				editor.pointerDown(20, 20).pointerUp()

				const postit = onlySelectedShape()
				expect(postit.parentId).toBe(editor.getCurrentPageId())
			})

			it('does draw inside the group if the group is focused', () => {
				editor.select(ids.boxA)
				expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

				editor.setCurrentTool('note')
				editor.pointerDown(20, 20).pointerUp()

				const postit = onlySelectedShape()
				expect(postit.parentId).toBe(groupA.id)
			})

			it('will reisze the group appropriately if the new shape changes the group bounds', () => {
				editor.select(ids.boxA)
				expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

				expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
					x: 0,
					y: 0,
					w: 100,
					h: 100,
				})

				editor.setCurrentTool('note')
				editor.pointerDown(80, 80)
				editor.pointerUp()
				// default size is 200x200, and it centers it, so add 100px around the pointer
				expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
					x: -20,
					y: -20,
					w: 200,
					h: 200,
				})

				editor.pointerMove(20, 20)
				editor.pointerUp(20, 20)
				expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
					x: -20,
					y: -20,
					w: 200,
					h: 200,
				})
			})

			it('works if the shape drawing begins outside of the current group bounds', () => {
				editor.select(ids.boxA)
				expect(editor.getFocusedGroupId() === groupA.id).toBe(true)

				editor.setCurrentTool('note')
				expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
					x: 0,
					y: 0,
					w: 100,
					h: 100,
				})
				editor.pointerDown(-20, -20).pointerUp(-20, -20)
				expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
					x: -120,
					y: -120,
					w: 220,
					h: 220,
				})
			})
		})
	})
})

describe('erasing', () => {
	let groupAId: TLShapeId
	let groupBId: TLShapeId
	let groupCId: TLShapeId
	beforeEach(() => {
		//  group C
		// ┌─────────────────────────────────────────────────────────┐
		// │ group A                         group B                 │
		// │ ┌────────────────────────┐     ┌──────────────────────┐ │
		// │ │  0             20      │     │ 40            60     │ │
		// │ │  ┌───┐          ┌───┐  │     │ ┌───┐          ┌───┐ │ │
		// │ │  │ A │          │ B │  │     │ │ C │          │ D │ │ │
		// │ │  └───┘          └───┘  │     │ └───┘          └───┘ │ │
		// │ └────────────────────────┘     └──────────────────────┘ │
		// └─────────────────────────────────────────────────────────┘
		//
		//  20  ┌───┐
		//      │ E │
		//      └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 0),
			box(ids.boxC, 60, 0),
			box(ids.boxD, 80, 0),
			box(ids.boxE, 0, 20),
		])
		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupAId = onlySelectedId()
		editor.select(ids.boxC, ids.boxD)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupBId = onlySelectedId()
		editor.select(groupAId, groupBId)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupCId = onlySelectedId()
		editor.selectNone()
	})

	it('erases whole groups if you hit one of their shapes', () => {
		editor.setCurrentTool('eraser')

		// erase D
		editor.pointerDown(65, 5, ids.boxD)
		expect(editor.getCurrentPageState().erasingShapeIds.length).toBe(1)
		expect(editor.getCurrentPageState().erasingShapeIds[0]).toBe(groupCId)
		editor.pointerUp()
		expect(editor.getShape(groupCId)).toBeFalsy()
	})

	it('does not erase whole groups if you do not hit on one of their shapes', () => {
		editor.setCurrentTool('eraser')
		editor.pointerDown(40, 5)
		expect(editor.getErasingShapeIds()).toEqual([])
	})

	it('works inside of groups', () => {
		editor.select(ids.boxA)
		expect(editor.getFocusedGroupId() === groupAId).toBe(true)
		const groupA = editor.getShape(groupAId)!

		editor.setCurrentTool('eraser')

		// erase B
		editor.pointerDown(25, 5, ids.boxB)
		expect(editor.getCurrentPageState().erasingShapeIds.length).toBe(1)
		expect(editor.getCurrentPageState().erasingShapeIds[0]).toBe(ids.boxB)
		editor.pointerUp()

		// group A disappears
		expect(isRemoved(groupA)).toBe(true)
	})

	it('works outside of the focus layer', () => {
		editor.select(ids.boxA)
		expect(editor.getFocusedGroupId() === groupAId).toBe(true)

		editor.setCurrentTool('eraser')

		// erase E
		editor.pointerDown(5, 25, ids.boxE)
		expect(editor.getCurrentPageState().erasingShapeIds.length).toBe(1)
		expect(editor.getCurrentPageState().erasingShapeIds[0]).toBe(ids.boxE)

		// move to group B
		editor.pointerMove(65, 5)

		expect(editor.getErasingShapeIds().length).toBe(3)
	})
})

describe('binding bug', () => {
	beforeEach(() => {
		editor.createShapes([box(ids.boxA, 0, 0, 10, 10), box(ids.boxB, 50, 0, 10, 10)])
		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		editor.selectNone()
	})

	it('can not be made from some child shape to a group shape', () => {
		editor.setCurrentTool('arrow')
		// go from A to group A
		editor.pointerDown(5, 5).pointerMove(25, 5).pointerUp()
		const arrow = onlySelectedShape() as TLArrowShape
		const bindings = getArrowBindings(editor, arrow)
		expect(bindings.start).toMatchObject({ toId: ids.boxA })
		expect(bindings.end).toBeUndefined()
	})
})

describe('bindings', () => {
	let groupAId: TLShapeId
	let groupBId: TLShapeId
	beforeEach(() => {
		//  group C
		// ┌─────────────────────────────────────────────────────────┐
		// │ group A                         group B                 │
		// │ ┌────────────────────────┐     ┌──────────────────────┐ │
		// │ │  0             20      │     │ 40            60     │ │
		// │ │  ┌───┐          ┌───┐  │     │ ┌───┐          ┌───┐ │ │
		// │ │  │ A │          │ B │  │     │ │ C │          │ D │ │ │
		// │ │  └───┘          └───┘  │     │ └───┘          └───┘ │ │
		// │ └────────────────────────┘     └──────────────────────┘ │
		// └─────────────────────────────────────────────────────────┘
		//
		//  20  ┌───┐
		//      │ E │
		//      └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 0),
			box(ids.boxC, 40, 0),
			box(ids.boxD, 60, 0),
			box(ids.boxE, 0, 20),
		])
		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupAId = onlySelectedId()
		editor.select(ids.boxC, ids.boxD)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupBId = onlySelectedId()
		editor.select(groupAId, groupBId)
		editor.groupShapes(editor.getSelectedShapeIds())
		editor.selectNone()
	})

	it('can not be made from some sibling shape to a group shape', () => {
		editor.setCurrentTool('arrow')
		// go from E to group C (not hovering over a leaf box)
		editor.pointerDown(5, 25).pointerMove(35, 5).pointerUp()
		const arrow = onlySelectedShape() as TLArrowShape
		const bindings = getArrowBindings(editor, arrow)

		expect(bindings.start).toMatchObject({ toId: ids.boxE })
		expect(bindings.end).toBeUndefined()
	})

	it('can not be made from a group shape to some sibling shape', () => {
		editor.setCurrentTool('arrow')
		// go from group C (not hovering over a leaf box) to E
		editor.pointerDown(35, 5).pointerMove(5, 25).pointerUp()

		const arrow = onlySelectedShape() as TLArrowShape
		const bindings = getArrowBindings(editor, arrow)

		expect(bindings.start).toBeUndefined()
		expect(bindings.end).toMatchObject({ toId: ids.boxE })
	})
	it('can be made from a shape within a group to some shape outside of the group', () => {
		editor.setCurrentTool('arrow')
		// go from A to E
		editor.pointerDown(5, 5).pointerMove(5, 25).pointerUp()
		const arrow = onlySelectedShape() as TLArrowShape
		const bindings = getArrowBindings(editor, arrow)

		expect(arrow.parentId).toBe(editor.getCurrentPageId())

		expect(bindings.start).toMatchObject({ toId: ids.boxA })
		expect(bindings.end).toMatchObject({ toId: ids.boxE })
	})

	it('can be made from a shape within a group to another shape within the group', () => {
		editor.setCurrentTool('arrow')
		// go from A to B
		editor.pointerDown(5, 5).pointerMove(25, 5).pointerUp()
		const arrow = onlySelectedShape() as TLArrowShape
		const bindings = getArrowBindings(editor, arrow)

		expect(arrow.parentId).toBe(groupAId)
		expect(bindings.start).toMatchObject({ toId: ids.boxA })
		expect(bindings.end).toMatchObject({ toId: ids.boxB })
	})

	it('can be made from a shape outside of a group to a shape within the group', () => {
		editor.setCurrentTool('arrow')
		// go from E to B
		editor.pointerDown(5, 25).pointerMove(27, 7).pointerMove(25, 5).pointerUp()
		const arrow = onlySelectedShape() as TLArrowShape
		const bindings = getArrowBindings(editor, arrow)

		expect(arrow.parentId).toBe(editor.getCurrentPageId())
		expect(bindings.start).toMatchObject({ toId: ids.boxE })
		expect(bindings.end).toMatchObject({ toId: ids.boxB })
	})
})

describe('grouping arrows', () => {
	// Fix for <https://linear.app/tldraw/issue/TLD-887/cant-duplicate-arrows-in-group>
	it('grouping 2 arrows should not change indexes', () => {
		const arrowAId = createShapeId('arrowA')
		const arrowBId = createShapeId('arrowB')

		editor.createShapes([
			arrow(arrowAId, { x: 0, y: 0 }, { x: 0, y: 10 }),
			arrow(arrowBId, { x: 10, y: 0 }, { x: 10, y: 10 }),
		])

		const arrowABefore = editor.getShape(arrowAId)!
		const arrowBBefore = editor.getShape(arrowBId)!

		expect(arrowABefore.parentId).toMatch(/^page:/)
		expect(arrowABefore.index).toBe('a1')
		expect(arrowBBefore.parentId).toMatch(/^page:/)
		expect(arrowBBefore.index).toBe('a2')

		editor.select(arrowAId, arrowBId)
		editor.groupShapes(editor.getSelectedShapeIds())

		const arrowAAfter = editor.getShape(arrowAId)!
		const arrowBAfter = editor.getShape(arrowBId)!

		expect(arrowAAfter.parentId).toMatch(/^shape:/)
		expect(arrowAAfter.index).toBe('a1')

		expect(arrowBAfter.parentId).toMatch(/^shape:/)
		expect(arrowBAfter.index).toBe('a2')
	})
})

describe('moving handles within a group', () => {
	let groupA: TLGroupShape
	beforeEach(() => {
		// group A
		// ┌──────────────────────────────┐
		// │      0   10         90   100 │
		// │      ┌───┐                   │
		// │      │ A │                   │
		// │  10  └───┘                   │
		// │                              │
		// │                              │
		// │                              │
		// │                              │
		// │  90                  ┌───┐   │
		// │                      │ B │   │
		// │ 100                  └───┘   │
		// └──────────────────────────────┘
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 90, 90)])
		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupA = onlySelectedShape() as TLGroupShape
		editor.selectNone()
	})
	it('resizes the group appropriately', () => {
		editor.select(ids.boxA)
		expect(editor.getFocusedGroupId()).toBe(groupA.id)

		editor.setCurrentTool('arrow')

		editor.pointerDown(50, 50).pointerMove(60, 60).pointerUp(60, 60)

		let arrow = onlySelectedShape() as TLArrowShape
		let bindings = getArrowBindings(editor, arrow)

		expect(arrow.parentId).toBe(groupA.id)

		expect(bindings.start).toBeUndefined()
		expect(arrow.props.start.x).toBe(0)
		expect(arrow.props.start.y).toBe(0)

		expect(bindings.end).toBeUndefined()
		expect(arrow.props.end.x).toBe(10)
		expect(arrow.props.end.y).toBe(10)

		editor.expectToBeIn('select.idle')

		expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
			x: 0,
			y: 0,
			w: 100,
			h: 100,
		})

		editor.pointerDown(60, 60, {
			target: 'handle',
			shape: arrow,
			handle: editor.getShapeHandles<TLArrowShape>(arrow)!.find((h) => h.id === 'end'),
		})

		editor.expectToBeIn('select.pointing_handle')
		editor.pointerMove(60, -10)
		editor.expectToBeIn('select.dragging_handle')
		editor.pointerMove(60, -10)

		arrow = editor.getShape(arrow.id)!
		bindings = getArrowBindings(editor, arrow)

		expect(arrow.parentId).toBe(groupA.id)

		expect(bindings.start).toBeUndefined()
		expect(arrow.props.start.x).toBe(0)
		expect(arrow.props.start.y).toBe(0)

		expect(bindings.end).toBeUndefined()
		expect(arrow.props.end.x).toBe(10)
		expect(arrow.props.end.y).toBe(-60)

		expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
			x: 0,
			y: -10,
			w: 100,
			h: 110,
		})

		editor.pointerMove(50, -10)
		for (let i = -10; i >= -30; i--) {
			editor.pointerMove(i, i)
		}

		editor.pointerUp()

		expect(editor.getShapePageBounds(groupA.id)).toCloselyMatchObject({
			x: -30,
			y: -30,
			w: 130,
			h: 130,
		})
	})
})

// ! Parked temporarily. This behavior has changed and may need to change back.

// describe('copy/pasting to/from groups', () => {
// 	let groupAId: TLShapeId
// 	let groupBId: TLShapeId
// 	let groupCId: TLShapeId
// 	beforeEach(() => {
// 		//  group C
// 		// ┌─────────────────────────────────────────────────────────┐
// 		// │ group A                         group B                 │
// 		// │ ┌────────────────────────┐     ┌──────────────────────┐ │
// 		// │ │  0             20      │     │ 40            60     │ │
// 		// │ │  ┌───┐          ┌───┐  │     │ ┌───┐          ┌───┐ │ │
// 		// │ │  │ A │          │ B │  │     │ │ C │          │ D │ │ │
// 		// │ │  └───┘          └───┘  │     │ └───┘          └───┘ │ │
// 		// │ └────────────────────────┘     └──────────────────────┘ │
// 		// └─────────────────────────────────────────────────────────┘
// 		//
// 		//  20  ┌───┐
// 		//      │ E │
// 		//      └───┘
// 		editor.createShapes([
// 			box(ids.boxA, 0, 0),
// 			box(ids.boxB, 20, 0),
// 			box(ids.boxC, 40, 0),
// 			box(ids.boxD, 60, 0),
// 			box(ids.boxE, 0, 20),
// 		])
// 		editor.select(ids.boxA, ids.boxB)
// 		editor.groupShapes(editor.selectedShapeIds)
// 		groupAId = onlySelectedId()
// 		editor.select(ids.boxC, ids.boxD)
// 		editor.groupShapes(editor.selectedShapeIds)
// 		groupBId = onlySelectedId()
// 		editor.select(groupAId, groupBId)
// 		editor.groupShapes(editor.selectedShapeIds)
// 		groupCId = onlySelectedId()
// 		editor.selectNone()
// 	})

// 	it('should allow copying and pasting within the same focus layer', () => {
// 		editor.select(groupAId)
// 		expect(editor.focusedGroupId).toBe(groupCId)
// 		editor.copy()
// 		editor.paste()
// 		expect(editor.focusedGroupId).toBe(groupCId)
// 		expect(onlySelectedId()).not.toBe(groupAId)
// 		expect(onlySelectedShape().type).toBe(GroupShapeUtil.type)
// 		expect(
// 			editor.getSortedChildIdsForParent(onlySelectedShape().id).map((id) => editor.getShape(id)!.type)
// 		).toEqual(['geo', 'geo'])
// 	})

// 	it('should allow copying from within a group and pasting into a higher focus level', () => {
// 		editor.select(groupAId)
// 		expect(editor.focusedGroupId).toBe(groupCId)
// 		editor.copy()
// 		editor.select(groupCId)
// 		expect(editor.focusedGroupId).toBe(editor.currentPageId)
// 		editor.paste()
// 		expect(editor.focusedGroupId).toBe(editor.currentPageId)
// 		expect(onlySelectedId()).not.toBe(groupAId)
// 		expect(onlySelectedShape().type).toBe(GroupShapeUtil.type)
// 		expect(
// 			editor.getSortedChildIdsForParent(onlySelectedShape().id).map((id) => editor.getShape(id)!.type)
// 		).toEqual(['geo', 'geo'])
// 		expect(editor.getPageBounds(groupAId)).toCloselyMatchObject(
// 			editor.getPageBounds(onlySelectedId())
// 		)
// 	})
// 	it('should allow copying from a higher focus level and pasting into a group', () => {
// 		editor.select(groupCId)
// 		expect(editor.focusedGroupId).toBe(editor.currentPageId)
// 		editor.copy()
// 		editor.select(ids.boxA)
// 		expect(editor.focusedGroupId).toBe(groupAId)
// 		editor.paste()
// 		expect(editor.focusedGroupId).toBe(groupAId)
// 		expect(onlySelectedId()).not.toBe(groupCId)
// 		expect(onlySelectedShape().parentId).toBe(groupAId)
// 		expect(onlySelectedShape().type).toBe(GroupShapeUtil.type)
// 		expect(editor.getSortedChildIdsForParent(onlySelectedId()).map((id) => editor.getShape(id)!.type)).toEqual(
// 			[GroupShapeUtil.type, GroupShapeUtil.type]
// 		)
// 	})
// })

describe('snapping', () => {
	let groupAId: TLShapeId
	let groupBId: TLShapeId
	let groupCId: TLShapeId
	beforeEach(() => {
		//  group C
		// ┌─────────────────────────────────────────────────────────┐
		// │ group A                         group B                 │
		// │ ┌────────────────────────┐     ┌──────────────────────┐ │
		// │ │  0             20      │     │ 40            60     │ │
		// │ │  ┌───┐          ┌───┐  │     │ ┌───┐          ┌───┐ │ │
		// │ │  │ A │          │ B │  │     │ │ C │          │ D │ │ │
		// │ │  └───┘          └───┘  │     │ └───┘          └───┘ │ │
		// │ └────────────────────────┘     └──────────────────────┘ │
		// └─────────────────────────────────────────────────────────┘
		//
		//  20  ┌───┐
		//      │ E │
		//      └───┘
		editor.createShapes([
			box(ids.boxA, 0, 0),
			box(ids.boxB, 20, 0),
			box(ids.boxC, 40, 0),
			box(ids.boxD, 60, 0),
			box(ids.boxE, 0, 20),
		])
		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupAId = onlySelectedId()
		editor.select(ids.boxC, ids.boxD)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupBId = onlySelectedId()
		editor.select(groupAId, groupBId)
		editor.groupShapes(editor.getSelectedShapeIds())
		groupCId = onlySelectedId()
		editor.selectNone()
	})

	it('does not happen between groups and their children', () => {
		editor.select(groupCId)
		editor.pointerDown(10, 5, groupCId)
		editor.pointerMove(80, 5, groupCId, { ctrlKey: true })
		expect(editor.snaps.getIndicators().length).toBe(0)
	})

	it('does not happen between children and their group', () => {
		editor.select(ids.boxD)
		editor.pointerDown(65, 5, ids.boxD)
		editor.pointerMove(80, 105, ids.boxD, { ctrlKey: true })
		expect(editor.snaps.getIndicators().length).toBe(0)
	})
})

describe('When pressing enter with selected group', () => {
	it('Should select the children of the group when enter is pressed', () => {
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])
		editor.select(ids.boxA, ids.boxB, ids.boxC)
		editor.groupShapes(editor.getSelectedShapeIds())
		editor.keyDown('Enter')
		editor.keyUp('Enter')
		expect(editor.getSelectedShapeIds()).toMatchObject([ids.boxA, ids.boxB, ids.boxC])
	})
	it('Should select the children of multiple groups when enter is pressed', () => {
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0)])
		editor.createShapes([box(ids.boxC, 40, 0), box(ids.boxD, 70, 0)])
		editor.select(ids.boxA, ids.boxB)
		editor.groupShapes(editor.getSelectedShapeIds())
		editor.select(ids.boxC, ids.boxD)
		editor.groupShapes(editor.getSelectedShapeIds())
		editor.selectAll() // both groups
		editor.keyDown('Enter')
		editor.keyUp('Enter')
		expect(editor.getSelectedShapeIds()).toMatchObject([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
	})
})

describe('Group opacity', () => {
	it("should set the group's opacity to max even if the selected style panel opacity is lower", () => {
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0)])
		editor.select(ids.boxA, ids.boxB)
		editor.setOpacityForSelectedShapes(0.5)
		editor.setOpacityForNextShapes(0.5)
		editor.groupShapes(editor.getSelectedShapeIds())
		const group = editor.getShape(onlySelectedId())!
		assert(editor.isShapeOfType(group, 'group'))
		expect(group.opacity).toBe(1)
	})
})

describe('Grouping / ungrouping locked shapes', () => {
	it('keeps locked shapes locked when grouped', () => {
		editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 200, 0)])
		editor.groupShapes([ids.boxA, ids.boxB], { groupId: ids.groupA })
		editor.select(ids.boxA)

		// Lock boxA
		editor.updateShape({ id: ids.boxA, type: 'geo', isLocked: true })

		// Select the group; when we ungroup, the children should be selected
		editor.select(ids.groupA)

		// Move the group shape; this should also move locked children
		editor.nudgeShapes([ids.groupA], { x: 100, y: 0 })

		// When we ungroup, the locked children should...
		editor.ungroupShapes([ids.groupA])

		// Still exist
		expect(editor.getShape(ids.boxA)).toBeTruthy()
		expect(editor.getShape(ids.boxB)).toBeTruthy()

		// Both be selected
		expect(editor.getSelectedShapeIds()).toMatchObject([ids.boxA, ids.boxB])

		// And be in the correct position
		expect(editor.getShape(ids.boxA)!.x).toBe(100)
		expect(editor.getShape(ids.boxB)!.x).toBe(300)

		// And have the correct locked state
		expect(editor.getShape(ids.boxA)!.isLocked).toBe(true)
		expect(editor.getShape(ids.boxB)!.isLocked).toBe(false)
	})
})
