import { strict as assert } from 'node:assert';

import type { RichTextNode, RichTextRoot } from '@unito/integration-api';

import type { CanRender } from '../richTextCodec.js';

// A richText codec for one field: parse provider wire to canonical AST,
// serialize canonical AST back to wire, and the renderability predicate
// the serializer uses to decide what gets sentinel-encoded.
type Field = {
  canRender: CanRender;
  parse: (wire: string) => RichTextRoot;
  serialize: (root: RichTextRoot) => string;
};

export type ConformanceField = { key: string; module: Field };

// `{type:'break'}` and `{type:'break',children:[]}` are the same canonical
// node; integrations differ on whether they carry the empty array. Compare
// modulo that variant so conformance flags data loss, not representation.
function canonicalize(node: RichTextNode): RichTextNode {
  const out: RichTextNode = { ...node };
  if (out.children) {
    if (out.children.length === 0) {
      delete out.children;
    } else {
      out.children = out.children.map(canonicalize);
    }
  }
  return out;
}

function normalize(root: RichTextRoot): RichTextRoot {
  return { type: 'root', children: (root.children ?? []).map(canonicalize) };
}

// Asserts one canonical fixture round-trips through a field's codec —
// serialize→parse must not throw and must reproduce the original AST. When
// `divergence` names a documented loss, the fixture is instead asserted to
// STILL diverge, so a later codec fix fails this assertion until the now-stale
// divergence is removed.
export function assertConforms(field: ConformanceField, ast: RichTextRoot, divergence?: string): void {
  const got = normalize(field.module.parse(field.module.serialize(ast)));
  const want = normalize(ast);
  if (divergence) {
    assert.notDeepEqual(got, want, `no longer diverges — remove divergence (${divergence})`);
  } else {
    assert.deepEqual(got, want);
  }
}
