import { EditorState } from "prosemirror-state"
import { EditorView } from "prosemirror-view"
import { afterEach, beforeEach, describe, expect, it } from "vitest"
import { MathBlockView } from "@/composables/useMathBlockView"
import { schema } from "@/lib/schema"

let views: EditorView[] = []

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

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

function makeDocWithMathBlock(content: string) {
  const textNode = content ? schema.text(content) : undefined
  const block = textNode
    ? schema.nodes.math_block.create(null, [textNode])
    : schema.nodes.math_block.create()
  return schema.node("doc", {}, [block])
}

function createView(content: string) {
  const doc = makeDocWithMathBlock(content)
  const state = EditorState.create({ doc, schema })
  const dom = document.createElement("div")
  const view = new EditorView(dom, { state })
  views.push(view)
  return view
}

describe("MathBlockView", () => {
  it("creates DOM structure with correct classes", () => {
    const node = schema.nodes.math_block.create(null, [
      schema.text("$$\nE = mc^2\n$$"),
    ])
    const view = createView("$$\nE = mc^2\n$$")

    const nodeView = new MathBlockView(node, view, () => 0)

    expect(nodeView.dom.className).toBe("math-block")
    expect(nodeView.dom.children).toHaveLength(2)

    const preview = nodeView.dom.children[0] as HTMLElement
    const source = nodeView.dom.children[1] as HTMLElement
    expect(preview.className).toBe("math-block-preview")
    expect(source.className).toBe("math-block-source")
    expect(nodeView.contentDOM).toBe(source)
  })

  it("returns false from update for non-math_block nodes", () => {
    const textNode = schema.text("$$")
    const mathNode = schema.nodes.math_block.create(null, [textNode])
    const view = createView("$$")

    const nodeView = new MathBlockView(mathNode, view, () => 0)

    const paraNode = schema.nodes.paragraph.create(null, [schema.text("hello")])
    expect(nodeView.update(paraNode)).toBe(false)
  })

  it("returns true from update for math_block nodes", () => {
    const textNode = schema.text("$$\nE = mc^2\n$$")
    const mathNode = schema.nodes.math_block.create(null, [textNode])
    const view = createView("$$\nE = mc^2\n$$")

    const nodeView = new MathBlockView(mathNode, view, () => 0)

    const updatedNode = schema.nodes.math_block.create(null, [
      schema.text("$$\nx^2\n$$"),
    ])
    expect(nodeView.update(updatedNode)).toBe(true)
  })

  it("stopEvent returns true", () => {
    const textNode = schema.text("$$")
    const mathNode = schema.nodes.math_block.create(null, [textNode])
    const view = createView("$$")

    const nodeView = new MathBlockView(mathNode, view, () => 0)

    expect(nodeView.stopEvent()).toBe(true)
  })
})

describe("CodeBlockView", () => {
  it("creates DOM with cm-code-block class", async () => {
    const { CodeBlockView } = await import("@/composables/useCodeBlockView")

    const textNode = schema.text("```js\nconst x = 1\n```")
    const node = schema.nodes.code_block.create({ language: "js" }, [textNode])
    const doc = schema.node("doc", {}, [node])
    const state = EditorState.create({ doc, schema })
    const dom = document.createElement("div")
    const view = new EditorView(dom, { state })
    views.push(view)

    const nodeView = new CodeBlockView(node, view, () => 0)

    expect(nodeView.dom.className).toBe("cm-code-block")
    expect(nodeView.dom.children.length).toBeGreaterThan(0)

    // Clean up CM6 view
    nodeView.destroy()
  })

  it("update returns false for non-code_block nodes", async () => {
    const { CodeBlockView } = await import("@/composables/useCodeBlockView")

    const textNode = schema.text("```\ncode\n```")
    const node = schema.nodes.code_block.create({ language: "" }, [textNode])
    const doc = schema.node("doc", {}, [node])
    const state = EditorState.create({ doc, schema })
    const dom = document.createElement("div")
    const view = new EditorView(dom, { state })
    views.push(view)

    const nodeView = new CodeBlockView(node, view, () => 0)

    const paraNode = schema.nodes.paragraph.create(null, [schema.text("hello")])
    expect(nodeView.update(paraNode)).toBe(false)

    nodeView.destroy()
  })

  it("update returns true for code_block nodes", async () => {
    const { CodeBlockView } = await import("@/composables/useCodeBlockView")

    const textNode = schema.text("```\ncode\n```")
    const node = schema.nodes.code_block.create({ language: "" }, [textNode])
    const doc = schema.node("doc", {}, [node])
    const state = EditorState.create({ doc, schema })
    const dom = document.createElement("div")
    const view = new EditorView(dom, { state })
    views.push(view)

    const nodeView = new CodeBlockView(node, view, () => 0)

    const updatedNode = schema.nodes.code_block.create({ language: "ts" }, [
      textNode,
    ])
    expect(nodeView.update(updatedNode)).toBe(true)

    nodeView.destroy()
  })

  it("stopEvent returns true for keyboard events", async () => {
    const { CodeBlockView } = await import("@/composables/useCodeBlockView")

    const textNode = schema.text("```\ncode\n```")
    const node = schema.nodes.code_block.create({ language: "" }, [textNode])
    const doc = schema.node("doc", {}, [node])
    const state = EditorState.create({ doc, schema })
    const dom = document.createElement("div")
    const view = new EditorView(dom, { state })
    views.push(view)

    const nodeView = new CodeBlockView(node, view, () => 0)

    expect(nodeView.stopEvent(new Event("keydown"))).toBe(true)
    expect(nodeView.stopEvent(new Event("keypress"))).toBe(true)
    expect(nodeView.stopEvent(new Event("keyup"))).toBe(true)

    nodeView.destroy()
  })

  it("stopEvent returns false for clipboard events", async () => {
    const { CodeBlockView } = await import("@/composables/useCodeBlockView")

    const textNode = schema.text("```\ncode\n```")
    const node = schema.nodes.code_block.create({ language: "" }, [textNode])
    const doc = schema.node("doc", {}, [node])
    const state = EditorState.create({ doc, schema })
    const dom = document.createElement("div")
    const view = new EditorView(dom, { state })
    views.push(view)

    const nodeView = new CodeBlockView(node, view, () => 0)

    expect(nodeView.stopEvent(new Event("copy"))).toBe(false)
    expect(nodeView.stopEvent(new Event("cut"))).toBe(false)
    expect(nodeView.stopEvent(new Event("paste"))).toBe(false)

    nodeView.destroy()
  })

  it("stopEvent returns false for composition events", async () => {
    const { CodeBlockView } = await import("@/composables/useCodeBlockView")

    const textNode = schema.text("```\ncode\n```")
    const node = schema.nodes.code_block.create({ language: "" }, [textNode])
    const doc = schema.node("doc", {}, [node])
    const state = EditorState.create({ doc, schema })
    const dom = document.createElement("div")
    const view = new EditorView(dom, { state })
    views.push(view)

    const nodeView = new CodeBlockView(node, view, () => 0)

    expect(nodeView.stopEvent(new Event("compositionstart"))).toBe(false)
    expect(nodeView.stopEvent(new Event("compositionupdate"))).toBe(false)
    expect(nodeView.stopEvent(new Event("compositionend"))).toBe(false)

    nodeView.destroy()
  })
})

describe("stripFences / addFences", () => {
  it("stripFences removes first and last fence lines", async () => {
    const { stripFences } = await import("@/composables/useCodeBlockView")
    expect(stripFences("```js\nconst x = 1\n```")).toBe("const x = 1")
  })

  it("stripFences handles empty body", async () => {
    const { stripFences } = await import("@/composables/useCodeBlockView")
    expect(stripFences("```js\n```")).toBe("")
  })

  it("stripFences handles multiline body", async () => {
    const { stripFences } = await import("@/composables/useCodeBlockView")
    expect(stripFences("```\nline1\nline2\n```")).toBe("line1\nline2")
  })

  it("stripFences handles no language", async () => {
    const { stripFences } = await import("@/composables/useCodeBlockView")
    expect(stripFences("```\nconst x = 1\n```")).toBe("const x = 1")
  })

  it("addFences wraps body with fence markers and language", async () => {
    const { addFences } = await import("@/composables/useCodeBlockView")
    expect(addFences("const x = 1", "js")).toBe("```js\nconst x = 1\n```")
  })

  it("addFences handles empty language", async () => {
    const { addFences } = await import("@/composables/useCodeBlockView")
    expect(addFences("const x = 1", "")).toBe("```\nconst x = 1\n```")
  })

  it("addFences normalizes trailing newline", async () => {
    const { addFences } = await import("@/composables/useCodeBlockView")
    expect(addFences("const x = 1\n", "js")).toBe("```js\nconst x = 1\n```")
  })

  it("addFences handles empty body", async () => {
    const { addFences } = await import("@/composables/useCodeBlockView")
    expect(addFences("", "js")).toBe("```js\n\n```")
  })

  it("stripFences / addFences roundtrip preserves content", async () => {
    const { stripFences, addFences } = await import(
      "@/composables/useCodeBlockView"
    )
    const original = "```ts\nconst x: number = 1\n```"
    const body = stripFences(original)
    const restored = addFences(body, "ts")
    expect(restored).toBe(original)
  })

  it("addFences adds newline after empty body", async () => {
    // addFences always appends a trailing newline after the body to ensure
    // the closing ``` is on its own line. This means stripFences→addFences
    // is not idempotent for the edge case of an empty code block
    // ("```\n```" → "```\n\n```"). The fence format is normalized on write.
    const { stripFences, addFences } = await import(
      "@/composables/useCodeBlockView"
    )
    const original = "```\n```"
    const body = stripFences(original)
    const restored = addFences(body, "")
    expect(restored).toBe("```\n\n```")
  })

  it("addFences preserves trailing whitespace in body", async () => {
    const { addFences } = await import("@/composables/useCodeBlockView")
    const body = "line with trailing   "
    const result = addFences(body, "")
    expect(result).toContain("line with trailing   ")
    expect(result.startsWith("```")).toBe(true)
    expect(result).toBe("```\nline with trailing   \n```")
  })
})
