import { TLShapeId, createShapeId } from '@tldraw/editor'
import { TestEditor } from './TestEditor'

let editor: TestEditor

beforeEach(() => {
	editor = new TestEditor()
})

describe('bindingsIndex', () => {
	it('keeps a mapping from bound shapes to their bindings', () => {
		const ids = {
			box1: createShapeId('box1'),
			box2: createShapeId('box2'),
		}
		editor.createShapes([
			{ id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100, fill: 'solid' } },
			{ id: ids.box2, type: 'geo', x: 200, y: 0, props: { w: 100, h: 100, fill: 'solid' } },
		])

		editor.selectNone()
		editor.setCurrentTool('arrow')
		editor.pointerDown(50, 50)
		expect(editor.getOnlySelectedShape()).toBe(null)
		expect(editor.getArrowsBoundTo(ids.box1)).toEqual([])

		editor.pointerMove(50, 55)
		expect(editor.getOnlySelectedShape()).not.toBe(null)
		const arrow = editor.getOnlySelectedShape()!
		expect(arrow.type).toBe('arrow')
		expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow])

		editor.pointerMove(250, 50)
		expect(editor.getArrowsBoundTo(ids.box1)).toEqual([editor.getShape(arrow.id)])
		expect(editor.getArrowsBoundTo(ids.box2)).toEqual([editor.getShape(arrow.id)])
	})

	it('works if there are many arrows', () => {
		const ids = {
			box1: createShapeId('box1'),
			box2: createShapeId('box2'),
		}

		editor.createShapes([
			{ type: 'geo', id: ids.box1, x: 0, y: 0, props: { w: 100, h: 100 } },
			{ type: 'geo', id: ids.box2, x: 200, y: 0, props: { w: 100, h: 100 } },
		])

		editor.setCurrentTool('arrow')
		// start at box 1 and end on box 2
		editor.pointerDown(50, 50)

		expect(editor.getArrowsBoundTo(ids.box1)).toEqual([])

		editor.pointerMove(250, 50)
		const arrow1 = editor.getOnlySelectedShape()!
		expect(arrow1.type).toBe('arrow')

		expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow1])
		expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1])

		editor.pointerUp()

		expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow1])
		expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1])

		// start at box 1 and end on the page
		editor.setCurrentTool('arrow')
		editor.pointerMove(50, 50).pointerDown().pointerMove(50, -50).pointerUp()
		const arrow2 = editor.getOnlySelectedShape()!
		expect(arrow2.type).toBe('arrow')

		expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow1, arrow2])

		// start outside box 1 and end in box 1
		editor.setCurrentTool('arrow')
		editor.pointerDown(0, -50).pointerMove(50, 50).pointerUp(50, 50)
		const arrow3 = editor.getOnlySelectedShape()!
		expect(arrow3.type).toBe('arrow')

		expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow1, arrow2, arrow3])

		expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1])

		// start at box 2 and end on the page
		editor.selectNone()
		editor.setCurrentTool('arrow')
		editor.pointerDown(250, 50)
		editor.expectToBeIn('arrow.pointing')
		editor.pointerMove(250, -50)
		editor.expectToBeIn('select.dragging_handle')
		const arrow4 = editor.getOnlySelectedShape()!

		expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1, arrow4])

		editor.pointerUp(250, -50)
		editor.expectToBeIn('select.idle')
		expect(arrow4.type).toBe('arrow')

		expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1, arrow4])

		// start outside box 2 and enter in box 2
		editor.setCurrentTool('arrow')
		editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50)
		const arrow5 = editor.getOnlySelectedShape()!
		expect(arrow5.type).toBe('arrow')

		expect(editor.getArrowsBoundTo(ids.box1)).toEqual([arrow1, arrow2, arrow3])

		expect(editor.getArrowsBoundTo(ids.box2)).toEqual([arrow1, arrow4, arrow5])
	})

	describe('updating shapes', () => {
		//     ▲ │              │  ▲
		//     │ │              │  │
		//     b c              e  d
		// ┌───┼─┴─┐         ┌──┴──┼─┐
		// │   │ ▼ │         │  ▼  │ │
		// │   └───┼─────a───┼───► │ │
		// │ 1     │         │ 2     │
		// └───────┘         └───────┘
		let arrowAId: TLShapeId
		let arrowBId: TLShapeId
		let arrowCId: TLShapeId
		let arrowDId: TLShapeId
		let arrowEId: TLShapeId
		let ids: Record<string, TLShapeId>
		beforeEach(() => {
			ids = {
				box1: createShapeId('box1'),
				box2: createShapeId('box2'),
			}
			editor.createShapes([
				{ id: ids.box1, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } },
				{ id: ids.box2, type: 'geo', x: 200, y: 0, props: { w: 100, h: 100 } },
			])

			// span both boxes
			editor.setCurrentTool('arrow')
			editor.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50)
			arrowAId = editor.getOnlySelectedShape()!.id
			// start at box 1 and leave
			editor.setCurrentTool('arrow')
			editor.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50)
			arrowBId = editor.getOnlySelectedShape()!.id
			// start outside box 1 and enter
			editor.setCurrentTool('arrow')
			editor.pointerDown(50, -50).pointerMove(50, 50).pointerUp(50, 50)
			arrowCId = editor.getOnlySelectedShape()!.id
			// start at box 2 and leave
			editor.setCurrentTool('arrow')
			editor.pointerDown(250, 50).pointerMove(250, -50).pointerUp(250, -50)
			arrowDId = editor.getOnlySelectedShape()!.id
			// start outside box 2 and enter
			editor.setCurrentTool('arrow')
			editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50)
			arrowEId = editor.getOnlySelectedShape()!.id
		})
		it('deletes the entry if you delete the bound shapes', () => {
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
			editor.deleteShapes([ids.box2])
			expect(editor.getArrowsBoundTo(ids.box2)).toEqual([])
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
		})
		it('deletes the entry if you delete an arrow', () => {
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
			editor.deleteShapes([arrowEId])
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(2)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)

			editor.deleteShapes([arrowDId])
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)

			editor.deleteShapes([arrowCId])
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(2)

			editor.deleteShapes([arrowBId])
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(1)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(1)

			editor.deleteShapes([arrowAId])
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(0)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(0)
		})

		it('deletes the entries in a batch too', () => {
			editor.deleteShapes([arrowAId, arrowBId, arrowCId, arrowDId, arrowEId])

			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(0)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(0)
		})

		it('adds new entries after initial creation', () => {
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)

			// draw from box 2 to box 1
			editor.setCurrentTool('arrow')
			editor.pointerDown(250, 50).pointerMove(50, 50).pointerUp(50, 50)
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(4)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(4)

			// create a new box

			const box3 = createShapeId('box3')
			editor.createShapes([{ id: box3, type: 'geo', x: 400, y: 0, props: { w: 100, h: 100 } }])

			// draw from box 2 to box 3

			editor.setCurrentTool('arrow')
			editor.pointerDown(250, 50).pointerMove(450, 50).pointerUp(450, 50)
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(5)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(4)
			expect(editor.getArrowsBoundTo(box3)).toHaveLength(1)
		})

		it('works when copy pasting', () => {
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)

			editor.selectAll()
			editor.duplicateShapes(editor.getSelectedShapeIds())

			const [box1Clone, box2Clone] = editor
				.getSelectedShapes()
				.filter((shape) => editor.isShapeOfType(shape, 'geo'))
				.sort((a, b) => a.x - b.x)

			expect(editor.getArrowsBoundTo(box2Clone.id)).toHaveLength(3)
			expect(editor.getArrowsBoundTo(box1Clone.id)).toHaveLength(3)
		})

		it('allows bound shapes to be moved', () => {
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)

			editor.nudgeShapes([ids.box2], { x: 0, y: -1 })

			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
		})

		it('allows the arrows bound shape to change', () => {
			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)

			// create another box

			const box3 = createShapeId('box3')
			editor.createShapes([{ id: box3, type: 'geo', x: 400, y: 0, props: { w: 100, h: 100 } }])

			// move arrowA end from box2 to box3
			const binding = editor
				.getBindingsInvolvingShape(ids.box2, 'arrow')
				.find((b) => b.props.terminal === 'end')!
			editor.updateBinding({ ...binding, toId: box3 })

			expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(2)
			expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
			expect(editor.getArrowsBoundTo(box3)).toHaveLength(1)
		})
	})
})
