/**
 * Canary integration tests — real-world membrane scenarios.
 *
 * These tests exercise the membrane the way users, developers, and LLMs
 * actually encounter it. Each test simulates a real operational scenario:
 *
 * 1. CLI user sets a membrane on a running actor
 * 2. Developer creates an actor with a rate-limiting membrane
 * 3. LLM agent tries to invoke an actor with a deny-all membrane
 * 4. Parent actor propagates audit membrane to child subtree
 * 5. Actor with broken membrane can still be recovered
 * 6. Membrane mutation rewrites payloads before delivery
 * 7. Multi-layer inheritance: grandparent → parent → child
 * 8. Hot-swap: change membrane while messages are flowing
 */
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { MatrixActor } from '../../MatrixActor.js';
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, unwrapReply } from './helpers.js';

// ─── Real-world actor classes ────────────────────────────────────────────────

/** An actor that accepts work and tracks what it received — simulates a real service */
class ServiceActor extends MatrixActor {
  static accepts = {
    'process': { data: 'any' },
    'query': { filter: 'string' },
    'admin.reset': {},
    'admin.config': {},
  };

  log: Array<{ op: string; payload: unknown; ts: number }> = [];

  onProcess(payload: unknown) {
    this.log.push({ op: 'process', payload, ts: Date.now() });
    return { ok: true, processed: true };
  }

  onQuery(payload: unknown) {
    this.log.push({ op: 'query', payload, ts: Date.now() });
    return { ok: true, results: [] };
  }

  onAdminReset(payload: unknown) {
    this.log.push({ op: 'admin.reset', payload, ts: Date.now() });
    this.log = [];
    return { ok: true, cleared: true };
  }

  onAdminConfig(payload: unknown) {
    this.log.push({ op: 'admin.config', payload, ts: Date.now() });
    return { ok: true };
  }
}

/** An actor that spawns children — simulates a supervisor */
class SupervisorActor extends MatrixActor {
  static accepts = {
    'task': { work: 'any' },
    'status': {},
  };

  tasks: unknown[] = [];

  onTask(payload: unknown) {
    this.tasks.push(payload);
    return { ok: true };
  }

  onStatus() {
    return { ok: true, taskCount: this.tasks.length };
  }
}

interface CanaryReply {
  ok?: boolean;
  status?: string;
  local?: string;
  error?: string;
  [key: string]: unknown;
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

function sendAndReply(
  transport: IMatrixContext['transport'],
  mount: string,
  op: string,
  payload: unknown,
  from?: string,
): Promise<CanaryReply | undefined> {
  return new Promise(async (resolve) => {
    const corrId = `canary-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    const replyTopic = `mx/1/test/$replies.${corrId}`;

    let replied = false;
    transport.subscribe(replyTopic, (p: unknown) => {
      replied = true;
      resolve(unwrapReply(p) as CanaryReply);
    });

    transport.publish(TopicRouter.inbox(mount), {
      op,
      payload,
      from: from ?? '',
      replyTo: replyTopic,
      correlationId: corrId,
    } as MxEnvelope);

    await tick(100);
    if (!replied) resolve(undefined);
  });
}

function sendFire(
  transport: IMatrixContext['transport'],
  mount: string,
  op: string,
  payload: unknown,
  from?: string,
): void {
  transport.publish(TopicRouter.inbox(mount), {
    op,
    payload,
    from: from ?? '',
  } as MxEnvelope);
}

// ─── Canary 1: CLI user sets membrane on running actor ───────────────────────

describe('Canary: CLI user manages membrane on live actor', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('set deny membrane via $membrane → blocks domain ops → clear → ops work again', async () => {
    const actor = new ServiceActor();
    const ctx = rootContext.derive('daemon.service');
    await actor.initialize(ctx, 'daemon.service');

    // Step 1: Actor works normally
    sendFire(ctx.transport, 'daemon.service', 'process', { data: 'hello' });
    await tick();
    assert.strictEqual(actor.log.length, 1, 'Pre-membrane: op should work');

    // Step 2: CLI sends $membrane set-local (simulating: matrix membrane daemon.service --set "...")
    const setReply = await sendAndReply(
      ctx.transport,
      'daemon.service',
      '$membrane',
      { action: 'set-local', source: '(effect membrane.deny "MAINTENANCE_MODE")' },
      'daemon.service', // self-set is allowed
    );
    assert.ok(setReply?.ok, '$membrane set-local should succeed');

    // Step 3: Domain ops are now blocked
    sendFire(ctx.transport, 'daemon.service', 'process', { data: 'should-be-blocked' });
    await tick();
    assert.strictEqual(actor.log.length, 1, 'Post-membrane: op should be blocked');

    // Step 4: $ping still works (system command exempt)
    const pingReply = await sendAndReply(ctx.transport, 'daemon.service', '$ping', {});
    assert.strictEqual(pingReply?.status, 'ok', '$ping works through deny membrane');

    // Step 5: $membrane get shows current state
    const getReply = await sendAndReply(
      ctx.transport,
      'daemon.service',
      '$membrane',
      { action: 'get' },
      'anyone',
    );
    assert.ok(getReply?.ok, '$membrane get should work');
    assert.ok(getReply?.local?.includes('MAINTENANCE_MODE'), 'Should show current membrane source');

    // Step 6: CLI clears the membrane
    const clearReply = await sendAndReply(
      ctx.transport,
      'daemon.service',
      '$membrane',
      { action: 'clear-local' },
      'daemon.service',
    );
    assert.ok(clearReply?.ok, 'clear-local should succeed');

    // Step 7: Ops work again
    sendFire(ctx.transport, 'daemon.service', 'process', { data: 'working-again' });
    await tick();
    assert.strictEqual(actor.log.length, 2, 'Post-clear: ops should work again');
  });

  it('unauthorized sender cannot set membrane', async () => {
    const actor = new ServiceActor();
    const ctx = rootContext.derive('daemon.secure');
    await actor.initialize(ctx, 'daemon.secure');

    // Evil actor tries to set membrane
    const evilReply = await sendAndReply(
      ctx.transport,
      'daemon.secure',
      '$membrane',
      { action: 'set-local', source: '(effect membrane.deny "pwned")' },
      'evil.hacker',
    );

    // Should be rejected
    assert.ok(!evilReply?.ok || evilReply?.error, 'Non-self/parent/system sender should be rejected');

    // Ops still work (membrane was not set)
    sendFire(ctx.transport, 'daemon.secure', 'process', { data: 'still-works' });
    await tick();
    assert.strictEqual(actor.log.length, 1, 'Actor should still accept domain ops');
  });
});

// ─── Canary 2: Rate-limiting membrane ────────────────────────────────────────

describe('Canary: Developer applies rate-limiting membrane', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('rate-limiting membrane allows first N calls, blocks the rest', async () => {
    const actor = new ServiceActor();
    const ctx = rootContext.derive('api.gateway');
    await actor.initialize(ctx, 'api.gateway');

    // Set rate-limiting membrane: 2 calls per 60s window
    actor.setLocalMembrane(
      '(if (effect membrane.rate-check sender 2 60000) (effect membrane.allow op payload) (effect membrane.deny "RATE_LIMITED"))',
    );

    // First 2 calls from same sender should work
    sendFire(ctx.transport, 'api.gateway', 'query', { filter: 'a' }, 'client-A');
    await tick();
    sendFire(ctx.transport, 'api.gateway', 'query', { filter: 'b' }, 'client-A');
    await tick();
    assert.strictEqual(actor.log.length, 2, 'First 2 calls within rate limit');

    // 3rd call from same sender should be blocked
    sendFire(ctx.transport, 'api.gateway', 'query', { filter: 'c' }, 'client-A');
    await tick();
    assert.strictEqual(actor.log.length, 2, '3rd call should be rate-limited');

    // Different sender should still work
    sendFire(ctx.transport, 'api.gateway', 'query', { filter: 'd' }, 'client-B');
    await tick();
    assert.strictEqual(actor.log.length, 3, 'Different sender should not be rate-limited');
  });
});

// ─── Canary 3: LLM agent encounters membrane ────────────────────────────────

describe('Canary: LLM agent invoke hits membrane', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('agent gets deny error, system commands still introspectable', async () => {
    const actor = new ServiceActor();
    const ctx = rootContext.derive('daemon.protected');
    await actor.initialize(ctx, 'daemon.protected');

    // Admin sets deny membrane
    actor.setLocalMembrane('(effect membrane.deny "ADMIN_LOCKED")');

    // Agent tries to invoke process (gets denied)
    const processReply = await sendAndReply(
      ctx.transport,
      'daemon.protected',
      'process',
      { data: 'from-agent' },
      'system.llm.session-123',
    );
    // When membrane denies, the handler doesn't fire — no reply or error reply
    assert.strictEqual(actor.log.length, 0, 'Membrane should block agent invoke');

    // But agent can still $introspect (exempt)
    const introReply = await sendAndReply(
      ctx.transport,
      'daemon.protected',
      '$introspect',
      {},
      'system.llm.session-123',
    );
    assert.ok(introReply, '$introspect should work through deny membrane');

    // And $membrane get works (exempt) — agent can SEE why it's blocked
    const memReply = await sendAndReply(
      ctx.transport,
      'daemon.protected',
      '$membrane',
      { action: 'get' },
      'system.llm.session-123',
    );
    assert.ok(memReply?.ok, 'Agent should be able to read membrane state');
    assert.ok(memReply?.local?.includes('ADMIN_LOCKED'), 'Agent should see the deny reason');
  });
});

// ─── Canary 4: Parent propagates audit membrane to subtree ───────────────────

describe('Canary: Parent propagates audit membrane to children', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('parent sets inherited membrane → child logs all ops + allows', async () => {
    const parent = new SupervisorActor();
    const pCtx = rootContext.derive('team.supervisor');
    await parent.initialize(pCtx, 'team.supervisor');

    const worker = new ServiceActor();
    const wCtx = rootContext.derive('team.supervisor.worker');
    await worker.initialize(wCtx, 'team.supervisor.worker');

    // Supervisor sets audit membrane on worker via $membrane-inherit
    // This membrane logs every op then allows it through
    sendFire(
      wCtx.transport,
      'team.supervisor.worker',
      '$membrane-inherit',
      {
        sources: [
          '(begin (effect membrane.log (string-append "AUDIT: " op " from " sender)) (effect membrane.allow op payload))',
        ],
      },
      'team.supervisor', // parent mount
    );
    await tick();

    // Worker should have inherited the membrane
    assert.ok(worker.getMembrane(), 'Worker should have membrane after inherit');

    // Send a domain op
    sendFire(wCtx.transport, 'team.supervisor.worker', 'process', { data: 'audited-work' }, 'external.client');
    await tick();

    // Op should have gone through (audit membrane allows)
    assert.strictEqual(worker.log.length, 1, 'Audit membrane should allow ops through');
    assert.deepStrictEqual(worker.log[0].payload, { data: 'audited-work' });
  });

  it('parent deny membrane cannot be overridden by child local allow', async () => {
    const worker = new ServiceActor();
    const wCtx = rootContext.derive('lockdown.worker');
    await worker.initialize(wCtx, 'lockdown.worker');

    // Parent sends deny-all inherited membrane
    sendFire(
      wCtx.transport,
      'lockdown.worker',
      '$membrane-inherit',
      { sources: ['(effect membrane.deny "PARENT_LOCKDOWN")'] },
      'lockdown',
    );
    await tick();

    // Child tries to set local allow (should NOT override inherited deny)
    worker.setLocalMembrane('(effect membrane.allow op payload)');

    // Domain op should STILL be blocked (inherited runs first, denies before local executes)
    sendFire(wCtx.transport, 'lockdown.worker', 'process', { data: 'escape-attempt' });
    await tick();
    assert.strictEqual(worker.log.length, 0, 'Inherited deny should not be bypassable by local allow');
  });
});

// ─── Canary 5: Broken membrane recovery ─────────────────────────────────────

describe('Canary: Broken membrane recovery', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('syntax error in membrane → fail-open → actor still usable', async () => {
    const actor = new ServiceActor();
    const ctx = rootContext.derive('daemon.buggy');
    await actor.initialize(ctx, 'daemon.buggy');

    // Set a syntactically broken membrane (unclosed paren)
    let setError = false;
    try {
      actor.setLocalMembrane('(effect membrane.deny "test"');
    } catch {
      setError = true;
    }
    // Compile error should be caught at set time
    assert.ok(setError, 'Broken membrane should throw at compile time');

    // Actor should still work (membrane was not set due to compile error)
    sendFire(ctx.transport, 'daemon.buggy', 'process', { data: 'still-alive' });
    await tick();
    assert.strictEqual(actor.log.length, 1, 'Actor should still work after failed membrane set');
  });

  it('$membrane clear-local fixes a bricked actor', async () => {
    const actor = new ServiceActor();
    const ctx = rootContext.derive('daemon.bricked');
    await actor.initialize(ctx, 'daemon.bricked');

    // Set deny-all membrane (simulates misconfiguration)
    actor.setLocalMembrane('(effect membrane.deny "OOPS")');

    // Verify it's blocked
    sendFire(ctx.transport, 'daemon.bricked', 'process', { data: 'blocked' });
    await tick();
    assert.strictEqual(actor.log.length, 0, 'Actor should be blocked');

    // Use $membrane clear-local to fix it (this is exempt from membrane)
    const clearReply = await sendAndReply(
      ctx.transport,
      'daemon.bricked',
      '$membrane',
      { action: 'clear-local' },
      'daemon.bricked',
    );
    assert.ok(clearReply?.ok, 'clear-local should work even on bricked actor');

    // Actor is unbricked
    sendFire(ctx.transport, 'daemon.bricked', 'process', { data: 'fixed' });
    await tick();
    assert.strictEqual(actor.log.length, 1, 'Actor should work after clear-local');
  });
});

// ─── Canary 6: Mutation membrane rewrites payloads ───────────────────────────

describe('Canary: Mutation membrane rewrites payloads', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('membrane injects audit metadata into payload', async () => {
    const actor = new ServiceActor();
    const ctx = rootContext.derive('daemon.audited');
    await actor.initialize(ctx, 'daemon.audited');

    // Set a mutation membrane that injects an audit field
    actor.setLocalMembrane(
      '(effect membrane.mutate (string-append "{\\\"audited\\\":true,\\\"data\\\":\\\"injected\\\"}") )',
    );

    sendFire(ctx.transport, 'daemon.audited', 'process', { data: 'original' });
    await tick();

    assert.strictEqual(actor.log.length, 1, 'Mutated message should be delivered');
    // The handler receives the MUTATED payload, not the original
    assert.deepStrictEqual(actor.log[0].payload, { audited: true, data: 'injected' });
  });
});

// ─── Canary 7: Multi-layer inheritance ───────────────────────────────────────

describe('Canary: Three-level membrane inheritance', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('grandparent audit + parent rate-limit + child allow = all three run', async () => {
    const grandchild = new ServiceActor();
    const ctx = rootContext.derive('org.team.worker');
    await grandchild.initialize(ctx, 'org.team.worker');

    // Parent sends both grandparent's and parent's membranes as inherited sources
    sendFire(
      ctx.transport,
      'org.team.worker',
      '$membrane-inherit',
      {
        sources: [
          // Grandparent: audit log
          '(begin (effect membrane.log (string-append "GP-AUDIT: " op)) (effect membrane.allow op payload))',
          // Parent: rate check (10 per 60s)
          '(if (effect membrane.rate-check sender 10 60000) (effect membrane.allow op payload) (effect membrane.deny "TEAM_RATE_LIMIT"))',
        ],
      },
      'org.team', // direct parent
    );
    await tick();

    assert.ok(grandchild.getMembrane(), 'Grandchild should have membrane from inheritance');

    // Send a domain op
    sendFire(ctx.transport, 'org.team.worker', 'process', { data: 'multi-layer' }, 'external');
    await tick();

    // Should go through both layers and arrive
    assert.strictEqual(grandchild.log.length, 1, 'Message should pass through all inherited layers');
  });
});

// ─── Canary 8: Hot-swap membrane while traffic flows ─────────────────────────

describe('Canary: Hot-swap membrane during traffic', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('switch from allow → deny → allow without losing messages', async () => {
    const actor = new ServiceActor();
    const ctx = rootContext.derive('daemon.hotswap');
    await actor.initialize(ctx, 'daemon.hotswap');

    // Phase 1: No membrane — ops flow
    sendFire(ctx.transport, 'daemon.hotswap', 'process', { data: 'phase1' });
    await tick();
    assert.strictEqual(actor.log.length, 1, 'Phase 1: no membrane, ops flow');

    // Phase 2: Set deny — ops blocked
    actor.setLocalMembrane('(effect membrane.deny "BLOCKED")');
    sendFire(ctx.transport, 'daemon.hotswap', 'process', { data: 'phase2' });
    await tick();
    assert.strictEqual(actor.log.length, 1, 'Phase 2: deny membrane, ops blocked');

    // Phase 3: Swap to allow — ops flow again
    actor.setLocalMembrane('(effect membrane.allow op payload)');
    sendFire(ctx.transport, 'daemon.hotswap', 'process', { data: 'phase3' });
    await tick();
    assert.strictEqual(actor.log.length, 2, 'Phase 3: swapped to allow, ops flow');

    // Phase 4: Swap to mutation — payloads rewritten
    actor.setLocalMembrane('(effect membrane.mutate "{\\\"rewritten\\\":true}")');
    sendFire(ctx.transport, 'daemon.hotswap', 'process', { data: 'phase4-original' });
    await tick();
    assert.strictEqual(actor.log.length, 3, 'Phase 4: mutation delivers');
    assert.deepStrictEqual(actor.log[2].payload, { rewritten: true }, 'Phase 4: payload rewritten');

    // Phase 5: Clear — back to normal
    actor.clearLocalMembrane();
    sendFire(ctx.transport, 'daemon.hotswap', 'process', { data: 'phase5-normal' });
    await tick();
    assert.strictEqual(actor.log.length, 4, 'Phase 5: cleared, normal flow');
    assert.deepStrictEqual(actor.log[3].payload, { data: 'phase5-normal' }, 'Phase 5: original payload');
  });
});

// ─── Canary 9: Multiple ops with selective membrane ──────────────────────────

describe('Canary: Selective membrane (allow some ops, deny others)', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('membrane that only allows query ops, blocks process and admin', async () => {
    const actor = new ServiceActor();
    const ctx = rootContext.derive('daemon.selective');
    await actor.initialize(ctx, 'daemon.selective');

    // Membrane: allow "query" ops only, deny everything else
    actor.setLocalMembrane(
      '(if (equal? op "query") (effect membrane.allow op payload) (effect membrane.deny "QUERY_ONLY"))',
    );

    // query should work
    sendFire(ctx.transport, 'daemon.selective', 'query', { filter: 'users' });
    await tick();
    assert.strictEqual(actor.log.length, 1, 'query op should be allowed');
    assert.strictEqual(actor.log[0].op, 'query');

    // process should be blocked
    sendFire(ctx.transport, 'daemon.selective', 'process', { data: 'nope' });
    await tick();
    assert.strictEqual(actor.log.length, 1, 'process op should be denied');

    // admin.reset should be blocked
    sendFire(ctx.transport, 'daemon.selective', 'admin.reset', {});
    await tick();
    assert.strictEqual(actor.log.length, 1, 'admin.reset op should be denied');

    // $ping still works (exempt)
    const pingReply = await sendAndReply(ctx.transport, 'daemon.selective', '$ping', {});
    assert.strictEqual(pingReply?.status, 'ok', '$ping exempt from selective membrane');
  });
});

// ─── Canary 10: Stress test — membrane handles rapid fire ───────────────────

describe('Canary: Membrane under rapid fire', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('100 messages through allow membrane — all delivered', async () => {
    const actor = new ServiceActor();
    const ctx = rootContext.derive('perf.rapid');
    await actor.initialize(ctx, 'perf.rapid');

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

    const N = 100;
    for (let i = 0; i < N; i++) {
      sendFire(ctx.transport, 'perf.rapid', 'process', { idx: i });
    }
    await tick(200); // give time for all to process

    assert.strictEqual(actor.log.length, N, `All ${N} messages should be delivered through membrane`);
  });

  it('100 messages through deny membrane — all blocked', async () => {
    const actor = new ServiceActor();
    const ctx = rootContext.derive('perf.deny');
    await actor.initialize(ctx, 'perf.deny');

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

    const N = 100;
    for (let i = 0; i < N; i++) {
      sendFire(ctx.transport, 'perf.deny', 'process', { idx: i });
    }
    await tick(200);

    assert.strictEqual(actor.log.length, 0, `All ${N} messages should be blocked by deny membrane`);
  });
});
