/**
 * Phase 2 dispatch integration tests — Tests #46-78
 *
 * Tests that the membrane is correctly wired into MatrixActor's dispatchEnvelope.
 * Uses InMemoryTransport directly (no MatrixRuntime — avoids HTMLElement dependency).
 */
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { MatrixActor } from '../../MatrixActor.js';
import { MatrixContext } from '../../../engine/core/MatrixContext.js';
import { InMemoryBroker } from '../../../transport/InMemoryBroker.js';
import { InMemoryTransport } from '../../../transport/InMemoryTransport.js';
import { TopicRouter } from '../../../engine/messaging/TopicRouter.js';
import type { IMatrixContext } from '../../../engine/core/IMatrixContext.js';
import type { IMatrixRuntime } from '../../../engine/core/IMatrixRuntime.js';
import type { MxEnvelope } from '../../../engine/messaging/MxEnvelope.js';

// ─── Test helper ─────────────────────────────────────────────────────────────

const tick = (ms = 50) => new Promise(r => setTimeout(r, ms));

/** Minimal IMatrixRuntime stub — tests don't need createChild/createElement */
function mockRuntime(): IMatrixRuntime {
  return {
    type: 'headless',
    supportsDom: false,
    createChild: async () => ({ mount: 'mock', created: true, reused: false }),
    createElement: async () => null as unknown,
    mount: async () => {},
    unmount: async () => {},
    remove: async () => {},
    createMailbox: () => null as unknown,
    shutdown: async () => {},
  } as unknown as IMatrixRuntime;
}

/** Build a root context backed by InMemoryTransport */
function buildContext(broker: InMemoryBroker): IMatrixContext {
  const transport = new InMemoryTransport(broker, { name: 'test' });
  return new MatrixContext('', transport, undefined, mockRuntime());
}

// ─── Test actor classes ──────────────────────────────────────────────────────

class TestActor extends MatrixActor {
  static accepts = {
    'doWork': { data: 'string' },
    'customOp': {},
  };

  calls: Array<{ op: string; payload: unknown }> = [];
  lastPayload: unknown = undefined;

  onDoWork(payload: unknown) {
    this.calls.push({ op: 'doWork', payload });
    this.lastPayload = payload;
    return { ok: true, echo: payload };
  }

  onCustomOp(payload: unknown) {
    this.calls.push({ op: 'customOp', payload });
    this.lastPayload = payload;
    return { ok: true };
  }
}

// ─── Dispatch wrapping — actor mode (#46-51) ────────────────────────────────

describe('Dispatch wrapping (direct mode)', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

  beforeEach(() => {
    broker = new InMemoryBroker();
    rootContext = buildContext(broker);
  });

  it('#46 default membrane → handler called with original payload', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test46');
    await actor.initialize(ctx, 'test46');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    ctx.transport.publish(TopicRouter.inbox('test46'), {
      op: 'doWork',
      payload: { data: 'hello' },
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 1);
    assert.deepStrictEqual(actor.calls[0].payload, { data: 'hello' });
  });

  it('#47 default membrane → handler return value propagated correctly', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test47');
    await actor.initialize(ctx, 'test47');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    ctx.transport.publish(TopicRouter.inbox('test47'), {
      op: 'doWork',
      payload: { data: 'test' },
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 1);
  });

  it('#48 deny membrane → handler NOT called', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test48');
    await actor.initialize(ctx, 'test48');

    actor.setLocalMembrane('(effect membrane.deny "BLOCKED")');

    ctx.transport.publish(TopicRouter.inbox('test48'), {
      op: 'doWork',
      payload: { data: 'hello' },
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 0, 'Handler should NOT be called when membrane denies');
  });

  it('#49 deny membrane → error reply sent back', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test49');
    await actor.initialize(ctx, 'test49');

    actor.setLocalMembrane('(effect membrane.deny "UNAUTHORIZED")');

    let replyReceived = false;
    let replyError: unknown = null;
    const replyTopic = 'mx/1/test/$replies.corr49';
    ctx.transport.subscribe(replyTopic, (payload: unknown) => {
      replyReceived = true;
      replyError = payload;
    });

    ctx.transport.publish(TopicRouter.inbox('test49'), {
      op: 'doWork',
      payload: { data: 'hello' },
      replyTo: replyTopic,
      correlationId: 'corr49',
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 0);
    assert.ok(replyReceived, 'Error reply should be sent');
  });

  it('#50 mutate membrane → handler receives MODIFIED payload', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test50');
    await actor.initialize(ctx, 'test50');

    actor.setLocalMembrane('(effect membrane.mutate "{\\"injected\\":true}")');

    ctx.transport.publish(TopicRouter.inbox('test50'), {
      op: 'doWork',
      payload: { original: true },
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 1);
    assert.deepStrictEqual(actor.lastPayload, { injected: true });
  });

  it('#51 mutate membrane → original sender envelope unchanged', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test51');
    await actor.initialize(ctx, 'test51');

    actor.setLocalMembrane('(effect membrane.mutate "{\\"new\\":1}")');
    const original = { op: 'doWork', payload: { old: 1 } } as MxEnvelope;

    ctx.transport.publish(TopicRouter.inbox('test51'), original);

    await tick();
    // Verify the original object wasn't mutated
    assert.deepStrictEqual(original.payload, { old: 1 });
  });
});

// ─── Direct mode (#52-55) ───────────────────────────────────────────────────

describe('Dispatch wrapping (direct mode specifics)', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

  beforeEach(() => {
    broker = new InMemoryBroker();
    rootContext = buildContext(broker);
  });

  it('#52 default membrane → direct handler called', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test52');
    await actor.initialize(ctx, 'test52');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    ctx.transport.publish(TopicRouter.inbox('test52'), {
      op: 'customOp',
      payload: {},
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 1);
    assert.strictEqual(actor.calls[0].op, 'customOp');
  });

  it('#53 deny membrane → direct handler NOT called', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test53');
    await actor.initialize(ctx, 'test53');

    actor.setLocalMembrane('(effect membrane.deny "no")');

    ctx.transport.publish(TopicRouter.inbox('test53'), {
      op: 'customOp',
      payload: {},
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 0);
  });

  it('#54 mutate membrane → direct handler receives modified payload', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test54');
    await actor.initialize(ctx, 'test54');

    actor.setLocalMembrane('(effect membrane.mutate "{\\"modified\\":42}")');

    ctx.transport.publish(TopicRouter.inbox('test54'), {
      op: 'doWork',
      payload: { original: 0 },
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 1);
    assert.deepStrictEqual(actor.lastPayload, { modified: 42 });
  });

  it('#55 RequestReplyHelper still works through membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test55');
    await actor.initialize(ctx, 'test55');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    let replyPayload: unknown = null;
    const replyTopic = 'mx/1/test/$replies.corr55';
    ctx.transport.subscribe(replyTopic, (payload: unknown) => {
      replyPayload = payload;
    });

    ctx.transport.publish(TopicRouter.inbox('test55'), {
      op: 'doWork',
      payload: { data: 'rr-test' },
      replyTo: replyTopic,
      correlationId: 'corr55',
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 1);
  });
});

// ─── System commands through membrane (#56-63) ──────────────────────────────

describe('System commands through membrane', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

  beforeEach(() => {
    broker = new InMemoryBroker();
    rootContext = buildContext(broker);
  });

  it('#56 $ping works through default membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test56');
    await actor.initialize(ctx, 'test56');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    let pongReceived = false;
    const replyTopic = 'mx/1/test/$replies.corr56';
    ctx.transport.subscribe(replyTopic, () => { pongReceived = true; });

    ctx.transport.publish(TopicRouter.inbox('test56'), {
      op: '$ping',
      payload: {},
      replyTo: replyTopic,
      correlationId: 'corr56',
    } as MxEnvelope);

    await tick();
    assert.ok(pongReceived, '$ping should work through membrane');
  });

  it('#57 $introspect works through default membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test57');
    await actor.initialize(ctx, 'test57');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    let introspectResult: unknown = null;
    const replyTopic = 'mx/1/test/$replies.corr57';
    ctx.transport.subscribe(replyTopic, (payload: unknown) => {
      introspectResult = payload;
    });

    ctx.transport.publish(TopicRouter.inbox('test57'), {
      op: '$introspect',
      payload: {},
      replyTo: replyTopic,
      correlationId: 'corr57',
    } as MxEnvelope);

    await tick();
    assert.ok(introspectResult !== null, '$introspect should return data through membrane');
  });

  it('#58 $join works through default membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test58');
    await actor.initialize(ctx, 'test58');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    // $join registers child in ChildManager — verify via $introspect
    ctx.transport.publish(TopicRouter.inbox('test58'), {
      op: '$join',
      payload: { id: 'child1', mount: 'test58.child1' },
    } as MxEnvelope);

    await tick();
    // Actor should NOT crash — domain handler count unchanged (no doWork/customOp calls)
    assert.strictEqual(actor.calls.length, 0, '$join is a system command, not a domain op');
  });

  it('#59 $join_ack works through default membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test59');
    await actor.initialize(ctx, 'test59');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    ctx.transport.publish(TopicRouter.inbox('test59'), {
      op: '$join_ack',
      payload: {},
    } as MxEnvelope);

    await tick();
    // $join_ack is a system command — no domain handler should fire
    assert.strictEqual(actor.calls.length, 0, '$join_ack is a system command, not a domain op');
  });

  it('#60 $prompt works through default membrane (headless actors)', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test60');
    await actor.initialize(ctx, 'test60');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    let replyReceived = false;
    const replyTopic = 'mx/1/test/$replies.corr60';
    ctx.transport.subscribe(replyTopic, () => { replyReceived = true; });

    ctx.transport.publish(TopicRouter.inbox('test60'), {
      op: '$prompt',
      payload: { text: 'hello' },
      replyTo: replyTopic,
      correlationId: 'corr60',
    } as MxEnvelope);

    await tick();
    // $prompt dispatches through membrane without crashing; domain ops untouched
    assert.strictEqual(actor.calls.length, 0, '$prompt is a system/framework op, not a domain op');
  });

  it('#61 $source-code works through default membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test61');
    await actor.initialize(ctx, 'test61');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    let replyReceived = false;
    const replyTopic = 'mx/1/test/$replies.corr61';
    ctx.transport.subscribe(replyTopic, () => { replyReceived = true; });

    ctx.transport.publish(TopicRouter.inbox('test61'), {
      op: '$source-code',
      payload: {},
      replyTo: replyTopic,
      correlationId: 'corr61',
    } as MxEnvelope);

    await tick();
    // $source-code is a framework op — domain handler count unchanged
    assert.strictEqual(actor.calls.length, 0, '$source-code is a framework op, not a domain op');
  });

  it('#62 $ping is EXEMPT from membrane (system command, prevents actor bricking)', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test62');
    await actor.initialize(ctx, 'test62');

    actor.setLocalMembrane(
      '(if (equal? op "$ping") (effect membrane.deny "ping blocked") (effect membrane.allow op payload))',
    );

    let pongReceived = false;
    const replyTopic = 'mx/1/test/$replies.corr62';
    ctx.transport.subscribe(replyTopic, (payload: unknown) => {
      const p = payload as Record<string, unknown>;
      const result = (p?.result ?? p) as Record<string, unknown>;
      if (result && result.status === 'ok') pongReceived = true;
    });

    ctx.transport.publish(TopicRouter.inbox('test62'), {
      op: '$ping',
      payload: {},
      replyTo: replyTopic,
      correlationId: 'corr62',
    } as MxEnvelope);

    await tick();
    assert.ok(pongReceived, '$ping should bypass membrane (system command exempt)');
  });

  it('#63 $introspect is EXEMPT from membrane (system command, prevents actor bricking)', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test63');
    await actor.initialize(ctx, 'test63');

    actor.setLocalMembrane(
      '(if (equal? op "$introspect") (effect membrane.deny "access denied") (effect membrane.allow op payload))',
    );

    let gotResult = false;
    const replyTopic = 'mx/1/test/$replies.corr63';
    ctx.transport.subscribe(replyTopic, (payload: unknown) => {
      const p = payload as Record<string, unknown>;
      if (p && !p.error) gotResult = true;
    });

    ctx.transport.publish(TopicRouter.inbox('test63'), {
      op: '$introspect',
      payload: {},
      replyTo: replyTopic,
      correlationId: 'corr63',
    } as MxEnvelope);

    await tick();
    assert.ok(gotResult, '$introspect should bypass membrane (system command exempt)');
  });
});

// ─── Framework ops through membrane (#64-67) ────────────────────────────────

describe('Framework ops through membrane', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

  beforeEach(() => {
    broker = new InMemoryBroker();
    rootContext = buildContext(broker);
  });

  it('#64 factory.create-from-code works through default membrane (headless)', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test64');
    await actor.initialize(ctx, 'test64');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    ctx.transport.publish(TopicRouter.inbox('test64'), {
      op: 'factory.create-from-code',
      payload: { name: 'test', code: 'class {}' },
    } as MxEnvelope);

    await tick();
    // Framework op dispatches without crash; no domain handler fires
    assert.strictEqual(actor.calls.length, 0, 'factory.create-from-code is a framework op');
  });

  it('#65 $style works through default membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test65');
    await actor.initialize(ctx, 'test65');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    ctx.transport.publish(TopicRouter.inbox('test65'), {
      op: '$style',
      payload: {},
    } as MxEnvelope);

    await tick();
    // $style is a framework op — no domain handler fires
    assert.strictEqual(actor.calls.length, 0, '$style is a framework op, not a domain op');
  });

  it('#66 snapshot.tree.oracle works through default membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test66');
    await actor.initialize(ctx, 'test66');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    ctx.transport.publish(TopicRouter.inbox('test66'), {
      op: 'snapshot.tree.oracle',
      payload: {},
    } as MxEnvelope);

    await tick();
    // snapshot.tree.oracle is a framework op — no domain handler fires
    assert.strictEqual(actor.calls.length, 0, 'snapshot.tree.oracle is a framework op');
  });

  it('#67 factory.create-from-code allowed through membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test67');
    await actor.initialize(ctx, 'test67');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    // Send a domain op AFTER the framework op to verify membrane still works
    ctx.transport.publish(TopicRouter.inbox('test67'), {
      op: 'factory.create-from-code',
      payload: {},
    } as MxEnvelope);

    ctx.transport.publish(TopicRouter.inbox('test67'), {
      op: 'doWork',
      payload: { data: 'after-framework' },
    } as MxEnvelope);

    await tick();
    // Framework op doesn't fire domain handler, but subsequent domain op does
    assert.strictEqual(actor.calls.length, 1, 'Domain op after framework op should pass through membrane');
    assert.strictEqual(actor.calls[0].op, 'doWork');
  });
});

// ─── Lifecycle (#68-72) ─────────────────────────────────────────────────────

describe('Membrane lifecycle', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

  beforeEach(() => {
    broker = new InMemoryBroker();
    rootContext = buildContext(broker);
  });

  it('#68 membrane set BEFORE first message → first message goes through membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test68');
    await actor.initialize(ctx, 'test68');

    actor.setLocalMembrane('(effect membrane.deny "blocked")');

    ctx.transport.publish(TopicRouter.inbox('test68'), {
      op: 'doWork',
      payload: {},
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 0, 'First message should be blocked');
  });

  it('#69 membrane set AFTER actor started → subsequent messages use new membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test69');
    await actor.initialize(ctx, 'test69');

    // First message without membrane → passes
    ctx.transport.publish(TopicRouter.inbox('test69'), {
      op: 'doWork',
      payload: { n: 1 },
    } as MxEnvelope);
    await tick();
    assert.strictEqual(actor.calls.length, 1);

    // Set deny membrane
    actor.setLocalMembrane('(effect membrane.deny "blocked")');

    // Second message → blocked
    ctx.transport.publish(TopicRouter.inbox('test69'), {
      op: 'doWork',
      payload: { n: 2 },
    } as MxEnvelope);
    await tick();
    assert.strictEqual(actor.calls.length, 1, 'Second message should be blocked');
  });

  it('#70 membrane cleared → messages bypass membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test70');
    await actor.initialize(ctx, 'test70');

    actor.setLocalMembrane('(effect membrane.deny "blocked")');

    ctx.transport.publish(TopicRouter.inbox('test70'), {
      op: 'doWork',
      payload: {},
    } as MxEnvelope);
    await tick();
    assert.strictEqual(actor.calls.length, 0);

    actor.clearLocalMembrane();

    ctx.transport.publish(TopicRouter.inbox('test70'), {
      op: 'doWork',
      payload: {},
    } as MxEnvelope);
    await tick();
    assert.strictEqual(actor.calls.length, 1);
  });

  it('#71 membrane program replaced at runtime → next message uses new program', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test71');
    await actor.initialize(ctx, 'test71');

    actor.setLocalMembrane('(effect membrane.deny "blocked")');

    ctx.transport.publish(TopicRouter.inbox('test71'), {
      op: 'doWork',
      payload: {},
    } as MxEnvelope);
    await tick();
    assert.strictEqual(actor.calls.length, 0);

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    ctx.transport.publish(TopicRouter.inbox('test71'), {
      op: 'doWork',
      payload: {},
    } as MxEnvelope);
    await tick();
    assert.strictEqual(actor.calls.length, 1);
  });

  it('#72 actor without membrane → messages dispatch normally', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test72');
    await actor.initialize(ctx, 'test72');

    ctx.transport.publish(TopicRouter.inbox('test72'), {
      op: 'doWork',
      payload: { data: 'no-membrane' },
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 1);
    assert.deepStrictEqual(actor.calls[0].payload, { data: 'no-membrane' });
  });
});

// ─── Concurrency (#73-75) ───────────────────────────────────────────────────

describe('Membrane concurrency', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

  beforeEach(() => {
    broker = new InMemoryBroker();
    rootContext = buildContext(broker);
  });

  it('#73 multiple rapid messages → each goes through membrane independently', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test73');
    await actor.initialize(ctx, 'test73');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    for (let i = 0; i < 10; i++) {
      ctx.transport.publish(TopicRouter.inbox('test73'), {
        op: 'doWork',
        payload: { i },
      } as MxEnvelope);
    }

    await tick(200);
    assert.strictEqual(actor.calls.length, 10, 'All 10 messages should arrive');
  });

  it('#74 slow membrane does NOT block other actors', async () => {
    const actor1 = new TestActor();
    const ctx1 = rootContext.derive('test74a');
    await actor1.initialize(ctx1, 'test74a');

    const actor2 = new TestActor();
    const ctx2 = rootContext.derive('test74b');
    await actor2.initialize(ctx2, 'test74b');

    actor1.setLocalMembrane('(effect membrane.allow op payload)');

    ctx1.transport.publish(TopicRouter.inbox('test74a'), {
      op: 'doWork', payload: {},
    } as MxEnvelope);
    ctx2.transport.publish(TopicRouter.inbox('test74b'), {
      op: 'doWork', payload: {},
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor1.calls.length, 1);
    assert.strictEqual(actor2.calls.length, 1);
  });

  it('#75 membrane error on one message → next message still processes', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test75');
    await actor.initialize(ctx, 'test75');

    actor.setLocalMembrane(
      '(if (equal? op "bad") (effect membrane.deny "error") (effect membrane.allow op payload))',
    );

    ctx.transport.publish(TopicRouter.inbox('test75'), {
      op: 'doWork',
      payload: { n: 1 },
    } as MxEnvelope);

    await tick();
    assert.strictEqual(actor.calls.length, 1, 'First message should pass');
  });
});

// ─── Performance (#76-78) ───────────────────────────────────────────────────

describe('Membrane dispatch performance', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

  beforeEach(() => {
    broker = new InMemoryBroker();
    rootContext = buildContext(broker);
  });

  it('#76 100 messages through membrane < 30s total', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test76');
    await actor.initialize(ctx, 'test76');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    const t0 = performance.now();
    for (let i = 0; i < 100; i++) {
      ctx.transport.publish(TopicRouter.inbox('test76'), {
        op: 'doWork',
        payload: { i },
      } as MxEnvelope);
    }

    await tick(5000);
    const elapsed = performance.now() - t0;
    // eslint-disable-next-line no-console -- integration perf output helps diagnose delivery regressions
    console.log(`  100 messages: ${elapsed.toFixed(0)}ms, ${actor.calls.length} delivered`);
    assert.ok(elapsed < 30000, `${elapsed.toFixed(0)}ms exceeds 30s budget`);
    assert.ok(actor.calls.length >= 90, `Only ${actor.calls.length}/100 messages delivered`);
  });

  it('#77 100 messages direct mode < 30s total', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test77');
    await actor.initialize(ctx, 'test77');

    actor.setLocalMembrane('(effect membrane.allow op payload)');

    const t0 = performance.now();
    for (let i = 0; i < 100; i++) {
      ctx.transport.publish(TopicRouter.inbox('test77'), {
        op: 'customOp',
        payload: { i },
      } as MxEnvelope);
    }

    await tick(5000);
    const elapsed = performance.now() - t0;
    // eslint-disable-next-line no-console -- integration perf output helps diagnose direct delivery regressions
    console.log(`  100 messages (direct): ${elapsed.toFixed(0)}ms, ${actor.calls.length} delivered`);
    assert.ok(elapsed < 30000);
    assert.ok(actor.calls.length >= 90);
  });

  it('#78 membrane adds reasonable latency per message', async () => {
    const actorWith = new TestActor();
    const ctxWith = rootContext.derive('test78a');
    await actorWith.initialize(ctxWith, 'test78a');
    actorWith.setLocalMembrane('(effect membrane.allow op payload)');

    const actorWithout = new TestActor();
    const ctxWithout = rootContext.derive('test78b');
    await actorWithout.initialize(ctxWithout, 'test78b');

    const count = 20;
    const t0 = performance.now();
    for (let i = 0; i < count; i++) {
      ctxWith.transport.publish(TopicRouter.inbox('test78a'), {
        op: 'doWork', payload: { i },
      } as MxEnvelope);
    }
    await tick(2000);
    const elapsedWith = performance.now() - t0;

    const t1 = performance.now();
    for (let i = 0; i < count; i++) {
      ctxWithout.transport.publish(TopicRouter.inbox('test78b'), {
        op: 'doWork', payload: { i },
      } as MxEnvelope);
    }
    await tick(500);
    const elapsedWithout = performance.now() - t1;

    // eslint-disable-next-line no-console -- integration perf comparison output helps diagnose membrane overhead
    console.log(`  With membrane: ${elapsedWith.toFixed(0)}ms, Without: ${elapsedWithout.toFixed(0)}ms`);
    assert.ok(actorWith.calls.length >= count - 2, 'With membrane: messages delivered');
    assert.ok(actorWithout.calls.length >= count - 2, 'Without membrane: messages delivered');
  });
});
