import { EditorState } from "prosemirror-state"
import { describe, expect, it } from "vitest"
import { createMarkdownDecorationsPlugin } from "@/composables/useMarkdownDecorations"
import { contentToDoc } from "@/lib/content-model"

describe("Markdown decorations plugin", () => {
  it("builds decorations on init with content when blurred", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("Hello **world**")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const pluginState = plugin.getState(editorState)
    expect(pluginState).not.toBeNull()
    expect(pluginState?.contentCaches.size).toBeGreaterThan(0)

    const decorations = plugin.spec.props?.decorations?.(editorState)
    expect(decorations?.find().length).toBeGreaterThan(0)
  })

  it("returns decorations with faded markers when focused", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("Hello **world**")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const focusedState = editorState.apply(
      editorState.tr.setMeta("decorations:focus", true),
    )

    const decorations = plugin.spec.props?.decorations?.(focusedState)
    // Should have decorations (structural content), not empty
    expect(decorations?.find().length).toBeGreaterThan(0)
  })

  it("returns full decorations when blurred", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("Hello **world**")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const blurredState = editorState.apply(
      editorState.tr.setMeta("decorations:focus", false),
    )

    const decorations = plugin.spec.props?.decorations?.(blurredState)
    expect(decorations?.find().length).toBeGreaterThan(0)
  })

  it("toggles marker visibility based on focus state", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("Hello **world**")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const focusedState = editorState.apply(
      editorState.tr.setMeta("decorations:focus", true),
    )
    const focusedDecorations = plugin.spec.props?.decorations?.(focusedState)
    expect(focusedDecorations?.find().length).toBeGreaterThan(0)

    const blurredState = editorState.apply(
      editorState.tr.setMeta("decorations:focus", false),
    )
    const blurredDecorations = plugin.spec.props?.decorations?.(blurredState)
    expect(blurredDecorations?.find().length).toBeGreaterThan(0)
  })

  it("returns empty decorations with empty document on init", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const pluginState = plugin.getState(editorState)
    expect(pluginState).not.toBeNull()

    const decorations = plugin.spec.props?.decorations?.(editorState)
    expect(decorations?.find().length).toBe(0)
  })

  it("rebuilds decorations when document changes", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("plain")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const tr = editorState.tr.insertText(" **bold**", 5)
    const newState = editorState.apply(tr)

    const decorations = plugin.spec.props?.decorations?.(newState)
    expect(decorations?.find().length).toBeGreaterThan(0)
  })

  it("reuses cached tokens when only cursor moves", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("Hello **world**")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const initialState = plugin.getState(editorState)
    expect(initialState).not.toBeNull()

    const tr = editorState.tr.setSelection(
      editorState.selection.constructor.create(editorState.doc, 3),
    )
    const movedState = editorState.apply(tr)
    const movedPluginState = plugin.getState(movedState)

    expect(movedPluginState?.contentCaches).toBe(initialState?.contentCaches)
  })

  it("invalidates cache when document changes", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("Hello **world**")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const initialState = plugin.getState(editorState)
    expect(initialState).not.toBeNull()

    const tr = editorState.tr.insertText(" **bold**", 5)
    const newState = editorState.apply(tr)
    const newPluginState = plugin.getState(newState)

    expect(newPluginState?.contentCaches).not.toBe(initialState?.contentCaches)
  })

  it("builds decorations for highlight", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("Hello ^^world^^")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const pluginState = plugin.getState(editorState)
    expect(pluginState).not.toBeNull()

    const decorations = plugin.spec.props?.decorations?.(editorState)
    expect(decorations?.find().length).toBeGreaterThan(0)
  })

  it("builds decorations for page reference", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("Hello [[Page]]")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const pluginState = plugin.getState(editorState)
    expect(pluginState).not.toBeNull()

    const decorations = plugin.spec.props?.decorations?.(editorState)
    expect(decorations?.find().length).toBeGreaterThan(0)
  })

  it("builds decorations for block reference", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("Hello ((block-id))")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const pluginState = plugin.getState(editorState)
    expect(pluginState).not.toBeNull()

    const decorations = plugin.spec.props?.decorations?.(editorState)
    expect(decorations?.find().length).toBeGreaterThan(0)
  })

  it("builds decorations for tag", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("Hello #tag")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const pluginState = plugin.getState(editorState)
    expect(pluginState).not.toBeNull()

    const decorations = plugin.spec.props?.decorations?.(editorState)
    expect(decorations?.find().length).toBeGreaterThan(0)
  })

  it("renders callout continuation lines with callout class", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("> [!NOTE] header\n> continuation")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const decorations = plugin.spec.props?.decorations?.(editorState)
    const found = decorations?.find() ?? []

    let mdCalloutCount = 0
    for (const d of found) {
      const dec = d as unknown as { type?: { attrs?: Record<string, unknown> } }
      const attrs = dec.type?.attrs
      if (
        attrs &&
        typeof attrs.class === "string" &&
        (attrs.class as string).includes("md-callout")
      ) {
        mdCalloutCount++
      }
    }

    // With hard_break preservation, both lines are in one paragraph.
    // Lezer produces a single callout block decoration for the pair.
    expect(mdCalloutCount).toBe(1)
  })

  it("builds decorations for inline math", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("solve $x^2 + 1$ for x")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const decorations = plugin.spec.props?.decorations?.(editorState)
    expect(decorations?.find().length).toBeGreaterThan(0)
  })

  it("produces same decoration count for inline math as bold", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    const doc = contentToDoc("$x^2$")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const decorations = plugin.spec.props?.decorations?.(editorState)
    const mathCount = decorations?.find().length ?? 0
    expect(mathCount).toBeGreaterThan(0)
  })

  it("positions heading content correctly when heading is followed by blank line", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    // contentToDoc splits on \n: heading, blank, text = 3 paragraphs
    const doc = contentToDoc("# Hello World\n\nSome text")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const decorations = plugin.spec.props?.decorations?.(editorState)
    const found = decorations?.find() ?? []

    const headingContent = found.find(
      (d) =>
        typeof d.type === "object" &&
        d.type.attrs &&
        typeof d.type.attrs.class === "string" &&
        (d.type.attrs.class as string).includes("md-heading-content"),
    )

    expect(headingContent).toBeDefined()
    if (headingContent) {
      // Para 0: "# Hello World" (13 chars) at PM positions [1, 14)
      // Heading content "Hello World" starts at PM pos 3, ends at 14
      expect(headingContent.from).toBe(3)
      expect(headingContent.to).toBe(14)
    }
  })

  it("recognises table across multiple PM paragraphs", () => {
    const { plugin } = createMarkdownDecorationsPlugin()
    // Each line becomes a PM paragraph; buildCombinedText must join
    // table rows with \n (not \n\n) so GFM sees a single Table node.
    const doc = contentToDoc("| A | B |\n|---|---|\n| 1 | 2 |")

    const editorState = EditorState.create({
      doc,
      plugins: [plugin],
    })

    const decorations = plugin.spec.props?.decorations?.(editorState)
    const found = decorations?.find() ?? []

    // Table wrapper uses "md-table" class
    const tableDecoration = found.find(
      (d) =>
        typeof d.type === "object" &&
        d.type.attrs &&
        typeof d.type.attrs.class === "string" &&
        (d.type.attrs.class as string).includes("md-table"),
    )

    expect(tableDecoration).toBeDefined()
  })
})
