/**
 * Phase 3 zone tests — Tests #79-97
 *
 * Tests two-zone execution (inherited + local), immutability, propagation
 * via addChild/on$join, and dynamic changes.
 */
import { describe, it, beforeEach } 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 helpers ────────────────────────────────────────────────────────────

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

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;
}

function buildContext(broker: InMemoryBroker): IMatrixContext {
  const transport = new InMemoryTransport(broker, { name: 'test' });
  return new MatrixContext('', transport, undefined, mockRuntime());
}

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

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

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

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

type ZoneActorInternals = ZoneTestActor & {
  _inheritedMembranes: unknown[];
  setInheritedMembrane?: unknown;
};

// ═══════════════════════════════════════════════════════════════════════════
// Two-zone execution (#79-85)
// ═══════════════════════════════════════════════════════════════════════════

describe('Two-zone execution', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('#79 inherited zone runs BEFORE local zone', async () => {
    const actor = new ZoneTestActor();
    const ctx = rootContext.derive('test79');
    await actor.initialize(ctx, 'test79');

    // Set inherited via $membrane-inherit (simulating parent propagation)
    ctx.transport.publish(TopicRouter.inbox('test79'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.allow op payload)'] },
      from: 'parent',
    } as MxEnvelope);
    await tick();

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

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

    await tick();
    assert.strictEqual(actor.calls.length, 1, 'Message should pass through both zones');
  });

  it('#80 inherited deny → local NEVER executes → message blocked', async () => {
    const actor = new ZoneTestActor();
    const ctx = rootContext.derive('test80');
    await actor.initialize(ctx, 'test80');

    // Inherited denies
    ctx.transport.publish(TopicRouter.inbox('test80'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.deny "inherited-block")'] },
      from: 'parent',
    } as MxEnvelope);
    await tick();

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

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

    await tick();
    assert.strictEqual(actor.calls.length, 0, 'Inherited deny should block before local runs');
  });

  it('#81 inherited allow + local deny → message blocked', async () => {
    const actor = new ZoneTestActor();
    const ctx = rootContext.derive('test81');
    await actor.initialize(ctx, 'test81');

    ctx.transport.publish(TopicRouter.inbox('test81'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.allow op payload)'] },
      from: 'parent',
    } as MxEnvelope);
    await tick();

    actor.setLocalMembrane('(effect membrane.deny "local-block")');

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

    await tick();
    assert.strictEqual(actor.calls.length, 0, 'Local deny should block');
  });

  it('#82 inherited allow + local allow → message delivered', async () => {
    const actor = new ZoneTestActor();
    const ctx = rootContext.derive('test82');
    await actor.initialize(ctx, 'test82');

    ctx.transport.publish(TopicRouter.inbox('test82'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.allow op payload)'] },
      from: 'parent',
    } as MxEnvelope);
    await tick();

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

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

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

  it('#83 inherited allow + local mutate → message delivered with modified payload', async () => {
    const actor = new ZoneTestActor();
    const ctx = rootContext.derive('test83');
    await actor.initialize(ctx, 'test83');

    ctx.transport.publish(TopicRouter.inbox('test83'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.allow op payload)'] },
      from: 'parent',
    } as MxEnvelope);
    await tick();

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

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

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

  it('#84 inherited mutate + local allow → local sees mutated payload', async () => {
    const actor = new ZoneTestActor();
    const ctx = rootContext.derive('test84');
    await actor.initialize(ctx, 'test84');

    ctx.transport.publish(TopicRouter.inbox('test84'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.mutate "{\\"fromInherited\\":1}")'] },
      from: 'parent',
    } as MxEnvelope);
    await tick();

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

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

    await tick();
    assert.strictEqual(actor.calls.length, 1);
    // Inherited mutates the payload, local allows it through
    assert.deepStrictEqual(actor.lastPayload, { fromInherited: 1 });
  });

  it('#85 inherited mutate + local mutate → both mutations applied in order', async () => {
    const actor = new ZoneTestActor();
    const ctx = rootContext.derive('test85');
    await actor.initialize(ctx, 'test85');

    ctx.transport.publish(TopicRouter.inbox('test85'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.mutate "{\\"step\\":1}")'] },
      from: 'parent',
    } as MxEnvelope);
    await tick();

    actor.setLocalMembrane('(effect membrane.mutate "{\\"step\\":2}")');

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

    await tick();
    assert.strictEqual(actor.calls.length, 1);
    // Local mutate overwrites — it gets the payload after inherited mutate
    // but produces its own replacement
    assert.deepStrictEqual(actor.lastPayload, { step: 2 });
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// Immutability (#86-89)
// ═══════════════════════════════════════════════════════════════════════════

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

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

  it('#86 _inheritedMembranes has NO public setter', () => {
    const actor = new ZoneTestActor();
    // _inheritedMembranes is protected — cannot be set externally
    // TypeScript enforces this at compile time. At runtime, we verify
    // there's no setInheritedMembrane method.
    assert.strictEqual(typeof (actor as ZoneActorInternals).setInheritedMembrane, 'undefined',
      'No public setInheritedMembrane method should exist');
  });

  it('#87 _inheritedMembranes is protected (compile-time enforcement)', () => {
    // This is a compile-time TypeScript check. At runtime, we verify
    // that inherited membranes can only be set via $membrane-inherit message.
    const actor = new ZoneTestActor();
    // Direct access is possible in JS but not in TS (protected)
    assert.ok(Array.isArray((actor as ZoneActorInternals)._inheritedMembranes),
      '_inheritedMembranes exists as an array');
    assert.strictEqual((actor as ZoneActorInternals)._inheritedMembranes.length, 0,
      'Initially empty');
  });

  it('#88 setLocalMembrane() works (public API)', async () => {
    const actor = new ZoneTestActor();
    const ctx = rootContext.derive('test88');
    await actor.initialize(ctx, 'test88');

    // setLocalMembrane is public and works
    actor.setLocalMembrane('(effect membrane.deny "blocked")');

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

    await tick();
    assert.strictEqual(actor.calls.length, 0, 'Local membrane should deny');
  });

  it('#89 child calling setLocalMembrane() does NOT affect parent', async () => {
    const parent = new ZoneTestActor();
    const parentCtx = rootContext.derive('parent89');
    await parent.initialize(parentCtx, 'parent89');

    const child = new ZoneTestActor();
    const childCtx = rootContext.derive('child89');
    await child.initialize(childCtx, 'child89');

    parent.setLocalMembrane('(effect membrane.allow op payload)');
    child.setLocalMembrane('(effect membrane.deny "child-only")');

    // Parent should still allow
    parentCtx.transport.publish(TopicRouter.inbox('parent89'), {
      op: 'doWork',
      payload: {},
    } as MxEnvelope);

    await tick();
    assert.strictEqual(parent.calls.length, 1, 'Parent should still allow');

    // Child should deny
    childCtx.transport.publish(TopicRouter.inbox('child89'), {
      op: 'doWork',
      payload: {},
    } as MxEnvelope);

    await tick();
    assert.strictEqual(child.calls.length, 0, 'Child should deny');
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// Propagation in addChild (#90-95)
// ═══════════════════════════════════════════════════════════════════════════

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

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

  it('#90 parent with inherited + local → child inherits both', async () => {
    const parent = new ZoneTestActor();
    const parentCtx = rootContext.derive('parent90');
    await parent.initialize(parentCtx, 'parent90');

    // Give parent an inherited membrane
    parentCtx.transport.publish(TopicRouter.inbox('parent90'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.allow op payload)'] },
      from: 'grandparent',
    } as MxEnvelope);
    await tick();

    // Set parent's local membrane
    parent.setLocalMembrane('(effect membrane.allow op payload)');

    // Create child and manually propagate (simulating what addChild does)
    const child = new ZoneTestActor();
    const childCtx = rootContext.derive('child90');
    await child.initialize(childCtx, 'child90');

    // Simulate propagation
    const sources = parent._getInheritedSourcesForChildren();
    assert.strictEqual(sources.length, 2, 'Should have inherited + local');

    childCtx.transport.publish(TopicRouter.inbox('child90'), {
      op: '$membrane-inherit',
      payload: { sources },
      from: 'parent90',
    } as MxEnvelope);
    await tick();

    assert.strictEqual((child as ZoneActorInternals)._inheritedMembranes.length, 2);
  });

  it('#91 parent with only inherited (no local) → child inherits parent\'s inherited', async () => {
    const parent = new ZoneTestActor();
    const parentCtx = rootContext.derive('parent91');
    await parent.initialize(parentCtx, 'parent91');

    parentCtx.transport.publish(TopicRouter.inbox('parent91'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.deny "grandparent")'] },
      from: 'grandparent',
    } as MxEnvelope);
    await tick();

    const sources = parent._getInheritedSourcesForChildren();
    assert.strictEqual(sources.length, 1, 'Only inherited');
    assert.ok(sources[0].includes('deny'));
  });

  it('#92 parent with only local (no inherited) → child inherits parent\'s local', async () => {
    const parent = new ZoneTestActor();
    const parentCtx = rootContext.derive('parent92');
    await parent.initialize(parentCtx, 'parent92');

    parent.setLocalMembrane('(effect membrane.deny "parent-only")');

    const sources = parent._getInheritedSourcesForChildren();
    assert.strictEqual(sources.length, 1);
    assert.ok(sources[0].includes('deny'));
  });

  it('#93 parent with neither → child has no inherited membrane', async () => {
    const parent = new ZoneTestActor();
    const parentCtx = rootContext.derive('parent93');
    await parent.initialize(parentCtx, 'parent93');

    const sources = parent._getInheritedSourcesForChildren();
    assert.strictEqual(sources.length, 0);
  });

  it('#94 grandchild inherits composed membrane from all ancestors', async () => {
    // Root → parent → grandchild
    // Root has local deny for $ping
    // Parent has local allow-all
    // Grandchild should inherit: [root.local, parent.local]
    const root = new ZoneTestActor();
    const rootCtx = rootContext.derive('root94');
    await root.initialize(rootCtx, 'root94');
    root.setLocalMembrane('(effect membrane.allow op payload)');

    // Parent inherits root's local
    const parent = new ZoneTestActor();
    const parentCtx = rootContext.derive('parent94');
    await parent.initialize(parentCtx, 'parent94');

    parentCtx.transport.publish(TopicRouter.inbox('parent94'), {
      op: '$membrane-inherit',
      payload: { sources: root._getInheritedSourcesForChildren() },
      from: 'root94',
    } as MxEnvelope);
    await tick();

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

    // Grandchild inherits [root.local, parent.local]
    const grandchild = new ZoneTestActor();
    const grandchildCtx = rootContext.derive('grandchild94');
    await grandchild.initialize(grandchildCtx, 'grandchild94');

    const gcSources = parent._getInheritedSourcesForChildren();
    assert.strictEqual(gcSources.length, 2, 'Should have root.local + parent.local');

    grandchildCtx.transport.publish(TopicRouter.inbox('grandchild94'), {
      op: '$membrane-inherit',
      payload: { sources: gcSources },
      from: 'parent94',
    } as MxEnvelope);
    await tick();

    assert.strictEqual((grandchild as ZoneActorInternals)._inheritedMembranes.length, 2);
  });

  it('#95 three-level hierarchy: root + mid + leaf all inherited', async () => {
    const root = new ZoneTestActor();
    const rootCtx = rootContext.derive('root95');
    await root.initialize(rootCtx, 'root95');
    root.setLocalMembrane('(effect membrane.allow op payload)');

    const mid = new ZoneTestActor();
    const midCtx = rootContext.derive('mid95');
    await mid.initialize(midCtx, 'mid95');
    midCtx.transport.publish(TopicRouter.inbox('mid95'), {
      op: '$membrane-inherit',
      payload: { sources: root._getInheritedSourcesForChildren() },
      from: 'root95',
    } as MxEnvelope);
    await tick();
    mid.setLocalMembrane('(effect membrane.allow op payload)');

    const leaf = new ZoneTestActor();
    const leafCtx = rootContext.derive('leaf95');
    await leaf.initialize(leafCtx, 'leaf95');
    leafCtx.transport.publish(TopicRouter.inbox('leaf95'), {
      op: '$membrane-inherit',
      payload: { sources: mid._getInheritedSourcesForChildren() },
      from: 'mid95',
    } as MxEnvelope);
    await tick();
    leaf.setLocalMembrane('(effect membrane.allow op payload)');

    // Leaf should have 2 inherited (root.local + mid.local) plus its own local
    assert.strictEqual((leaf as ZoneActorInternals)._inheritedMembranes.length, 2);

    // All three zones allow → message passes
    leafCtx.transport.publish(TopicRouter.inbox('leaf95'), {
      op: 'doWork',
      payload: { data: 'hello' },
    } as MxEnvelope);

    await tick();
    assert.strictEqual(leaf.calls.length, 1, 'All three zones allow → message passes');
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// Dynamic changes (#96-97)
// ═══════════════════════════════════════════════════════════════════════════

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

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

  it('#96 parent changes local AFTER child created → child inherited NOT updated (snapshot)', async () => {
    const parent = new ZoneTestActor();
    const parentCtx = rootContext.derive('parent96');
    await parent.initialize(parentCtx, 'parent96');
    parent.setLocalMembrane('(effect membrane.allow op payload)');

    // Propagate to child
    const child = new ZoneTestActor();
    const childCtx = rootContext.derive('child96');
    await child.initialize(childCtx, 'child96');

    childCtx.transport.publish(TopicRouter.inbox('child96'), {
      op: '$membrane-inherit',
      payload: { sources: parent._getInheritedSourcesForChildren() },
      from: 'parent96',
    } as MxEnvelope);
    await tick();

    // Child has 1 inherited (parent's allow)
    assert.strictEqual((child as ZoneActorInternals)._inheritedMembranes.length, 1);

    // Parent changes to deny AFTER child was created
    parent.setLocalMembrane('(effect membrane.deny "new-rule")');

    // Child's inherited is still the old allow — NOT updated
    assert.strictEqual((child as ZoneActorInternals)._inheritedMembranes.length, 1);

    // Child should still allow (old snapshot)
    childCtx.transport.publish(TopicRouter.inbox('child96'), {
      op: 'doWork',
      payload: {},
    } as MxEnvelope);

    await tick();
    assert.strictEqual(child.calls.length, 1, 'Child uses snapshot, not live parent membrane');
  });

  it('#97 adding new child AFTER parent membrane change → new child gets current membrane', async () => {
    const parent = new ZoneTestActor();
    const parentCtx = rootContext.derive('parent97');
    await parent.initialize(parentCtx, 'parent97');
    parent.setLocalMembrane('(effect membrane.allow op payload)');

    // Change parent's membrane
    parent.setLocalMembrane('(effect membrane.deny "blocked")');

    // New child gets current (deny)
    const sources = parent._getInheritedSourcesForChildren();
    assert.strictEqual(sources.length, 1);
    assert.ok(sources[0].includes('deny'), 'New child should get current membrane');

    const child = new ZoneTestActor();
    const childCtx = rootContext.derive('child97');
    await child.initialize(childCtx, 'child97');

    childCtx.transport.publish(TopicRouter.inbox('child97'), {
      op: '$membrane-inherit',
      payload: { sources },
      from: 'parent97',
    } as MxEnvelope);
    await tick();

    // Child should deny (inherits parent's current deny)
    childCtx.transport.publish(TopicRouter.inbox('child97'), {
      op: 'doWork',
      payload: {},
    } as MxEnvelope);

    await tick();
    assert.strictEqual(child.calls.length, 0, 'New child inherits current deny membrane');
  });
});
