import { describe, expect, it } from "vitest"
import { contentToDoc, docToContent, inlineToString } from "@/lib/content-model"
import { schema } from "@/lib/schema"

describe("contentToDoc", () => {
  it("handles plain text", () => {
    const doc = contentToDoc("hello world")
    expect(doc.childCount).toBe(1)
    expect(doc.child(0).textContent).toBe("hello world")
  })

  it("handles newlines as hard_break within a single paragraph", () => {
    const doc = contentToDoc("line1\nline2")
    expect(doc.childCount).toBe(1)
    expect(doc.child(0).type.name).toBe("paragraph")
    expect(inlineToString(doc.child(0))).toBe("line1\nline2")
  })

  it("handles fenced code block with backticks", () => {
    const doc = contentToDoc("```python\ndef foo():\n    pass\n```")
    expect(doc.childCount).toBe(1)
    expect(doc.child(0).type.name).toBe("code_block")
    expect(doc.child(0).attrs.language).toBe("python")
    expect(doc.child(0).textContent).toBe(
      "```python\ndef foo():\n    pass\n```",
    )
  })

  it("handles fenced code block with tildes", () => {
    const doc = contentToDoc("~~~js\nconsole.log('hi')\n~~~")
    expect(doc.childCount).toBe(1)
    expect(doc.child(0).type.name).toBe("code_block")
    expect(doc.child(0).attrs.language).toBe("js")
    expect(doc.child(0).textContent).toBe("~~~js\nconsole.log('hi')\n~~~")
  })

  it("handles fenced code block without language", () => {
    const doc = contentToDoc("```\nplain code\n```")
    expect(doc.childCount).toBe(1)
    expect(doc.child(0).type.name).toBe("code_block")
    expect(doc.child(0).attrs.language).toBe("")
    expect(doc.child(0).textContent).toBe("```\nplain code\n```")
  })

  it("handles code block with empty content", () => {
    const doc = contentToDoc("```\n```")
    expect(doc.childCount).toBe(1)
    expect(doc.child(0).type.name).toBe("code_block")
    expect(doc.child(0).textContent).toBe("```\n```")
  })

  it("mixes code blocks and paragraphs", () => {
    const doc = contentToDoc("before\n```\ncode\n```\nafter")
    expect(doc.childCount).toBe(3)
    expect(doc.child(0).type.name).toBe("paragraph")
    expect(doc.child(0).textContent).toBe("before")
    expect(doc.child(1).type.name).toBe("code_block")
    expect(doc.child(1).textContent).toBe("```\ncode\n```")
    expect(doc.child(2).type.name).toBe("paragraph")
    expect(doc.child(2).textContent).toBe("after")
  })

  it("handles unicode and emoji", () => {
    const doc = contentToDoc("Hello 🌍 你好 🔥")
    expect(doc.child(0).textContent).toBe("Hello 🌍 你好 🔥")
  })

  it("handles empty string", () => {
    const doc = contentToDoc("")
    expect(doc.childCount).toBe(1)
    expect(doc.child(0).textContent).toBe("")
  })

  it("skips blank lines (paragraph separators)", () => {
    const doc = contentToDoc("\n\n")
    // All-blank content falls back to a single empty paragraph
    expect(doc.childCount).toBe(1)
    expect(doc.child(0).textContent).toBe("")
  })
})

describe("docToContent", () => {
  it("converts doc to content string", () => {
    const doc = schema.nodes.doc.create(null, [
      schema.nodes.paragraph.create(null, [schema.text("hello")]),
    ])
    expect(docToContent(doc)).toBe("hello")
  })

  it("joins paragraphs with blank line separators", () => {
    const doc = schema.nodes.doc.create(null, [
      schema.nodes.paragraph.create(null, [schema.text("line1")]),
      schema.nodes.paragraph.create(null, [schema.text("line2")]),
    ])
    expect(docToContent(doc)).toBe("line1\n\nline2")
  })

  it("serializes code block with language", () => {
    const doc = schema.nodes.doc.create(null, [
      schema.nodes.code_block.create({ language: "python" }, [
        schema.text("```python\ndef foo():\n    pass\n```"),
      ]),
    ])
    expect(docToContent(doc)).toBe("```python\ndef foo():\n    pass\n```")
  })

  it("serializes code block without language", () => {
    const doc = schema.nodes.doc.create(null, [
      schema.nodes.code_block.create(null, [
        schema.text("```\nplain code\n```"),
      ]),
    ])
    expect(docToContent(doc)).toBe("```\nplain code\n```")
  })

  it("serializes code block with empty content", () => {
    const doc = schema.nodes.doc.create(null, [
      schema.nodes.code_block.create(null, [schema.text("```\n```")]),
    ])
    expect(docToContent(doc)).toBe("```\n```")
  })

  it("serializes mixed paragraphs and code blocks", () => {
    const doc = schema.nodes.doc.create(null, [
      schema.nodes.paragraph.create(null, [schema.text("before")]),
      schema.nodes.code_block.create({ language: "js" }, [
        schema.text("```js\ncode\n```"),
      ]),
      schema.nodes.paragraph.create(null, [schema.text("after")]),
    ])
    expect(docToContent(doc)).toBe("before\n\n```js\ncode\n```\n\nafter")
  })

  it("handles empty paragraph", () => {
    const doc = schema.nodes.doc.create(null, [schema.nodes.paragraph.create()])
    expect(docToContent(doc)).toBe("")
  })
})

describe("roundtrip", () => {
  it("plain text roundtrips", () => {
    const original = "hello world"
    const doc = contentToDoc(original)
    expect(docToContent(doc)).toBe(original)
  })

  it("multiline roundtrips as single paragraph with hard_breaks", () => {
    const original = "line1\nline2\nline3"
    const doc = contentToDoc(original)
    expect(doc.childCount).toBe(1)
    expect(inlineToString(doc.child(0))).toBe("line1\nline2\nline3")
  })

  it("unicode roundtrips", () => {
    const original = "Hello 🌍 你好 🔥"
    const doc = contentToDoc(original)
    expect(docToContent(doc)).toBe(original)
  })

  it("empty string roundtrips", () => {
    const original = ""
    const doc = contentToDoc(original)
    expect(docToContent(doc)).toBe(original)
  })

  it("fenced code block with language roundtrips", () => {
    const original = "```python\ndef foo():\n    pass\n```"
    const doc = contentToDoc(original)
    expect(docToContent(doc)).toBe(original)
  })

  it("fenced code block without language roundtrips", () => {
    const original = "```\nplain code\n```"
    const doc = contentToDoc(original)
    expect(docToContent(doc)).toBe(original)
  })

  it("mixed paragraphs and code blocks roundtrips", () => {
    const original = "before\n\n```js\ncode\n```\n\nafter"
    const doc = contentToDoc(original)
    expect(docToContent(doc)).toBe(original)
  })

  it("tildes fenced code block roundtrips", () => {
    const original = "~~~js\nconsole.log('hi')\n~~~"
    const doc = contentToDoc(original)
    // Fences are preserved as-is in the text content
    expect(docToContent(doc)).toBe(original)
  })

  it("code_block node has code spec flag", () => {
    const doc = contentToDoc("```\ncode\n```")
    expect(doc.child(0).type.spec.code).toBe(true)
  })

  it("math_block node has code spec flag", () => {
    const doc = contentToDoc("$$\nmath\n$$")
    expect(doc.child(0).type.spec.code).toBe(true)
  })

  it("tildes code block preserves language", () => {
    const doc = contentToDoc("~~~python\ndef foo():\n    pass\n~~~")
    expect(doc.child(0).type.name).toBe("code_block")
    expect(doc.child(0).attrs.language).toBe("python")
  })
})

describe("math block (content model)", () => {
  it("detects $$ fence in contentToDoc", () => {
    const doc = contentToDoc("before\n$$\n\\int_a^b f(x) dx\n$$\nafter")
    expect(doc.childCount).toBe(3)
    expect(doc.child(0).type.name).toBe("paragraph")
    expect(doc.child(0).textContent).toBe("before")
    expect(doc.child(1).type.name).toBe("math_block")
    expect(doc.child(1).textContent).toBe("$$\n\\int_a^b f(x) dx\n$$")
    expect(doc.child(2).type.name).toBe("paragraph")
    expect(doc.child(2).textContent).toBe("after")
  })

  it("serializes math_block in docToContent", () => {
    const doc = schema.nodes.doc.create(null, [
      schema.nodes.paragraph.create(null, [schema.text("intro")]),
      schema.nodes.math_block.create(null, [schema.text("$$\nE = mc^2\n$$")]),
      schema.nodes.paragraph.create(null, [schema.text("outro")]),
    ])
    expect(docToContent(doc)).toBe("intro\n\n$$\nE = mc^2\n$$\n\noutro")
  })

  it("roundtrips block math", () => {
    const original = "text\n\n$$\na^2 + b^2 = c^2\n$$\n\nmore"
    const doc = contentToDoc(original)
    expect(docToContent(doc)).toBe(original)
  })

  it("handles empty math block", () => {
    const doc = contentToDoc("$$\n$$")
    expect(doc.childCount).toBe(1)
    expect(doc.child(0).type.name).toBe("math_block")
    expect(doc.child(0).textContent).toBe("$$\n$$")
  })
})

describe("roundtrip (hard_break preservation)", () => {
  const ROUNDTRIP_CASES: [string, string][] = [
    ["plain text", "hello world"],
    ["single newline", "line1\nline2"],
    ["double newline (paragraph break)", "para1\n\npara2"],
    ["mixed newlines", "section1\nline1\nline2\n\nsection2"],
    ["triple line", "line1\nline2\nline3"],
    ["multiple paragraphs", "para1\n\npara2\n\npara3"],
    ["code block", "```js\nconst x = 1\n```"],
    [
      "code block with internal blank lines",
      "text\n\n```\nline1\n\nline2\n```\n\nafter",
    ],
    ["math block", "$$\nE = mc^2\n$$"],
    ["wrapped math", "text\n\n$$\na^2 + b^2 = c^2\n$$\n\nmore"],
    [
      "properties (motivating case)",
      "## Properties\n\nauthor:: Enesis Editor\nversion:: 0.1.0",
    ],
    ["single line only", "line1\nline2\nline3"],
    ["empty string", ""],
  ]

  for (const [label, input] of ROUNDTRIP_CASES) {
    it(`roundtrips: ${label}`, () => {
      expect(docToContent(contentToDoc(input))).toBe(input)
    })
  }
})
