import './assert_dev_env.js';
/**
 * Structural diff between two snapshots — empty array means parity holds.
 *
 * Order of diffs is deterministic: tables in sorted order (with
 * column/index/constraint sub-diffs grouped per table), then sequences.
 * Consumers can rely on this for stable diff output.
 */
export const diff_schema_snapshots = (a, b) => {
    const diffs = [];
    const all_tables = new Set([...Object.keys(a.tables), ...Object.keys(b.tables)]);
    for (const table of [...all_tables].sort()) {
        const ta = a.tables[table];
        const tb = b.tables[table];
        if (!ta) {
            diffs.push({ kind: 'table_only_in', where: 'b', table });
            continue;
        }
        if (!tb) {
            diffs.push({ kind: 'table_only_in', where: 'a', table });
            continue;
        }
        diff_table(table, ta, tb, diffs);
    }
    const all_sequences = new Set([...Object.keys(a.sequences), ...Object.keys(b.sequences)]);
    for (const sequence of [...all_sequences].sort()) {
        const sa = a.sequences[sequence];
        const sb = b.sequences[sequence];
        if (!sa) {
            diffs.push({ kind: 'sequence_only_in', where: 'b', sequence });
            continue;
        }
        if (!sb) {
            diffs.push({ kind: 'sequence_only_in', where: 'a', sequence });
            continue;
        }
        diff_sequence(sequence, sa, sb, diffs);
    }
    return diffs;
};
const COLUMN_FIELDS = [
    'data_type',
    'udt_name',
    'is_nullable',
    'column_default',
    'is_identity',
];
const diff_table = (table, a, b, out) => {
    const all_columns = new Set([...Object.keys(a.columns), ...Object.keys(b.columns)]);
    for (const column of [...all_columns].sort()) {
        const ca = a.columns[column];
        const cb = b.columns[column];
        if (!ca) {
            out.push({ kind: 'column_only_in', where: 'b', table, column });
            continue;
        }
        if (!cb) {
            out.push({ kind: 'column_only_in', where: 'a', table, column });
            continue;
        }
        for (const field of COLUMN_FIELDS) {
            if (ca[field] !== cb[field]) {
                out.push({
                    kind: 'column_field_differs',
                    table,
                    column,
                    field,
                    a: ca[field],
                    b: cb[field],
                });
            }
        }
    }
    const a_indexes = new Map(a.indexes.map((i) => [i.name, i.definition]));
    const b_indexes = new Map(b.indexes.map((i) => [i.name, i.definition]));
    const all_indexes = new Set([...a_indexes.keys(), ...b_indexes.keys()]);
    for (const index of [...all_indexes].sort()) {
        const def_a = a_indexes.get(index);
        const def_b = b_indexes.get(index);
        if (def_a === undefined) {
            out.push({ kind: 'index_only_in', where: 'b', table, index });
            continue;
        }
        if (def_b === undefined) {
            out.push({ kind: 'index_only_in', where: 'a', table, index });
            continue;
        }
        if (def_a !== def_b) {
            out.push({ kind: 'index_definition_differs', table, index, a: def_a, b: def_b });
        }
    }
    const a_constraints = new Map(a.constraints.map((c) => [c.name, c]));
    const b_constraints = new Map(b.constraints.map((c) => [c.name, c]));
    const all_constraints = new Set([...a_constraints.keys(), ...b_constraints.keys()]);
    for (const constraint of [...all_constraints].sort()) {
        const ca = a_constraints.get(constraint);
        const cb = b_constraints.get(constraint);
        if (!ca) {
            out.push({ kind: 'constraint_only_in', where: 'b', table, constraint });
            continue;
        }
        if (!cb) {
            out.push({ kind: 'constraint_only_in', where: 'a', table, constraint });
            continue;
        }
        if (ca.type !== cb.type || ca.definition !== cb.definition) {
            out.push({
                kind: 'constraint_differs',
                table,
                constraint,
                a: { type: ca.type, definition: ca.definition },
                b: { type: cb.type, definition: cb.definition },
            });
        }
    }
};
const diff_sequence = (sequence, a, b, out) => {
    if (a.data_type !== b.data_type) {
        out.push({
            kind: 'sequence_data_type_differs',
            sequence,
            a: a.data_type,
            b: b.data_type,
        });
    }
};
/**
 * Render a diff list as a human-readable multi-line string. Empty diffs
 * produce an empty string.
 */
export const format_schema_diffs = (diffs, labels = {}) => {
    if (diffs.length === 0)
        return '';
    const label_a = labels.a ?? 'a';
    const label_b = labels.b ?? 'b';
    const where_label = (where) => (where === 'a' ? label_a : label_b);
    const lines = [];
    for (const d of diffs) {
        switch (d.kind) {
            case 'table_only_in':
                lines.push(`  table ${d.table} only in ${where_label(d.where)}`);
                break;
            case 'column_only_in':
                lines.push(`  ${d.table}.${d.column} only in ${where_label(d.where)}`);
                break;
            case 'column_field_differs':
                lines.push(`  ${d.table}.${d.column} ${d.field} differs: ${label_a}=${JSON.stringify(d.a)}, ${label_b}=${JSON.stringify(d.b)}`);
                break;
            case 'index_only_in':
                lines.push(`  index ${d.index} on ${d.table} only in ${where_label(d.where)}`);
                break;
            case 'index_definition_differs':
                lines.push(`  index ${d.index} on ${d.table} differs:\n    ${label_a}: ${d.a}\n    ${label_b}: ${d.b}`);
                break;
            case 'constraint_only_in':
                lines.push(`  constraint ${d.constraint} on ${d.table} only in ${where_label(d.where)}`);
                break;
            case 'constraint_differs':
                lines.push(`  constraint ${d.constraint} on ${d.table} differs:\n    ${label_a}: ${d.a.type} ${d.a.definition}\n    ${label_b}: ${d.b.type} ${d.b.definition}`);
                break;
            case 'sequence_only_in':
                lines.push(`  sequence ${d.sequence} only in ${where_label(d.where)}`);
                break;
            case 'sequence_data_type_differs':
                lines.push(`  sequence ${d.sequence} data_type differs: ${label_a}=${d.a}, ${label_b}=${d.b}`);
                break;
            default:
                // Compile-time exhaustiveness — a new SchemaDiff variant without a
                // case here makes `d` non-never and fails type-check.
                d;
                break;
        }
    }
    return lines.join('\n');
};
/**
 * Throw if the two snapshots disagree. The error message names the impls
 * (via `labels`) and lists every diff, so the failure is self-diagnosing.
 *
 * Consumers wire this after bootstrapping each impl against an isolated DB:
 *
 * ```ts
 * await drop_recreate_db('zzz_test');
 * await spawn_backend(deno_config);
 * const snapshot_deno = await query_schema_snapshot(db, {});
 * await drop_recreate_db('zzz_test');
 * await spawn_backend(rust_config);
 * const snapshot_rust = await query_schema_snapshot(db, {});
 * assert_schema_snapshots_equal(snapshot_deno, snapshot_rust, {a: 'deno', b: 'rust'});
 * ```
 */
export const assert_schema_snapshots_equal = (a, b, labels = {}) => {
    const diffs = diff_schema_snapshots(a, b);
    if (diffs.length === 0)
        return;
    const label_a = labels.a ?? 'a';
    const label_b = labels.b ?? 'b';
    throw new Error(`Schema parity failed: ${diffs.length} diff(s) between ${label_a} and ${label_b}\n${format_schema_diffs(diffs, labels)}`);
};
