import type { Node } from "prosemirror-model"
import { EditorState, TextSelection } from "prosemirror-state"
import { EditorView } from "prosemirror-view"
import { afterEach, beforeEach, describe, expect, it } from "vitest"
import { makeParaDoc } from "@/__test-utils__"
import {
  autoClosePlugin,
  createPairRule,
  dollarSignRule,
  doubleAsteriskRule,
  doubleTildeRule,
  singleUnderscoreRule,
  tripleBacktickRule,
} from "@/lib/auto-close-plugin"
import { schema } from "@/lib/schema"

/**
 * Position mapping for doc(paragraph("content")):
 *   doc starts at 0
 *   paragraph starts at 1 → text content starts at 1
 *   character positions: 1, 2, 3, ..., length
 *   end-of-text position: 1 + length
 * So textStart = 1.
 */

let views: EditorView[] = []

beforeEach(() => {
  views = []
})

afterEach(() => {
  for (const v of views) {
    v.destroy()
  }
  views = []
})

function createPlugin() {
  return autoClosePlugin([
    tripleBacktickRule,
    dollarSignRule,
    doubleAsteriskRule,
    doubleTildeRule,
    singleUnderscoreRule,
    createPairRule("(", ")"),
    createPairRule("[", "]"),
    createPairRule("{", "}"),
    createPairRule('"', '"', { context: /^$|[\s([{:>]/ }),
    createPairRule("'", "'", { context: /^$|[\s([{:"]/ }),
  ])
}

function getView(content: string, cursorOffset?: number) {
  const doc = makeParaDoc(content)
  const pos = 1 + (cursorOffset ?? content.length)
  const state = EditorState.create({
    doc,
    schema,
    selection: TextSelection.near(doc.resolve(pos)),
    plugins: [createPlugin()],
  })
  const dom = document.createElement("div")
  const view = new EditorView(dom, { state })
  views.push(view)
  return view
}

function getViewWithRange(
  content: string,
  fromOffset: number,
  toOffset: number,
) {
  const doc = makeParaDoc(content)
  const from = 1 + fromOffset
  const to = 1 + toOffset
  const state = EditorState.create({
    doc,
    schema,
    selection: TextSelection.create(doc, from, to),
    plugins: [createPlugin()],
  })
  const dom = document.createElement("div")
  const view = new EditorView(dom, { state })
  views.push(view)
  return view
}

function typeAtCursor(view: EditorView, text: string) {
  const { head } = view.state.selection
  let handled = false
  view.someProp("handleTextInput", (fn) => {
    handled = fn(view, head, head, text)
  })
  return handled
}

function typeChar(
  view: EditorView,
  from: number,
  to: number,
  text: string,
): boolean {
  let handled = false
  view.someProp("handleTextInput", (fn) => {
    handled = fn(view, from, to, text)
  })
  return handled
}

function pressBackspace(view: EditorView): boolean {
  let handled = false
  for (const plugin of view.state.plugins) {
    const handler = plugin.props.handleDOMEvents?.keydown
    if (handler) {
      handled = handler(
        view,
        new KeyboardEvent("keydown", { key: "Backspace" }),
      )
      if (handled) break
    }
  }
  return handled
}

function textAt(doc: Node, startOffset: number, length: number) {
  return doc.textBetween(1 + startOffset, 1 + startOffset + length)
}

describe("createPairRule", () => {
  it("auto-closes parentheses when typing ( at end of text", () => {
    const view = getView("hello", 5)

    const handled = typeAtCursor(view, "(")

    expect(handled).toBe(true)
    expect(textAt(view.state.doc, 0, 7)).toBe("hello()")
    // cursor between ( and ) → pos = 1 + 5 + 1 = 7
    expect(view.state.selection.anchor).toBe(7)
  })

  it("wraps selection in parentheses", () => {
    const view = getViewWithRange("hello world", 6, 11)

    const { from, to } = view.state.selection
    const handled = typeChar(view, from, to, "(")

    expect(handled).toBe(true)
    expect(textAt(view.state.doc, 0, 13)).toBe("hello (world)")
    // cursor after closing paren: from + open + selected + close = 7 + 1 + 5 + 1 = 14
    expect(view.state.selection.anchor).toBe(14)
  })

  it("skips over closing paren when next character matches", () => {
    const view = getView("()", 1)

    const handled = typeAtCursor(view, ")")

    expect(handled).toBe(true)
    expect(textAt(view.state.doc, 0, 2)).toBe("()")
    // cursor moved past ) → pos = 1 + 2 = 3
    expect(view.state.selection.anchor).toBe(3)
  })

  it("does not skip over when there is a selection", () => {
    const view = getViewWithRange("()", 0, 2)

    const { from, to } = view.state.selection
    const handled = typeChar(view, from, to, ")")

    expect(handled).toBe(false)
  })

  it("lets normal close-paren pass when next char does not match", () => {
    const view = getView("hello", 5)

    const handled = typeAtCursor(view, ")")

    expect(handled).toBe(false)
  })
})

describe("createPairRule context-aware quotes", () => {
  it("auto-closes double-quote after space", () => {
    const view = getView("hello ", 6)

    const handled = typeAtCursor(view, '"')

    expect(handled).toBe(true)
    expect(textAt(view.state.doc, 0, 8)).toBe('hello ""')
  })

  it("does NOT auto-close double-quote mid-word", () => {
    const view = getView("hello", 5)

    const handled = typeAtCursor(view, '"')

    expect(handled).toBe(false)
  })
})

describe("createPairRule backspace delete-pair", () => {
  it("deletes empty paren pair on backspace", () => {
    const view = getView("()", 1)

    const handled = pressBackspace(view)

    expect(handled).toBe(true)
    // Both parens removed → empty paragraph
    expect(view.state.doc.textBetween(1, 2)).toBe("")
  })

  it("lets normal backspace pass when not in a pair", () => {
    const view = getView("hello", 5)

    const handled = pressBackspace(view)

    expect(handled).toBe(false)
  })
})

describe("doubleAsteriskRule (toggle)", () => {
  it("auto-closes ** when user types second asterisk", () => {
    const view = getView("hello *", 7)

    const handled = typeAtCursor(view, "*")

    expect(handled).toBe(true)
    expect(textAt(view.state.doc, 0, 10)).toBe("hello ****")
    // cursor between ** pairs: 1 + 7 + 1 = 9
    expect(view.state.selection.anchor).toBe(9)
  })
})

describe("doubleTildeRule (toggle)", () => {
  it("auto-closes ~~ when user types second tilde", () => {
    const view = getView("~", 1)

    const handled = typeAtCursor(view, "~")

    expect(handled).toBe(true)
    expect(textAt(view.state.doc, 0, 4)).toBe("~~~~")
    // cursor between ~~ pairs: start(1) + delimiter.length(2) = 3
    expect(view.state.selection.anchor).toBe(3)
  })
})

describe("singleUnderscoreRule", () => {
  it("wraps selection in underscores regardless of preceding character", () => {
    const view = getViewWithRange("hello world", 6, 11)

    const { from, to } = view.state.selection
    const handled = typeChar(view, from, to, "_")

    expect(handled).toBe(true)
    expect(textAt(view.state.doc, 0, 13)).toBe("hello _world_")
    // cursor after closing underscore: from(7) + 1 + 5 + 1 = 14
    expect(view.state.selection.anchor).toBe(14)
  })

  it("auto-closes _ at word boundary", () => {
    const view = getView("hello ", 6)

    const handled = typeAtCursor(view, "_")

    expect(handled).toBe(true)
    expect(textAt(view.state.doc, 0, 8)).toBe("hello __")
    // cursor between underscores: 1 + 6 + 1 = 8
    expect(view.state.selection.anchor).toBe(8)
  })

  it("does NOT auto-close mid-word", () => {
    const view = getView("hello", 3)

    const handled = typeAtCursor(view, "_")

    expect(handled).toBe(false)
  })
})

describe("tripleBacktickRule", () => {
  it("creates code block when ``` typed at start of paragraph", () => {
    const view = getView("``", 2)

    const handled = typeAtCursor(view, "`")

    expect(handled).toBe(true)
    const doc = view.state.doc
    expect(doc.childCount).toBe(1)
    expect(doc.child(0).type.name).toBe("code_block")
  })

  it("does NOT create code block mid-line", () => {
    const view = getView("hello ``", 8)

    const handled = typeAtCursor(view, "`")

    expect(handled).toBe(false)
  })
})

describe("dollarSignRule", () => {
  it("auto-closes single $ at word boundary", () => {
    const view = getView("hello ", 6)

    const handled = typeAtCursor(view, "$")

    expect(handled).toBe(true)
    expect(textAt(view.state.doc, 0, 8)).toBe("hello $$")
  })

  it("does not auto-close after word character", () => {
    const view = getView("hello", 5)

    const handled = typeAtCursor(view, "$")

    expect(handled).toBe(false)
  })

  it("converts $$ at paragraph start to math_block", () => {
    const view = getView("", 0)

    const first = typeAtCursor(view, "$")
    expect(first).toBe(true)

    // After first $ auto-close, text is "$$", cursor between them.
    // Type second $: should trigger block math conversion.
    const second = typeAtCursor(view, "$")
    expect(second).toBe(true)

    const doc = view.state.doc
    expect(doc.child(0).type.name).toBe("math_block")
  })

  it("provides skip-over when cursor is before closing $", () => {
    const view = getView("$x^2$", 4)

    const handled = typeAtCursor(view, "$")

    expect(handled).toBe(true)
    expect(view.state.selection.head).toBe(1 + 5) // after closing $
  })

  it("deletes pair on backspace between $$", () => {
    const view = getView("$$", 1)

    const handled = pressBackspace(view)

    expect(handled).toBe(true)
    expect(view.state.doc.textBetween(1, 2)).toBe("")
  })
})
