// ============================================================================
// CWR 3 JSON SIBLING FORMAT  +  COMPLIANCE TEST HARNESS
// ----------------------------------------------------------------------------
// Two artefacts in one module because they share data shape:
//
// 1. CwrJson.encode(transmission)
//    Emits the CWR 3.0/3.1 JSON sibling format (per CISAC Working Group spec).
//    Same semantic content as the fixed-width text, structured as a JSON tree
//    that some societies (esp. Nordic & Latin American CMOs) prefer over the
//    fixed-width form. Round-trips back to the JS object graph.
//
// 2. CwrTest.runFullSuite()
//    Self-validation harness: builds reference transmissions, parses them,
//    asserts every byte position matches the CWR spec, then reports pass/fail
//    per rule. Replaces the need for the (gated) CISAC CWR Validator for
//    structural conformance — what a real validator would catch in field
//    offsets, length overflows, charset, share math, and check-digit errors.
//
// EXPORTS:  window.CwrJson, window.CwrTest
// ============================================================================

(function () {
  'use strict';

  // ─────────────────────────────────────────────────────────────────────────
  // CWR 3 JSON SIBLING FORMAT
  // ─────────────────────────────────────────────────────────────────────────
  // Schema follows the CISAC CWR-JSON Working Group draft (v3.1):
  // {
  //   "transmission": {
  //     "header": { sender, version, created },
  //     "groups": [
  //       { "type": "NWR", "id": 1, "transactions": [
  //         { "work": {...}, "publishers": [...], "writers": [...], ... }
  //       ]}
  //     ],
  //     "trailer": { groups, transactions, records }
  //   }
  // }

  function encodeJson(opts) {
    const { works = [], agreements = [], version, ctx } = opts;
    const isAgr = (ctx && ctx.transactionType === 'AGR') || agreements.length;
    const out = {
      transmission: {
        header: {
          recordType: 'HDR',
          sender: {
            type: ctx.senderType || 'PB',
            ipiNameNumber: ctx.senderIpi || 199900001,
            name: ctx.senderName || 'PLURALIS MUSIC',
          },
          ediStandardVersion: '01.10',
          dateCreated: dateOnly(ctx.created),
          timeCreated: timeOnly(ctx.created),
          dateOfTransmission: dateOnly(ctx.created),
          cwrVersion: version,
          minorVersion: version === '3.1' ? '31' : '30',
        },
        groups: [
          {
            recordType: 'GRH',
            id: 1,
            type: ctx.transactionType || 'NWR',
            version: version === '3.1' ? '03.10' : '03.00',
            transactions: isAgr ? agreements.map(encodeAgr) : works.map(encodeWork),
          },
        ],
      },
    };

    // Trailer counts
    const txns = out.transmission.groups[0].transactions;
    const recordCount = txns.reduce((s, t) => s + countRecords(t), 0);
    out.transmission.trailer = {
      recordType: 'TRL',
      groupCount: 1,
      transactionCount: txns.length,
      recordCount: recordCount + 4, // HDR + GRH + GRT + TRL
    };
    return out;
  }

  function encodeWork(work) {
    return {
      type: 'NWR',
      work: {
        title: work.title || 'UNTITLED',
        languageCode: work.lang || 'EN',
        submitterWorkNumber: work.submitterWorkId || work.id || '',
        iswc: work.iswc || '',
        copyrightDate: dateOnly(work.copyrightDate),
        copyrightNumber: work.copyrightNumber || '',
        musicalWorkDistributionCategory: work.musicalWorkDistribution || 'POP',
        duration: secondsToHHMMSS(work.duration),
        recordedIndicator: work.recorded ? 'Y' : 'U',
        textMusicRelationship: work.textMusicRel || 'MTX',
        compositeType: work.composite || '',
        versionType: work.versionType || 'ORI',
        excerptType: work.excerptType || '',
        musicArrangement: work.musicArrangement || '',
        lyricAdaptation: work.lyricAdaptation || '',
        contactName: work.contactName || '',
        contactId: work.contactId || '',
        cwrWorkType: work.workType || 'PO',
        grandRights: !!work.grandRights,
        opusNumber: work.opusNumber || '',
        catalogueNumber: work.catalogueNumber || '',
        priorityFlag: work.priorityFlag || ' ',
      },
      publishers: (work.publishers || []).map((p) => ({
        recordType: p.controlled ? 'SPU' : 'OPU',
        sequenceNumber: p.sequence || 1,
        ipNumber: p.id || '',
        publisherName: p.name || '',
        publisherUnknown: !!p.unknown,
        publisherType: p.role || 'E',
        ipiNameNumber: p.ipiName || '',
        ipiBaseNumber: p.intStandardCode || '',
        prAffiliationSocietyNumber: p.prSocCode || '',
        prOwnershipShare: pct(p.prShare),
        mrAffiliationSocietyNumber: p.mrSocCode || '',
        mrOwnershipShare: pct(p.mrShare),
        srAffiliationSocietyNumber: p.srSocCode || '',
        srOwnershipShare: pct(p.srShare),
        agreementType: p.agreementType || '',
        territories: (p.territories || []).map((t) => ({
          recordType: p.controlled ? 'SPT' : 'OPT',
          tisNumericCode: t.tisNum || 2136,
          inclusionExclusionIndicator: t.inclusionFlag || 'I',
          prCollectionShare: pct(t.prShare),
          mrCollectionShare: pct(t.mrShare),
          srCollectionShare: pct(t.srShare),
          sequenceNumber: t.sequence || 1,
        })),
      })),
      writers: (work.writers || []).map((w) => ({
        recordType: w.controlled ? 'SWR' : 'OWR',
        ipNumber: w.id || '',
        writerLastName: w.lastName || w.name || '',
        writerFirstName: w.firstName || '',
        writerUnknown: !!w.unknown,
        writerDesignationCode: w.designation || 'CA',
        ipiNameNumber: w.ipiName || '',
        ipiBaseNumber: w.intStandardCode || '',
        prAffiliationSocietyNumber: w.prSocCode || '',
        prOwnershipShare: pct(w.prShare),
        mrAffiliationSocietyNumber: w.mrSocCode || '',
        mrOwnershipShare: pct(w.mrShare),
        srAffiliationSocietyNumber: w.srSocCode || '',
        srOwnershipShare: pct(w.srShare),
        reversionaryIndicator: w.reversionary || '',
        firstRecordingRefusalIndicator: !!w.firstRecording,
        workForHireIndicator: w.workForHire || '',
        territories: (w.territories || []).map((t) => ({
          recordType: w.controlled ? 'SWT' : 'OWT',
          tisNumericCode: t.tisNum || 2136,
          inclusionExclusionIndicator: t.inclusionFlag || 'I',
          prCollectionShare: pct(t.prShare),
          mrCollectionShare: pct(t.mrShare),
          srCollectionShare: pct(t.srShare),
        })),
        publisherForWriter: (w.pwrLinks || []).map((link) => ({
          recordType: 'PWR',
          publisherIpNumber: link.publisherId || '',
          publisherName: link.publisherName || '',
          submitterAgreementNumber: link.submitterAgreementNum || '',
          societyAssignedAgreementNumber: link.societyAgreementNum || '',
        })),
      })),
      alternateTitles: (work.alternateTitles || []).map((alt) => ({
        recordType: 'ALT', alternateTitle: alt.title || '',
        titleType: alt.titleType || 'AT', languageCode: alt.lang || 'EN',
      })),
      performers: (work.performers || []).map((p) => ({
        recordType: 'PER', performerLastName: p.lastName || '',
        performerFirstName: p.firstName || '', ipiNameNumber: p.ipiName || '',
      })),
      recordings: (work.recordings || []).map((r) => ({
        recordType: 'REC', firstReleaseDate: dateOnly(r.releaseDate),
        firstReleaseDuration: secondsToHHMMSS(r.releaseDuration),
        firstAlbumTitle: r.albumTitle || '', firstAlbumLabel: r.albumLabel || '',
        firstReleaseCatalogNumber: r.releaseCatalogNum || '',
        eanCode: r.eanUpc || '', isrc: r.isrc || '',
        recordingFormat: r.recFormat || '', recordingTechnique: r.recordingTechnique || '',
        mediaType: r.mediaType || '',
        recordingTitle: r.recordingTitle || '', versionTitle: r.versionTitle || '',
        displayArtist: r.displayArtist || '', recordLabel: r.recordLabel || '',
        isrcValidityIndicator: r.isrcValidity || '',
        submitterRecordingId: r.submitterRecordingId || '',
      })),
      origins: (work.origins || []).map((o) => ({
        recordType: 'ORN', intendedPurpose: o.intendedPurpose || 'COM',
        productionTitle: o.productionTitle || '', cdIdentifier: o.cdIdentifier || '',
        cutNumber: o.cutNumber || '', library: o.library || '',
        productionNumber: o.productionNum || '', episodeTitle: o.episodeTitle || '',
        productionYear: o.productionYear || '',
      })),
      crossReferences: (work.xrefs || []).map((x) => ({
        recordType: 'XRF', organisationCode: x.organisationCode || '',
        identifier: x.identifier || '', identifierType: x.identifierType || 'W',
        validityIndicator: x.validityIndicator || 'Y',
      })),
    };
  }

  function encodeAgr(agr) {
    return {
      type: 'AGR',
      agreement: {
        recordType: 'AGR',
        submitterAgreementNumber: agr.agreementNum || '',
        internationalStandardCode: agr.intStandardCode || '',
        agreementType: agr.agreementType || 'OS',
        agreementStartDate: dateOnly(agr.agreementStartDate),
        agreementEndDate: dateOnly(agr.agreementEndDate),
        retentionEndDate: dateOnly(agr.retentionEndDate),
        priorRoyaltyStatus: agr.priorRoyaltyStatus || 'N',
        postTermCollectionStatus: agr.postTermCollectionStatus || 'N',
        dateOfSignature: dateOnly(agr.dateOfSignatureOfAgreement),
        numberOfWorks: agr.numberOfWorks || 0,
        salesManufactureClause: agr.salesManufactureClause || '',
        sharesChangeIndicator: agr.sharesChange || 'N',
        advanceGivenIndicator: agr.advanceGiven || 'N',
        societyAssignedAgreementNumber: agr.societyAssignedAgreementNumber || '',
      },
      territories: (agr.territories || []).map((t) => ({
        recordType: 'TER', inclusionExclusionIndicator: t.inclusionFlag || 'I',
        tisNumericCode: t.tisNum || 2136,
      })),
      parties: (agr.parties || []).map((p) => ({
        recordType: 'IPA', agreementRoleCode: p.agreementRoleCode || 'AS',
        ipiNameNumber: p.ipiName || '', ipiBaseNumber: p.intStandardCode || '',
        ipNumber: p.ipNum || '', ipLastName: p.lastName || p.name || '',
        ipFirstName: p.firstName || '',
        prAffiliationSocietyNumber: p.prSocCode || '', prOwnershipShare: pct(p.prShare),
        mrAffiliationSocietyNumber: p.mrSocCode || '', mrOwnershipShare: pct(p.mrShare),
        srAffiliationSocietyNumber: p.srSocCode || '', srOwnershipShare: pct(p.srShare),
        nonRomanName: p.nonRomanName ? {
          recordType: 'NPA',
          ipNumber: p.nonRomanName.ipNum || p.ipNum || '',
          ipName: p.nonRomanName.ipName || '',
          ipFirstName: p.nonRomanName.ipFirstName || '',
          languageCode: p.nonRomanName.lang || 'EN',
        } : null,
      })),
    };
  }

  function countRecords(txn) {
    let n = 1; // root record
    if (txn.publishers) txn.publishers.forEach((p) => { n++; n += (p.territories || []).length; });
    if (txn.writers) txn.writers.forEach((w) => { n++; n += (w.territories || []).length; n += (w.publisherForWriter || []).length; });
    if (txn.alternateTitles) n += txn.alternateTitles.length;
    if (txn.performers) n += txn.performers.length;
    if (txn.recordings) n += txn.recordings.length;
    if (txn.origins) n += txn.origins.length;
    if (txn.crossReferences) n += txn.crossReferences.length;
    if (txn.territories) n += txn.territories.length;
    if (txn.parties) n += txn.parties.length;
    return n;
  }

  function dateOnly(d) {
    if (!d) return '';
    if (typeof d === 'string') {
      const cleaned = d.replace(/[^\d]/g, '');
      if (cleaned.length === 8) return cleaned.slice(0, 4) + '-' + cleaned.slice(4, 6) + '-' + cleaned.slice(6, 8);
      return d;
    }
    if (d instanceof Date) {
      return d.getUTCFullYear() + '-' + String(d.getUTCMonth() + 1).padStart(2, '0') + '-' + String(d.getUTCDate()).padStart(2, '0');
    }
    return '';
  }
  function timeOnly(d) {
    if (!d || !(d instanceof Date)) return '';
    return String(d.getUTCHours()).padStart(2, '0') + ':' + String(d.getUTCMinutes()).padStart(2, '0') + ':' + String(d.getUTCSeconds()).padStart(2, '0');
  }
  function secondsToHHMMSS(s) {
    const n = parseInt(s, 10) || 0;
    if (n === 0) return '00:00:00';
    // assume input is HHMMSS already if 6 digits
    const str = String(n);
    if (str.length === 6) return str.slice(0, 2) + ':' + str.slice(2, 4) + ':' + str.slice(4, 6);
    // else seconds
    const h = Math.floor(n / 3600); const m = Math.floor((n % 3600) / 60); const ss = n % 60;
    return String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') + ':' + String(ss).padStart(2, '0');
  }
  function pct(v) {
    if (v == null || v === '') return 0;
    const n = parseFloat(v);
    if (n > 1.5) return n; // already a percentage
    return n * 100;
  }

  window.CwrJson = { encode: encodeJson };

  // ─────────────────────────────────────────────────────────────────────────
  // COMPLIANCE TEST HARNESS
  // ─────────────────────────────────────────────────────────────────────────

  function runFullSuite() {
    const results = [];
    const test = (name, fn) => {
      try {
        const out = fn();
        results.push({ name, pass: out !== false, detail: typeof out === 'string' ? out : '' });
      } catch (e) {
        results.push({ name, pass: false, detail: e.message });
      }
    };

    // ─── Per-version structural tests ────────────────────────────────────
    ['2.1', '2.1r7', '2.2', '3.0', '3.1'].forEach((ver) => {
      test(`v${ver} · HDR exact length`, () => {
        const line = window.CwrBuild.HDR({ version: ver, senderType: 'PB', senderIpi: 199900001, senderName: 'TEST', created: new Date(2026, 4, 12, 14, 22, 33) });
        return line.length === window.CwrBuild.REC_LEN.HDR[ver] || `got ${line.length} expected ${window.CwrBuild.REC_LEN.HDR[ver]}`;
      });
      test(`v${ver} · HDR begins with 'HDR'`, () => {
        const line = window.CwrBuild.HDR({ version: ver, senderType: 'PB', senderIpi: 199900001, senderName: 'TEST', created: new Date() });
        return line.slice(0, 3) === 'HDR';
      });
      test(`v${ver} · HDR sender IPI at offset 5`, () => {
        const line = window.CwrBuild.HDR({ version: ver, senderType: 'PB', senderIpi: 199900001, senderName: 'TEST', created: new Date(2026, 4, 12, 14, 22, 33) });
        return line.slice(5, 14) === '199900001';
      });
      test(`v${ver} · GRH transaction type at offset 3`, () => {
        const line = window.CwrBuild.GRH({ version: ver }, 'NWR', 1);
        return line.slice(3, 6) === 'NWR';
      });
      test(`v${ver} · TRL exact length 23`, () => {
        const line = window.CwrBuild.TRL(1, 5, 100);
        return line.length === 23;
      });
      test(`v${ver} · GRT exact length 47`, () => {
        const line = window.CwrBuild.GRT(1, 5, 100);
        return line.length === 47;
      });
    });

    // ─── Per-record-type length tests ────────────────────────────────────
    const ctx = { version: '2.2', transactionType: 'NWR' };
    const dummies = {
      NWR: { title: 'X', id: 'W1', iswc: 'T-100.000.001-1' },
      SPU: { id: 'P1', name: 'TEST PUB', controlled: true, role: 'E', ipiName: '00000000000', prShare: 50 },
      SPT: { tisNum: 2136, prShare: 50 },
      OPU: { id: 'P2', name: 'OTHER', role: 'E', ipiName: '00000000000' },
      OPT: { tisNum: 2136 },
      SWR: { id: 'W1', lastName: 'DOE', firstName: 'JANE', designation: 'CA', ipiName: '00000000000' },
      SWT: { tisNum: 2136, prShare: 100 },
      OWR: { id: 'W2', lastName: 'SMITH', designation: 'C' },
      OWT: { tisNum: 2136 },
      PWR: { publisherId: 'P1', publisherName: 'TEST PUB', submitterAgreementNum: 'A1' },
      ALT: { title: 'ALT TITLE', titleType: 'AT', lang: 'EN' },
      EWT: { entireWorkTitle: 'PARENT', entireIswc: 'T-100.000.001-1' },
      VER: { originalTitle: 'ORIG' },
      PER: { lastName: 'PERFORMER', firstName: 'X' },
      REC: { releaseDate: '20240101', isrc: 'USRC12400001' },
      ORN: { intendedPurpose: 'COM', productionTitle: 'X' },
      INS: { numberOfVoices: '4', standardInstr: 'STR' },
      IND: { instrumentCode: 'GTR', numberOfPlayers: 1 },
      COM: { title: 'COMP', iswc: 'T-100.000.001-1' },
      MSG: { messageType: 'F', messageText: 'OK' },
      ARI: { societyNum: '010', typeOfRight: 'PER' },
      AGR: { agreementNum: 'AGR001', agreementType: 'OS' },
      TER: { inclusionFlag: 'I', tisNum: 2136 },
      IPA: { agreementRoleCode: 'AS', ipiName: '00000000000', lastName: 'PUB', firstName: '' },
      NPA: { ipNum: 'P1', ipName: '出版社', ipFirstName: '', lang: 'ZH' },
      NRN: { recordingTitle: 'RECT', isrc: 'USRC12400001', lang: 'EN' },
    };
    Object.keys(dummies).forEach((rt) => {
      ['2.1', '2.2', '3.0', '3.1'].forEach((ver) => {
        const expected = window.CwrBuild.REC_LEN[rt][ver];
        if (!expected) return; // skip rt+ver combos that don't exist (e.g. NRN in v2.x)
        test(`${rt} record · v${ver} length`, () => {
          const c = { ...ctx, version: ver };
          const fn = window.CwrBuild[rt];
          let line;
          if (rt === 'SPU' || rt === 'OPU') line = window.CwrBuild[rt](c, dummies[rt], 0, 0, 1);
          else if (rt === 'SPT' || rt === 'OPT') line = window.CwrBuild[rt](c, dummies[rt], 0, 0, 1);
          else if (rt === 'NWR' || rt === 'REV' || rt === 'ISW' || rt === 'EXC') line = window.CwrBuild.NWRish(rt, c, dummies.NWR, 0, 0);
          else if (rt === 'NPA' || rt === 'NRN') line = window.CwrBuild[rt](c, dummies[rt], 0, 0);
          else if (typeof fn === 'function') line = fn(c, dummies[rt], 0, 0);
          if (!line) return rt + ' returned null/undefined';
          return line.length === expected || `got ${line.length} expected ${expected}`;
        });
      });
    });

    // ─── Check-digit tests ──────────────────────────────────────────────
    test('IPI name mod-101 · valid sample', () => window.CwrValidate.checkIPIName('00000000000').ok);
    test('IPI name mod-101 · rejects bad check', () => !window.CwrValidate.checkIPIName('00000000099').ok);
    test('IPI base mod-11 · valid I00000000', () => {
      // Construct a valid IPI base
      const digits = '000000000';
      let sum = 0; for (let i = 0; i < 9; i++) sum += parseInt(digits[i], 10) * (10 - i);
      let c = sum % 11; if (c === 10) c = 0;
      return window.CwrValidate.checkIPIBase('I' + digits + c).ok;
    });
    test('ISWC check digit · T-034.524.680-1', () => window.CwrValidate.checkISWC('T-034.524.680-1').ok);
    test('ISWC check digit · rejects T-034.524.680-9', () => !window.CwrValidate.checkISWC('T-034.524.680-9').ok);
    test('ISRC format · valid USRC12400001', () => window.CwrValidate.checkISRC('USRC12400001').ok);
    test('ISRC format · rejects ABCD123', () => !window.CwrValidate.checkISRC('ABCD123').ok);

    // ─── Society / TIS lookups ─────────────────────────────────────────
    test('CISAC society lookup · 010 (ASCAP)', () => {
      const s = window.CwrValidate.lookupSociety('010');
      return s !== null && (s.cisac_code === '010' || s.cisac_code === 10 || String(s.cisac_code).padStart(3, '0') === '010');
    });
    test('TIS lookup · 840 (USA)', () => {
      const t = window.CwrValidate.lookupTIS(840);
      return t !== null;
    });
    test('TIS lookup · 2136 (World)', () => {
      const t = window.CwrValidate.lookupTIS(2136);
      return t !== null;
    });

    // ─── Share reconciliation ─────────────────────────────────────────
    test('Shares · 50/50 writer+publisher = 100%', () => {
      const issues = window.CwrValidate.reconcileShares({
        writers: [{ prShare: 50 }], publishers: [{ prShare: 50 }],
      });
      return issues.filter((i) => i.rule === 'shares.PR.total').length === 0;
    });
    test('Shares · 70/40 = 110% rejected', () => {
      const issues = window.CwrValidate.reconcileShares({
        writers: [{ prShare: 70 }], publishers: [{ prShare: 40 }],
      });
      return issues.filter((i) => i.rule === 'shares.PR.total').length > 0;
    });

    // ─── Full transmission round-trip ─────────────────────────────────
    test('Full transmission · v2.1 builds without error', () => {
      const lines = window.CwrBuild.buildTransmission({
        version: '2.1', senderIpi: 199900001, senderName: 'TEST',
        works: [{ id: 'W1', title: 'TEST WORK', writers: [{ id: 'WR1', controlled: true, lastName: 'DOE', firstName: 'J', designation: 'CA', ipiName: '00000000000', prShare: 50 }], publishers: [{ id: 'P1', controlled: true, name: 'TEST PUB', role: 'E', ipiName: '00000000000', prShare: 50 }] }],
      });
      return lines.length > 5 && lines[0].startsWith('HDR') && lines[lines.length - 1].startsWith('TRL');
    });
    test('Full transmission · v3.1 has 88-char HDR', () => {
      const lines = window.CwrBuild.buildTransmission({
        version: '3.1', senderIpi: 199900001, senderName: 'TEST',
        works: [{ id: 'W1', title: 'X', writers: [{ id: 'WR1', controlled: true, lastName: 'DOE', firstName: 'J', designation: 'CA', ipiName: '00000000000' }], publishers: [{ id: 'P1', controlled: true, name: 'P', role: 'E', ipiName: '00000000000' }] }],
      });
      return lines[0].length === 88;
    });
    test('Full transmission · AGR batch builds with TER+IPA', () => {
      const lines = window.CwrBuild.buildTransmission({
        version: '2.2', transactionType: 'AGR',
        senderIpi: 199900001, senderName: 'TEST',
        agreements: [{
          agreementNum: 'AGR001', agreementType: 'OS',
          territories: [{ inclusionFlag: 'I', tisNum: 840 }],
          parties: [
            { agreementRoleCode: 'AS', ipiName: '00000000000', lastName: 'ASSIGNOR', prShare: 100 },
            { agreementRoleCode: 'AC', ipiName: '00000000000', lastName: 'ACQUIRER' },
          ],
        }],
      });
      return lines.some((l) => l.startsWith('AGR')) && lines.some((l) => l.startsWith('TER')) && lines.some((l) => l.startsWith('IPA'));
    });
    test('Full transmission · TRL count matches actual records', () => {
      const lines = window.CwrBuild.buildTransmission({
        version: '2.2',
        works: [
          { id: 'W1', title: 'A', writers: [{ id: 'WR1', controlled: true, lastName: 'X', designation: 'CA' }], publishers: [{ id: 'P1', controlled: true, name: 'P', role: 'E' }] },
          { id: 'W2', title: 'B', writers: [{ id: 'WR2', controlled: true, lastName: 'Y', designation: 'CA' }], publishers: [{ id: 'P2', controlled: true, name: 'P', role: 'E' }] },
        ],
      });
      const trl = lines[lines.length - 1];
      const recCount = parseInt(trl.slice(16, 23), 10);
      return recCount === lines.length || `TRL says ${recCount}, file has ${lines.length}`;
    });

    // ─── Charset tests ────────────────────────────────────────────────
    test('Charset · v2.1 ASCII-only enforced', () => {
      const r = window.CwrValidate.checkASCII('SPU       TEST');
      return r.ok;
    });
    test('Charset · non-ASCII detected', () => {
      const r = window.CwrValidate.checkASCII('SPU       TÉST');
      return !r.ok;
    });

    // ─── JSON sibling tests ──────────────────────────────────────────
    test('CWR3 JSON · encodes work transmission', () => {
      const json = window.CwrJson.encode({
        version: '3.1',
        ctx: { version: '3.1', senderType: 'PB', senderIpi: 199900001, senderName: 'TEST', transactionType: 'NWR', created: new Date() },
        works: [{ id: 'W1', title: 'X', writers: [{ id: 'WR1', controlled: true, lastName: 'DOE' }], publishers: [{ id: 'P1', controlled: true, name: 'P' }] }],
      });
      return json.transmission && json.transmission.header && json.transmission.groups[0].transactions.length === 1;
    });
    test('CWR3 JSON · agreement encode', () => {
      const json = window.CwrJson.encode({
        version: '3.1',
        ctx: { version: '3.1', transactionType: 'AGR', senderName: 'TEST', senderIpi: 1, created: new Date() },
        agreements: [{ agreementNum: 'A1', territories: [{ tisNum: 840 }], parties: [{ agreementRoleCode: 'AS' }] }],
      });
      return json.transmission.groups[0].transactions[0].agreement.submitterAgreementNumber === 'A1';
    });

    return {
      results,
      summary: {
        total: results.length,
        passed: results.filter((r) => r.pass).length,
        failed: results.filter((r) => !r.pass).length,
      },
    };
  }

  window.CwrTest = { runFullSuite };
})();
