import { describe, test, expect } from 'bun:test'

import { createUndoManager, undoEntry } from '#tests/helpers/undo'

describe('UndoManager', () => {
  test('initial state', () => {
    const undo = createUndoManager()
    expect(undo.canUndo).toBe(false)
    expect(undo.canRedo).toBe(false)
    expect(undo.undoLabel).toBeNull()
    expect(undo.redoLabel).toBeNull()
  })

  test('apply executes forward and enables undo', () => {
    const undo = createUndoManager()
    let value = 0
    undo.apply(
      undoEntry(
        'increment',
        () => {
          value = 1
        },
        () => {
          value = 0
        }
      )
    )
    expect(value).toBe(1)
    expect(undo.canUndo).toBe(true)
    expect(undo.undoLabel).toBe('increment')
  })

  test('push does not execute forward', () => {
    const undo = createUndoManager()
    let value = 0
    undo.push(
      undoEntry(
        'set',
        () => {
          value = 1
        },
        () => {
          value = 0
        }
      )
    )
    expect(value).toBe(0)
    expect(undo.canUndo).toBe(true)
  })

  test('undo calls inverse and returns label', () => {
    const undo = createUndoManager()
    let value = 0
    undo.apply(
      undoEntry(
        'set',
        () => {
          value = 1
        },
        () => {
          value = 0
        }
      )
    )
    const label = undo.undo()
    expect(label).toBe('set')
    expect(value).toBe(0)
    expect(undo.canUndo).toBe(false)
    expect(undo.canRedo).toBe(true)
  })

  test('redo calls forward and returns label', () => {
    const undo = createUndoManager()
    let value = 0
    undo.apply(
      undoEntry(
        'set',
        () => {
          value = 1
        },
        () => {
          value = 0
        }
      )
    )
    undo.undo()
    const label = undo.redo()
    expect(label).toBe('set')
    expect(value).toBe(1)
    expect(undo.canRedo).toBe(false)
  })

  test('new action clears redo stack', () => {
    const undo = createUndoManager()
    undo.apply(undoEntry('a'))
    undo.undo()
    expect(undo.canRedo).toBe(true)
    undo.apply(undoEntry('b'))
    expect(undo.canRedo).toBe(false)
  })

  test('undo on empty returns null', () => {
    const undo = createUndoManager()
    expect(undo.undo()).toBeNull()
  })

  test('redo on empty returns null', () => {
    const undo = createUndoManager()
    expect(undo.redo()).toBeNull()
  })

  test('clear empties both stacks', () => {
    const undo = createUndoManager()
    undo.apply(undoEntry('a'))
    undo.undo()
    undo.clear()
    expect(undo.canUndo).toBe(false)
    expect(undo.canRedo).toBe(false)
  })

  test('history limit drops oldest entries', () => {
    const undo = createUndoManager({ limit: 2 })
    let val = 0
    undo.apply(
      undoEntry(
        'to1',
        () => {
          val = 1
        },
        () => {
          val = 0
        }
      )
    )
    undo.apply(
      undoEntry(
        'to2',
        () => {
          val = 2
        },
        () => {
          val = 1
        }
      )
    )
    undo.apply(
      undoEntry(
        'to3',
        () => {
          val = 3
        },
        () => {
          val = 2
        }
      )
    )

    expect(undo.undoLabel).toBe('to3')
    undo.undo()
    undo.undo()
    expect(val).toBe(1)
    expect(undo.undo()).toBeNull()
  })

  test('batch combines into single entry', () => {
    const undo = createUndoManager()
    const log: number[] = []
    undo.beginBatch('batch')
    undo.apply(
      undoEntry(
        'a',
        () => log.push(1),
        () => log.push(-1)
      )
    )
    undo.apply(
      undoEntry(
        'b',
        () => log.push(2),
        () => log.push(-2)
      )
    )
    undo.commitBatch()
    expect(log).toEqual([1, 2])
    expect(undo.undoLabel).toBe('batch')

    undo.undo()
    expect(log).toEqual([1, 2, -2, -1])
  })

  test('runBatch commits on success', () => {
    const undo = createUndoManager()
    const log: number[] = []

    undo.runBatch('batch', () => {
      undo.apply(
        undoEntry(
          'a',
          () => log.push(1),
          () => log.push(-1)
        )
      )
      undo.apply(
        undoEntry(
          'b',
          () => log.push(2),
          () => log.push(-2)
        )
      )
    })

    expect(log).toEqual([1, 2])
    expect(undo.undoLabel).toBe('batch')
    undo.undo()
    expect(log).toEqual([1, 2, -2, -1])
  })

  test('runBatch rolls back on throw', () => {
    const undo = createUndoManager()
    const log: number[] = []

    expect(() =>
      undo.runBatch('batch', () => {
        undo.apply(
          undoEntry(
            'a',
            () => log.push(1),
            () => log.push(-1)
          )
        )
        throw new Error('boom')
      })
    ).toThrow('boom')

    expect(log).toEqual([1, -1])
    expect(undo.canUndo).toBe(false)
  })

  test('nested batches commit as a single outer entry', () => {
    const undo = createUndoManager()
    const log: number[] = []

    undo.runBatch('outer', () => {
      undo.apply(
        undoEntry(
          'a',
          () => log.push(1),
          () => log.push(-1)
        )
      )
      undo.runBatch('inner', () => {
        undo.apply(
          undoEntry(
            'b',
            () => log.push(2),
            () => log.push(-2)
          )
        )
      })
    })

    expect(undo.undoLabel).toBe('outer')
    undo.undo()
    expect(log).toEqual([1, 2, -2, -1])
  })

  test('empty batch commits nothing', () => {
    const undo = createUndoManager()
    undo.beginBatch('empty')
    undo.commitBatch()
    expect(undo.canUndo).toBe(false)
  })

  test('multiple undo/redo', () => {
    const undo = createUndoManager()
    let val = 0
    undo.apply(
      undoEntry(
        'to1',
        () => {
          val = 1
        },
        () => {
          val = 0
        }
      )
    )
    undo.apply(
      undoEntry(
        'to2',
        () => {
          val = 2
        },
        () => {
          val = 1
        }
      )
    )
    undo.apply(
      undoEntry(
        'to3',
        () => {
          val = 3
        },
        () => {
          val = 2
        }
      )
    )

    undo.undo()
    expect(val).toBe(2)
    undo.undo()
    expect(val).toBe(1)
    undo.redo()
    expect(val).toBe(2)
    undo.redo()
    expect(val).toBe(3)
  })
})
