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 {
  insertBlockMath,
  insertBlockRef,
  insertInlineMath,
  insertLink,
  insertPageRef,
  insertTag,
  isFormatActive,
  setHeading,
  toggleBold,
  toggleCode,
  toggleHighlight,
  toggleItalic,
  toggleStrikethrough,
  toggleTask,
} from "@/lib/formatting"
import { schema } from "@/lib/schema"

let views: EditorView[] = []

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

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

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)),
  })
  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),
  })
  const dom = document.createElement("div")
  const view = new EditorView(dom, { state })
  views.push(view)
  return view
}

describe("isFormatActive", () => {
  it("returns true when cursor is inside bold", () => {
    const view = getView("**bold**", 4)
    expect(isFormatActive(view, "bold")).toBe(true)
  })

  it("returns false when cursor is outside bold", () => {
    const view = getView("**bold**", 0)
    expect(isFormatActive(view, "bold")).toBe(false)
  })

  it("returns false for plain text", () => {
    const view = getView("hello", 2)
    expect(isFormatActive(view, "bold")).toBe(false)
  })

  it("detects italic", () => {
    const view = getView("*italic*", 3)
    expect(isFormatActive(view, "italic")).toBe(true)
  })

  it("detects code", () => {
    const view = getView("`code`", 2)
    expect(isFormatActive(view, "code")).toBe(true)
  })

  it("detects strikethrough", () => {
    const view = getView("~~strike~~", 4)
    expect(isFormatActive(view, "strikethrough")).toBe(true)
  })

  it("detects highlight", () => {
    const view = getView("^^highlight^^", 5)
    expect(isFormatActive(view, "highlight")).toBe(true)
  })
})

describe("toggleBold", () => {
  it("wraps selection in bold", () => {
    const view = getViewWithRange("hello world", 6, 11)
    toggleBold(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello **world**")
  })

  it("unwraps bold when cursor is inside", () => {
    const view = getView("**bold**", 4)
    toggleBold(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("bold")
  })

  it("inserts empty delimiters at cursor when no selection", () => {
    const view = getView("hello ", 6)
    toggleBold(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello ****")
  })
})

describe("toggleItalic", () => {
  it("wraps selection in italic", () => {
    const view = getViewWithRange("hello world", 6, 11)
    toggleItalic(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello *world*")
  })

  it("unwraps italic when cursor is inside", () => {
    const view = getView("*italic*", 3)
    toggleItalic(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("italic")
  })
})

describe("toggleCode", () => {
  it("wraps selection in code", () => {
    const view = getViewWithRange("hello world", 6, 11)
    toggleCode(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello `world`")
  })

  it("unwraps code when cursor is inside", () => {
    const view = getView("`code`", 3)
    toggleCode(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("code")
  })
})

describe("toggleStrikethrough", () => {
  it("wraps selection in strikethrough", () => {
    const view = getViewWithRange("hello world", 6, 11)
    toggleStrikethrough(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello ~~world~~")
  })

  it("unwraps strikethrough when cursor is inside", () => {
    const view = getView("~~strike~~", 4)
    toggleStrikethrough(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("strike")
  })
})

describe("toggleHighlight", () => {
  it("wraps selection in highlight", () => {
    const view = getViewWithRange("hello world", 6, 11)
    toggleHighlight(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello ^^world^^")
  })

  it("unwraps highlight when cursor is inside", () => {
    const view = getView("^^highlight^^", 5)
    toggleHighlight(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("highlight")
  })
})

describe("insertLink", () => {
  it("wraps selection in link with url", () => {
    const view = getViewWithRange("hello world", 6, 11)
    insertLink(view, "https://example.com")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe(
      "hello [world](https://example.com)",
    )
  })

  it("inserts placeholder link with selection on link text when no url", () => {
    const view = getView("hello ", 6)
    insertLink(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello [link](url)")
    const sel = view.state.selection
    const selected = doc.textBetween(sel.from, sel.to)
    expect(selected).toBe("link")
  })
})

describe("setHeading", () => {
  it("adds heading markers to plain text", () => {
    const view = getView("Title", 0)
    setHeading(view, 1)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("# Title")
  })

  it("changes heading level", () => {
    const view = getView("## Section", 0)
    setHeading(view, 3)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("### Section")
  })

  it("removes heading when setting same level", () => {
    const view = getView("# Title", 0)
    setHeading(view, 1)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("Title")
  })

  it("removes heading when level is null", () => {
    const view = getView("## Section", 0)
    setHeading(view, null)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("Section")
  })
})

describe("toggleTask", () => {
  it("adds TODO to plain text", () => {
    const view = getView("do the thing", 0)
    toggleTask(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("TODO do the thing")
  })

  it("cycles TODO → DOING", () => {
    const view = getView("TODO fix bug", 4)
    toggleTask(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("DOING fix bug")
  })

  it("cycles DOING → DONE", () => {
    const view = getView("DOING fix bug", 5)
    toggleTask(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("DONE fix bug")
  })

  it("removes task when cycling from DONE", () => {
    const view = getView("DONE fix bug", 4)
    toggleTask(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("fix bug")
  })

  it("sets specific task state", () => {
    const view = getView("TODO fix bug", 4)
    toggleTask(view, "DONE")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("DONE fix bug")
  })

  it("removes task when setting same state", () => {
    const view = getView("TODO fix bug", 4)
    toggleTask(view, "TODO")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("fix bug")
  })

  it("removes LATER task on cycle (orthogonal state)", () => {
    const view = getView("LATER review", 6)
    toggleTask(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("review")
  })

  it("removes WAITING task on cycle (orthogonal state)", () => {
    const view = getView("WAITING review", 7)
    toggleTask(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("review")
  })

  it("removes CANCELLED task on cycle (orthogonal state)", () => {
    const view = getView("CANCELLED task", 10)
    toggleTask(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("task")
  })

  it("sets orthogonal state explicitly", () => {
    const view = getView("TODO fix bug", 4)
    toggleTask(view, "WAITING")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("WAITING fix bug")
  })
})

describe("insertBlockMath", () => {
  it("replaces empty paragraph with math block", () => {
    const view = getView("", 0)
    insertBlockMath(view)
    const doc = view.state.doc
    expect(doc.child(0).type.name).toBe("math_block")
  })

  it("inserts math block after non-empty paragraph", () => {
    const view = getView("hello", 5)
    insertBlockMath(view)
    const doc = view.state.doc
    expect(doc.childCount).toBe(2)
    expect(doc.child(0).type.name).toBe("paragraph")
    expect(doc.child(1).type.name).toBe("math_block")
  })

  it("preserves content when inserting after non-empty paragraph", () => {
    const view = getView("hello", 5)
    insertBlockMath(view)
    const doc = view.state.doc
    expect(doc.textContent).toContain("hello")
    expect(doc.textContent).toContain("$$")
  })

  it("sets cursor inside the math block after insertion", () => {
    const view = getView("", 0)
    insertBlockMath(view)
    const sel = view.state.selection
    const $pos = view.state.doc.resolve(sel.from)
    expect($pos.parent.type.name).toBe("math_block")
  })
})

describe("insertInlineMath", () => {
  it("wraps selection in $...$", () => {
    const view = getViewWithRange("hello world", 6, 11)
    insertInlineMath(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello $world$")
  })

  it("unwraps $...$ when cursor is inside", () => {
    const view = getView("$x^2$", 3)
    insertInlineMath(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("x^2")
  })

  it("inserts empty delimiters at cursor when no selection", () => {
    const view = getView("hello ", 6)
    insertInlineMath(view)
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello $$")
  })
})

describe("insertPageRef", () => {
  it("inserts [[Page Name]] at cursor", () => {
    const view = getView("hello ", 6)
    insertPageRef(view, "Page Name")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello [[Page Name]]")
  })

  it("inserts [[Page Name|Alias]] when alias is given", () => {
    const view = getView("hello ", 6)
    insertPageRef(view, "Page Name", "Alias")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe(
      "hello [[Page Name|Alias]]",
    )
  })

  it("wraps selected text as the page ref", () => {
    const view = getViewWithRange("hello world", 6, 11)
    insertPageRef(view, "Renamed")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello [[Renamed]]")
  })

  it("does nothing when pageName is empty", () => {
    const view = getView("hello", 5)
    insertPageRef(view, "")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello")
  })

  it("places cursor after closing ]]", () => {
    const view = getView("hello ", 6)
    insertPageRef(view, "Page")
    expect(view.state.selection.head).toBe(1 + 6 + "[[Page]]".length)
  })

  it("discards selection when alias is given", () => {
    const view = getViewWithRange("hello world", 6, 11)
    insertPageRef(view, "Page", "Alias")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello [[Page|Alias]]")
  })

  it("places cursor after closing ]] with selection", () => {
    const view = getViewWithRange("hello world", 6, 11)
    insertPageRef(view, "Page")
    expect(view.state.selection.head).toBe(1 + 6 + "[[Page]]".length)
  })
})

describe("insertBlockRef", () => {
  it("inserts ((block-id)) at cursor", () => {
    const view = getView("hello ", 6)
    insertBlockRef(view, "abc123")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello ((abc123))")
  })

  it("wraps selected text as the block ref", () => {
    const view = getViewWithRange("hello world", 6, 11)
    insertBlockRef(view, "xyz")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello ((xyz))")
  })

  it("does nothing when blockId is empty", () => {
    const view = getView("hello", 5)
    insertBlockRef(view, "")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello")
  })

  it("places cursor after closing ))", () => {
    const view = getView("hello ", 6)
    insertBlockRef(view, "id")
    expect(view.state.selection.head).toBe(1 + 6 + "((id))".length)
  })
})

describe("insertTag", () => {
  it("inserts #tag at cursor", () => {
    const view = getView("hello ", 6)
    insertTag(view, "bug")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello #bug")
  })

  it("does nothing when tag contains space", () => {
    const view = getView("hello ", 6)
    insertTag(view, "my tag")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello ")
  })

  it("does nothing when tag is empty", () => {
    const view = getView("hello ", 6)
    insertTag(view, "")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello ")
  })

  it("replaces selection with tagged text", () => {
    const view = getViewWithRange("hello world", 6, 11)
    insertTag(view, "greeting")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello #greeting")
  })

  it("does nothing when selection contains space", () => {
    const view = getViewWithRange("hello big world", 6, 10)
    insertTag(view, "greeting")
    const doc = view.state.doc
    expect(doc.textBetween(1, doc.content.size)).toBe("hello big world")
  })

  it("places cursor after the complete tag", () => {
    const view = getView("hello ", 6)
    insertTag(view, "bug")
    expect(view.state.selection.head).toBe(1 + 6 + "#bug".length)
  })

  it("places cursor after tag when replacing selection", () => {
    const view = getViewWithRange("hello world", 6, 11)
    insertTag(view, "greeting")
    expect(view.state.selection.head).toBe(1 + 6 + "#greeting".length)
  })
})
