import { beforeEach, describe, expect, it } from "vitest"
import {
  computeInverse,
  createOperationHistory,
  type EditorOperation,
  setTimeSource,
} from "@/lib/operation-history"

beforeEach(() => {
  setTimeSource(() => 1000)
})

describe("computeInverse", () => {
  it("split-block inverse is merge-block at index+1", () => {
    const op: EditorOperation = {
      type: "split-block",
      index: 2,
      splitBlockId: "new-block",
      depth: 0,
      beforeContent: "hello",
      afterContent: "world",
      timestamp: 1,
    }
    const inverse = computeInverse(op)
    expect(inverse).toEqual({
      type: "merge-block",
      index: 3,
      removedBlockId: "new-block",
      removedBlockDepth: 0,
      prevContent: "hello",
      appendedContent: "world",
      timestamp: 1000,
    })
  })

  it("merge-block inverse is split-block at index-1", () => {
    const op: EditorOperation = {
      type: "merge-block",
      index: 1,
      removedBlockId: "block-b",
      removedBlockDepth: 0,
      prevContent: "hello",
      appendedContent: "world",
      timestamp: 1,
    }
    const inverse = computeInverse(op)
    expect(inverse).toEqual({
      type: "split-block",
      index: 0,
      splitBlockId: "block-b",
      depth: 0,
      beforeContent: "hello",
      afterContent: "world",
      timestamp: 1000,
    })
  })

  it("insert-block and delete-block are inverses of each other", () => {
    const insert: EditorOperation = {
      type: "insert-block",
      index: 0,
      blockId: "x",
      content: "test",
      depth: 1,
      timestamp: 1,
    }
    const deleteInverse = computeInverse(insert)
    expect(deleteInverse).toEqual({
      type: "delete-block",
      index: 0,
      blockId: "x",
      content: "test",
      depth: 1,
      timestamp: 1000,
    })

    const del: EditorOperation = {
      type: "delete-block",
      index: 0,
      blockId: "x",
      content: "test",
      depth: 1,
      timestamp: 2,
    }
    const insertInverse = computeInverse(del)
    expect(insertInverse).toEqual({
      type: "insert-block",
      index: 0,
      blockId: "x",
      content: "test",
      depth: 1,
      timestamp: 1000,
    })
  })

  it("indent-block and outdent-block are inverses of each other", () => {
    const indent: EditorOperation = {
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      timestamp: 1,
    }
    expect(computeInverse(indent)).toEqual({
      type: "outdent-block",
      index: 0,
      previousDepth: 1,
      timestamp: 1000,
    })

    const outdent: EditorOperation = {
      type: "outdent-block",
      index: 0,
      previousDepth: 1,
      timestamp: 2,
    }
    expect(computeInverse(outdent)).toEqual({
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      timestamp: 1000,
    })
  })

  it("indent-block inverse carries childrenPreviousDepths", () => {
    const op: EditorOperation = {
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      childrenPreviousDepths: [
        { index: 1, previousDepth: 1 },
        { index: 2, previousDepth: 1 },
      ],
      timestamp: 1,
    }
    const inverse = computeInverse(op)
    expect(inverse).toEqual({
      type: "outdent-block",
      index: 0,
      previousDepth: 1,
      childrenPreviousDepths: [
        { index: 1, previousDepth: 2 },
        { index: 2, previousDepth: 2 },
      ],
      timestamp: 1000,
    })
  })

  it("outdent-block inverse carries childrenPreviousDepths", () => {
    const op: EditorOperation = {
      type: "outdent-block",
      index: 0,
      previousDepth: 1,
      childrenPreviousDepths: [
        { index: 1, previousDepth: 2 },
        { index: 2, previousDepth: 2 },
      ],
      timestamp: 1,
    }
    const inverse = computeInverse(op)
    expect(inverse).toEqual({
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      childrenPreviousDepths: [
        { index: 1, previousDepth: 1 },
        { index: 2, previousDepth: 1 },
      ],
      timestamp: 1000,
    })
  })

  it("set-block-content swaps previous and new content", () => {
    const op: EditorOperation = {
      type: "set-block-content",
      blockId: "b",
      previousContent: "old",
      newContent: "new",
      timestamp: 1,
    }
    expect(computeInverse(op)).toEqual({
      type: "set-block-content",
      blockId: "b",
      previousContent: "new",
      newContent: "old",
      timestamp: 1000,
    })
  })
})

describe("createOperationHistory", () => {
  it("starts with no undo/redo available", () => {
    const h = createOperationHistory()
    expect(h.canUndo).toBe(false)
    expect(h.canRedo).toBe(false)
    expect(h.undo()).toBeNull()
    expect(h.redo()).toBeNull()
  })

  it("execute returns inverse and makes undo available", () => {
    const h = createOperationHistory()
    const op: EditorOperation = {
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      timestamp: 1,
    }
    const result = h.execute(op)
    expect(result.inverse.type).toBe("outdent-block")
    expect(h.canUndo).toBe(true)
    expect(h.canRedo).toBe(false)
  })

  it("undo returns inverse operation", () => {
    const h = createOperationHistory()
    h.execute({
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      timestamp: 1,
    })
    const inverse = h.undo()
    expect(inverse).not.toBeNull()
    expect(inverse?.type).toBe("outdent-block")
    expect(h.canUndo).toBe(false)
    expect(h.canRedo).toBe(true)
  })

  it("redo re-executes the undone operation", () => {
    const h = createOperationHistory()
    h.execute({
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      timestamp: 1,
    })
    h.undo()
    expect(h.canRedo).toBe(true)

    const redoOp = h.redo()
    expect(redoOp).not.toBeNull()
    expect(redoOp?.type).toBe("indent-block")
    expect((redoOp as { previousDepth: number }).previousDepth).toBe(0)
    expect(h.canUndo).toBe(true)
    expect(h.canRedo).toBe(false)
  })

  it("execute clears future stack", () => {
    const h = createOperationHistory()
    h.execute({
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      timestamp: 1,
    })
    h.undo()
    expect(h.canRedo).toBe(true)

    // New operation clears future
    h.execute({
      type: "indent-block",
      index: 1,
      previousDepth: 1,
      timestamp: 2,
    })
    expect(h.canRedo).toBe(false)
    expect(h.canUndo).toBe(true)
  })

  it("enforces max size by dropping oldest operations", () => {
    const h = createOperationHistory(2)
    for (let i = 0; i < 5; i++) {
      h.execute({
        type: "indent-block",
        index: i,
        previousDepth: 0,
        timestamp: i,
      })
    }
    // Only the last 2 operations remain
    expect(h.canUndo).toBe(true)
    const firstUndo = h.undo() as { index: number } & EditorOperation
    expect(firstUndo.index).toBe(4)
    const secondUndo = h.undo() as { index: number } & EditorOperation
    expect(secondUndo.index).toBe(3)
    expect(h.undo()).toBeNull()
  })

  it("multiple undo/redo round-trip", () => {
    const h = createOperationHistory()
    h.execute({
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      timestamp: 1,
    })
    h.execute({
      type: "indent-block",
      index: 1,
      previousDepth: 1,
      timestamp: 2,
    })
    h.execute({
      type: "indent-block",
      index: 2,
      previousDepth: 2,
      timestamp: 3,
    })

    // Undo 2
    expect((h.undo() as { index: number } & EditorOperation).index).toBe(2)
    expect((h.undo() as { index: number } & EditorOperation).index).toBe(1)

    // Redo 1
    expect((h.redo() as { index: number } & EditorOperation).index).toBe(1)

    // Undo 1
    expect((h.undo() as { index: number } & EditorOperation).index).toBe(1)

    // Redo all
    expect((h.redo() as { index: number } & EditorOperation).index).toBe(1)
    expect((h.redo() as { index: number } & EditorOperation).index).toBe(2)
    expect(h.redo()).toBeNull()
  })

  it("clear resets both stacks", () => {
    const h = createOperationHistory()
    h.execute({
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      timestamp: 1,
    })
    h.undo()
    h.clear()
    expect(h.canUndo).toBe(false)
    expect(h.canRedo).toBe(false)
    expect(h.undo()).toBeNull()
    expect(h.redo()).toBeNull()
  })

  it("peek returns last operation without popping", () => {
    const h = createOperationHistory()
    expect(h.peek()).toBeNull()

    h.execute({
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      timestamp: 1,
    })
    expect(h.peek()).toEqual({
      type: "indent-block",
      index: 0,
      previousDepth: 0,
      timestamp: 1,
    })
    expect(h.canUndo).toBe(true)
    expect(h.canUndo).toBe(true) // peek did not pop
  })

  it("move-block inverse swaps fromIndex and toIndex", () => {
    const op: EditorOperation = {
      type: "move-block",
      fromIndex: 0,
      toIndex: 3,
      blockId: "test-block",
      content: "moved content",
      depth: 1,
      timestamp: 1000,
    }
    const inverse = computeInverse(op)
    expect(inverse).toEqual({
      type: "move-block",
      fromIndex: 3,
      toIndex: 0,
      blockId: "test-block",
      content: "moved content",
      depth: 1,
      timestamp: 1000,
    })
  })

  describe("coalesce", () => {
    it("returns false when history is empty", () => {
      const h = createOperationHistory()
      expect(
        h.coalesce({
          type: "set-block-content",
          blockId: "a",
          previousContent: "",
          newContent: "hello",
          timestamp: 1,
        }),
      ).toBe(false)
    })

    it("returns false when type does not match last entry", () => {
      const h = createOperationHistory()
      h.execute({
        type: "indent-block",
        index: 0,
        previousDepth: 0,
        timestamp: 1,
      })
      expect(
        h.coalesce({
          type: "set-block-content",
          blockId: "a",
          previousContent: "",
          newContent: "hello",
          timestamp: 2,
        }),
      ).toBe(false)
    })

    it("replaces last operation and recomputes inverse", () => {
      const h = createOperationHistory()
      h.execute({
        type: "set-block-content",
        blockId: "a",
        previousContent: "",
        newContent: "h",
        timestamp: 1,
      })

      const result = h.coalesce({
        type: "set-block-content",
        blockId: "a",
        previousContent: "",
        newContent: "hello",
        timestamp: 2,
      })
      expect(result).toBe(true)

      // Inverse should restore to the preserved previousContent
      const inverse = h.undo()
      expect(inverse).not.toBeNull()
      expect(inverse?.type).toBe("set-block-content")
      expect((inverse as any).newContent).toBe("")
      expect((inverse as any).previousContent).toBe("hello")
    })

    it("replaces the operation and recomputes inverse for set-block-content", () => {
      const h = createOperationHistory()

      // First burst
      h.execute({
        type: "set-block-content",
        blockId: "a",
        previousContent: "",
        newContent: "hi",
        timestamp: 1,
      })

      // Coalesce with preserved previousContent
      const result = h.coalesce({
        type: "set-block-content",
        blockId: "a",
        previousContent: "",
        newContent: "hi there",
        timestamp: 2,
      })
      expect(result).toBe(true)

      // Undo step 1: restore to "" (original previousContent)
      const undo1 = h.undo()
      expect(undo1).not.toBeNull()
      expect((undo1 as any).newContent).toBe("")
      expect((undo1 as any).previousContent).toBe("hi there")
      expect(h.canUndo).toBe(false)

      // Redo
      const redo1 = h.redo()
      expect(redo1).not.toBeNull()
      expect(redo1?.type).toBe("set-block-content")
      expect((redo1 as any).newContent).toBe("hi there")
      expect(h.canUndo).toBe(true)
      expect(h.canRedo).toBe(false)
    })

    it("clears future stack on coalesce", () => {
      const h = createOperationHistory()
      h.execute({
        type: "set-block-content",
        blockId: "a",
        previousContent: "",
        newContent: "hi",
        timestamp: 1,
      })
      h.execute({
        type: "set-block-content",
        blockId: "a",
        previousContent: "hi",
        newContent: "hello",
        timestamp: 2,
      })
      // past: [hi, hello], future: []

      // Undo the second entry → future has hello entry
      h.undo()
      expect(h.canRedo).toBe(true)
      expect(h.canUndo).toBe(true)

      // Coalesce on the remaining past entry
      h.coalesce({
        type: "set-block-content",
        blockId: "a",
        previousContent: "",
        newContent: "wasup",
        timestamp: 3,
      })
      expect(h.canRedo).toBe(false)
      expect(h.canUndo).toBe(true)

      // Undo should restore to the preserved previousContent
      const undo1 = h.undo()
      expect(undo1).not.toBeNull()
      expect(undo1).toHaveProperty("newContent", "")
    })
  })
})
