/**
 * Security & auth tests for membrane hardening.
 * Tests that $membrane and $membrane-inherit have proper authorization.
 * Tests system command exemption from membrane.
 * Tests subtree guard fix in ShellEffectBackend.
 */
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';
import { ShellEffectBackend, type ShellDelegate } from '../ShellEffectBackend.js';
import { MembraneStepper } from '../MembraneStepper.js';

// ─── $membrane auth tests ────────────────────────────────────────────────────

describe('$membrane auth (set-local)', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('self can set own membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('parent.child');
    await actor.initialize(ctx, 'parent.child');

    let reply: unknown;
    ctx.transport.subscribe('mx/1/test/$replies.self1', (p: unknown) => { reply = p; });
    ctx.transport.publish(TopicRouter.inbox('parent.child'), {
      op: '$membrane',
      payload: { action: 'set-local', source: '(effect membrane.allow op payload)' },
      from: 'parent.child',
      replyTo: 'mx/1/test/$replies.self1',
      correlationId: 'self1',
    } as MxEnvelope);
    await tick();
    const r1 = unwrapReply(reply) as { ok?: boolean; error?: string } | undefined;
    assert.ok(!r1?.error || r1?.ok, 'Self should be able to set membrane');
  });

  it('parent can set child membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('parent.child');
    await actor.initialize(ctx, 'parent.child');

    let reply: unknown;
    ctx.transport.subscribe('mx/1/test/$replies.parent1', (p: unknown) => { reply = p; });
    ctx.transport.publish(TopicRouter.inbox('parent.child'), {
      op: '$membrane',
      payload: { action: 'set-local', source: '(effect membrane.allow op payload)' },
      from: 'parent',
      replyTo: 'mx/1/test/$replies.parent1',
      correlationId: 'parent1',
    } as MxEnvelope);
    await tick();
    const r2 = unwrapReply(reply) as { ok?: boolean; error?: string } | undefined;
    assert.ok(!r2?.error || r2?.ok, 'Parent should be able to set child membrane');
  });

  it('foreign actor CANNOT set membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('parent.child');
    await actor.initialize(ctx, 'parent.child');

    let reply: unknown;
    ctx.transport.subscribe('mx/1/test/$replies.foreign1', (p: unknown) => { reply = p; });
    ctx.transport.publish(TopicRouter.inbox('parent.child'), {
      op: '$membrane',
      payload: { action: 'set-local', source: '(effect membrane.deny "pwned")' },
      from: 'evil.actor',
      replyTo: 'mx/1/test/$replies.foreign1',
      correlationId: 'foreign1',
    } as MxEnvelope);
    await tick();
    // The reply should contain UNAUTHORIZED error
    const replyData = unwrapReply(reply) as { ok?: boolean; error?: string } | undefined;
    assert.ok(
      replyData?.error?.includes('UNAUTHORIZED') || replyData?.ok === false,
      `Foreign actor should be rejected, got: ${JSON.stringify(replyData)}`
    );
  });

  it('CLI (cli) can set membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('parent.child');
    await actor.initialize(ctx, 'parent.child');

    let reply: unknown;
    ctx.transport.subscribe('mx/1/test/$replies.cli1', (p: unknown) => { reply = p; });
    ctx.transport.publish(TopicRouter.inbox('parent.child'), {
      op: '$membrane',
      payload: { action: 'set-local', source: '(effect membrane.allow op payload)' },
      from: 'cli',
      replyTo: 'mx/1/test/$replies.cli1',
      correlationId: 'cli1',
    } as MxEnvelope);
    await tick();
    const r4 = unwrapReply(reply) as { ok?: boolean; error?: string } | undefined;
    assert.ok(!r4?.error || r4?.ok, 'CLI should be able to set membrane');
  });

  it('anyone can READ membrane state (get)', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('parent.child');
    await actor.initialize(ctx, 'parent.child');

    let reply: unknown;
    ctx.transport.subscribe('mx/1/test/$replies.read1', (p: unknown) => { reply = p; });
    ctx.transport.publish(TopicRouter.inbox('parent.child'), {
      op: '$membrane',
      payload: { action: 'get' },
      from: 'random.actor',
      replyTo: 'mx/1/test/$replies.read1',
      correlationId: 'read1',
    } as MxEnvelope);
    await tick();
    const replyData = unwrapReply(reply) as { ok?: boolean } | undefined;
    assert.ok(replyData?.ok !== false, 'Anyone should be able to read membrane state');
  });
});

// ─── $membrane-inherit auth tests ────────────────────────────────────────────

describe('$membrane-inherit auth', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('parent can send $membrane-inherit', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('parent.child');
    await actor.initialize(ctx, 'parent.child');

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

    const membrane = actor.getMembrane();
    assert.ok(membrane, 'Membrane should be initialized after parent sends $membrane-inherit');
  });

  it('foreign actor $membrane-inherit is IGNORED', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('parent.child');
    await actor.initialize(ctx, 'parent.child');

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

    const membrane = actor.getMembrane();
    assert.ok(!membrane, 'Membrane should NOT be initialized from foreign sender');
  });
});

// ─── System command exemption tests ──────────────────────────────────────────

describe('System command exemption', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('$ping works through deny-all membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test.exempt1');
    await actor.initialize(ctx, 'test.exempt1');
    actor.setLocalMembrane('(effect membrane.deny "blocked")');

    let reply: unknown;
    ctx.transport.subscribe('mx/1/test/$replies.ping1', (p: unknown) => { reply = p; });
    ctx.transport.publish(TopicRouter.inbox('test.exempt1'), {
      op: '$ping',
      payload: {},
      replyTo: 'mx/1/test/$replies.ping1',
      correlationId: 'ping1',
    } as MxEnvelope);
    await tick();

    const replyData = unwrapReply(reply) as { status?: string } | undefined;
    assert.strictEqual(replyData?.status, 'ok', '$ping should work despite deny-all membrane');
  });

  it('$introspect works through deny-all membrane', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test.exempt2');
    await actor.initialize(ctx, 'test.exempt2');
    actor.setLocalMembrane('(effect membrane.deny "blocked")');

    let reply: unknown;
    ctx.transport.subscribe('mx/1/test/$replies.intro1', (p: unknown) => { reply = p; });
    ctx.transport.publish(TopicRouter.inbox('test.exempt2'), {
      op: '$introspect',
      payload: {},
      replyTo: 'mx/1/test/$replies.intro1',
      correlationId: 'intro1',
    } as MxEnvelope);
    await tick();

    const replyData = unwrapReply(reply) as { accepts?: unknown; mount?: string } | undefined;
    assert.ok(replyData?.accepts || replyData?.mount, '$introspect should work despite deny-all membrane');
  });

  it('$membrane works through deny-all membrane (can fix broken membrane)', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test.exempt3');
    await actor.initialize(ctx, 'test.exempt3');
    actor.setLocalMembrane('(effect membrane.deny "blocked")');

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

    const replyData = unwrapReply(reply) as { ok?: boolean } | undefined;
    assert.ok(replyData?.ok, '$membrane clear-local should work despite deny-all');
  });

  it('deny-all membrane blocks domain ops', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test.exempt4');
    await actor.initialize(ctx, 'test.exempt4');
    actor.setLocalMembrane('(effect membrane.deny "blocked")');

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

    assert.strictEqual(actor.calls.length, 0, 'Domain op should be blocked by deny-all membrane');
  });

  it('broken membrane (syntax error) does not brick actor', async () => {
    const actor = new TestActor();
    const ctx = rootContext.derive('test.exempt5');
    await actor.initialize(ctx, 'test.exempt5');

    // Set a syntactically broken membrane — this will cause compile error
    try {
      actor.setLocalMembrane('(((invalid lisp');
    } catch {
      // compile might throw — that's fine, the actor shouldn't brick
    }

    let reply: unknown;
    ctx.transport.subscribe('mx/1/test/$replies.brick1', (p: unknown) => { reply = p; });
    ctx.transport.publish(TopicRouter.inbox('test.exempt5'), {
      op: '$ping',
      payload: {},
      replyTo: 'mx/1/test/$replies.brick1',
      correlationId: 'brick1',
    } as MxEnvelope);
    await tick();

    const replyData = unwrapReply(reply) as { status?: string } | undefined;
    assert.strictEqual(replyData?.status, 'ok', '$ping should work even after broken membrane set');
  });
});

// ─── Subtree guard tests ─────────────────────────────────────────────────────

describe('ShellEffectBackend subtree guard (hardened)', () => {
  it('cross-realm target (/) is now checked by subtree guard', async () => {
    const calls: Array<{ method: string; args: unknown[] }> = [];
    let checked = false;

    const delegate: ShellDelegate = {
      isInSubtree: (target: string) => { checked = true; return false; }, // deny all
      executeInvoke: async () => { calls.push({ method: 'invoke', args: [] }); return { ok: true, data: {} }; },
      executeEmit: async () => ({ ok: true, data: {} }),
      executeRespond: async () => ({ ok: true, data: {} }),
      executeDefineComponent: async () => ({ ok: true, data: {} }),
      executeLoadComponent: async () => ({ ok: true, data: {} }),
      executeSaveComponent: async () => ({ ok: true, data: {} }),
      executeListComponents: async () => ({ ok: true, data: {} }),
      executeEvalLisp: async () => ({ ok: true, data: {} }),
      executeFindComponents: async () => ({ ok: true, data: {} }),
      executeGetComponentSource: async () => ({ ok: true, data: {} }),
      executePublishComponent: async () => ({ ok: true, data: {} }),
      executeInstallComponent: async () => ({ ok: true, data: {} }),
      executeResolveIntent: async () => ({ ok: true, data: {} }),
      executeTraceQuery: async () => ({ ok: true, data: {} }),
      executeReplPackages: async () => ({ ok: true, data: {} }),
      executeReplDoc: async () => ({ ok: true, data: {} }),
    };

    const stepper = MembraneStepper.create([new ShellEffectBackend(delegate)]);
    const program = stepper.compile('(effect shell.invoke "evil-realm/system.llm" "$prompt" "{}")');
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'a', meta: {} });

    assert.ok(checked, 'isInSubtree should be called even for cross-realm targets');
    assert.strictEqual(calls.length, 0, 'invoke should NOT be called when subtree guard rejects');
  });

  it('cross-realm introspect is also guarded', async () => {
    let checked = false;

    const delegate: ShellDelegate = {
      isInSubtree: () => { checked = true; return false; },
      executeInvoke: async () => ({ ok: true, data: {} }),
      executeEmit: async () => ({ ok: true, data: {} }),
      executeRespond: async () => ({ ok: true, data: {} }),
      executeDefineComponent: async () => ({ ok: true, data: {} }),
      executeLoadComponent: async () => ({ ok: true, data: {} }),
      executeSaveComponent: async () => ({ ok: true, data: {} }),
      executeListComponents: async () => ({ ok: true, data: {} }),
      executeEvalLisp: async () => ({ ok: true, data: {} }),
      executeFindComponents: async () => ({ ok: true, data: {} }),
      executeGetComponentSource: async () => ({ ok: true, data: {} }),
      executePublishComponent: async () => ({ ok: true, data: {} }),
      executeInstallComponent: async () => ({ ok: true, data: {} }),
      executeResolveIntent: async () => ({ ok: true, data: {} }),
      executeTraceQuery: async () => ({ ok: true, data: {} }),
      executeReplPackages: async () => ({ ok: true, data: {} }),
      executeReplDoc: async () => ({ ok: true, data: {} }),
    };

    const stepper = MembraneStepper.create([new ShellEffectBackend(delegate)]);
    const program = stepper.compile('(effect shell.introspect "evil-realm/actor")');
    const result = await stepper.run(program, { op: 'test', payload: {}, sender: 'a', meta: {} });

    assert.ok(checked, 'isInSubtree should be called for cross-realm introspect');
  });
});
