/**
 * Membrane propagation tests.
 * Tests inherited membrane propagation from parent to child.
 * Tests timeout protection at the MatrixActor level.
 * Tests that membrane state survives across message sequences.
 */
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { InMemoryBroker } from '../../../transport/InMemoryBroker.js';
import { TopicRouter } from '../../../engine/messaging/TopicRouter.js';
import type { IMatrixContext } from '../../../engine/core/IMatrixContext.js';
import type { MxEnvelope } from '../../../engine/messaging/MxEnvelope.js';
import { tick, buildContext, TestActor, unwrapReply } from './helpers.js';

// ─── Inherited membrane propagation ─────────────────────────────────────────

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

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

  it('child receives inherited membrane from parent', async () => {
    const child = new TestActor();
    const ctx = rootContext.derive('parent.child');
    await child.initialize(ctx, 'parent.child');

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

    // Verify membrane was set
    const membrane = child.getMembrane();
    assert.ok(membrane, 'Child should have membrane after inherit');

    // Domain op should be blocked
    ctx.transport.publish(TopicRouter.inbox('parent.child'), {
      op: 'doWork',
      payload: { data: 'test' },
    } as MxEnvelope);
    await tick();
    assert.strictEqual(child.calls.length, 0, 'Inherited deny membrane should block domain ops');
  });

  it('child inherits multiple membrane sources', async () => {
    const child = new TestActor();
    const ctx = rootContext.derive('grandparent.parent.child');
    await child.initialize(ctx, 'grandparent.parent.child');

    // Parent sends two inherited sources (grandparent's + parent's local)
    ctx.transport.publish(TopicRouter.inbox('grandparent.parent.child'), {
      op: '$membrane-inherit',
      payload: {
        sources: [
          '(effect membrane.mutate "{\\\"from-grandparent\\\":true}")',
          '(effect membrane.allow op payload)',
        ],
      },
      from: 'grandparent.parent',
    } as MxEnvelope);
    await tick();

    const membrane = child.getMembrane();
    assert.ok(membrane, 'Child should have membrane with multiple inherited sources');
  });

  it('inherited membrane deny blocks but system commands still work', async () => {
    const child = new TestActor();
    const ctx = rootContext.derive('parent.child2');
    await child.initialize(ctx, 'parent.child2');

    // Parent sets inherited deny-all
    ctx.transport.publish(TopicRouter.inbox('parent.child2'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.deny "parent-blocked")'] },
      from: 'parent',
    } as MxEnvelope);
    await tick();

    // Domain op should be blocked
    ctx.transport.publish(TopicRouter.inbox('parent.child2'), {
      op: 'doWork',
      payload: {},
    } as MxEnvelope);
    await tick();
    assert.strictEqual(child.calls.length, 0, 'Domain op should be blocked');

    // $ping should still work (exempt from membrane)
    let pingReply: unknown;
    ctx.transport.subscribe('mx/1/test/$replies.pprop1', (p: unknown) => { pingReply = p; });
    ctx.transport.publish(TopicRouter.inbox('parent.child2'), {
      op: '$ping',
      payload: {},
      replyTo: 'mx/1/test/$replies.pprop1',
      correlationId: 'pprop1',
    } as MxEnvelope);
    await tick();

    const data = unwrapReply(pingReply) as { status?: string } | undefined;
    assert.strictEqual(data?.status, 'ok', '$ping should work through inherited deny membrane');
  });

  it('forged $membrane-inherit from non-parent is ignored', async () => {
    const child = new TestActor();
    const ctx = rootContext.derive('parent.child3');
    await child.initialize(ctx, 'parent.child3');

    // Evil actor tries to set inherited membrane
    ctx.transport.publish(TopicRouter.inbox('parent.child3'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.deny "evil")'] },
      from: 'evil.actor',
    } as MxEnvelope);
    await tick();

    const membrane = child.getMembrane();
    assert.ok(!membrane, 'Forged $membrane-inherit should be ignored');

    // Domain op should still work (no membrane set)
    ctx.transport.publish(TopicRouter.inbox('parent.child3'), {
      op: 'doWork',
      payload: { data: 'should-work' },
    } as MxEnvelope);
    await tick();
    assert.strictEqual(child.calls.length, 1, 'Domain op should work — no membrane was set');
  });

  it('inherited membrane can be updated by parent', async () => {
    const child = new TestActor();
    const ctx = rootContext.derive('parent.child4');
    await child.initialize(ctx, 'parent.child4');

    // First: parent sets deny
    ctx.transport.publish(TopicRouter.inbox('parent.child4'), {
      op: '$membrane-inherit',
      payload: { sources: ['(effect membrane.deny "blocked")'] },
      from: 'parent',
    } as MxEnvelope);
    await tick();

    // Domain op should be blocked
    ctx.transport.publish(TopicRouter.inbox('parent.child4'), {
      op: 'doWork',
      payload: {},
    } as MxEnvelope);
    await tick();
    assert.strictEqual(child.calls.length, 0, 'Should be blocked initially');

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

    // Domain op should now work
    ctx.transport.publish(TopicRouter.inbox('parent.child4'), {
      op: 'doWork',
      payload: { data: 'now-allowed' },
    } as MxEnvelope);
    await tick();
    assert.strictEqual(child.calls.length, 1, 'Should be allowed after parent updates membrane');
  });
});

// ─── Membrane state introspection ───────────────────────────────────────────

describe('Membrane state introspection via $membrane get', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('$membrane get returns inherited + local state', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('parent.child');
    await actor.initialize(ctx, 'parent.child');

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

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

    // Introspect
    let reply: unknown;
    ctx.transport.subscribe('mx/1/test/$replies.get1', (p: unknown) => { reply = p; });
    ctx.transport.publish(TopicRouter.inbox('parent.child'), {
      op: '$membrane',
      payload: { action: 'get' },
      from: 'anyone',
      replyTo: 'mx/1/test/$replies.get1',
      correlationId: 'get1',
    } as MxEnvelope);
    await tick();

    const data = unwrapReply(reply) as {
      ok?: boolean;
      inherited?: string[];
      local?: string | null;
      hasMembraneEngine?: boolean;
    } | undefined;
    assert.ok(data?.ok, '$membrane get should succeed');
    assert.ok(Array.isArray(data?.inherited), 'Should return inherited sources array');
    assert.strictEqual(data?.inherited?.length, 1, 'Should have one inherited source');
    assert.ok(data?.local, 'Should return local source');
    assert.ok(data?.hasMembraneEngine, 'Should have membrane engine');
  });

  it('clear-local removes local membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test.clearlocal');
    await actor.initialize(ctx, 'test.clearlocal');

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

    let reply: unknown;
    ctx.transport.subscribe('mx/1/test/$replies.cl1', (p: unknown) => { reply = p; });
    ctx.transport.publish(TopicRouter.inbox('test.clearlocal'), {
      op: '$membrane',
      payload: { action: 'clear-local' },
      from: 'test.clearlocal',
      replyTo: 'mx/1/test/$replies.cl1',
      correlationId: 'cl1',
    } as MxEnvelope);
    await tick();

    const data = unwrapReply(reply) as { ok?: boolean; action?: string } | undefined;
    assert.ok(data?.ok, 'clear-local should succeed');

    // Domain op should work now
    ctx.transport.publish(TopicRouter.inbox('test.clearlocal'), {
      op: 'doWork',
      payload: { data: 'test' },
    } as MxEnvelope);
    await tick();
    assert.strictEqual(actor.calls.length, 1, 'Domain op should work after clear-local');
  });
});

// ─── Membrane timeout at actor level ────────────────────────────────────────

describe('Membrane timeout (fail-open)', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('fast membrane completes within timeout', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test.fast');
    await actor.initialize(ctx, 'test.fast');

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

    ctx.transport.publish(TopicRouter.inbox('test.fast'), {
      op: 'doWork',
      payload: { data: 'should-arrive' },
    } as MxEnvelope);
    await tick();

    assert.strictEqual(actor.calls.length, 1, 'Fast membrane should allow message through');
  });

  it('deny membrane actually blocks (not just timing out)', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test.deny');
    await actor.initialize(ctx, 'test.deny');

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

    ctx.transport.publish(TopicRouter.inbox('test.deny'), {
      op: 'doWork',
      payload: { data: 'should-not-arrive' },
    } as MxEnvelope);
    await tick();

    assert.strictEqual(actor.calls.length, 0, 'Deny membrane should actually block, not timeout-allow');
  });
});
