import { EditorState } from "prosemirror-state"
import { EditorView } from "prosemirror-view"
import { describe, expect, it, vi } from "vitest"
import { ref } from "vue"
import { stableId } from "@/__test-utils__"
import { createPasteHandler } from "@/composables/usePasteHandler"
import type { PatternOpenPayload } from "@/composables/usePatternPlugin"
import {
  buildMentionGroups,
  buildSlashGroups,
  useMentionGroups,
  useSlashGroups,
} from "@/composables/useSuggestionGroups"
import { contentToDoc } from "@/lib/content-model"
import { insertBlock, moveBlock } from "@/lib/editor-operations"
import type { FormattingHandlers } from "@/lib/formatting"
import { classifyBlock } from "@/lib/markdown-rules/block-classifier"
import { createOperationHistory } from "@/lib/operation-history"
import { schema } from "@/lib/schema"

function createMockSession(
  kind: string = "command",
  query: string = "/",
): PatternOpenPayload {
  return {
    kind: kind as any,
    query,
    position: { x: 100, y: 200 },
    range: { from: 0, to: query.length },
    respond: vi.fn(),
    close: vi.fn(),
  }
}

describe("Editor integration — menu routing", () => {
  it("builds slash and mention groups reactively from session", () => {
    const activeSession = ref<PatternOpenPayload | null>(null)
    const handlers = ref<FormattingHandlers | null>(null)
    const onClose = vi.fn()

    const slashGroups = useSlashGroups(activeSession, handlers, onClose)
    const mentionGroups = useMentionGroups(activeSession, onClose)

    expect(slashGroups.value).toEqual([])
    expect(mentionGroups.value).toEqual([])

    activeSession.value = createMockSession("command")
    expect(slashGroups.value.length).toBeGreaterThan(0)
    expect(mentionGroups.value).toEqual([])

    activeSession.value = createMockSession("reference", "[[Page")
    expect(slashGroups.value.length).toBeGreaterThan(0)
    expect(mentionGroups.value.length).toBeGreaterThan(0)
    expect(mentionGroups.value[0].id).toBe("pages")
  })

  it("creates active session on pattern-open and clears on pattern-close", () => {
    const activeSession = ref<PatternOpenPayload | null>(null)

    function onPatternOpen(payload: PatternOpenPayload) {
      activeSession.value = payload
    }

    function onPatternClose() {
      activeSession.value = null
    }

    expect(activeSession.value).toBeNull()
    onPatternOpen(createMockSession("command"))
    expect(activeSession.value).not.toBeNull()
    expect(activeSession.value?.kind).toBe("command")
    onPatternClose()
    expect(activeSession.value).toBeNull()
  })

  it("closeSession calls close and clears state", () => {
    const activeSession = ref<PatternOpenPayload | null>(
      createMockSession("command"),
    )

    function closeSession() {
      activeSession.value?.close("cancelled")
      activeSession.value = null
    }

    closeSession()
    expect(activeSession.value).toBeNull()
  })

  it("updates query and position on pattern-update", () => {
    const activeSession = ref<PatternOpenPayload | null>(
      createMockSession("command", "/test"),
    )

    function onPatternUpdate(payload: {
      query: string
      position: { x: number; y: number }
      range: { from: number; to: number }
    }) {
      if (!activeSession.value) return
      activeSession.value = {
        ...activeSession.value,
        query: payload.query,
        position: payload.position,
        range: payload.range,
      }
    }

    onPatternUpdate({
      query: "/newquery",
      position: { x: 50, y: 100 },
      range: { from: 0, to: 9 },
    })
    expect(activeSession.value?.query).toBe("/newquery")
    expect(activeSession.value?.position).toEqual({ x: 50, y: 100 })
    expect(activeSession.value?.range).toEqual({ from: 0, to: 9 })
  })
})

describe("content-change-op coalescing", () => {
  it("coalesces rapid changes for the same block", () => {
    const history = createOperationHistory()
    const COALESCE_MS = 500
    let canUndo = false
    let _canRedo = false

    function handleOp(op: {
      blockId: string
      previousContent: string
      newContent: string
      timestamp: number
    }) {
      const last = history.peek()
      if (
        last?.type === "set-block-content" &&
        last.blockId === op.blockId &&
        op.timestamp - last.timestamp < COALESCE_MS
      ) {
        history.coalesce({
          type: "set-block-content",
          blockId: op.blockId,
          previousContent: last.previousContent,
          newContent: op.newContent,
          timestamp: op.timestamp,
        })
      } else {
        void history.execute({
          type: "set-block-content",
          blockId: op.blockId,
          previousContent: op.previousContent,
          newContent: op.newContent,
          timestamp: op.timestamp,
        })
      }
      canUndo = history.canUndo
      _canRedo = history.canRedo
    }

    // Rapid typing — should coalesce into 1 entry
    handleOp({
      blockId: "a",
      previousContent: "",
      newContent: "h",
      timestamp: 1000,
    })
    handleOp({
      blockId: "a",
      previousContent: "h",
      newContent: "he",
      timestamp: 1050,
    })
    handleOp({
      blockId: "a",
      previousContent: "he",
      newContent: "hel",
      timestamp: 1120,
    })
    handleOp({
      blockId: "a",
      previousContent: "hel",
      newContent: "hell",
      timestamp: 1180,
    })
    handleOp({
      blockId: "a",
      previousContent: "hell",
      newContent: "hello",
      timestamp: 1250,
    })

    expect(canUndo).toBe(true)
    expect(history.peek()?.type).toBe("set-block-content")
    expect(history.peek()).toHaveProperty("newContent", "hello")

    // Undo should revert directly from "hello" to ""
    const inverse = history.undo()
    expect(inverse).not.toBeNull()
    expect(inverse).toHaveProperty("newContent", "")
    canUndo = history.canUndo
    expect(canUndo).toBe(false)
  })

  it("creates separate entries after a pause", () => {
    const history = createOperationHistory()
    const COALESCE_MS = 500

    function handleOp(op: {
      blockId: string
      previousContent: string
      newContent: string
      timestamp: number
    }) {
      const last = history.peek()
      if (
        last?.type === "set-block-content" &&
        last.blockId === op.blockId &&
        op.timestamp - last.timestamp < COALESCE_MS
      ) {
        history.coalesce({
          type: "set-block-content",
          blockId: op.blockId,
          previousContent: last.previousContent,
          newContent: op.newContent,
          timestamp: op.timestamp,
        })
      } else {
        void history.execute({
          type: "set-block-content",
          blockId: op.blockId,
          previousContent: op.previousContent,
          newContent: op.newContent,
          timestamp: op.timestamp,
        })
      }
    }

    // First burst
    handleOp({
      blockId: "a",
      previousContent: "",
      newContent: "hi",
      timestamp: 1000,
    })
    // Pause > 500ms
    handleOp({
      blockId: "a",
      previousContent: "hi",
      newContent: "hi there",
      timestamp: 1600,
    })

    // Two separate entries
    const first = history.undo()
    expect(first).toHaveProperty("newContent", "hi")
    expect(history.canUndo).toBe(true)
    const second = history.undo()
    expect(second).toHaveProperty("newContent", "")
    expect(history.canUndo).toBe(false)
  })

  it("creates separate entries for different blocks", () => {
    const history = createOperationHistory()
    const COALESCE_MS = 500

    function handleOp(op: {
      blockId: string
      previousContent: string
      newContent: string
      timestamp: number
    }) {
      const last = history.peek()
      if (
        last?.type === "set-block-content" &&
        last.blockId === op.blockId &&
        op.timestamp - last.timestamp < COALESCE_MS
      ) {
        history.coalesce({
          type: "set-block-content",
          blockId: op.blockId,
          previousContent: last.previousContent,
          newContent: op.newContent,
          timestamp: op.timestamp,
        })
      } else {
        void history.execute({
          type: "set-block-content",
          blockId: op.blockId,
          previousContent: op.previousContent,
          newContent: op.newContent,
          timestamp: op.timestamp,
        })
      }
    }

    handleOp({
      blockId: "a",
      previousContent: "",
      newContent: "hello",
      timestamp: 1000,
    })
    handleOp({
      blockId: "b",
      previousContent: "",
      newContent: "world",
      timestamp: 1100,
    })

    // Two separate entries (different block)
    const first = history.undo()
    expect(first).toHaveProperty("blockId", "b")
    expect(first).toHaveProperty("newContent", "") // reverted to ""
    expect(history.canUndo).toBe(true)
    expect(history.undo()).not.toBeNull()
  })

  it("typing → split → typing → undo cycles through operations correctly", () => {
    const history = createOperationHistory()
    const COALESCE_MS = 500

    function handleContentOp(op: {
      blockId: string
      previousContent: string
      newContent: string
      timestamp: number
    }) {
      const last = history.peek()
      if (
        last?.type === "set-block-content" &&
        last.blockId === op.blockId &&
        op.timestamp - last.timestamp < COALESCE_MS
      ) {
        history.coalesce({
          type: "set-block-content",
          blockId: op.blockId,
          previousContent: last.previousContent,
          newContent: op.newContent,
          timestamp: op.timestamp,
        })
      } else {
        void history.execute({
          type: "set-block-content",
          blockId: op.blockId,
          previousContent: op.previousContent,
          newContent: op.newContent,
          timestamp: op.timestamp,
        })
      }
    }

    // 1. Type "hello" in block A
    handleContentOp({
      blockId: "a",
      previousContent: "",
      newContent: "hello",
      timestamp: 1000,
    })

    // 2. Split at index 0 (creates block B with "world")
    history.execute({
      type: "split-block",
      index: 0,
      splitBlockId: "b",
      depth: 0,
      beforeContent: "hello",
      afterContent: "world",
      timestamp: 2000,
    })

    // 3. Type " test" in block B
    handleContentOp({
      blockId: "b",
      previousContent: "",
      newContent: " test",
      timestamp: 2100,
    })

    // Step 3 undo — revert block B typing
    const undo1 = history.undo()
    expect(undo1).not.toBeNull()
    expect(undo1?.type).toBe("set-block-content")
    expect(undo1).toHaveProperty("blockId", "b")
    expect(undo1).toHaveProperty("newContent", "")
    expect(history.canUndo).toBe(true)

    // Step 2 undo — revert the split (merge block B back into block A)
    const undo2 = history.undo()
    expect(undo2).not.toBeNull()
    expect(undo2?.type).toBe("merge-block")
    expect(history.canUndo).toBe(true)

    // Step 1 undo — revert block A typing
    const undo3 = history.undo()
    expect(undo3).not.toBeNull()
    expect(undo3?.type).toBe("set-block-content")
    expect(undo3).toHaveProperty("blockId", "a")
    expect(undo3).toHaveProperty("newContent", "")
    expect(history.canUndo).toBe(false)
    expect(history.undo()).toBeNull()
  })
})

describe("paste handling", () => {
  it("inserts multiple blocks when pasting into zone", () => {
    const ids = stableId("zone")
    const blocks: Array<{ id: string; depth: number; content: string }> = [
      { id: "a", depth: 0, content: "first" },
    ]

    // Simulate onZoneActivate with 3 non-empty lines
    const lines = ["line1", "line2", "line3"]
    let insertIndex = 1
    for (const line of lines) {
      const result = insertBlock(blocks, insertIndex, line, 0, ids)
      blocks.length = 0
      blocks.push(...result.blocks)
      insertIndex++
    }

    expect(blocks).toHaveLength(4) // original 1 + 3 new
    expect(blocks[1].content).toBe("line1")
    expect(blocks[2].content).toBe("line2")
    expect(blocks[3].content).toBe("line3")
  })

  it("skips empty lines during zone paste", () => {
    const ids = stableId("zone")
    const blocks: Array<{ id: string; depth: number; content: string }> = [
      { id: "a", depth: 0, content: "first" },
    ]

    // Lines with empty trailing line (like onZoneActivate skips them)
    const lines = ["line1", "", "line2"]
    let insertIndex = 1
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]
      if (line === undefined) continue
      if (line === "" && i < lines.length - 1) continue
      const result = insertBlock(blocks, insertIndex, line, 0, ids)
      blocks.length = 0
      blocks.push(...result.blocks)
      insertIndex++
    }

    expect(blocks).toHaveLength(3)
    expect(blocks[1].content).toBe("line1")
    expect(blocks[2].content).toBe("line2")
  })

  it("does not split on single-line paste", () => {
    const onSplit = vi.fn()
    const handlePaste = createPasteHandler(onSplit)

    // Single line — paste handler returns false
    const text = "single line"
    const doc = contentToDoc(text)
    const state = EditorState.create({ doc, schema })
    const dom = document.createElement("div")
    const view = new EditorView(dom, { state })

    const event = {
      clipboardData: {
        getData: () => "single line",
      },
      preventDefault: vi.fn(),
    } as unknown as ClipboardEvent

    const result = handlePaste(view, event)
    expect(result).toBe(false)
    expect(onSplit).not.toHaveBeenCalled()

    view.destroy()
  })

  it("calls onSplit on multi-line paste with mixed types", () => {
    const onSplit = vi.fn()
    const handlePaste = createPasteHandler(onSplit)

    const doc = contentToDoc("existing content")
    const state = EditorState.create({ doc, schema })
    const dom = document.createElement("div")
    const view = new EditorView(dom, { state })

    const event = {
      clipboardData: {
        getData: () => "para1\npara2\n> blockquote",
      },
      preventDefault: vi.fn(),
    } as unknown as ClipboardEvent

    const result = handlePaste(view, event)
    expect(result).toBe(true)
    expect(onSplit).toHaveBeenCalled()
    expect(event.preventDefault).toHaveBeenCalled()

    view.destroy()
  })

  it("classifies blockquote and callout blocks correctly", () => {
    expect(classifyBlock("> [!NOTE] note text").kind).toBe("callout")
    expect(classifyBlock("> blockquote text").kind).toBe("blockquote")
    expect(classifyBlock("TODO task text").kind).toBe("task")
    expect(classifyBlock("normal text").kind).toBe("paragraph")
    expect(classifyBlock("").kind).toBe("paragraph")
  })
})

describe("drag reorder", () => {
  it("reorders blocks via moveBlock", () => {
    const blocks: Array<{ id: string; depth: number; content: string }> = [
      { id: "a", depth: 0, content: "first" },
      { id: "b", depth: 1, content: "second" },
      { id: "c", depth: 2, content: "third" },
    ]

    // Move block 0 to index 2
    const reordered = moveBlock(blocks, 0, 2)
    expect(reordered[0].id).toBe("b")
    expect(reordered[1].id).toBe("c")
    expect(reordered[2].id).toBe("a")

    // Move block 2 back to index 0 (inverse)
    const restored = moveBlock(reordered, 2, 0)
    expect(restored[0].id).toBe("a")
    expect(restored[1].id).toBe("b")
    expect(restored[2].id).toBe("c")
  })
})

describe("suggestion menu selection", () => {
  it("slash command Heading 1 inserts # markdown syntax", () => {
    const respond = vi.fn()
    const closeSession = vi.fn()
    const onClose = vi.fn()

    const payload: PatternOpenPayload = {
      kind: "command",
      trigger: "/",
      query: "/he",
      position: { x: 0, y: 0 },
      range: { from: 0, to: 3 },
      respond,
      close: closeSession,
    }

    const groups = buildSlashGroups(payload, null, onClose)
    const heading1 = groups
      .flatMap((g) => g.items ?? [])
      .find((item) => item.label === "Heading 1")

    expect(heading1).toBeDefined()
    heading1?.onSelect()

    expect(respond).toHaveBeenCalledWith({ text: "# ", mode: "replace" })
    expect(closeSession).toHaveBeenCalledWith("completed")
    expect(onClose).toHaveBeenCalled()
  })

  it("[[ mention menu selects page and inserts [[Page Name]]", () => {
    const respond = vi.fn()
    const closeSession = vi.fn()
    const onClose = vi.fn()

    const payload: PatternOpenPayload = {
      kind: "reference",
      trigger: "[[",
      query: "[[Page",
      position: { x: 0, y: 0 },
      range: { from: 0, to: 7 },
      respond,
      close: closeSession,
    }

    const groups = buildMentionGroups(payload, onClose)
    const firstPage = groups
      .flatMap((g) => g.items ?? [])
      .find((item: any) => item.suffix === "Page")

    expect(firstPage).toBeDefined()
    expect(groups[0].id).toBe("pages")
    firstPage?.onSelect()

    expect(respond).toHaveBeenCalledWith({
      text: "[[Getting Started]]",
      mode: "replace",
    })
    expect(closeSession).toHaveBeenCalledWith("completed")
    expect(onClose).toHaveBeenCalled()
  })

  it("drag block 1 to position 3 reorders correctly", () => {
    const blocks = [
      { id: "a", depth: 0, content: "first" },
      { id: "b", depth: 1, content: "second" },
      { id: "c", depth: 2, content: "third" },
      { id: "d", depth: 0, content: "fourth" },
    ]

    // Drag block at index 1 ("second") to index 3
    const reordered = moveBlock(blocks, 1, 3)
    expect(reordered[0].id).toBe("a")
    expect(reordered[1].id).toBe("c")
    expect(reordered[2].id).toBe("d")
    expect(reordered[3].id).toBe("b")

    // Undo — drag back from 3 to 1
    const restored = moveBlock(reordered, 3, 1)
    expect(restored[0].id).toBe("a")
    expect(restored[1].id).toBe("b")
    expect(restored[2].id).toBe("c")
    expect(restored[3].id).toBe("d")
  })
})
