import { describe, it } from 'node:test';
import assertStrict from 'node:assert/strict';

import { encodeSentinels, decodeSentinels } from '../src/richTextCodec.js';
import type { RichTextRoot } from '@unito/integration-api';

describe('encodeSentinels', () => {
  it('passes through when the predicate accepts every node', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [{ type: 'paragraph', children: [{ type: 'text', value: 'hi' }] }],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, () => true),
      tree,
    );
  });

  it('replaces unsupported nodes with paired markers, serializing data as space-separated key="value"', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: 'a' },
            {
              type: 'jira_panel',
              data: { panelType: 'info', color: '#deebff' },
              children: [{ type: 'text', value: 'x' }],
            },
            { type: 'text', value: 'b' },
          ],
        },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'jira_panel'),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [
              { type: 'text', value: 'a' },
              { type: 'text', value: '【 jira_panel panelType="info" color="#deebff" 】' },
              { type: 'text', value: 'x' },
              { type: 'text', value: '【 end jira_panel 】' },
              { type: 'text', value: 'b' },
            ],
          },
        ],
      },
    );
  });

  it('emits a self-closing marker for childless unsupported nodes', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: 'see ' },
            { type: 'asana_macro' },
            { type: 'text', value: ', then ' },
            { type: 'asana_macro', data: { kind: 'attachment', id: 42 } },
            { type: 'text', value: '.' },
          ],
        },
        { type: 'thematicBreak' },
        { type: 'paragraph', children: [{ type: 'text', value: 'after' }] },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'asana_macro' && node.type !== 'thematicBreak'),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [
              { type: 'text', value: 'see ' },
              { type: 'text', value: '【 asana_macro / 】' },
              { type: 'text', value: ', then ' },
              { type: 'text', value: '【 asana_macro kind="attachment" id=42 / 】' },
              { type: 'text', value: '.' },
            ],
          },
          { type: 'paragraph', children: [{ type: 'text', value: '【 thematicBreak / 】' }] },
          { type: 'paragraph', children: [{ type: 'text', value: 'after' }] },
        ],
      },
    );
  });

  it('recursively encodes children inside an encoded parent (depth-first)', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            {
              type: 'jira_panel',
              children: [{ type: 'asana_macro', children: [{ type: 'text', value: 'inside' }] }],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => !['jira_panel', 'asana_macro'].includes(node.type)),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [
              { type: 'text', value: '【 jira_panel 】' },
              { type: 'text', value: '【 asana_macro 】' },
              { type: 'text', value: 'inside' },
              { type: 'text', value: '【 end asana_macro 】' },
              { type: 'text', value: '【 end jira_panel 】' },
            ],
          },
        ],
      },
    );
  });

  it('passes the ancestor chain to the predicate, innermost-first with root last', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [{ type: 'strong', children: [{ type: 'text', value: 'bold' }] }],
        },
      ],
    };
    const seen: string[][] = [];
    encodeSentinels(tree, (node, ancestors) => {
      seen.push([node.type, ...ancestors.map(a => a.type)]);
      return true;
    });
    assertStrict.deepEqual(
      seen.find(s => s[0] === 'strong'),
      ['strong', 'paragraph', 'root'],
    );
  });

  it('places markers as inline text siblings when the parent holds inline content', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: 'a ' },
            { type: 'strong', children: [{ type: 'text', value: 'bold' }] },
            { type: 'text', value: ' b' },
          ],
        },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'strong'),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [
              { type: 'text', value: 'a ' },
              { type: 'text', value: '【 strong 】' },
              { type: 'text', value: 'bold' },
              { type: 'text', value: '【 end strong 】' },
              { type: 'text', value: ' b' },
            ],
          },
        ],
      },
    );
  });

  it('wraps each marker in its own paragraph when encoding under a block-content parent', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        { type: 'paragraph', children: [{ type: 'text', value: 'before' }] },
        {
          type: 'jira_panel',
          children: [{ type: 'paragraph', children: [{ type: 'text', value: 'inside' }] }],
        },
        { type: 'paragraph', children: [{ type: 'text', value: 'after' }] },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'jira_panel'),
      {
        type: 'root',
        children: [
          { type: 'paragraph', children: [{ type: 'text', value: 'before' }] },
          { type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】' }] },
          { type: 'paragraph', children: [{ type: 'text', value: 'inside' }] },
          { type: 'paragraph', children: [{ type: 'text', value: '【 end jira_panel 】' }] },
          { type: 'paragraph', children: [{ type: 'text', value: 'after' }] },
        ],
      },
    );
  });

  it('emits non-string primitive data values as JSON literals', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [{ type: 'checkbox', data: { checked: true, indent: 2 } }],
        },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'checkbox'),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [{ type: 'text', value: '【 checkbox checked=true indent=2 / 】' }],
          },
        ],
      },
    );
  });

  it('drops individual data values that cannot survive the wire format, keeping the rest', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            {
              type: 'jira_panel',
              data: {
                safe: 'ok',
                hasQuote: 'has "quote"',
                hasBracket: 'has 】 inside',
                nonPrimitive: { x: 1 },
              },
              children: [{ type: 'text', value: 'x' }],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'jira_panel'),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [
              { type: 'text', value: '【 jira_panel safe="ok" 】' },
              { type: 'text', value: 'x' },
              { type: 'text', value: '【 end jira_panel 】' },
            ],
          },
        ],
      },
    );
  });
});

describe('encodeSentinels — table substitution', () => {
  it('renders an unsupported table as a single markdown paragraph with break-delimited rows', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'table',
          children: [
            {
              type: 'tableRow',
              children: [
                { type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Name' }] },
                { type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Status' }] },
              ],
            },
            {
              type: 'tableRow',
              children: [
                { type: 'tableCell', children: [{ type: 'text', value: 'Alice' }] },
                { type: 'tableCell', children: [{ type: 'text', value: 'Active' }] },
              ],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'table'),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [
              { type: 'text', value: '| Name | Status |' },
              { type: 'break' },
              { type: 'text', value: '| --- | --- |' },
              { type: 'break' },
              { type: 'text', value: '| Alice | Active |' },
            ],
          },
        ],
      },
    );
  });

  it('emits the header separator after row 1 unconditionally', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'table',
          children: [
            {
              type: 'tableRow',
              children: [
                { type: 'tableCell', children: [{ type: 'text', value: 'a' }] },
                { type: 'tableCell', children: [{ type: 'text', value: 'b' }] },
              ],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'table'),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [
              { type: 'text', value: '| a | b |' },
              { type: 'break' },
              { type: 'text', value: '| --- | --- |' },
            ],
          },
        ],
      },
    );
  });

  it('preserves supported wrappers as canonical nodes inside cells', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'table',
          children: [
            {
              type: 'tableRow',
              children: [
                {
                  type: 'tableCell',
                  children: [
                    { type: 'link', data: { url: 'https://x' }, children: [{ type: 'text', value: 'click' }] },
                  ],
                },
                { type: 'tableCell', children: [{ type: 'text', value: 'Status' }] },
              ],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'table'),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [
              { type: 'text', value: '| ' },
              { type: 'link', data: { url: 'https://x' }, children: [{ type: 'text', value: 'click' }] },
              { type: 'text', value: ' | Status |' },
              { type: 'break' },
              { type: 'text', value: '| --- | --- |' },
            ],
          },
        ],
      },
    );
  });

  it('escapes backslashes, pipes, and newlines in cell text', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'table',
          children: [
            {
              type: 'tableRow',
              children: [
                { type: 'tableCell', children: [{ type: 'text', value: 'a | b' }] },
                { type: 'tableCell', children: [{ type: 'text', value: 'line1\nline2' }] },
                { type: 'tableCell', children: [{ type: 'text', value: 'back\\slash' }] },
              ],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'table'),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [
              { type: 'text', value: '| a \\| b | line1 line2 | back\\\\slash |' },
              { type: 'break' },
              { type: 'text', value: '| --- | --- | --- |' },
            ],
          },
        ],
      },
    );
  });

  it('drops empty tables', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        { type: 'table', children: [] },
        { type: 'paragraph', children: [{ type: 'text', value: 'after' }] },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'table'),
      {
        type: 'root',
        children: [{ type: 'paragraph', children: [{ type: 'text', value: 'after' }] }],
      },
    );
  });

  it('preserves unsupported nodes in cells via sentinel markers', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'table',
          children: [
            {
              type: 'tableRow',
              children: [
                {
                  type: 'tableCell',
                  children: [
                    { type: 'image', data: { src: 'thumb.png' } },
                    { type: 'text', value: ' ' },
                    { type: 'mention', data: { id: '123' }, children: [{ type: 'text', value: '@alice' }] },
                  ],
                },
              ],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, node => node.type !== 'table' && node.type !== 'image' && node.type !== 'mention'),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [
              {
                type: 'text',
                value: '| 【 image src="thumb.png" / 】 【 mention id="123" 】@alice【 end mention 】 |',
              },
              { type: 'break' },
              { type: 'text', value: '| --- |' },
            ],
          },
        ],
      },
    );
  });

  it('keeps the table when supported', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'table',
          children: [
            {
              type: 'tableRow',
              children: [{ type: 'tableCell', children: [{ type: 'text', value: 'x' }] }],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(
      encodeSentinels(tree, () => true),
      tree,
    );
  });
});

describe('decodeSentinels', () => {
  it('passes through when no markers are present', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [{ type: 'paragraph', children: [{ type: 'text', value: 'hi' }] }],
    };
    assertStrict.deepEqual(decodeSentinels(tree), tree);
  });

  it('reconstructs paired markers, with and without attrs', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: '【 jira_panel 】' },
            { type: 'text', value: 'plain' },
            { type: 'text', value: '【 end jira_panel 】' },
            { type: 'text', value: ' ' },
            { type: 'text', value: '【 jira_panel panelType="info" color="#deebff" 】' },
            { type: 'text', value: 'with attrs' },
            { type: 'text', value: '【 end jira_panel 】' },
          ],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'jira_panel', children: [{ type: 'text', value: 'plain' }] },
            { type: 'text', value: ' ' },
            {
              type: 'jira_panel',
              data: { panelType: 'info', color: '#deebff' },
              children: [{ type: 'text', value: 'with attrs' }],
            },
          ],
        },
      ],
    });
  });

  it('reconstructs nested markers innermost-first', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: '【 jira_panel 】' },
            { type: 'text', value: '【 asana_macro 】' },
            { type: 'text', value: 'inside' },
            { type: 'text', value: '【 end asana_macro 】' },
            { type: 'text', value: '【 end jira_panel 】' },
          ],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            {
              type: 'jira_panel',
              children: [{ type: 'asana_macro', children: [{ type: 'text', value: 'inside' }] }],
            },
          ],
        },
      ],
    });
  });

  it('merges open markers fragmented across adjacent text nodes', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: '【 jira_' },
            { type: 'text', value: 'panel 】' },
            { type: 'text', value: 'inside' },
            { type: 'text', value: '【 end jira_panel 】' },
          ],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [{ type: 'jira_panel', children: [{ type: 'text', value: 'inside' }] }],
        },
      ],
    });
  });

  it('reconstructs across paragraph siblings when open and close land in different blocks', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        { type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】' }] },
        { type: 'paragraph', children: [{ type: 'text', value: 'inside' }] },
        { type: 'paragraph', children: [{ type: 'text', value: '【 end jira_panel 】' }] },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'jira_panel',
          children: [{ type: 'paragraph', children: [{ type: 'text', value: 'inside' }] }],
        },
      ],
    });
  });

  it('leaves an orphan open marker as plain text', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [{ type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】unmatched' }] }],
    };
    assertStrict.deepEqual(decodeSentinels(tree), tree);
  });

  it('leaves an orphan close marker as plain text', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [{ type: 'paragraph', children: [{ type: 'text', value: 'unmatched 【 end jira_panel 】' }] }],
    };
    assertStrict.deepEqual(decodeSentinels(tree), tree);
  });

  it('splits a text node when markers wrap inline content mid-text', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [{ type: 'text', value: 'before 【 strong 】middle【 end strong 】 after' }],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: 'before ' },
            { type: 'strong', children: [{ type: 'text', value: 'middle' }] },
            { type: 'text', value: ' after' },
          ],
        },
      ],
    });
  });

  it('handles multiple paired and cross-sibling sentinels in one tree', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        // Multiple paired sentinels packed into a single text node.
        {
          type: 'paragraph',
          children: [{ type: 'text', value: '【 strong 】a【 end strong 】 and 【 emphasis 】b【 end emphasis 】' }],
        },
        // Cross-sibling outer (jira_panel) wrapping a paragraph that itself contains a paired inner (strong).
        { type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】' }] },
        {
          type: 'paragraph',
          children: [{ type: 'text', value: '【 strong 】Hello【 end strong 】 world' }],
        },
        { type: 'paragraph', children: [{ type: 'text', value: '【 end jira_panel 】' }] },
        // Back-to-back cross-sibling sentinels.
        { type: 'paragraph', children: [{ type: 'text', value: '【 asana_macro 】' }] },
        { type: 'paragraph', children: [{ type: 'text', value: 'macro' }] },
        { type: 'paragraph', children: [{ type: 'text', value: '【 end asana_macro 】' }] },
        { type: 'paragraph', children: [{ type: 'text', value: '【 notion_block 】' }] },
        { type: 'paragraph', children: [{ type: 'text', value: 'block' }] },
        { type: 'paragraph', children: [{ type: 'text', value: '【 end notion_block 】' }] },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'strong', children: [{ type: 'text', value: 'a' }] },
            { type: 'text', value: ' and ' },
            { type: 'emphasis', children: [{ type: 'text', value: 'b' }] },
          ],
        },
        {
          type: 'jira_panel',
          children: [
            {
              type: 'paragraph',
              children: [
                { type: 'strong', children: [{ type: 'text', value: 'Hello' }] },
                { type: 'text', value: ' world' },
              ],
            },
          ],
        },
        {
          type: 'asana_macro',
          children: [{ type: 'paragraph', children: [{ type: 'text', value: 'macro' }] }],
        },
        {
          type: 'notion_block',
          children: [{ type: 'paragraph', children: [{ type: 'text', value: 'block' }] }],
        },
      ],
    });
  });

  it('decodes legacy production forms — positional args and pre-RFC tag spellings', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: '【 highlight #ff0000 】' },
            { type: 'text', value: 'hot1' },
            { type: 'text', value: '【 end highlight 】' },
            { type: 'text', value: ' ' },
            { type: 'text', value: '【 highlighted #00ff00 】' },
            { type: 'text', value: 'hot2' },
            { type: 'text', value: '【 end highlighted 】' },
            { type: 'text', value: ' ' },
            { type: 'text', value: '【 header 2 】' },
            { type: 'text', value: 'Title' },
            { type: 'text', value: '【 end header 】' },
            { type: 'text', value: ' ' },
            { type: 'text', value: '【 code python 】' },
            { type: 'text', value: 'print("hi")' },
            { type: 'text', value: '【 end code 】' },
          ],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            {
              type: 'highlight',
              data: { color: '#ff0000' },
              children: [{ type: 'text', value: 'hot1' }],
            },
            { type: 'text', value: ' ' },
            {
              type: 'highlight',
              data: { color: '#00ff00' },
              children: [{ type: 'text', value: 'hot2' }],
            },
            { type: 'text', value: ' ' },
            { type: 'heading2', children: [{ type: 'text', value: 'Title' }] },
            { type: 'text', value: ' ' },
            {
              type: 'code',
              data: { lang: 'python' },
              children: [{ type: 'text', value: 'print("hi")' }],
            },
          ],
        },
      ],
    });
  });

  it('decodes self-closing markers with varied spacing around the slash', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: '【 a/ 】' },
            { type: 'text', value: ' ' },
            { type: 'text', value: '【 b /】' },
            { type: 'text', value: ' ' },
            { type: 'text', value: '【 c/】' },
            { type: 'text', value: ' ' },
            { type: 'text', value: '【 d kind="x"/ 】' },
          ],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'a' },
            { type: 'text', value: ' ' },
            { type: 'b' },
            { type: 'text', value: ' ' },
            { type: 'c' },
            { type: 'text', value: ' ' },
            { type: 'd', data: { kind: 'x' } },
          ],
        },
      ],
    });
  });

  it('decodes nested same-tag sentinels by tracking marker depth', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        // Inline: outer foo and inner foo packed into one paragraph's text.
        {
          type: 'paragraph',
          children: [{ type: 'text', value: '【 foo 】outer 【 foo 】inner【 end foo 】 more【 end foo 】' }],
        },
        // Cross-paragraph: outer bar's open and close in different paragraph
        // siblings, with an inner bar pair inside the middle paragraph.
        { type: 'paragraph', children: [{ type: 'text', value: '【 bar 】' }] },
        {
          type: 'paragraph',
          children: [{ type: 'text', value: '【 bar 】content【 end bar 】' }],
        },
        { type: 'paragraph', children: [{ type: 'text', value: '【 end bar 】' }] },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            {
              type: 'foo',
              children: [
                { type: 'text', value: 'outer ' },
                { type: 'foo', children: [{ type: 'text', value: 'inner' }] },
                { type: 'text', value: ' more' },
              ],
            },
          ],
        },
        {
          type: 'bar',
          children: [
            {
              type: 'paragraph',
              children: [{ type: 'bar', children: [{ type: 'text', value: 'content' }] }],
            },
          ],
        },
      ],
    });
  });
});

describe('decodeSentinels — markdown table reconstruction', () => {
  it('reconstructs a markdown table from a single text node with newline-delimited rows', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [{ type: 'text', value: '| Name | Status |\n| --- | --- |\n| Alice | Active |' }],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'table',
          children: [
            {
              type: 'tableRow',
              children: [
                { type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Name' }] },
                { type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Status' }] },
              ],
            },
            {
              type: 'tableRow',
              children: [
                { type: 'tableCell', children: [{ type: 'text', value: 'Alice' }] },
                { type: 'tableCell', children: [{ type: 'text', value: 'Active' }] },
              ],
            },
          ],
        },
      ],
    });
  });

  it('reconstructs a markdown table from break-delimited inline content', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        { type: 'text', value: '| Name | Status |' },
        { type: 'break' },
        { type: 'text', value: '| --- | --- |' },
        { type: 'break' },
        { type: 'text', value: '| Alice | Active |' },
      ],
    };
    const result = decodeSentinels(tree);
    assertStrict.equal(result.children[0]?.type, 'table');
    assertStrict.equal(result.children[0]?.children?.length, 2);
  });

  it('unwraps a paragraph wrapping the table content', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [{ type: 'text', value: '| Name | Status |\n| --- | --- |\n| Alice | Active |' }],
        },
      ],
    };
    const result = decodeSentinels(tree);
    assertStrict.equal(result.children[0]?.type, 'table');
  });

  it('recovers cell content with supported wrappers preserved as canonical nodes', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        { type: 'text', value: '| ' },
        { type: 'link', data: { url: 'https://x' }, children: [{ type: 'text', value: 'click' }] },
        { type: 'text', value: ' | Status |\n| --- | --- |' },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'table',
          children: [
            {
              type: 'tableRow',
              children: [
                {
                  type: 'tableCell',
                  data: { header: true },
                  children: [
                    { type: 'link', data: { url: 'https://x' }, children: [{ type: 'text', value: 'click' }] },
                  ],
                },
                {
                  type: 'tableCell',
                  data: { header: true },
                  children: [{ type: 'text', value: 'Status' }],
                },
              ],
            },
          ],
        },
      ],
    });
  });

  it('unescapes pipes and backslashes in cell text', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [{ type: 'text', value: '| a \\| b | back\\\\slash |\n| --- | --- |' }],
    };
    const root = decodeSentinels(tree);
    const cells = (root.children[0]?.children?.[0]?.children ?? []) as Array<{ children?: Array<{ value?: string }> }>;
    assertStrict.equal(cells[0]?.children?.[0]?.value, 'a | b');
    assertStrict.equal(cells[1]?.children?.[0]?.value, 'back\\slash');
  });

  it('rejects when the separator is not exactly ---', () => {
    for (const value of ['| h |\n| -- |', '| h |\n| ---- |', '| h |\n| :---: |']) {
      const tree: RichTextRoot = { type: 'root', children: [{ type: 'text', value }] };
      assertStrict.deepEqual(decodeSentinels(tree), tree);
    }
  });

  it('rejects column-count mismatches', () => {
    for (const value of ['| a | b |\n| --- |', '| a |\n| --- |\n| x | y |']) {
      const tree: RichTextRoot = { type: 'root', children: [{ type: 'text', value }] };
      assertStrict.deepEqual(decodeSentinels(tree), tree);
    }
  });

  it('leaves regular text alone', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        { type: 'text', value: 'Hello | world' },
        { type: 'text', value: '| Note: see above |' },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), tree);
  });

  it('recurses into block containers', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'blockquote',
          children: [{ type: 'text', value: '| a | b |\n| --- | --- |' }],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'blockquote',
          children: [
            {
              type: 'table',
              children: [
                {
                  type: 'tableRow',
                  children: [
                    { type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'a' }] },
                    { type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'b' }] },
                  ],
                },
              ],
            },
          ],
        },
      ],
    });
  });
});

describe('encodeSentinels + decodeSentinels round-trip', () => {
  it('preserves childless unsupported nodes via the self-closing marker form', () => {
    const original: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: 'see ' },
            { type: 'asana_macro', data: { kind: 'attachment', id: 42 } },
            { type: 'text', value: '.' },
          ],
        },
        { type: 'thematicBreak' },
      ],
    };
    assertStrict.deepEqual(
      decodeSentinels(encodeSentinels(original, node => node.type !== 'asana_macro' && node.type !== 'thematicBreak')),
      {
        type: 'root',
        children: [
          {
            type: 'paragraph',
            children: [
              { type: 'text', value: 'see ' },
              { type: 'asana_macro', data: { kind: 'attachment', id: 42 } },
              { type: 'text', value: '.' },
            ],
          },
          { type: 'paragraph', children: [{ type: 'thematicBreak' }] },
        ],
      },
    );
  });

  it('preserves typed primitive data values (boolean, number) — not just stringified copies', () => {
    const original: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            {
              type: 'tableCell',
              data: { header: true, indent: 2, kind: 'summary' },
              children: [{ type: 'text', value: 'cell' }],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, node => node.type !== 'tableCell')), original);
  });

  it('decodes a quoted-string attribute as a string even when the contents read like a JSON literal', () => {
    const tree: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: '【 checkbox checked="true" indent="2" 】' },
            { type: 'text', value: '【 end checkbox 】' },
          ],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(tree), {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [{ type: 'checkbox', data: { checked: 'true', indent: '2' } }],
        },
      ],
    });
  });

  it('preserves same-tag nesting (custom node containing itself)', () => {
    const original: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            {
              type: 'jira_panel',
              children: [
                { type: 'text', value: 'outer ' },
                { type: 'jira_panel', children: [{ type: 'text', value: 'inner' }] },
                { type: 'text', value: ' more' },
              ],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, node => node.type !== 'jira_panel')), original);
  });

  it('preserves supported children verbatim inside an encoded parent (block-context placement)', () => {
    const original: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'jira_panel',
          children: [
            {
              type: 'paragraph',
              children: [
                { type: 'text', value: 'a ' },
                { type: 'strong', children: [{ type: 'text', value: 'bold' }] },
                { type: 'text', value: ' b' },
              ],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, node => node.type !== 'jira_panel')), original);
  });

  it('preserves a tree mixing custom nodes with data, childless nodes, nested rejection, and block-context placement', () => {
    const original: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            { type: 'text', value: 'before ' },
            { type: 'image', data: { src: 'https://example.com/x.png', alt: 'x' } },
            { type: 'text', value: ' after' },
          ],
        },
        {
          type: 'jira_panel',
          data: { panelType: 'info' },
          children: [
            {
              type: 'paragraph',
              children: [
                {
                  type: 'asana_macro',
                  data: { id: '42' },
                  children: [{ type: 'text', value: 'nested' }],
                },
              ],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(
      decodeSentinels(encodeSentinels(original, node => !['image', 'jira_panel', 'asana_macro'].includes(node.type))),
      original,
    );
  });

  it('preserves nested unsupported block nodes — each marker pair wrapped at block level, no bare text at block scope', () => {
    const original: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'jira_panel',
          children: [
            {
              type: 'asana_panel',
              children: [{ type: 'paragraph', children: [{ type: 'text', value: 'x' }] }],
            },
          ],
        },
      ],
    };
    const reject = (node: { type: string }): boolean => !['jira_panel', 'asana_panel'].includes(node.type);

    assertStrict.deepEqual(encodeSentinels(original, reject), {
      type: 'root',
      children: [
        { type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】' }] },
        { type: 'paragraph', children: [{ type: 'text', value: '【 asana_panel 】' }] },
        { type: 'paragraph', children: [{ type: 'text', value: 'x' }] },
        { type: 'paragraph', children: [{ type: 'text', value: '【 end asana_panel 】' }] },
        { type: 'paragraph', children: [{ type: 'text', value: '【 end jira_panel 】' }] },
      ],
    });

    assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, reject)), original);
  });

  it('preserves nested unsupported block nodes inside a non-root block container (blockquote)', () => {
    const original: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'blockquote',
          children: [
            {
              type: 'jira_panel',
              children: [
                {
                  type: 'asana_panel',
                  children: [{ type: 'paragraph', children: [{ type: 'text', value: 'x' }] }],
                },
              ],
            },
          ],
        },
      ],
    };
    const reject = (node: { type: string }): boolean => !['jira_panel', 'asana_panel'].includes(node.type);

    assertStrict.deepEqual(encodeSentinels(original, reject), {
      type: 'root',
      children: [
        {
          type: 'blockquote',
          children: [
            { type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】' }] },
            { type: 'paragraph', children: [{ type: 'text', value: '【 asana_panel 】' }] },
            { type: 'paragraph', children: [{ type: 'text', value: 'x' }] },
            { type: 'paragraph', children: [{ type: 'text', value: '【 end asana_panel 】' }] },
            { type: 'paragraph', children: [{ type: 'text', value: '【 end jira_panel 】' }] },
          ],
        },
      ],
    });

    assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, reject)), original);
  });

  it('round-trips a markdown-encoded table whose first row declares headers', () => {
    const original: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'table',
          children: [
            {
              type: 'tableRow',
              children: [
                { type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Name' }] },
                { type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Status' }] },
              ],
            },
            {
              type: 'tableRow',
              children: [
                { type: 'tableCell', children: [{ type: 'text', value: 'Alice' }] },
                { type: 'tableCell', children: [{ type: 'text', value: 'Active' }] },
              ],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, node => node.type !== 'table')), original);
  });

  it('round-trips a table whose cells contain unsupported types', () => {
    const original: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'table',
          children: [
            {
              type: 'tableRow',
              children: [
                {
                  type: 'tableCell',
                  data: { header: true },
                  children: [
                    { type: 'image', data: { src: 'thumb.png' } },
                    { type: 'text', value: ' ' },
                    { type: 'mention', data: { id: '123' }, children: [{ type: 'text', value: '@alice' }] },
                  ],
                },
              ],
            },
          ],
        },
      ],
    };
    const reject = (node: { type: string }) =>
      node.type !== 'table' && node.type !== 'image' && node.type !== 'mention';
    assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, reject)), original);
  });

  it('round-trips a table whose cells contain a supported wrapper (link)', () => {
    const original: RichTextRoot = {
      type: 'root',
      children: [
        {
          type: 'table',
          children: [
            {
              type: 'tableRow',
              children: [
                {
                  type: 'tableCell',
                  data: { header: true },
                  children: [
                    { type: 'link', data: { url: 'https://x' }, children: [{ type: 'text', value: 'click' }] },
                  ],
                },
                {
                  type: 'tableCell',
                  data: { header: true },
                  children: [{ type: 'text', value: 'Status' }],
                },
              ],
            },
          ],
        },
      ],
    };
    assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, node => node.type !== 'table')), original);
  });
});
