import { describe, expect, it } from "vitest"
import {
  type BlockDecoration,
  type BlockRefDecoration,
  type ImageDecoration,
  type InlineMathDecoration,
  type PageRefDecoration,
  parseMarkdown,
  type TagDecoration,
} from "@/lib/markdown-parser"

describe("parseMarkdown", () => {
  it("parses bold", () => {
    const result = parseMarkdown("**bold**")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("strong")
    const decorationRanges = result.content[0].decorationRanges
    expect(decorationRanges).toHaveLength(3)
    expect(decorationRanges[0]).toEqual({ type: "hidden", from: 0, to: 2 })
    expect(decorationRanges[1]).toEqual({
      type: "content",
      from: 2,
      to: 6,
      className: "md-strong",
    })
    expect(decorationRanges[2]).toEqual({ type: "hidden", from: 6, to: 8 })
  })

  it("parses italic", () => {
    const result = parseMarkdown("*italic*")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("em")
  })

  it("parses code", () => {
    const result = parseMarkdown("`code`")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("code")
  })

  it("parses strikethrough", () => {
    const result = parseMarkdown("~~strike~~")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("strike")
  })

  it("parses highlight", () => {
    const result = parseMarkdown("^^highlight^^")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("highlight")
    const decorationRanges = result.content[0].decorationRanges
    expect(decorationRanges).toHaveLength(3)
    expect(decorationRanges[0]).toEqual({ type: "hidden", from: 0, to: 2 })
    expect(decorationRanges[1]).toEqual({
      type: "content",
      from: 2,
      to: 11,
      className: "md-highlight",
    })
    expect(decorationRanges[2]).toEqual({ type: "hidden", from: 11, to: 13 })
  })

  it("parses link", () => {
    const result = parseMarkdown("[link](http://example.com)")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("link")
    const decorationRanges = result.content[0].decorationRanges
    expect(decorationRanges).toHaveLength(3)
    expect(decorationRanges[0]).toEqual({ type: "hidden", from: 0, to: 1 })
    expect(decorationRanges[1]).toEqual({
      type: "content",
      from: 1,
      to: 5,
      className: "md-link",
    })
    expect(decorationRanges[2]).toEqual({ type: "hidden", from: 5, to: 26 })
  })

  it("parses page reference [[Page Name]]", () => {
    const result = parseMarkdown("[[Page Name]]")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("page-ref")
    const ref = result.content[0] as PageRefDecoration
    expect(ref.pageName).toBe("Page Name")
    expect(ref.alias).toBeUndefined()
  })

  it("parses page reference with alias [[Page Name|Alias]]", () => {
    const result = parseMarkdown("[[Page Name|Alias]]")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("page-ref")
    const ref = result.content[0] as PageRefDecoration
    expect(ref.pageName).toBe("Page Name")
    expect(ref.alias).toBe("Alias")
  })

  it("parses block reference ((block-id))", () => {
    const result = parseMarkdown("((block-id))")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("block-ref")
    const ref = result.content[0] as BlockRefDecoration
    expect(ref.blockId).toBe("block-id")
  })

  it("parses tag #tag", () => {
    const result = parseMarkdown("#tag")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("tag")
    const tag = result.content[0] as TagDecoration
    expect(tag.tag).toBe("tag")
  })

  it("parses nested italic+bold", () => {
    const result = parseMarkdown("***italicbold***")
    expect(result.content).toHaveLength(2)
    const types = result.content.map((t) => t.type)
    expect(types).toContain("strong")
    expect(types).toContain("em")
  })

  it("parses property syntax", () => {
    const result = parseMarkdown("key:: value")
    expect(result.properties).toHaveLength(1)
    expect(result.properties[0].key).toBe("key")
    expect(result.properties[0].value).toBe("value")
    expect(result.properties[0].keyRange).toEqual([0, 3])
    expect(result.properties[0].delimiterRange).toEqual([3, 5])
    expect(result.properties[0].valueRange).toEqual([5, 11])
  })

  it("parses multiple tokens in one string", () => {
    const result = parseMarkdown("**bold** and *italic*")
    expect(result.content).toHaveLength(2)
  })

  it("returns empty for plain text", () => {
    const result = parseMarkdown("just plain text")
    expect(result.content).toHaveLength(0)
    expect(result.properties).toHaveLength(0)
  })

  it("property with no value", () => {
    const result = parseMarkdown("key::")
    expect(result.properties).toHaveLength(1)
    expect(result.properties[0].value).toBe("")
  })

  it("does not parse property mid-sentence", () => {
    const result = parseMarkdown("some text key:: value")
    expect(result.properties).toHaveLength(0)
  })

  it("parses property only at start of text", () => {
    const result = parseMarkdown("key:: value")
    expect(result.properties).toHaveLength(1)
    expect(result.properties[0].key).toBe("key")
  })

  it("does not parse property when text before it", () => {
    const result = parseMarkdown("text key:: value")
    expect(result.properties).toHaveLength(0)
  })

  it("gracefully handles malformed markdown", () => {
    const result = parseMarkdown("**unclosed")
    expect(result.content).toHaveLength(0)
  })

  it("gracefully handles nested malformed tokens", () => {
    const result = parseMarkdown("**bold *italic**")
    // Should not throw, returns what it can parse
    expect(typeof result.content).toBe("object")
  })

  it("handles unmatched delimiters", () => {
    const result = parseMarkdown("*no closing")
    expect(result.content).toHaveLength(0)
  })
})

describe("unmatched delimiters", () => {
  it("unmatched bold opening only", () => {
    const result = parseMarkdown("**bold")
    expect(result.content).toHaveLength(0)
  })

  it("unmatched bold closing only", () => {
    const result = parseMarkdown("bold**")
    expect(result.content).toHaveLength(0)
  })

  it("unmatched italic opening only", () => {
    const result = parseMarkdown("*text")
    expect(result.content).toHaveLength(0)
  })

  it("unmatched strikethrough only", () => {
    const result = parseMarkdown("~~strike")
    expect(result.content).toHaveLength(0)
  })

  it("unmatched code opening only", () => {
    const result = parseMarkdown("`code")
    expect(result.content).toHaveLength(0)
  })

  it("unmatched highlight opening only", () => {
    const result = parseMarkdown("^^highlight")
    expect(result.content).toHaveLength(0)
  })

  it("unmatched page ref opening only", () => {
    const result = parseMarkdown("[[Page")
    expect(result.content).toHaveLength(0)
  })

  it("unmatched block ref opening only", () => {
    const result = parseMarkdown("((block")
    expect(result.content).toHaveLength(0)
  })

  it("unmatched tag no content", () => {
    const result = parseMarkdown("#")
    expect(result.content).toHaveLength(0)
  })

  it("multiple unmatched delimiters", () => {
    const result = parseMarkdown("**bold and *italic")
    expect(result.content).toHaveLength(0)
  })
})

describe("nested malformed tokens", () => {
  it("italic containing unmatched bold - parses partial", () => {
    const result = parseMarkdown("*text **bold*")
    expect(result.content.length).toBeGreaterThanOrEqual(0)
  })

  it("bold containing unmatched italic - parses partial", () => {
    const result = parseMarkdown("**text *italic")
    expect(result.content.length).toBeGreaterThanOrEqual(0)
  })

  it("strikethrough containing unmatched bold - parses partial", () => {
    const result = parseMarkdown("~~text **bold~~")
    expect(result.content.length).toBeGreaterThanOrEqual(0)
  })

  it("nested triple delimiters", () => {
    const result = parseMarkdown("***text***")
    expect(result.content.length).toBeGreaterThan(0)
  })

  it("code inside bold", () => {
    const result = parseMarkdown("**`code`**")
    expect(result.content).toHaveLength(2)
  })

  it("link inside bold", () => {
    const result = parseMarkdown("**[text](url)**")
    expect(result.content).toHaveLength(2)
  })
})

describe("incomplete patterns at end of text", () => {
  it("incomplete page ref at end", () => {
    const result = parseMarkdown("text [[Page")
    expect(result.content).toHaveLength(0)
  })

  it("incomplete block ref at end", () => {
    const result = parseMarkdown("text ((block")
    expect(result.content).toHaveLength(0)
  })

  it("incomplete highlight at end", () => {
    const result = parseMarkdown("text ^^high")
    expect(result.content).toHaveLength(0)
  })

  it("tag at end of word not at boundary", () => {
    const result = parseMarkdown("word#tag")
    expect(result.content).toHaveLength(0)
  })

  it("tag at boundary", () => {
    const result = parseMarkdown("word #tag")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("tag")
  })
})

describe("whitespace variations", () => {
  it("multiple spaces before tag", () => {
    const result = parseMarkdown("text  #tag")
    expect(result.content).toHaveLength(1)
  })

  it("tab before tag", () => {
    const result = parseMarkdown("text\t#tag")
    expect(result.content).toHaveLength(1)
  })

  it("newlines in page ref - Lezer allows but content split", () => {
    const result = parseMarkdown("[[Page\nName]]")
    expect(result.content.length).toBeGreaterThanOrEqual(0)
  })
})

describe("block decorations", () => {
  it("parses heading level 1", () => {
    const result = parseMarkdown("# Hello World")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("heading")
    expect(result.blocks[0].metadata?.level).toBe(1)
    expect(result.blocks[0].decorationRanges).toHaveLength(2)
    expect(result.blocks[0].decorationRanges[0]).toEqual({
      type: "hidden",
      from: 0,
      to: 1,
      className: "md-heading-marker",
    })
    expect(result.blocks[0].decorationRanges[1]).toEqual({
      type: "content",
      from: 2,
      to: 13,
      className: "md-heading-content",
    })
  })

  it("parses heading level 3", () => {
    const result = parseMarkdown("### Title")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("heading")
    expect(result.blocks[0].metadata?.level).toBe(3)
    expect(result.blocks[0].decorationRanges).toHaveLength(2)
    expect(result.blocks[0].decorationRanges[0]).toEqual({
      type: "hidden",
      from: 0,
      to: 3,
      className: "md-heading-marker",
    })
    expect(result.blocks[0].decorationRanges[1]).toEqual({
      type: "content",
      from: 4,
      to: 9,
      className: "md-heading-content",
    })
  })

  it("parses task TODO", () => {
    const result = parseMarkdown("TODO buy milk")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("task")
    expect(result.blocks[0].decorationRanges).toHaveLength(2)
    expect(result.blocks[0].decorationRanges[0]).toEqual({
      type: "hidden",
      from: 0,
      to: 4,
      className: "md-task-marker",
    })
    expect(result.blocks[0].decorationRanges[1]).toEqual({
      type: "content",
      from: 5,
      to: 13,
      className: "md-task-content",
    })
    expect(result.blocks[0].metadata?.taskState).toBe("TODO")
  })

  it("parses task with inline content", () => {
    const result = parseMarkdown("TODO buy milk [#A]")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("task")
    expect(result.blocks[0].decorationRanges).toHaveLength(2)
    expect(result.blocks[0].decorationRanges[0]).toEqual({
      type: "hidden",
      from: 0,
      to: 4,
      className: "md-task-marker",
    })
    expect(result.blocks[0].decorationRanges[1]).toEqual({
      type: "content",
      from: 5,
      to: 18,
      className: "md-task-content",
    })
    expect(result.blocks[0].metadata?.taskState).toBe("TODO")
  })

  it("parses blockquote", () => {
    const result = parseMarkdown("> Quote text")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("blockquote")
  })

  it("parses callout note", () => {
    const result = parseMarkdown("> [!NOTE] important info")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("callout")
    expect(result.blocks[0].metadata?.calloutType).toBe("NOTE")
  })

  it("parses callout warning", () => {
    const result = parseMarkdown("> [!WARNING] be careful")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("callout")
    expect(result.blocks[0].metadata?.calloutType).toBe("WARNING")
  })

  it("returns empty blocks for plain text", () => {
    const result = parseMarkdown("Just some plain text")
    expect(result.blocks).toHaveLength(0)
  })

  it("handles all task states", () => {
    const states = [
      "TODO",
      "DOING",
      "DONE",
      "LATER",
      "NOW",
      "WAITING",
      "CANCELLED",
    ]
    for (const state of states) {
      const result = parseMarkdown(`${state} task`)
      expect(result.blocks).toHaveLength(1)
      expect(result.blocks[0].metadata?.taskState).toBe(state)
    }
  })
})

describe("leading whitespace handling", () => {
  it("heading with leading spaces positions marker correctly", () => {
    const result = parseMarkdown("  # Title")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].decorationRanges[0]).toEqual({
      type: "hidden",
      from: 2,
      to: 3,
      className: "md-heading-marker",
    })
    expect(result.blocks[0].decorationRanges[1]).toEqual({
      type: "content",
      from: 4,
      to: 9,
      className: "md-heading-content",
    })
  })

  it("task with leading spaces positions marker correctly", () => {
    const result = parseMarkdown("  TODO buy milk")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].decorationRanges[0]).toEqual({
      type: "hidden",
      from: 2,
      to: 6,
      className: "md-task-marker",
    })
    expect(result.blocks[0].decorationRanges[1]).toEqual({
      type: "content",
      from: 7,
      to: 15,
      className: "md-task-content",
    })
  })

  it("task with leading spaces includes full content", () => {
    const result = parseMarkdown("  TODO buy milk [#A]")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("task")
    expect(result.blocks[0].decorationRanges[0]).toEqual({
      type: "hidden",
      from: 2,
      to: 6,
      className: "md-task-marker",
    })
    expect(result.blocks[0].decorationRanges[1]).toEqual({
      type: "content",
      from: 7,
      to: 20,
      className: "md-task-content",
    })
  })

  it("blockquote with leading spaces", () => {
    const result = parseMarkdown("  > Quote text")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].decorationRanges[0]).toEqual({
      type: "hidden",
      from: 2,
      to: 3,
      className: "md-marker",
    })
  })

  it("callout with leading spaces", () => {
    const result = parseMarkdown("  > [!NOTE] important")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].decorationRanges[0]).toEqual({
      type: "hidden",
      from: 4,
      to: 11,
      className: "md-marker",
    })
  })
})

describe("whitespace variations in task rules", () => {
  it("task with multiple spaces after keyword", () => {
    const result = parseMarkdown("TODO    buy milk")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].decorationRanges[0]).toEqual({
      type: "hidden",
      from: 0,
      to: 4,
      className: "md-task-marker",
    })
    expect(result.blocks[0].decorationRanges[1]).toEqual({
      type: "content",
      from: 8,
      to: 16,
      className: "md-task-content",
    })
  })

  it("task with trailing content includes everything", () => {
    const result = parseMarkdown("TODO buy milk    [#A]")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("task")
    expect(result.blocks[0].decorationRanges[1].to).toBe(21)
  })

  // Per-paragraph parsing tests (re-architecture: one paragraph at a time)
  describe("per-paragraph block positions", () => {
    it("heading positions relative to paragraph start", () => {
      const result = parseMarkdown("# Title")
      expect(result.blocks).toHaveLength(1)
      expect(result.blocks[0].decorationRanges[0]).toEqual({
        type: "hidden",
        from: 0,
        to: 1,
        className: "md-heading-marker",
      })
      expect(result.blocks[0].decorationRanges[1]).toEqual({
        type: "content",
        from: 2,
        to: 7,
        className: "md-heading-content",
      })
    })

    it("task positions relative to paragraph start", () => {
      const result = parseMarkdown("TODO buy milk")
      expect(result.blocks).toHaveLength(1)
      expect(result.blocks[0].decorationRanges[0]).toEqual({
        type: "hidden",
        from: 0,
        to: 4,
        className: "md-task-marker",
      })
      expect(result.blocks[0].decorationRanges[1]).toEqual({
        type: "content",
        from: 5,
        to: 13,
        className: "md-task-content",
      })
    })

    it("inline tokens on plain text paragraph", () => {
      const result = parseMarkdown("**bold** text")
      expect(result.content).toHaveLength(1)
      expect(result.content[0].decorationRanges[0]).toEqual({
        type: "hidden",
        from: 0,
        to: 2,
      })
    })

    it("tag in paragraph is parsed", () => {
      const result = parseMarkdown("some text #tag")
      expect(result.content).toHaveLength(1)
      expect(result.content[0].type).toBe("tag")
    })

    it("property at paragraph start is parsed", () => {
      const result = parseMarkdown("key:: value")
      expect(result.properties).toHaveLength(1)
      expect(result.properties[0].key).toBe("key")
    })
  })
})

describe("wrapperAttrs output", () => {
  it("bold token has wrapperAttrs", () => {
    const result = parseMarkdown("**bold**")
    expect(result.content[0].wrapperAttrs).toEqual({
      "data-md-type": "strong",
    })
  })

  it("page ref token has wrapperAttrs", () => {
    const result = parseMarkdown("[[Page Name]]")
    const ref = result.content[0] as PageRefDecoration
    expect(ref.wrapperAttrs).toEqual({
      "data-page-name": "Page Name",
      role: "link",
      tabindex: "0",
    })
  })

  it("page ref with alias includes data-alias", () => {
    const result = parseMarkdown("[[Page Name|Alias]]")
    const ref = result.content[0] as PageRefDecoration
    expect(ref.wrapperAttrs).toEqual({
      "data-page-name": "Page Name",
      "data-alias": "Alias",
      role: "link",
      tabindex: "0",
    })
  })

  it("block ref token has wrapperAttrs", () => {
    const result = parseMarkdown("((block-id))")
    const ref = result.content[0] as BlockRefDecoration
    expect(ref.wrapperAttrs).toEqual({ "data-block-id": "block-id" })
  })

  it("tag token has wrapperAttrs", () => {
    const result = parseMarkdown("#tag")
    const tag = result.content[0] as TagDecoration
    expect(tag.wrapperAttrs).toEqual({ "data-tag": "tag" })
  })

  it("highlight token has wrapperAttrs", () => {
    const result = parseMarkdown("^^highlight^^")
    expect(result.content[0].wrapperAttrs).toEqual({
      "data-md-type": "highlight",
    })
  })

  it("heading block has wrapperAttrs", () => {
    const result = parseMarkdown("# Title")
    expect(result.blocks[0].wrapperAttrs).toEqual({
      "data-heading-level": "1",
    })
  })

  it("task block has wrapperAttrs", () => {
    const result = parseMarkdown("TODO buy milk")
    expect(result.blocks[0].wrapperAttrs).toEqual({
      "data-task-state": "TODO",
    })
  })

  it("callout block has wrapperAttrs via nodeWrapper", () => {
    const result = parseMarkdown("> [!NOTE] important info")
    expect(result.blocks[0].nodeWrapper?.attrs).toEqual({
      "data-callout-type": "NOTE",
    })
  })

  it("callout block has nodeWrapper and md-marker", () => {
    const result = parseMarkdown("> [!NOTE] important info")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("callout")
    expect(result.blocks[0].nodeWrapper?.className).toBe("md-callout")
    expect(result.blocks[0].nodeWrapper?.attrs).toEqual({
      "data-callout-type": "NOTE",
    })
    expect(result.blocks[0].decorationRanges[0].className).toBe("md-marker")
  })

  it("blockquote continuation line uses md-marker", () => {
    const result = parseMarkdown("> continuation")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("blockquote")
    expect(result.blocks[0].decorationRanges).toHaveLength(1)
    expect(result.blocks[0].decorationRanges[0]).toEqual({
      type: "hidden",
      from: 0,
      to: 1,
      className: "md-marker",
    })
  })
})

describe("InlineMath", () => {
  it("parses $x^2$ as inline-math", () => {
    const result = parseMarkdown("$x^2$")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("inline-math")
    const token = result.content[0] as InlineMathDecoration
    expect(token.wrapperAttrs?.["data-math"]).toBe("x^2")
    const ranges = token.decorationRanges
    expect(ranges).toHaveLength(3)
    expect(ranges[0]).toEqual({ type: "hidden", from: 0, to: 1 })
    expect(ranges[1]).toEqual({
      type: "content",
      from: 1,
      to: 4,
      className: "md-math-inline",
    })
    expect(ranges[2]).toEqual({ type: "hidden", from: 4, to: 5 })
  })

  it("parses inline math in a sentence", () => {
    const result = parseMarkdown("solve $x^2 + 1$ for x")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("inline-math")
    const token = result.content[0] as InlineMathDecoration
    expect(token.wrapperAttrs?.["data-math"]).toBe("x^2 + 1")
  })

  it("does not match $$ as inline math", () => {
    const result = parseMarkdown("$$")
    expect(result.content).toHaveLength(0)
  })

  it("does not match bare $ without closing", () => {
    const result = parseMarkdown("$5")
    expect(result.content).toHaveLength(0)
  })

  it("does not match $ followed by whitespace", () => {
    const result = parseMarkdown("$ x^2$")
    expect(result.content).toHaveLength(0)
  })

  it("does not match closing $ followed by digit", () => {
    const result = parseMarkdown("$x^2$3")
    expect(result.content).toHaveLength(0)
  })
})

describe("Image", () => {
  it("parses ![alt](url) as image decoration", () => {
    const result = parseMarkdown("![alt](url)")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("image")
    const token = result.content[0] as ImageDecoration
    expect(token.wrapperAttrs?.["data-src"]).toBe("url")
    expect(token.wrapperAttrs?.["data-alt"]).toBe("alt")
    const ranges = token.decorationRanges
    expect(ranges).toHaveLength(3)
    expect(ranges[0]).toEqual({ type: "hidden", from: 0, to: 2 })
    expect(ranges[1]).toEqual({ type: "content", from: 2, to: 5 })
    expect(ranges[2]).toEqual({ type: "hidden", from: 5, to: 11 })
  })

  it("parses image with empty alt text", () => {
    const result = parseMarkdown("![](https://example.com/pic.png)")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("image")
    const token = result.content[0] as ImageDecoration
    expect(token.wrapperAttrs?.["data-src"]).toBe("https://example.com/pic.png")
    expect(token.wrapperAttrs?.["data-alt"]).toBe("")
  })

  it("parses image inline in a sentence", () => {
    const result = parseMarkdown("see ![photo](img.jpg) below")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("image")
    const token = result.content[0] as ImageDecoration
    expect(token.wrapperAttrs?.["data-alt"]).toBe("photo")
    expect(token.wrapperAttrs?.["data-src"]).toBe("img.jpg")
  })

  it("does not parse bare [text](url) as image", () => {
    const result = parseMarkdown("[text](url)")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("link")
  })
})

describe("horizontal-rule", () => {
  it("parses --- as horizontal rule", () => {
    const result = parseMarkdown("---")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("horizontal-rule")
    expect((result.blocks[0] as BlockDecoration).nodeWrapper?.className).toBe(
      "md-hr",
    )
  })

  it("parses *** as horizontal rule", () => {
    const result = parseMarkdown("***")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("horizontal-rule")
  })

  it("hides --- content with hidden decoration range", () => {
    const result = parseMarkdown("---")
    const block = result.blocks[0] as BlockDecoration
    expect(block.decorationRanges).toHaveLength(1)
    expect(block.decorationRanges[0]).toEqual({
      type: "hidden",
      from: 0,
      to: 3,
      className: "md-hr-marker",
    })
  })

  it("does not parse --- with trailing text as horizontal rule", () => {
    const result = parseMarkdown("--- not a break")
    expect(result.blocks).toHaveLength(0)
  })

  it("parses ---- as horizontal rule", () => {
    const result = parseMarkdown("----")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("horizontal-rule")
  })

  it("does not parse **** as horizontal rule (bold conflict)", () => {
    const result = parseMarkdown("****")
    expect(result.blocks).toHaveLength(0)
  })

  it("parses ***** as horizontal rule (odd length)", () => {
    const result = parseMarkdown("*****")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("horizontal-rule")
  })
})

describe("error recovery", () => {
  it("handles empty callout > [!NOTE]", () => {
    const result = parseMarkdown("> [!NOTE]")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("callout")
    expect(result.blocks[0].metadata?.calloutType).toBe("NOTE")
  })

  it("handles callout with unrecognized type as blockquote", () => {
    const result = parseMarkdown("> [!INVALID] text")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("blockquote")
  })

  it("handles callout with no space after type", () => {
    const result = parseMarkdown("> [!NOTE]text")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("callout")
  })

  it("handles heading with no content", () => {
    const result = parseMarkdown("# ")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("heading")
    expect(result.blocks[0].metadata?.level).toBe(1)
  })

  it("handles task with no content after keyword", () => {
    const result = parseMarkdown("TODO ")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("task")
    expect(result.blocks[0].metadata?.taskState).toBe("TODO")
  })

  it("handles extra closing bold delimiters", () => {
    const result = parseMarkdown("**bold**text**")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("strong")
  })

  it("handles unclosed link URL", () => {
    const result = parseMarkdown("[link](url")
    expect(Array.isArray(result.content)).toBe(true)
  })

  it("handles triple nested brackets [[[Page]]]", () => {
    const result = parseMarkdown("[[[Page]]]")
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("page-ref")
  })

  it("handles trailing text after blockquote", () => {
    const result = parseMarkdown("> Quote")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("blockquote")
  })

  it("handles empty heading marker only", () => {
    const result = parseMarkdown("#")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("heading")
  })

  it("handles multiple blockquotes sequentially in same block", () => {
    const result = parseMarkdown("> Line 1")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("blockquote")
  })
})

describe("table", () => {
  it("parses a simple table as block decoration", () => {
    const result = parseMarkdown("| A | B |\n| --- | --- |\n| C | D |")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("table")
  })

  it("parses table without leading/trailing pipes", () => {
    const result = parseMarkdown("A | B\n--- | ---\nC | D")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("table")
  })

  it("hides table source with hidden decoration range", () => {
    const result = parseMarkdown("| A | B |\n| --- | --- |\n| C | D |")
    const block = result.blocks[0] as BlockDecoration
    expect(block.decorationRanges).toHaveLength(1)
    expect(block.decorationRanges[0]).toEqual({
      type: "hidden",
      from: 0,
      to: 33,
      className: "md-table-hidden",
    })
  })

  it("stores raw source in wrapperAttrs", () => {
    const result = parseMarkdown("| A | B |\n| --- | --- |\n| C | D |")
    expect(result.blocks[0].wrapperAttrs?.["data-table-source"]).toBe(
      "| A | B |\n| --- | --- |\n| C | D |",
    )
  })

  it("has nodeWrapper with md-table class", () => {
    const result = parseMarkdown("| A | B |\n| --- | --- |\n| C | D |")
    expect((result.blocks[0] as BlockDecoration).nodeWrapper?.className).toBe(
      "md-table",
    )
  })

  it("parses table with alignment markers", () => {
    const result = parseMarkdown(
      "| Left | Center | Right |\n| :--- | :---: | ---: |\n| 1 | 2 | 3 |",
    )
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("table")
  })

  it("parses table with header only (no body rows)", () => {
    const result = parseMarkdown("| A | B |\n| --- | --- |")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("table")
  })

  it("does not create table block for plain text", () => {
    const result = parseMarkdown("not a table")
    expect(result.blocks).toHaveLength(0)
  })

  it("tables coexist with other blocks in the same text", () => {
    const result = parseMarkdown(
      "# Title\n\n| A | B |\n| --- | --- |\n| C | D |",
    )
    expect(result.blocks).toHaveLength(2)
    expect(result.blocks[0].type).toBe("heading")
    expect(result.blocks[1].type).toBe("table")
  })

  it("parses table with inline markdown in header cells", () => {
    const result = parseMarkdown(
      "| `code` | **bold** |\n| --- | --- |\n| a | b |",
    )
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("table")
  })

  it("parses table with pipes inside cell content (escaped)", () => {
    const result = parseMarkdown("| A \\| B | C |\n| --- | --- |\n| D | E |")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("table")
  })

  it("handles table with uneven columns as non-table (column count mismatch)", () => {
    const result = parseMarkdown("| A | B | C |\n| --- | --- |\n| D | E | F |")
    expect(result.blocks).toHaveLength(0)
  })

  it("handles table with missing separator row", () => {
    const result = parseMarkdown("| A | B |\n| C | D |")
    expect(result.blocks).toHaveLength(0)
  })

  it("handles table with empty cells", () => {
    const result = parseMarkdown("| A || B |\n| --- | --- | --- |\n| C | | D |")
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("table")
  })

  it("handles table with inline markdown in cells", () => {
    const result = parseMarkdown(
      "| **bold** | *italic* |\n| --- | --- |\n| `code` | text |",
    )
    expect(result.blocks).toHaveLength(1)
    expect(result.blocks[0].type).toBe("table")
  })
})

describe("tokenization depth exhaustion", () => {
  it("handles deeply nested bold delimiters without crashing", () => {
    const result = parseMarkdown("******deep******")
    expect(Array.isArray(result.content)).toBe(true)
  })

  it("handles 10+ layers of emphasis nesting without crashing", () => {
    const input = `${"*".repeat(20)}text${"*".repeat(20)}`
    const result = parseMarkdown(input)
    expect(Array.isArray(result.content)).toBe(true)
  })

  it("handles extreme nested brackets for page refs", () => {
    const input = "[[" + "[[" + "Page" + "]]" + "]]"
    const result = parseMarkdown(input)
    expect(Array.isArray(result.content)).toBe(true)
  })

  it("handles very long unbroken inline syntax", () => {
    const input = `**${"x".repeat(500)}**`
    const result = parseMarkdown(input)
    expect(result.content).toHaveLength(1)
    expect(result.content[0].type).toBe("strong")
  })

  it("handles many inline tokens in a single paragraph", () => {
    const tokens = Array.from({ length: 50 }, (_, i) => `**bold${i}**`)
    const input = tokens.join(" ")
    const result = parseMarkdown(input)
    expect(result.content.length).toBeGreaterThan(0)
  })
})

describe("comark — block components", () => {
  it("parses ::query{type=\"TODO\"} and produces BlockComponent in tree", () => {
    const result = parseMarkdown("::query{type=\"TODO\"}\nResults\n::")
    const names: string[] = []
    result.tree.iterate({
      enter: (n) => { names.push(n.type.name) },
    })
    expect(names).toContain("BlockComponent")
    expect(names).toContain("ComponentName")
    expect(names).toContain("ComponentAttrs")
    expect(names).toContain("ComponentBody")
  })

  it("parses ::divider with no body or attrs", () => {
    const result = parseMarkdown("::divider\n::")
    const names: string[] = []
    result.tree.iterate({
      enter: (n) => { names.push(n.type.name) },
    })
    expect(names).toContain("BlockComponent")
  })
})

describe("comark — inline components", () => {
  it("parses :priority{value=\"high\"} and produces InlineComponent in tree", () => {
    const result = parseMarkdown(":priority{value=\"high\"}")
    const names: string[] = []
    result.tree.iterate({
      enter: (n) => { names.push(n.type.name) },
    })
    expect(names).toContain("InlineComponent")
    expect(names).toContain("ComponentName")
    expect(names).toContain("ComponentAttrs")
  })

  it("parses :badge[Active]{color=\"green\"} with body and attrs", () => {
    const result = parseMarkdown(":badge[Active]{color=\"green\"}")
    const names: string[] = []
    result.tree.iterate({
      enter: (n) => { names.push(n.type.name) },
    })
    expect(names).toContain("InlineComponent")
    expect(names).toContain("ComponentBody")
    expect(names).toContain("ComponentAttrs")
  })

  it("parses :icon-check as standalone component", () => {
    const result = parseMarkdown(":icon-check")
    const names: string[] = []
    result.tree.iterate({
      enter: (n) => { names.push(n.type.name) },
    })
    expect(names).toContain("InlineComponent")
    expect(names).toContain("ComponentName")
    expect(names).not.toContain("ComponentBody")
    expect(names).not.toContain("ComponentAttrs")
  })

  it("does not match emoji syntax (:smile:)", () => {
    const result = parseMarkdown(":smile:")
    const names: string[] = []
    result.tree.iterate({
      enter: (n) => { names.push(n.type.name) },
    })
    expect(names).not.toContain("InlineComponent")
  })
})

describe("comark — span attributes", () => {
  it("parses [text]{.highlight} and produces Span in tree", () => {
    const result = parseMarkdown("[text]{.highlight}")
    const names: string[] = []
    result.tree.iterate({
      enter: (n) => { names.push(n.type.name) },
    })
    expect(names).toContain("Span")
    expect(names).toContain("SpanBody")
    expect(names).toContain("SpanAttrs")
  })

  it("does not match [text] {.class} with space before braces", () => {
    const result = parseMarkdown("[text] {.class}")
    const names: string[] = []
    result.tree.iterate({
      enter: (n) => { names.push(n.type.name) },
    })
    expect(names).not.toContain("Span")
  })
})
