import { describe, expect, it } from "vitest"
import { stableId } from "@/__test-utils__"
import type { EditorBlockData } from "@/lib/block-parser"
import {
  deleteIfEmpty,
  indentBlock,
  insertBlock,
  mergePrevious,
  moveBlock,
  outdentBlock,
  setBlockContent,
  splitBlocks,
} from "@/lib/editor-operations"
import { classifyBlock } from "@/lib/markdown-rules/block-classifier"

function block(
  overrides: Partial<EditorBlockData> & { content: string },
): EditorBlockData {
  return { id: "auto", depth: 0, ...overrides }
}

// ── splitBlocks ───────────────────────────────────────────────────

describe("splitBlocks", () => {
  it("splits a block into two at the given index", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", depth: 0, content: "first" }),
      block({ id: "b", depth: 1, content: "second" }),
    ]
    const ids = stableId("new")
    const result = splitBlocks(blocks, 1, "bef", "aft", ids)

    // Original block gets `before` content
    expect(result.blocks[0]).toMatchObject({ id: "a", content: "first" })

    // Original at index is updated
    expect(result.blocks[1]).toMatchObject({ id: "b", content: "bef" })

    // New block inserted after
    expect(result.blocks[2]).toMatchObject({
      id: "new-0",
      depth: 1,
      content: "aft",
    })

    expect(result.blocks).toHaveLength(3)
    expect(result.newBlock).toMatchObject({ id: "new-0", content: "aft" })
  })

  it("preserves depth of the original block", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", depth: 2, content: "x" }),
    ]
    const result = splitBlocks(blocks, 0, "a", "b", stableId("n"))
    expect(result.blocks[0].depth).toBe(2)
    expect(result.blocks[1].depth).toBe(2)
  })

  it("returns original array copy when index is out of bounds", () => {
    const blocks: EditorBlockData[] = [block({ id: "a", content: "x" })]
    expect(splitBlocks(blocks, -1, "", "", stableId("n")).newBlock).toBeNull()
    expect(splitBlocks(blocks, 5, "", "", stableId("n")).newBlock).toBeNull()
  })

  it("does not mutate the original array", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", depth: 1, content: "original" }),
    ]
    const copy = [...blocks]
    splitBlocks(blocks, 0, "before", "after", stableId("n"))
    expect(blocks).toEqual(copy)
  })
})

// ── mergePrevious ─────────────────────────────────────────────────

describe("mergePrevious", () => {
  it("merges two adjacent blocks, appending content", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "hello" }),
      block({ id: "b", content: " world" }),
    ]
    const result = mergePrevious(blocks, 1)

    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0]).toMatchObject({ id: "a", content: "hello world" })
    expect(result.mergedContent).toBe("hello world")
    expect(result.joinPos).toBe(5) // "hello".length — at end of previous block
  })

  it("joinPos is 0 when previous block is empty", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "" }),
      block({ id: "b", content: "content" }),
    ]
    const result = mergePrevious(blocks, 1)
    expect(result.joinPos).toBe(0)
    expect(result.blocks[0].content).toBe("content")
  })

  it("returns no change when index is 0 or out of bounds", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "x" }),
      block({ id: "b", content: "y" }),
    ]
    const atZero = mergePrevious(blocks, 0)
    expect(atZero.blocks).toHaveLength(2)

    const oob = mergePrevious(blocks, 5)
    expect(oob.blocks).toHaveLength(2)
  })

  it("does not mutate the original array", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "hello" }),
      block({ id: "b", content: " world" }),
    ]
    const copy = [...blocks]
    mergePrevious(blocks, 1)
    expect(blocks).toEqual(copy)
  })
})

// ── deleteIfEmpty ─────────────────────────────────────────────────

describe("deleteIfEmpty", () => {
  it("removes a block at the given index", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "first" }),
      block({ id: "b", content: "second" }),
      block({ id: "c", content: "third" }),
    ]
    const result = deleteIfEmpty(blocks, 1)
    expect(result.blocks).toHaveLength(2)
    expect(result.blocks[0].id).toBe("a")
    expect(result.blocks[1].id).toBe("c")
  })

  it("focuses the next block when removing from the beginning", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "first" }),
      block({ id: "b", content: "second" }),
    ]
    const result = deleteIfEmpty(blocks, 0)
    expect(result.nextId).toBe("b")
  })

  it("focuses the previous block when removing the last block", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "first" }),
      block({ id: "b", content: "second" }),
    ]
    const result = deleteIfEmpty(blocks, 1)
    expect(result.nextId).toBe("a")
  })

  it("does nothing when only one block exists", () => {
    const blocks: EditorBlockData[] = [block({ id: "a", content: "only" })]
    const result = deleteIfEmpty(blocks, 0)
    expect(result.blocks).toHaveLength(1)
    expect(result.nextId).toBeNull()
  })

  it("does not mutate the original array", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "x" }),
      block({ id: "b", content: "y" }),
    ]
    const copy = [...blocks]
    deleteIfEmpty(blocks, 0)
    expect(blocks).toEqual(copy)
  })
})

// ── indentBlock ───────────────────────────────────────────────────

describe("indentBlock", () => {
  it("increments depth of the block", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", depth: 0, content: "x" }),
    ]
    const result = indentBlock(blocks, 0)
    expect(result.blocks[0].depth).toBe(1)
  })

  it("also indents child blocks", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "p", depth: 0, content: "parent" }),
      block({ id: "c", depth: 1, content: "child" }),
      block({ id: "g", depth: 2, content: "grandchild" }),
      block({ id: "s", depth: 0, content: "sibling" }),
    ]
    const result = indentBlock(blocks, 0)
    expect(result.childIndices).toEqual([1, 2])
    expect(result.blocks[0].depth).toBe(1)
    expect(result.blocks[1].depth).toBe(2)
    expect(result.blocks[2].depth).toBe(3)
    // Sibling at same depth as parent is NOT indented
    expect(result.blocks[3].depth).toBe(0)
  })

  it("caps depth at 10", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", depth: 10, content: "x" }),
    ]
    const result = indentBlock(blocks, 0)
    expect(result.blocks[0].depth).toBe(10)
    expect(result.childIndices).toEqual([])
  })

  it("does not mutate the original array", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", depth: 0, content: "x" }),
    ]
    const copy = [...blocks]
    indentBlock(blocks, 0)
    expect(blocks).toEqual(copy)
  })
})

// ── outdentBlock ──────────────────────────────────────────────────

describe("outdentBlock", () => {
  it("decrements depth of the block", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", depth: 2, content: "x" }),
    ]
    const result = outdentBlock(blocks, 0)
    expect(result.blocks[0].depth).toBe(1)
  })

  it("also outdents child blocks", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "p", depth: 2, content: "parent" }),
      block({ id: "c", depth: 3, content: "child" }),
      block({ id: "g", depth: 4, content: "grandchild" }),
      block({ id: "s", depth: 0, content: "sibling" }),
    ]
    const result = outdentBlock(blocks, 0)
    expect(result.blocks[0].depth).toBe(1)
    expect(result.blocks[1].depth).toBe(2)
    expect(result.blocks[2].depth).toBe(3)
    // Sibling not affected
    expect(result.blocks[3].depth).toBe(0)
  })

  it("floors depth at 0", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", depth: 0, content: "x" }),
    ]
    const result = outdentBlock(blocks, 0)
    expect(result.blocks[0].depth).toBe(0)
    expect(result.childIndices).toEqual([])
  })

  it("does not mutate the original array", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", depth: 2, content: "x" }),
    ]
    const copy = [...blocks]
    outdentBlock(blocks, 0)
    expect(blocks).toEqual(copy)
  })
})

// ── insertBlock ───────────────────────────────────────────────────

describe("insertBlock", () => {
  it("inserts a block at the given index", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "first" }),
      block({ id: "b", content: "last" }),
    ]
    const ids = stableId("new")
    const result = insertBlock(blocks, 1, "middle", 0, ids)

    expect(result.blocks).toHaveLength(3)
    expect(result.blocks[0].id).toBe("a")
    expect(result.blocks[1]).toMatchObject({ id: "new-0", content: "middle" })
    expect(result.blocks[2].id).toBe("b")
    expect(result.newBlock).toMatchObject({ id: "new-0", content: "middle" })
  })

  it("inserts at the beginning", () => {
    const blocks: EditorBlockData[] = [block({ id: "b", content: "second" })]
    const result = insertBlock(blocks, 0, "first", 0, stableId("n"))
    expect(result.blocks).toHaveLength(2)
    expect(result.blocks[0].content).toBe("first")
  })

  it("does not mutate the original array", () => {
    const blocks: EditorBlockData[] = [block({ id: "a", content: "x" })]
    const copy = [...blocks]
    insertBlock(blocks, 0, "y", 0, stableId("n"))
    expect(blocks).toEqual(copy)
  })
})

// ── setBlockContent ───────────────────────────────────────────────

describe("setBlockContent", () => {
  it("updates content of the matching block", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "old" }),
      block({ id: "b", content: "keep" }),
    ]
    const result = setBlockContent(blocks, "a", "new")
    expect(result[0].content).toBe("new")
    expect(result[1].content).toBe("keep")
  })

  it("does not mutate the original array", () => {
    const blocks: EditorBlockData[] = [block({ id: "a", content: "x" })]
    setBlockContent(blocks, "a", "y")
    expect(blocks[0].content).toBe("x")
  })
})

describe("moveBlock", () => {
  it("moves a block from lower to higher index", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "first" }),
      block({ id: "b", content: "second" }),
      block({ id: "c", content: "third" }),
    ]
    const result = moveBlock(blocks, 0, 2)
    expect(result[0].id).toBe("b")
    expect(result[1].id).toBe("c")
    expect(result[2].id).toBe("a")
  })

  it("moves a block from higher to lower index", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "first" }),
      block({ id: "b", content: "second" }),
      block({ id: "c", content: "third" }),
    ]
    const result = moveBlock(blocks, 2, 0)
    expect(result[0].id).toBe("c")
    expect(result[1].id).toBe("a")
    expect(result[2].id).toBe("b")
  })

  it("returns the same array when fromIndex equals toIndex", () => {
    const blocks: EditorBlockData[] = [
      block({ id: "a", content: "x" }),
      block({ id: "b", content: "y" }),
    ]
    const result = moveBlock(blocks, 1, 1)
    expect(result).toHaveLength(2)
    expect(result[0].id).toBe("a")
    expect(result[1].id).toBe("b")
  })
})

describe("classifyBlock", () => {
  it("classifies heading at various levels", () => {
    expect(classifyBlock("# H1").kind).toBe("heading")
    expect(classifyBlock("## H2").kind).toBe("heading")
    expect(classifyBlock("###### H6").kind).toBe("heading")
  })

  it("classifies heading with correct level", () => {
    const result = classifyBlock("### H3")
    if (result.kind === "heading") {
      expect(result.level).toBe(3)
    }
  })

  it("classifies code fence with language", () => {
    const result = classifyBlock("```python")
    expect(result.kind).toBe("code")
    if (result.kind === "code") {
      expect(result.language).toBe("python")
    }
  })

  it("classifies code fence without language", () => {
    const result = classifyBlock("```")
    expect(result.kind).toBe("code")
    if (result.kind === "code") {
      expect(result.language).toBe("")
    }
  })

  it("classifies tilde code fence", () => {
    const result = classifyBlock("~~~")
    expect(result.kind).toBe("code")
  })

  it("classifies lowercase task state", () => {
    const result = classifyBlock("todo buy milk")
    expect(result.kind).toBe("task")
    if (result.kind === "task") {
      expect(result.taskState).toBe("TODO")
    }
  })

  it("classifies all task states", () => {
    const states = [
      "TODO",
      "DOING",
      "DONE",
      "LATER",
      "NOW",
      "WAITING",
      "CANCELLED",
    ]
    for (const s of states) {
      const result = classifyBlock(`${s} task`)
      expect(result.kind).toBe("task")
      if (result.kind === "task") {
        expect(result.taskState).toBe(s)
      }
    }
  })

  it("classifies empty callout as callout", () => {
    const result = classifyBlock("> [!NOTE]")
    expect(result.kind).toBe("callout")
    if (result.kind === "callout") {
      expect(result.calloutType).toBe("NOTE")
    }
  })

  it("classifies callout with arbitrary type", () => {
    const result = classifyBlock("> [!CUSTOM] text")
    expect(result.kind).toBe("callout")
    if (result.kind === "callout") {
      expect(result.calloutType).toBe("CUSTOM")
    }
  })

  it("classifies callout with extra whitespace", () => {
    const result = classifyBlock(">  [!NOTE]  text")
    expect(result.kind).toBe("callout")
  })

  it("classifies blockquote", () => {
    const result = classifyBlock("> quoted text")
    expect(result.kind).toBe("blockquote")
  })

  it("classifies plain text as paragraph", () => {
    expect(classifyBlock("some text").kind).toBe("paragraph")
  })

  it("classifies empty string as paragraph", () => {
    expect(classifyBlock("").kind).toBe("paragraph")
  })

  it("classifies string with only whitespace as paragraph", () => {
    expect(classifyBlock("   ").kind).toBe("paragraph")
  })

  it("classifies heading with leading spaces as paragraph (classifyBlock is regex-based)", () => {
    const result = classifyBlock("  # Title")
    expect(result.kind).toBe("paragraph")
  })

  it("classifies blockquote with leading space as paragraph (classifyBlock is regex-based)", () => {
    const result = classifyBlock(" > text")
    expect(result.kind).toBe("paragraph")
  })
})
