/**
 * Regression Test Suite — Tests #165-175
 *
 * Verifies that existing MatrixActor functionality still works correctly
 * with the membrane layer installed. Headless tests using InMemoryBroker.
 */
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';
import type { ChildManager } from '../../children/ChildManager.js';

// ─── 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());
}

interface ReplyPayload {
  ok?: boolean;
  inherited?: unknown[];
  local?: string | null;
  params?: ReplyPayload;
  [key: string]: unknown;
}

type ParentWithChildren = ParentActor & { _children?: ChildManager };

// ─── Test actors ─────────────────────────────────────────────────────────────

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

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

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

  onGetState() {
    this.calls.push({ op: 'getState', payload: undefined });
    return { ok: true };
  }
}

class ParentActor extends MatrixActor {
  static accepts = { 'parentOp': {} };

  onParentOp(payload: unknown) {
    return { ok: true };
  }
}

// ═════════════════════════════════════════════════════════════════════════════
// #165 All SYSTEM_COMMANDS dispatch correctly
// ═════════════════════════════════════════════════════════════════════════════

describe('Regression: SYSTEM_COMMANDS (#165)', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('#165a $ping works (no membrane)', async () => {
    const ctx = rootContext.derive('ping-a');
    const actor = new WorkerActor();
    await actor.initialize(ctx, 'ping-a');
    await tick();

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

    ctx.transport.publish(TopicRouter.inbox('ping-a'), {
      op: '$ping',
      payload: {},
      replyTo: replyTopic,
      correlationId: 'ping-a',
    } as MxEnvelope);
    await tick(100);

    assert.ok(replyReceived, '$ping should get a reply');
  });

  it('#165b $introspect works (no membrane)', async () => {
    const ctx = rootContext.derive('intro-a');
    const actor = new WorkerActor();
    await actor.initialize(ctx, 'intro-a');
    await tick();

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

    ctx.transport.publish(TopicRouter.inbox('intro-a'), {
      op: '$introspect',
      payload: {},
      replyTo: replyTopic,
      correlationId: 'intro-a',
    } as MxEnvelope);
    await tick(100);

    assert.ok(replyPayload, '$introspect should get a reply');
  });

  it('#165c $membrane returns state (no membrane installed)', async () => {
    const ctx = rootContext.derive('mem-a');
    const actor = new WorkerActor();
    await actor.initialize(ctx, 'mem-a');
    await tick();

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

    ctx.transport.publish(TopicRouter.inbox('mem-a'), {
      op: '$membrane',
      payload: {},
      replyTo: replyTopic,
      correlationId: 'mem-a',
    } as MxEnvelope);
    await tick(100);

    assert.ok(replyPayload, '$membrane should get a reply');
    const params = ((replyPayload as { params?: ReplyPayload } | null)?.params ?? replyPayload) as ReplyPayload;
    assert.strictEqual(params.ok, true);
    // inherited may be [] or undefined depending on envelope wrapping
    assert.ok(!params.inherited?.length, 'Should have no inherited membranes');
    assert.ok(!params.local, 'Should have no local membrane');
  });

  it('#165d $membrane set-local installs membrane', async () => {
    const ctx = rootContext.derive('set-a');
    const actor = new WorkerActor();
    await actor.initialize(ctx, 'set-a');
    await tick();

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

    ctx.transport.publish(TopicRouter.inbox('set-a'), {
      op: '$membrane',
      payload: { action: 'set-local', source: '(effect membrane.allow op payload)' },
      replyTo: replyTopic,
      correlationId: 'set-a',
    } as MxEnvelope);
    await tick(100);

    assert.ok(replyPayload, '$membrane set-local should reply');
    const params = ((replyPayload as { params?: ReplyPayload } | null)?.params ?? replyPayload) as ReplyPayload;
    assert.strictEqual(params.ok, true);
    assert.ok(actor.getMembrane(), 'Membrane engine should exist');
  });
});

// ═════════════════════════════════════════════════════════════════════════════
// #166 addChild still works
// ═════════════════════════════════════════════════════════════════════════════

describe('Regression: addChild (#166)', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('#166 addChild creates child in parent registry', async () => {
    const ctx = rootContext.derive('parent');
    const parent = new ParentActor();
    await parent.initialize(ctx, 'parent');
    await tick();

    const result = await parent.addChild('child1', WorkerActor);
    await tick(200);

    // Verify addChild returned successfully
    assert.ok(result, 'addChild should return a result');

    // Verify parent has ChildManager with the child registered
    const cm = (parent as ParentWithChildren)._children;
    assert.ok(cm, 'ChildManager should exist after addChild');
  });
});

// ═════════════════════════════════════════════════════════════════════════════
// #167 removeChild still works
// ═════════════════════════════════════════════════════════════════════════════

describe('Regression: removeChild (#167)', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('#167 removeChild removes child from parent', async () => {
    const ctx = rootContext.derive('parent');
    const parent = new ParentActor();
    await parent.initialize(ctx, 'parent');
    await tick();

    await parent.addChild('child1', WorkerActor);
    await tick(200);

    await parent.removeChild('child1');
    await tick(100);

    // Child manager should no longer have the child
    const cm = (parent as ParentWithChildren)._children;
    const child = cm?.getChildMount?.('child1');
    assert.ok(!child, 'Child should be removed');
  });
});

// ═════════════════════════════════════════════════════════════════════════════
// #168 Request/Reply still works
// ═════════════════════════════════════════════════════════════════════════════

describe('Regression: Request/Reply (#168)', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('#168 request with replyTo gets response', async () => {
    const ctx = rootContext.derive('rr');
    const actor = new WorkerActor();
    await actor.initialize(ctx, 'rr');
    await tick();

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

    ctx.transport.publish(TopicRouter.inbox('rr'), {
      op: 'doWork',
      payload: { data: 'hello' },
      replyTo: replyTopic,
      correlationId: 'rr-168',
    } as MxEnvelope);
    await tick(100);

    assert.ok(replyPayload, 'Should receive reply');
    const params = ((replyPayload as { params?: ReplyPayload } | null)?.params ?? replyPayload) as ReplyPayload;
    assert.strictEqual(params.ok, true);
  });
});

// ═════════════════════════════════════════════════════════════════════════════
// #169 Fire-and-forget still works
// ═════════════════════════════════════════════════════════════════════════════

describe('Regression: Fire-and-forget (#169)', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('#169 message without replyTo dispatches to handler', async () => {
    const ctx = rootContext.derive('ff');
    const actor = new WorkerActor();
    await actor.initialize(ctx, 'ff');
    await tick();

    ctx.transport.publish(TopicRouter.inbox('ff'), {
      op: 'doWork',
      payload: { data: 'fire-forget' },
    } as MxEnvelope);
    await tick(100);

    assert.strictEqual(actor.calls.length, 1);
    assert.deepStrictEqual(actor.calls[0].payload, { data: 'fire-forget' });
  });
});

// ═════════════════════════════════════════════════════════════════════════════
// #170-175 With active membrane
// ═════════════════════════════════════════════════════════════════════════════

describe('Regression: With active membrane (#170-175)', () => {
  let broker: InMemoryBroker;
  let rootContext: IMatrixContext;

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

  it('#170 system commands work through allow-all membrane', async () => {
    const ctx = rootContext.derive('am170');
    const actor = new WorkerActor();
    await actor.initialize(ctx, 'am170');
    actor.setLocalMembrane('(effect membrane.allow op payload)');
    await tick();

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

    ctx.transport.publish(TopicRouter.inbox('am170'), {
      op: '$ping',
      payload: {},
      replyTo: replyTopic,
      correlationId: 'am170',
    } as MxEnvelope);
    await tick(100);

    assert.ok(replyReceived, '$ping should work through membrane');
  });

  it('#171 domain ops work through allow-all membrane', async () => {
    const ctx = rootContext.derive('am171');
    const actor = new WorkerActor();
    await actor.initialize(ctx, 'am171');
    actor.setLocalMembrane('(effect membrane.allow op payload)');
    await tick();

    ctx.transport.publish(TopicRouter.inbox('am171'), {
      op: 'doWork',
      payload: { data: 'through-membrane' },
    } as MxEnvelope);
    await tick(100);

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

  it('#172 addChild works when parent has membrane', async () => {
    const ctx = rootContext.derive('am172');
    const parent = new ParentActor();
    await parent.initialize(ctx, 'am172');
    parent.setLocalMembrane('(effect membrane.allow op payload)');
    await tick();

    const result = await parent.addChild('child1', WorkerActor);
    await tick(200);

    // Verify addChild succeeded even with membrane active
    assert.ok(result, 'addChild should succeed when parent has membrane');
    const cm = (parent as ParentWithChildren)._children;
    assert.ok(cm, 'ChildManager should exist');
  });

  it('#173 request/reply works through membrane', async () => {
    const ctx = rootContext.derive('am173');
    const actor = new WorkerActor();
    await actor.initialize(ctx, 'am173');
    actor.setLocalMembrane('(effect membrane.allow op payload)');
    await tick();

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

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

    assert.ok(replyPayload, 'Should receive reply through membrane');
    const params = ((replyPayload as { params?: ReplyPayload } | null)?.params ?? replyPayload) as ReplyPayload;
    assert.strictEqual(params.ok, true);
  });

  it('#174 membrane deny produces error reply', async () => {
    const ctx = rootContext.derive('am174');
    const actor = new WorkerActor();
    await actor.initialize(ctx, 'am174');
    actor.setLocalMembrane('(effect membrane.deny "access-denied")');
    await tick();

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

    ctx.transport.publish(TopicRouter.inbox('am174'), {
      op: 'doWork',
      payload: { data: 'denied' },
      replyTo: replyTopic,
      correlationId: 'am174',
    } as MxEnvelope);
    await tick(100);

    assert.ok(replyReceived, 'Should receive error reply for denied message');
    assert.strictEqual(actor.calls.length, 0, 'Handler should NOT be called');
  });

  it('#175 membrane mutate modifies payload before handler', async () => {
    const ctx = rootContext.derive('am175');
    const actor = new WorkerActor();
    await actor.initialize(ctx, 'am175');
    actor.setLocalMembrane('(effect membrane.mutate "{\\"injected\\":true}")');
    await tick();

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

    assert.strictEqual(actor.calls.length, 1, 'Handler should be called with mutated payload');
    assert.deepStrictEqual(actor.calls[0].payload, { injected: true });
  });
});
