/**
 * ICD Test Runner — CRUD: Workflows
 * Admin workflow lifecycle (create → stages → activate → publish →
 * start instance → advance → reject → delete), visitor E2E,
 * assignment claim/complete lifecycle.
 */
(function () {
  'use strict';
  var T = window.ICD;
  if (!T) return;
  if (!T.crud) T.crud = {};

  T.crud.workflows = async function (testTag) {

    // ── Workflows CRUD (admin — requires workflow.create permission) ──
    if (T.user.role === 'admin') {
      var wfId = null;
      var wfSlug = testTag.toLowerCase().replace(/[^a-z0-9-]/g, '-');

      await T.test('crud', 'workflows', 'POST /workflows (create)', async function () {
        var d = await T.apiPost('/workflows', {
          name: testTag + '-workflow',
          slug: wfSlug,
          description: 'ICD integration test workflow',
          entry_mode: 'team_only',
          branding: { accent_color: '#2563eb', tagline: 'ICD Test' },
          retention: { mode: 'archive', delete_after_days: 1 }
        });
        T.assertShape(d, T.S.workflow, 'workflow');
        T.assert(d.slug === wfSlug, 'slug mismatch: expected ' + wfSlug + ', got ' + d.slug);
        wfId = d.id;
        T.registerCleanup(function () { if (wfId) return T.safeDelete('/workflows/' + wfId); });
      });

      if (wfId) {
        await T.test('crud', 'workflows', 'GET /workflows/:id (read)', async function () {
          var d = await T.apiGet('/workflows/' + wfId);
          T.assertShape(d, T.S.workflow, 'workflow');
          T.assert(d.id === wfId, 'id mismatch');
          T.assert(d.name.indexOf(testTag) !== -1, 'name mismatch');
        });

        await T.test('crud', 'workflows', 'PATCH /workflows/:id (update)', async function () {
          var d = await T.apiPatch('/workflows/' + wfId, {
            description: 'Updated by ICD test runner',
            webhook_url: 'https://example.com/hook'
          });
          T.assert(typeof d === 'object', 'expected object');
        });

        // ── Stages ──
        var stageIds = [];

        await T.test('crud', 'workflows', 'POST /workflows/:id/stages (create #1)', async function () {
          var d = await T.apiPost('/workflows/' + wfId + '/stages', {
            name: 'Intake',
            ordinal: 0,
            persona_id: null,
            form_template: { fields: ['name', 'email'] },
            history_mode: 'full',
            auto_transition: false,
            transition_rules: {}
          });
          T.assertShape(d, T.S.workflowStage, 'stage');
          T.assert(d.name === 'Intake', 'name mismatch');
          stageIds.push(d.id);
        });

        await T.test('crud', 'workflows', 'POST /workflows/:id/stages (create #2)', async function () {
          var d = await T.apiPost('/workflows/' + wfId + '/stages', {
            name: 'Review',
            ordinal: 1,
            persona_id: null,
            form_template: {},
            history_mode: 'summary',
            auto_transition: false,
            transition_rules: {}
          });
          T.assertShape(d, T.S.workflowStage, 'stage');
          stageIds.push(d.id);
        });

        await T.test('crud', 'workflows', 'GET /workflows/:id/stages (list)', async function () {
          var d = await T.apiGet('/workflows/' + wfId + '/stages');
          T.assertHasKey(d, 'data', '/stages');
          T.assert(Array.isArray(d.data), 'data should be array');
          T.assert(d.data.length >= 2, 'expected at least 2 stages, got ' + d.data.length);
          T.assertShape(d.data[0], T.S.workflowStage, 'stage[0]');
        });

        await T.test('crud', 'workflows', 'PUT /workflows/:id/stages/:sid (update)', async function () {
          var d = await T.apiPut('/workflows/' + wfId + '/stages/' + stageIds[0], {
            name: 'Intake (updated)',
            ordinal: 0,
            persona_id: null,
            form_template: { fields: ['name', 'email', 'phone'] },
            history_mode: 'full',
            auto_transition: false,
            transition_rules: {}
          });
          T.assert(typeof d === 'object', 'expected object');
        });

        if (stageIds.length >= 2) {
          await T.test('crud', 'workflows', 'PATCH /workflows/:id/stages/reorder', async function () {
            var reversed = stageIds.slice().reverse();
            var d = await T.apiPatch('/workflows/' + wfId + '/stages/reorder', {
              ordered_ids: reversed
            });
            T.assert(typeof d === 'object', 'expected object');
          });

          // Restore original order for subsequent tests
          await T.apiPatch('/workflows/' + wfId + '/stages/reorder', { ordered_ids: stageIds });
        }

        // ── Activate workflow (required before starting instances) ──
        await T.test('crud', 'workflows', 'PATCH /workflows/:id (activate)', async function () {
          var d = await T.apiPatch('/workflows/' + wfId, { is_active: true });
          T.assert(typeof d === 'object', 'expected object');
        });

        // ── Publish version ──
        var versionNum = null;

        await T.test('crud', 'workflows', 'POST /workflows/:id/publish', async function () {
          var d = await T.apiPost('/workflows/' + wfId + '/publish', {});
          T.assertShape(d, T.S.workflowVersion, 'version');
          T.assertHasKey(d, 'snapshot', 'version');
          versionNum = d.version_number;
        });

        if (versionNum !== null) {
          await T.test('crud', 'workflows', 'GET /workflows/:id/versions/:v (read)', async function () {
            var d = await T.apiGet('/workflows/' + wfId + '/versions/' + versionNum);
            T.assertShape(d, T.S.workflowVersion, 'version');
            T.assert(d.version_number === versionNum, 'version mismatch');
            T.assertHasKey(d, 'snapshot', 'version');
          });

          await T.test('crud', 'workflows', 'POST /workflows/:id/publish (409 duplicate)', async function () {
            try {
              await T.apiPost('/workflows/' + wfId + '/publish', {});
              throw new Error('expected 409 conflict, got success');
            } catch (e) {
              // 409 is the expected result
              if (e.message.indexOf('409') === -1 && e.message.indexOf('conflict') === -1 &&
                  e.message.indexOf('already') === -1 && e.message.indexOf('expected 409') !== -1)
                throw e;
              // Otherwise: got the expected rejection
            }
          });
        }

        // ── Start instance → status → advance → reject ──
        var instanceChannelId = null;

        await T.test('crud', 'workflows', 'POST /workflows/:id/start (instance)', async function () {
          var d = await T.apiPost('/workflows/' + wfId + '/start', {});
          // Response may be: direct channel { id }, or { channel_id }, or { channel: { id } }, or { data: { id } }
          var cid = d.id || d.channel_id || (d.channel && d.channel.id) || (d.data && d.data.id);
          T.assert(cid, 'no channel ID in start response, got keys: ' + Object.keys(d).join(', '));
          instanceChannelId = cid;
          T.registerCleanup(function () { if (instanceChannelId) return T.safeDelete('/channels/' + instanceChannelId); });
        });

        if (instanceChannelId) {
          await T.test('crud', 'workflows', 'GET /channels/:id/workflow/status', async function () {
            var d = await T.apiGet('/channels/' + instanceChannelId + '/workflow/status');
            T.assertShape(d, T.S.workflowStatus, 'status');
            T.assert(d.current_stage === 0, 'expected stage 0, got ' + d.current_stage);
            T.assert(d.status === 'active', 'expected active, got ' + d.status);
          });

          await T.test('crud', 'workflows', 'POST /channels/:id/workflow/advance', async function () {
            var d = await T.apiPost('/channels/' + instanceChannelId + '/workflow/advance', {
              data: { name: 'ICD Test', email: 'test@icd.local' }
            });
            T.assert(typeof d === 'object', 'expected object');
          });

          await T.test('crud', 'workflows', 'GET workflow/status (after advance)', async function () {
            var d = await T.apiGet('/channels/' + instanceChannelId + '/workflow/status');
            T.assertShape(d, T.S.workflowStatus, 'status');
            // Should be at stage 1 (or completed if only 2 stages and auto-completed)
            T.assert(d.current_stage >= 1 || d.status === 'completed',
              'expected stage >= 1 or completed, got stage=' + d.current_stage + ' status=' + d.status);
          });

          // Reject back to stage 0 (only if still active and not at stage 0)
          await T.test('crud', 'workflows', 'POST /channels/:id/workflow/reject', async function () {
            var st = await T.apiGet('/channels/' + instanceChannelId + '/workflow/status');
            if (st.status === 'completed' || st.current_stage === 0) {
              // Can't reject from stage 0 or completed — skip gracefully
              return;
            }
            var d = await T.apiPost('/channels/' + instanceChannelId + '/workflow/reject', {
              reason: 'ICD test: rejecting back to previous stage'
            });
            T.assert(typeof d === 'object', 'expected object');
          });

          // Verify reject went back
          await T.test('crud', 'workflows', 'GET workflow/status (after reject)', async function () {
            var d = await T.apiGet('/channels/' + instanceChannelId + '/workflow/status');
            T.assertShape(d, T.S.workflowStatus, 'status');
            // Should be back at 0 or still active
            T.assert(d.status === 'active' || d.status === 'completed',
              'expected active or completed, got ' + d.status);
          });

          // Clean up instance channel before workflow delete (cascade may do it, but be explicit)
          await T.test('crud', 'workflows', 'DELETE instance channel', async function () {
            await T.safeDelete('/channels/' + instanceChannelId);
            instanceChannelId = null;
          });
        }

        // ── Stage delete ──
        if (stageIds.length >= 2) {
          await T.test('crud', 'workflows', 'DELETE /workflows/:id/stages/:sid', async function () {
            await T.safeDelete('/workflows/' + wfId + '/stages/' + stageIds[stageIds.length - 1]);
            stageIds.pop();
          });
        }

        // ── Workflow delete ──
        await T.test('crud', 'workflows', 'DELETE /workflows/:id', async function () {
          await T.safeDelete('/workflows/' + wfId);
          wfId = null;
        });
      }
    }

    // ── Visitor Workflow E2E ──────────────────────────────────
    // Tests the anonymous session flow: create public_link workflow → visitor
    // starts via entry API (no JWT) → sends messages via session cookie.
    await (async function () {
      var vwfId = null;
      var vwfSlug = testTag + '-visitor-wf';
      var visitorChannelId = null;

      await T.test('crud', 'workflows', 'POST visitor workflow (public_link)', async function () {
        var d = await T.apiPost('/workflows', {
          name: testTag + ' Visitor WF',
          slug: vwfSlug,
          entry_mode: 'public_link'
        });
        T.assertShape(d, T.S.workflow, 'visitor workflow');
        vwfId = d.id;
        T.registerCleanup(function () { if (vwfId) return T.safeDelete('/workflows/' + vwfId); });
      });

      if (vwfId) {
        await T.test('crud', 'workflows', 'POST visitor workflow stage', async function () {
          await T.apiPost('/workflows/' + vwfId + '/stages', {
            name: 'Intake', ordinal: 0, history_mode: 'full'
          });
        });

        await T.test('crud', 'workflows', 'PATCH+publish visitor workflow', async function () {
          await T.apiPatch('/workflows/' + vwfId, { is_active: true });
          var pub = await T.apiPost('/workflows/' + vwfId + '/publish', {});
          T.assert(pub._status === undefined || pub._status === 201 || pub.version_number,
            'publish failed');
        });

        await T.test('crud', 'workflows', 'POST /workflow-entry (visitor start, no auth)', async function () {
          var d = await T.publicPost('/workflow-entry/global/' + vwfSlug, {});
          T.assert(d._status === 201, 'expected 201, got ' + d._status);
          T.assertHasKey(d, 'channel_id', 'visitor start');
          T.assertHasKey(d, 'session_id', 'visitor start');
          T.assertHasKey(d, 'redirect_to', 'visitor start');
          visitorChannelId = d.channel_id;
          T.registerCleanup(function () {
            if (visitorChannelId) return T.safeDelete('/channels/' + visitorChannelId);
          });
        });

        if (visitorChannelId) {
          await T.test('crud', 'workflows', 'POST /w/:id/messages (visitor session)', async function () {
            var d = await T.sessionPost('/w/' + visitorChannelId + '/messages', {
              content: 'Hello from visitor test',
              role: 'user'
            });
            T.assert(d._status === 200 || d._status === 201, 'visitor message: got ' + d._status);
          });

          await T.test('crud', 'workflows', 'GET /w/:id/messages (visitor session)', async function () {
            var d = await T.sessionGet('/w/' + visitorChannelId + '/messages');
            T.assert(d._status === 200, 'visitor list messages: got ' + d._status);
            var msgs = d.data || d.messages || d;
            T.assert(Array.isArray(msgs), 'expected messages array');
            T.assert(msgs.length > 0, 'no messages returned for visitor channel');
          });

          await T.test('crud', 'workflows', 'GET workflow/status (visitor instance)', async function () {
            var d = await T.apiGet('/channels/' + visitorChannelId + '/workflow/status');
            T.assertShape(d, T.S.workflowStatus, 'visitor workflow status');
            T.assert(d.status === 'active', 'visitor workflow should be active');
          });
        }

        await T.test('crud', 'workflows', 'DELETE visitor workflow', async function () {
          if (visitorChannelId) { await T.safeDelete('/channels/' + visitorChannelId); visitorChannelId = null; }
          if (vwfId) { await T.safeDelete('/workflows/' + vwfId); vwfId = null; }
        });
      }
    })();

    // ── Assignment Claim / Complete ────────────────────────────
    // Tests the full assignment lifecycle: advance creates assignment →
    // claim → double-claim 409 → complete → double-complete 409.
    await (async function () {
      var awfId = null;
      var aTeamId = null;
      var aChannelId = null;
      var assignmentId = null;

      await T.test('crud', 'workflows', 'assignment: create team + workflow', async function () {
        var team = await T.apiPost('/admin/teams', {
          name: testTag + '-assign-team', description: 'Assignment test'
        });
        aTeamId = team.id;
        T.registerCleanup(function () { if (aTeamId) return T.safeDelete('/admin/teams/' + aTeamId); });

        try { await T.apiPost('/admin/teams/' + aTeamId + '/members', { user_id: T.user.id, role: 'admin' }); }
        catch (e) { /* auto-add */ }

        var wf = await T.apiPost('/workflows', {
          name: testTag + '-assign-wf', entry_mode: 'team_only'
        });
        awfId = wf.id;
        T.registerCleanup(function () { if (awfId) return T.safeDelete('/workflows/' + awfId); });

        await T.apiPost('/workflows/' + awfId + '/stages', {
          name: 'Collect', ordinal: 0, history_mode: 'full'
        });
        await T.apiPost('/workflows/' + awfId + '/stages', {
          name: 'Review', ordinal: 1, history_mode: 'full',
          assignment_team_id: aTeamId
        });

        await T.apiPatch('/workflows/' + awfId, { is_active: true });
        await T.apiPost('/workflows/' + awfId + '/publish', {});
      });

      if (awfId) {
        await T.test('crud', 'workflows', 'assignment: start + advance to review stage', async function () {
          var inst = await T.apiPost('/workflows/' + awfId + '/start', {});
          T.assertHasKey(inst, 'channel_id', 'instance start');
          aChannelId = inst.channel_id;
          T.registerCleanup(function () { if (aChannelId) return T.safeDelete('/channels/' + aChannelId); });

          var adv = await T.apiPost('/channels/' + aChannelId + '/workflow/advance', {
            data: { intake: 'done' }
          });
          T.assert(adv.current_stage === 1, 'should be at stage 1 after advance');
        });

        await T.test('crud', 'workflows', 'assignment: GET /workflow-assignments/mine', async function () {
          var d = await T.apiGet('/workflow-assignments/mine');
          var arr = d.data || [];
          T.assert(arr.length > 0, 'expected at least 1 assignment for my teams');
          var ours = arr.find(function (a) { return a.team_id === aTeamId; });
          T.assert(ours, 'no assignment found for test team');
          assignmentId = ours.id;
        });

        if (assignmentId) {
          await T.test('crud', 'workflows', 'assignment: POST claim', async function () {
            var d = await T.apiPost('/workflow-assignments/' + assignmentId + '/claim', {});
            T.assert(d.claimed === true, 'claim should return claimed=true');
          });

          await T.test('crud', 'workflows', 'assignment: POST claim (duplicate → 409)', async function () {
            var d = await T.authFetch(await T.getAuthToken(), 'POST',
              '/workflow-assignments/' + assignmentId + '/claim', {});
            T.assert(d._status === 409, 'double claim should return 409, got ' + d._status);
          });

          await T.test('crud', 'workflows', 'assignment: POST complete', async function () {
            var d = await T.apiPost('/workflow-assignments/' + assignmentId + '/complete', {});
            T.assert(d.completed === true, 'complete should return completed=true');
          });

          await T.test('crud', 'workflows', 'assignment: POST complete (duplicate → 409)', async function () {
            var d = await T.authFetch(await T.getAuthToken(), 'POST',
              '/workflow-assignments/' + assignmentId + '/complete', {});
            T.assert(d._status === 409, 'double complete should return 409, got ' + d._status);
          });
        }

        await T.test('crud', 'workflows', 'assignment: cleanup', async function () {
          if (aChannelId) { await T.safeDelete('/channels/' + aChannelId); aChannelId = null; }
          if (awfId) { await T.safeDelete('/workflows/' + awfId); awfId = null; }
          if (aTeamId) { await T.safeDelete('/admin/teams/' + aTeamId); aTeamId = null; }
        });
      }
    })();

    // ── Form Submission Lifecycle (v0.29.3) ──────────────────
    // Tests typed form_template with stage_mode, form validation,
    // and the /w/:id/form-submit endpoint.
    if (T.user.role === 'admin') {
      await (async function () {
        var fwfId = null;
        var fwfSlug = testTag + '-form-wf';
        var fChannelId = null;

        await T.test('crud', 'workflows', 'form: create form_only workflow', async function () {
          var wf = await T.apiPost('/workflows', {
            name: testTag + ' Form WF',
            slug: fwfSlug,
            entry_mode: 'public_link'
          });
          T.assertShape(wf, T.S.workflow, 'form workflow');
          fwfId = wf.id;
          T.registerCleanup(function () { if (fwfId) return T.safeDelete('/workflows/' + fwfId); });

          await T.apiPost('/workflows/' + fwfId + '/stages', {
            name: 'Contact Info',
            ordinal: 0,
            history_mode: 'full',
            stage_mode: 'form_only',
            form_template: {
              fields: [
                { key: 'name', type: 'text', label: 'Full Name', required: true, validation: { min_length: 2 } },
                { key: 'email', type: 'email', label: 'Email', required: true },
                { key: 'dept', type: 'select', label: 'Department', options: [
                  { value: 'eng', label: 'Engineering' },
                  { value: 'sales', label: 'Sales' }
                ]}
              ]
            }
          });

          await T.apiPost('/workflows/' + fwfId + '/stages', {
            name: 'Done', ordinal: 1, history_mode: 'full', stage_mode: 'chat_only'
          });

          await T.apiPatch('/workflows/' + fwfId, { is_active: true });
          await T.apiPost('/workflows/' + fwfId + '/publish', {});
        });

        if (fwfId) {
          await T.test('crud', 'workflows', 'form: start instance', async function () {
            var inst = await T.apiPost('/workflows/' + fwfId + '/start', {});
            fChannelId = inst.channel_id || inst.id;
            T.assert(fChannelId, 'no channel_id in form start');
            T.registerCleanup(function () { if (fChannelId) return T.safeDelete('/channels/' + fChannelId); });
          });

          if (fChannelId) {
            await T.test('crud', 'workflows', 'form: GET /w/:id/form (template)', async function () {
              var d = await T.sessionGet('/w/' + fChannelId + '/form');
              T.assert(d._status === 200, 'expected 200, got ' + d._status);
              T.assertHasKey(d, 'stage_mode', 'form response');
              T.assert(d.stage_mode === 'form_only', 'expected form_only, got ' + d.stage_mode);
              T.assertHasKey(d, 'form_template', 'form response');
              T.assert(d.form_template.fields && d.form_template.fields.length === 3,
                'expected 3 fields, got ' + (d.form_template.fields ? d.form_template.fields.length : 0));
            });

            await T.test('crud', 'workflows', 'form: submit missing required → 400', async function () {
              var d = await T.sessionPost('/w/' + fChannelId + '/form-submit', {
                dept: 'eng'
              });
              T.assert(d._status === 400, 'expected 400, got ' + d._status);
              T.assertHasKey(d, 'errors', 'validation response');
              T.assert(Array.isArray(d.errors) && d.errors.length > 0, 'expected field errors');
            });

            await T.test('crud', 'workflows', 'form: submit invalid email → 400', async function () {
              var d = await T.sessionPost('/w/' + fChannelId + '/form-submit', {
                name: 'Test User',
                email: 'not-an-email'
              });
              T.assert(d._status === 400, 'expected 400, got ' + d._status);
              T.assertHasKey(d, 'errors', 'validation response');
              var emailErr = d.errors.find(function (e) { return e.key === 'email'; });
              T.assert(emailErr, 'expected email validation error');
            });

            await T.test('crud', 'workflows', 'form: submit valid → 200', async function () {
              var d = await T.sessionPost('/w/' + fChannelId + '/form-submit', {
                name: 'ICD Test User',
                email: 'test@icd.local',
                dept: 'eng'
              });
              T.assert(d._status === 200, 'expected 200, got ' + d._status);
            });

            await T.test('crud', 'workflows', 'form: verify stage_data after submit', async function () {
              var d = await T.apiGet('/channels/' + fChannelId + '/workflow/status');
              T.assertShape(d, T.S.workflowStatus, 'status');
              // stage_data should contain submitted form values
              if (d.stage_data) {
                var sd = typeof d.stage_data === 'string' ? JSON.parse(d.stage_data) : d.stage_data;
                T.assert(sd.name === 'ICD Test User' || sd.form_name === 'ICD Test User',
                  'stage_data should contain form values');
              }
            });
          }

          await T.test('crud', 'workflows', 'form: cleanup', async function () {
            if (fChannelId) { await T.safeDelete('/channels/' + fChannelId); fChannelId = null; }
            if (fwfId) { await T.safeDelete('/workflows/' + fwfId); fwfId = null; }
          });
        }
      })();
    }

    // ── Cross-Visitor Isolation (v0.29.3) ─────────────────────
    // Two instances of the same workflow should have isolated stage_data
    // and one visitor cannot access the other's form endpoint.
    if (T.user.role === 'admin') {
      await (async function () {
        var xwfId = null;
        var xwfSlug = testTag + '-xvisitor';
        var chA = null, chB = null;

        await T.test('crud', 'workflows', 'xvisitor: setup form_only workflow', async function () {
          var wf = await T.apiPost('/workflows', {
            name: testTag + ' XVisitor',
            slug: xwfSlug,
            entry_mode: 'public_link'
          });
          xwfId = wf.id;
          T.registerCleanup(function () { if (xwfId) return T.safeDelete('/workflows/' + xwfId); });

          await T.apiPost('/workflows/' + xwfId + '/stages', {
            name: 'Form', ordinal: 0, stage_mode: 'form_only', history_mode: 'full',
            form_template: {
              fields: [{ key: 'name', type: 'text', label: 'Name', required: true }]
            }
          });
          await T.apiPost('/workflows/' + xwfId + '/stages', {
            name: 'Done', ordinal: 1, history_mode: 'full'
          });
          await T.apiPatch('/workflows/' + xwfId, { is_active: true });
          await T.apiPost('/workflows/' + xwfId + '/publish', {});
        });

        if (xwfId) {
          await T.test('crud', 'workflows', 'xvisitor: start two instances', async function () {
            var a = await T.apiPost('/workflows/' + xwfId + '/start', {});
            chA = a.channel_id || a.id;
            T.assert(chA, 'no channel A');
            T.registerCleanup(function () { if (chA) return T.safeDelete('/channels/' + chA); });

            var b = await T.apiPost('/workflows/' + xwfId + '/start', {});
            chB = b.channel_id || b.id;
            T.assert(chB, 'no channel B');
            T.registerCleanup(function () { if (chB) return T.safeDelete('/channels/' + chB); });

            T.assert(chA !== chB, 'channels should be different');
          });

          if (chA && chB) {
            await T.test('crud', 'workflows', 'xvisitor: submit different data to each', async function () {
              // Submit to A (admin token)
              var dA = await T.apiPost('/channels/' + chA + '/workflow/advance', {
                data: { name: 'Alice' }
              });
              T.assert(typeof dA === 'object', 'advance A');

              // Submit to B
              var dB = await T.apiPost('/channels/' + chB + '/workflow/advance', {
                data: { name: 'Bob' }
              });
              T.assert(typeof dB === 'object', 'advance B');
            });

            await T.test('crud', 'workflows', 'xvisitor: verify stage_data isolated', async function () {
              var stA = await T.apiGet('/channels/' + chA + '/workflow/status');
              var stB = await T.apiGet('/channels/' + chB + '/workflow/status');

              // Parse stage_data
              var sdA = stA.stage_data;
              if (typeof sdA === 'string') sdA = JSON.parse(sdA);
              var sdB = stB.stage_data;
              if (typeof sdB === 'string') sdB = JSON.parse(sdB);

              // A should have Alice, B should have Bob
              var aName = sdA && (sdA.name || sdA.form_name);
              var bName = sdB && (sdB.name || sdB.form_name);
              T.assert(aName === 'Alice', 'channel A stage_data should have Alice, got ' + aName);
              T.assert(bName === 'Bob', 'channel B stage_data should have Bob, got ' + bName);
            });
          }

          await T.test('crud', 'workflows', 'xvisitor: cleanup', async function () {
            if (chA) { await T.safeDelete('/channels/' + chA); chA = null; }
            if (chB) { await T.safeDelete('/channels/' + chB); chB = null; }
            if (xwfId) { await T.safeDelete('/workflows/' + xwfId); xwfId = null; }
          });
        }
      })();
    }

    // ── Workflow Package Export/Import (v0.30.2) ──
    if (T.user.role === 'admin') {
      await (async function () {
        var wpWfId = null;
        var wpStage1Id = null;
        var wpStage2Id = null;
        var wpPkgId = null;
        var wpSlug = testTag.toLowerCase().replace(/[^a-z0-9-]/g, '-') + '-wp';

        await T.test('crud', 'workflows', 'wfpkg: create workflow for export', async function () {
          var d = await T.apiPost('/workflows', {
            name: testTag + '-wfpkg',
            slug: wpSlug,
            description: 'Workflow package export/import test',
            entry_mode: 'public_link'
          });
          T.assertShape(d, T.S.workflow, 'workflow');
          wpWfId = d.id;
          T.registerCleanup(function () {
            if (wpWfId) return T.safeDelete('/workflows/' + wpWfId);
          });
        });

        if (wpWfId) {
          await T.test('crud', 'workflows', 'wfpkg: add stages with form_template', async function () {
            var s1 = await T.apiPost('/workflows/' + wpWfId + '/stages', {
              name: 'Intake',
              ordinal: 0,
              stage_mode: 'form_only',
              history_mode: 'full',
              form_template: { fields: [{ key: 'name', type: 'text', label: 'Full Name', required: true }] }
            });
            T.assertShape(s1, T.S.workflowStage, 'stage1');
            wpStage1Id = s1.id;

            var s2 = await T.apiPost('/workflows/' + wpWfId + '/stages', {
              name: 'Review',
              ordinal: 1,
              stage_mode: 'review',
              history_mode: 'summary'
            });
            T.assertShape(s2, T.S.workflowStage, 'stage2');
            wpStage2Id = s2.id;
          });

          // Test surface_pkg_id round-trip
          await T.test('crud', 'workflows', 'wfpkg: surface_pkg_id persists on update', async function () {
            // Stage update is PUT (full replace), so send all required fields.
            // Use the ICD test runner's own package ID (always installed when tests run).
            var realPkgId = 'icd-test-runner';
            var stageBase = {
              name: 'Intake', ordinal: 0, stage_mode: 'form_only', history_mode: 'full',
              form_template: { fields: [{ key: 'name', type: 'text', label: 'Full Name', required: true }] }
            };

            // Set surface_pkg_id
            var withPkg = Object.assign({}, stageBase, { surface_pkg_id: realPkgId });
            var updated = await T.apiPut('/workflows/' + wpWfId + '/stages/' + wpStage1Id, withPkg);
            T.assert(updated.surface_pkg_id === realPkgId,
              'surface_pkg_id should persist, got: ' + updated.surface_pkg_id);

            // Clear it
            var cleared = await T.apiPut('/workflows/' + wpWfId + '/stages/' + wpStage1Id, stageBase);
            T.assert(!cleared.surface_pkg_id,
              'surface_pkg_id should be cleared, got: ' + cleared.surface_pkg_id);
          });

          // Export workflow as .pkg
          var exportBlob = null;
          await T.test('crud', 'workflows', 'wfpkg: GET export .pkg', async function () {
            var token = await T.getAuthToken();
            var resp = await fetch(T.base + '/api/v1/admin/workflows/' + wpWfId + '/export', {
              headers: { 'Authorization': 'Bearer ' + token }
            });
            T.assert(resp.ok, 'export should return 200, got ' + resp.status);
            var ct = resp.headers.get('content-type') || '';
            T.assert(ct.indexOf('zip') !== -1 || ct.indexOf('octet') !== -1,
              'content-type should be zip/octet, got ' + ct);
            exportBlob = await resp.blob();
            T.assert(exportBlob.size > 0, 'export blob should not be empty');
          });

          // Delete original workflow before import to test clean install
          await T.test('crud', 'workflows', 'wfpkg: delete original workflow', async function () {
            await T.apiDelete('/workflows/' + wpWfId);
            wpWfId = null;
          });

          // Import the .pkg
          if (exportBlob) {
            await T.test('crud', 'workflows', 'wfpkg: POST install .pkg (re-import)', async function () {
              var token = await T.getAuthToken();
              var formData = new FormData();
              formData.append('file', exportBlob, 'test-workflow.pkg');
              var resp = await fetch(T.base + '/api/v1/admin/packages/install', {
                method: 'POST',
                headers: { 'Authorization': 'Bearer ' + token },
                body: formData
              });
              var result = await resp.json();
              T.assert(resp.ok, 'install should return 200, got ' + resp.status + ': ' + (result.error || ''));
              T.assert(result.id, 'install should return package id');
              wpPkgId = result.id;
              T.registerCleanup(function () {
                if (wpPkgId) return T.safeDelete('/admin/packages/' + wpPkgId);
              });
            });

            // Verify the workflow was recreated
            await T.test('crud', 'workflows', 'wfpkg: verify imported workflow has stages', async function () {
              var wfs = await T.apiGet('/workflows');
              var list = wfs.data || wfs || [];
              var found = list.find(function (w) { return w.slug === wpSlug; });
              T.assert(found, 'imported workflow with slug ' + wpSlug + ' should exist');
              wpWfId = found.id;

              var stages = await T.apiGet('/workflows/' + wpWfId + '/stages');
              var stList = stages.data || stages || [];
              T.assert(stList.length === 2, 'should have 2 stages, got ' + stList.length);
              T.assert(stList[0].name === 'Intake', 'stage 0 name should be Intake');
              T.assert(stList[1].name === 'Review', 'stage 1 name should be Review');
              T.assert(stList[0].stage_mode === 'form_only', 'stage 0 mode should be form_only');
              T.assert(stList[1].stage_mode === 'review', 'stage 1 mode should be review');
            });
          }

          // Cleanup
          await T.test('crud', 'workflows', 'wfpkg: cleanup', async function () {
            if (wpPkgId) { await T.safeDelete('/admin/packages/' + wpPkgId); wpPkgId = null; }
            if (wpWfId) { await T.safeDelete('/workflows/' + wpWfId); wpWfId = null; }
          });
        }
      })();
    }

  };
})();
