import {
  type AnyExtension,
  createBlockMarkdownSpec,
  createInlineMarkdownSpec,
  Extension,
  Node,
} from '@tiptap/core'
import { Bold } from '@tiptap/extension-bold'
import { Document } from '@tiptap/extension-document'
import { Heading } from '@tiptap/extension-heading'
import { Italic } from '@tiptap/extension-italic'
import { Link } from '@tiptap/extension-link'
import { BulletList, ListItem, OrderedList, TaskItem, TaskList } from '@tiptap/extension-list'
import { Mention } from '@tiptap/extension-mention'
import { Paragraph } from '@tiptap/extension-paragraph'
import Strike from '@tiptap/extension-strike'
import { Text } from '@tiptap/extension-text'
import Underline from '@tiptap/extension-underline'
import { Youtube } from '@tiptap/extension-youtube'
import { MarkdownManager } from './MarkdownManager.js'
import { Marked } from 'marked'
import { beforeEach, describe, expect, it } from 'vite-plus/test'

// Create test extensions
const TestCallout = Node.create({
  name: 'callout',
  group: 'block',
  content: 'block+',
  defining: true,

  addAttributes() {
    return {
      type: {
        default: 'info',
      },
    }
  },

  ...createBlockMarkdownSpec({
    nodeName: 'callout',
    allowedAttributes: ['type'],
  }),
})

const TestTag = Node.create({
  name: 'tag',
  group: 'inline',
  inline: true,
  atom: true,

  addAttributes() {
    return {
      label: {
        type: null,
      },
    }
  },

  ...createInlineMarkdownSpec({
    nodeName: 'tag',
    selfClosing: true,
    allowedAttributes: ['label'],
  }),
})

const TestProbe = Extension.create({
  name: 'probeBlock',

  markdownTokenName: 'probeBlock',

  parseMarkdown: (token, helpers) => {
    return {
      type: 'paragraph',
      content: helpers.parseInline(token.tokens || []),
    }
  },

  markdownTokenizer: {
    name: 'probeBlock',
    level: 'block',
    start: ':::probe ',
    tokenize: (src, _tokens, lexer) => {
      const match = src.match(/^:::probe\s+(.+?)\s+:::/)

      if (!match) {
        return undefined
      }

      return {
        type: 'probeBlock',
        raw: match[0],
        text: match[1],
        tokens: lexer.inlineTokens(match[1]),
      }
    },
  },
})

describe('MarkdownManager Direct Tests', () => {
  let markdownManager: MarkdownManager
  let basicExtensions: AnyExtension[]
  let extendedExtensions: AnyExtension[]

  beforeEach(() => {
    // Basic extensions for standard markdown
    basicExtensions = [
      Document,
      Paragraph,
      Text,
      Heading,
      Bold,
      Italic,
      Strike,
      Link,
      BulletList,
      OrderedList,
      ListItem,
      Underline,
    ]

    // Extended extensions with custom markdown features
    extendedExtensions = [
      ...basicExtensions,
      TaskList,
      TaskItem,
      Mention.configure({
        HTMLAttributes: {
          class: 'mention',
        },
      }),
      Youtube.configure({
        width: 640,
        height: 360,
      }),
      TestCallout,
      TestTag,
    ]
  })

  describe('Basic Markdown Parsing', () => {
    beforeEach(() => {
      markdownManager = new MarkdownManager({ extensions: basicExtensions })
    })

    it('should parse simple text', () => {
      const markdown = 'Hello world'
      const doc = markdownManager.parse(markdown)

      expect(doc.type).toBe('doc')
      expect(Array.isArray(doc.content)).toBe(true)
      expect(doc.content![0].type).toBe('paragraph')
      expect(doc.content![0].content![0].text).toBe('Hello world')
    })

    it('should parse headings', () => {
      const markdown = `# Heading 1

## Heading 2`
      const doc = markdownManager.parse(markdown)

      expect(doc.content).toHaveLength(2)
      expect(doc.content![0].type).toBe('heading')
      expect(doc.content![0].attrs!.level).toBe(1)
      expect(doc.content![0].content![0].text).toBe('Heading 1')

      expect(doc.content![1].type).toBe('heading')
      expect(doc.content![1].attrs!.level).toBe(2)
      expect(doc.content![1].content![0].text).toBe('Heading 2')
    })

    it('should parse multiple paragraphs', () => {
      const markdown = `First paragraph.

Second paragraph.`
      const doc = markdownManager.parse(markdown)

      expect(doc.content).toHaveLength(2)
      expect(doc.content![0].type).toBe('paragraph')
      expect(doc.content![0].content![0].text).toBe('First paragraph.')
      expect(doc.content![1].type).toBe('paragraph')
      expect(doc.content![1].content![0].text).toBe('Second paragraph.')
    })

    it('keeps inline tokens after ordered list tokenization', () => {
      const markdown = `1. Item 1
   - Nested bullet item

**After** ordered list`

      const doc = markdownManager.parse(markdown)

      expect(doc.content).toHaveLength(2)
      expect(doc.content![0].type).toBe('orderedList')
      expect(doc.content![1]).toEqual({
        type: 'paragraph',
        content: [
          {
            type: 'text',
            text: 'After',
            marks: [{ type: 'bold' }],
          },
          {
            type: 'text',
            text: ' ordered list',
          },
        ],
      })
    })

    it('matches isolated Marked instance output for ordered-list parsing', () => {
      const markdown = `1. Item 1
   - Nested bullet item

**After** ordered list`
      const isolatedManager = new MarkdownManager({
        marked: new Marked() as unknown as typeof import('marked').marked,
        extensions: basicExtensions,
      })

      expect(markdownManager.parse(markdown)).toEqual(isolatedManager.parse(markdown))
    })

    it('keeps later inline parsing stable after a custom tokenizer uses inlineTokens', () => {
      const manager = new MarkdownManager({ extensions: [...basicExtensions, TestProbe] })

      const markdown = `:::probe **Probe** text :::

**After** custom tokenizer`
      const doc = manager.parse(markdown)

      expect(doc.content).toEqual([
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: 'Probe',
              marks: [{ type: 'bold' }],
            },
            {
              type: 'text',
              text: ' text',
            },
          ],
        },
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: 'After',
              marks: [{ type: 'bold' }],
            },
            {
              type: 'text',
              text: ' custom tokenizer',
            },
          ],
        },
      ])
    })

    it('runs custom tokenizers registered on an injected Marked instance', () => {
      // Regression: custom tokenizers must still fire when a dedicated `marked`
      // instance is injected, not only on the default shared instance.
      const manager = new MarkdownManager({
        marked: new Marked() as unknown as typeof import('marked').marked,
        extensions: [...basicExtensions, TestProbe],
      })

      const doc = manager.parse(':::probe **Probe** text :::')

      expect(doc.content).toEqual([
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: 'Probe',
              marks: [{ type: 'bold' }],
            },
            {
              type: 'text',
              text: ' text',
            },
          ],
        },
      ])
    })
  })
  describe('simple nested Marks parsing', () => {
    beforeEach(() => {
      markdownManager = new MarkdownManager({ extensions: basicExtensions })
    })
    const nestedMarkdown = `**...++abc++**`

    const expectedJSON = {
      type: 'doc',
      content: [
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: '...',
              marks: [{ type: 'bold' }],
            },
            {
              type: 'text',
              text: 'abc',
              marks: [{ type: 'underline' }, { type: 'bold' }],
            },
          ],
        },
      ],
    }
    it('should convert nested markdown to expected JSON structure', () => {
      const json = markdownManager.parse(nestedMarkdown)
      expect(json).toEqual(expectedJSON)
    })

    it('should convert nested JSON structure back to expected markdown', () => {
      const md = markdownManager.serialize(expectedJSON)
      expect(md).toEqual(nestedMarkdown)
    })
  })
  describe('complex nested Marks parsing', () => {
    beforeEach(() => {
      markdownManager = new MarkdownManager({ extensions: basicExtensions })
    })
    const nestedMarkdown = `**...~~++abc*abc*++~~**`

    const expectedJSON = {
      type: 'doc',
      content: [
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: '...',
              marks: [{ type: 'bold' }],
            },
            {
              type: 'text',
              text: 'abc',
              marks: [{ type: 'underline' }, { type: 'strike' }, { type: 'bold' }],
            },
            {
              type: 'text',
              text: 'abc',
              marks: [
                { type: 'italic' },
                { type: 'underline' },
                { type: 'strike' },
                { type: 'bold' },
              ],
            },
          ],
        },
      ],
    }
    it('should convert nested markdown to expected JSON structure', () => {
      const json = markdownManager.parse(nestedMarkdown)
      expect(json).toEqual(expectedJSON)
    })

    it('should convert nested JSON structure back to expected markdown', () => {
      const md = markdownManager.serialize(expectedJSON)
      expect(md).toEqual(nestedMarkdown)
    })
  })
  describe('Extended Markdown Parsing', () => {
    beforeEach(() => {
      markdownManager = new MarkdownManager({ extensions: extendedExtensions })
    })

    it('should parse mentions', () => {
      const markdown = '[@ id="user123" label="John Doe"] said hello'
      const doc = markdownManager.parse(markdown)

      expect(doc.content![0].type).toBe('paragraph')
      expect(doc.content![0].content![0].type).toBe('mention')
      expect(doc.content![0].content![0].attrs!.id).toBe('user123')
      expect(doc.content![0].content![0].attrs!.label).toBe('John Doe')
      expect(doc.content![0].content![1].text).toBe(' said hello')
    })

    it('should parse YouTube videos', () => {
      const markdown = ':::youtube {src="https://youtube.com/watch?v=test"} :::'
      const doc = markdownManager.parse(markdown)

      expect(doc.content![0].type).toBe('youtube')
      expect(doc.content![0].attrs!.src).toBe('https://youtube.com/watch?v=test')
    })

    it('should handle complex documents with mixed content', () => {
      const markdown = `# Project Title

This is a description with [@ id="user" label="User Name"].

:::youtube {src="https://youtube.com/watch?v=example"} :::

## Notes

Final paragraph.`

      const doc = markdownManager.parse(markdown)

      expect(doc.content).toHaveLength(5)
      expect(doc.content![0].type).toBe('heading')
      expect(doc.content![1].type).toBe('paragraph')
      expect(doc.content![1].content![0].type).toBe('text')
      expect(doc.content![1].content![1].type).toBe('mention')
      expect(doc.content![2].type).toBe('youtube')
      expect(doc.content![3].type).toBe('heading')
      expect(doc.content![4].type).toBe('paragraph')
    })
  })

  describe('Markdown Rendering', () => {
    beforeEach(() => {
      markdownManager = new MarkdownManager({ extensions: extendedExtensions })
    })

    it('should render simple text', () => {
      const doc = {
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [{ type: 'text', text: 'Hello world' }],
          },
        ],
      }

      const markdown = markdownManager.renderNodes(doc)
      expect(markdown).toBe('Hello world')
    })

    it('should return empty string when serializing empty document', () => {
      // When the editor is empty, it contains a doc with an empty paragraph
      const emptyDoc = {
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [],
          },
        ],
      }

      const markdown = markdownManager.serialize(emptyDoc)
      expect(markdown).toBe('')
    })

    it('should preserve blank lines between paragraphs with content', () => {
      // Multiple paragraphs with content should preserve blank lines
      const doc = {
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [{ type: 'text', text: 'First paragraph' }],
          },
          {
            type: 'paragraph',
            content: [],
          },
          {
            type: 'paragraph',
            content: [{ type: 'text', text: 'Second paragraph' }],
          },
        ],
      }

      const markdown = markdownManager.serialize(doc)
      expect(markdown).toBe('First paragraph\n\n\n\nSecond paragraph')
    })

    it('should move trailing whitespace outside of mark closing (Issue #7180)', () => {
      // When text has trailing space and bold mark, output should be "**text** " not "**text **"
      const doc = {
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [
              {
                type: 'text',
                text: 'bold text ',
                marks: [{ type: 'bold' }],
              },
              {
                type: 'text',
                text: 'continues here',
              },
            ],
          },
        ],
      }

      const markdown = markdownManager.renderNodes(doc)
      // The trailing space should be outside the ** markers
      expect(markdown).toBe('**bold text** continues here')
    })

    it('should move leading whitespace outside of mark opening (Issue #7180)', () => {
      // When text has leading space and bold mark, output should be " **text**" not "** text**"
      const doc = {
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [
              {
                type: 'text',
                text: 'normal text',
              },
              {
                type: 'text',
                text: ' bold text',
                marks: [{ type: 'bold' }],
              },
            ],
          },
        ],
      }

      const markdown = markdownManager.renderNodes(doc)
      // The leading space should be outside the ** markers
      expect(markdown).toBe('normal text **bold text**')
    })

    it('should handle both leading and trailing whitespace in marked text (Issue #7180)', () => {
      // Text like " bold " with bold mark should output " **bold** " not "** bold **"
      const doc = {
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [
              {
                type: 'text',
                text: 'before',
              },
              {
                type: 'text',
                text: ' bold ',
                marks: [{ type: 'bold' }],
              },
              {
                type: 'text',
                text: 'after',
              },
            ],
          },
        ],
      }

      const markdown = markdownManager.renderNodes(doc)
      // Both leading and trailing spaces should be outside the ** markers
      expect(markdown).toBe('before **bold** after')
    })

    it('should handle trailing whitespace in italic marks (Issue #7180)', () => {
      const doc = {
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [
              {
                type: 'text',
                text: 'italic text ',
                marks: [{ type: 'italic' }],
              },
              {
                type: 'text',
                text: 'normal',
              },
            ],
          },
        ],
      }

      const markdown = markdownManager.renderNodes(doc)
      expect(markdown).toBe('*italic text* normal')
    })

    it('should handle whitespace at end of paragraph with mark (Issue #7180)', () => {
      // Bold text with trailing space at end of paragraph
      const doc = {
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [
              {
                type: 'text',
                text: 'some text ',
                marks: [{ type: 'bold' }],
              },
            ],
          },
        ],
      }

      const markdown = markdownManager.renderNodes(doc)
      // Trailing space should be outside the marks
      expect(markdown).toBe('**some text** ')
    })

    it('should render nested marks with correct tag order', () => {
      // Marks come in ProseMirror's canonical order (rank ascending) — Bold is
      // registered before Strike so it gets the lower rank. The serializer
      // places lower-rank marks outermost, mirroring how a real editor would
      // wrap the text: bold outer, strike inner.
      const doc = {
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [
              {
                type: 'text',
                text: 'Test: ',
                marks: [],
              },
              {
                type: 'text',
                text: 'abcd',
                marks: [
                  { type: 'bold' }, // Opens first: **
                  { type: 'strike' }, // Opens second: ~
                ],
              },
              {
                type: 'text',
                text: ' end.',
                marks: [],
              },
            ],
          },
        ],
      }

      const docAtEnd = {
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [
              {
                type: 'text',
                text: 'abcd',
                marks: [
                  { type: 'bold' }, // Opens first: **
                  { type: 'strike' }, // Opens second: ~
                ],
              },
            ],
          },
        ],
      }

      expect(markdownManager.renderNodes(doc)).to.equal('Test: **~~abcd~~** end.')
      expect(markdownManager.renderNodes(docAtEnd)).to.equal('**~~abcd~~**')
    })

    it('should render headings', () => {
      const doc = {
        type: 'doc',
        content: [
          {
            type: 'heading',
            attrs: { level: 1 },
            content: [{ type: 'text', text: 'Main Title' }],
          },
          {
            type: 'heading',
            attrs: { level: 2 },
            content: [{ type: 'text', text: 'Subtitle' }],
          },
        ],
      }

      const markdown = markdownManager.renderNodes(doc)
      expect(markdown).toBe('# Main Title\n\n## Subtitle')
    })

    it('should render mentions', () => {
      const doc = {
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [
              {
                type: 'mention',
                attrs: { id: 'user123', label: 'John Doe' },
              },
              { type: 'text', text: ' said hello' },
            ],
          },
        ],
      }

      const markdown = markdownManager.renderNodes(doc)
      expect(markdown).toBe('[@ id="user123" label="John Doe"] said hello')
    })

    it('should render YouTube videos', () => {
      const doc = {
        type: 'doc',
        content: [
          {
            type: 'youtube',
            attrs: { src: 'https://youtube.com/watch?v=test' },
          },
        ],
      }

      const markdown = markdownManager.renderNodes(doc)
      expect(markdown).toBe(':::youtube {src="https://youtube.com/watch?v=test"} :::')
    })
  })

  it('does not reopen marks after inline atom that does not carry them', () => {
    const doc = {
      type: 'doc',
      content: [
        {
          type: 'paragraph',
          content: [
            { type: 'text', text: 'Bold ', marks: [{ type: 'bold' }] },
            { type: 'tag', attrs: { label: 'node' } },
            { type: 'text', text: ' some text' },
          ],
        },
      ],
    }

    const markdown = markdownManager.renderNodes(doc)
    // Trailing space is moved outside bold markers (per Issue #7180 whitespace convention)
    expect(markdown).toBe('**Bold** [tag label="node"] some text')
  })

  describe('Round-trip Tests', () => {
    beforeEach(() => {
      markdownManager = new MarkdownManager({ extensions: extendedExtensions })
    })

    const testCases = [
      {
        name: 'simple text',
        markdown: 'Hello world',
      },
      {
        name: 'headings',
        markdown: '# Main Title\n\n## Subtitle',
      },
      {
        name: 'mention',
        markdown: '[@ id="user123" label="John Doe"] said hello',
      },
      {
        name: 'youtube video',
        markdown: ':::youtube {src="https://youtube.com/watch?v=test"} :::',
      },
      {
        name: 'mixed content',
        markdown:
          '# Title\n\nHello [@ id="user" label="User"].\n\n:::youtube {src="https://youtube.com/watch?v=example"} :::',
      },
    ]

    testCases.forEach(({ name, markdown }) => {
      it(`should round-trip ${name}`, () => {
        const doc = markdownManager.parse(markdown)
        const roundtripMarkdown = markdownManager.renderNodes(doc)

        // Normalize whitespace for comparison
        const normalizeWhitespace = (str: string) => str.replace(/\s+/g, ' ').trim()

        expect(normalizeWhitespace(roundtripMarkdown)).toBe(normalizeWhitespace(markdown))
      })
    })
  })

  describe('Extension Integration', () => {
    it('should work with different extension combinations', () => {
      // Test with minimal extensions
      const minimalExtensions = [Document, Paragraph, Text]
      const minimalManager = new MarkdownManager({ extensions: minimalExtensions })

      const simpleDoc = minimalManager.parse('Hello world')
      expect(simpleDoc.content![0].type).toBe('paragraph')

      // Test with extended extensions
      const extendedManager = new MarkdownManager({ extensions: extendedExtensions })

      const complexDoc = extendedManager.parse('# Title\n\n[@ id="user" label="User"]')
      expect(complexDoc.content![0].type).toBe('heading')
      expect(complexDoc.content![1].content![0].type).toBe('mention')
    })

    it('should handle unknown markdown gracefully', () => {
      const manager = new MarkdownManager({ extensions: basicExtensions })

      // Test various unknown markdown patterns that should be preserved as text
      const testCases = [
        'Hello [unknown syntax] world',
        'Text with [custom element] here',
        'Some [shortcode attr="value"] content',
        'Using [component prop="test"] in text',
      ]

      testCases.forEach(markdown => {
        const doc = manager.parse(markdown)

        // Should treat it as regular text since it's not recognized markdown
        expect(doc.content![0].type).toBe('paragraph')
        expect(doc.content![0].content![0].text).toBe(markdown)
      })
    })
  })

  describe('Performance Tests', () => {
    beforeEach(() => {
      markdownManager = new MarkdownManager({ extensions: extendedExtensions })
    })

    it('should handle large documents efficiently', () => {
      // Generate a large markdown document
      const sections = []
      for (let i = 1; i <= 100; i += 1) {
        sections.push(`## Section ${i}`)
        sections.push(`This is paragraph ${i} with some **bold** text.`)
        sections.push(`And mention [@ id="user${i}" label="User ${i}"].`)
      }
      const largeMarkdown = sections.join('\n\n')

      const startTime = Date.now()
      const doc = markdownManager.parse(largeMarkdown)
      const parseTime = Date.now() - startTime

      const renderStartTime = Date.now()
      const rendered = markdownManager.renderNodes(doc)
      const renderTime = Date.now() - renderStartTime

      // Basic performance expectations (these would need to be tuned)
      expect(parseTime).toBeLessThan(1000) // Less than 1 second
      expect(renderTime).toBeLessThan(1000) // Less than 1 second
      expect(doc.content!.length).toBeGreaterThan(200) // Should have parsed many nodes
      expect(rendered).toContain('Section 1')
      expect(rendered).toContain('Section 100')
    })

    it('should handle deeply nested content', () => {
      const nestedMarkdown = `# Level 1

:::callout {type="info"}
## Level 2 inside callout

Some text with [@ id="user" label="User"].

:::youtube {src="https://youtube.com/watch?v=test"} :::
:::`

      const doc = markdownManager.parse(nestedMarkdown)
      const rendered = markdownManager.renderNodes(doc)

      expect(doc.content!.length).toBeGreaterThan(1)
      expect(rendered).toContain('Level 1')
      expect(rendered).toContain('Level 2')
    })
  })

  describe('configured extension context for markdown hooks', () => {
    const createParagraphWithUrlHooks = () =>
      Node.create({
        name: 'paragraph',
        group: 'block',
        content: 'inline*',

        addOptions() {
          return {
            repairUrl: (url: string) => url,
            serializeMarkdownUrl: (url: string) => url,
          }
        },

        parseMarkdown(token, helpers) {
          return helpers.createNode(
            this.name,
            { url: this.options.repairUrl(token.text || '') },
            helpers.parseInline(token.tokens || []),
          )
        },

        renderMarkdown(node) {
          return this.options.serializeMarkdownUrl(node.attrs?.url)
        },
      })

    const configuredParagraph = createParagraphWithUrlHooks().configure({
      repairUrl: (url: string) => `https://example.com/${url}`,
      serializeMarkdownUrl: (url: string) => `<${url}>`,
    })

    const createManager = (paragraph: AnyExtension) =>
      new MarkdownManager({
        extensions: [paragraph, Text],
      })

    it('exposes configured options and name while parsing and serializing', () => {
      const manager = createManager(configuredParagraph)
      const parsed = manager.parse('asset.png')

      expect(parsed).toEqual({
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            attrs: { url: 'https://example.com/asset.png' },
            content: [{ type: 'text', text: 'asset.png' }],
          },
        ],
      })

      expect(
        manager.serialize({
          type: 'paragraph',
          attrs: { url: 'https://example.com/asset.png' },
          content: [{ type: 'text', text: 'asset.png' }],
        }),
      ).toBe('<https://example.com/asset.png>')
    })

    it('keeps independent configured instances isolated', () => {
      const first = createParagraphWithUrlHooks().configure({
        repairUrl: (url: string) => `https://first.example/${url}`,
        serializeMarkdownUrl: (url: string) => `<first:${url}>`,
      })
      const second = createParagraphWithUrlHooks().configure({
        repairUrl: (url: string) => `https://second.example/${url}`,
        serializeMarkdownUrl: (url: string) => `<second:${url}>`,
      })

      expect(createManager(first).parse('asset.png').content![0].attrs).toEqual({
        url: 'https://first.example/asset.png',
      })
      expect(createManager(second).parse('asset.png').content![0].attrs).toEqual({
        url: 'https://second.example/asset.png',
      })
    })

    it('merges chained configure() calls into markdown hooks', () => {
      const paragraph = createParagraphWithUrlHooks()
        .configure({
          repairUrl: (url: string) => `https://first.example/${url}`,
        })
        .configure({
          serializeMarkdownUrl: (url: string) => `<${url}>`,
          repairUrl: (url: string) => `https://chained.example/${url}`,
        })

      const manager = createManager(paragraph)
      const parsed = manager.parse('asset.png')

      expect(parsed.content![0].attrs).toEqual({ url: 'https://chained.example/asset.png' })
      expect(manager.serialize(parsed.content![0])).toBe('<https://chained.example/asset.png>')
    })

    it('lets markdown hooks call their parent with the configured context', () => {
      const paragraph = createParagraphWithUrlHooks()
        .extend({
          parseMarkdown(token, helpers) {
            return this.parent?.(token, helpers)
          },
          renderMarkdown(node, helpers, ctx) {
            return this.parent?.(node, helpers, ctx) ?? ''
          },
        })
        .configure({
          repairUrl: (url: string) => `https://example.com/${url}`,
          serializeMarkdownUrl: (url: string) => `<${url}>`,
        })

      const manager = createManager(paragraph)
      const parsed = manager.parse('asset.png')

      expect(parsed.content![0].attrs).toEqual({ url: 'https://example.com/asset.png' })
      expect(manager.serialize(parsed.content![0])).toBe('<https://example.com/asset.png>')
    })
  })
})
