/**
 * runtime-integration.test.ts — Runtime-level integration tests.
 *
 * Tests the REAL MatrixRuntime with multiple actors communicating
 * over the real transport. No mocks. These verify the system works
 * end-to-end at the runtime level.
 *
 * Covers:
 *   - VISION.md §2: Runtime is an actor
 *   - VISION.md §3: Terminology (no daemon.*)
 *   - VISION.md §7: Shell as system service
 *   - CONSTRAINTS.md Invariant 1: Actor = process + directory + file
 *   - CONSTRAINTS.md Invariant 2: Runtime IS root actor
 *   - CONSTRAINTS.md Invariant 7: Service cascade
 *   - CONSTRAINTS.md Invariant 8: System commands membrane-exempt
 *   - CONSTRAINTS.md Invariant 9: No "daemon" in architecture
 */
import '../../../_node-compat.js';

import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { MatrixRuntime } from '../../../runtime/MatrixRuntime.js';
import { MatrixActor } from '../../MatrixActor.js';
import { TopicRouter } from '../../../engine/messaging/TopicRouter.js';
import type { MxEnvelope } from '../../../engine/messaging/MxEnvelope.js';
import { replyErrorMessage, type TestTransport, unwrapReply } from './helpers.js';

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

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

class CounterActor extends MatrixActor {
  static accepts = {
    'counter.increment': { description: 'Increment counter' },
    'counter.get': { description: 'Get current value' },
    'counter.reset': { description: 'Reset to zero' },
  };
  private _value = 0;

  onCounterIncrement(payload: { amount?: number }) {
    this._value += payload?.amount ?? 1;
    return { ok: true, value: this._value };
  }

  onCounterGet() {
    return { ok: true, value: this._value };
  }

  onCounterReset() {
    this._value = 0;
    return { ok: true, value: 0 };
  }
}

class EchoActor extends MatrixActor {
  static accepts = {
    'echo': { description: 'Echo back payload' },
    'echo.upper': { description: 'Echo uppercase' },
  };

  onEcho(payload: unknown) {
    return { ok: true, echo: payload };
  }

  onEchoUpper(payload: { text: string }) {
    return { ok: true, echo: payload.text?.toUpperCase?.() ?? '' };
  }
}

class HealthActor extends MatrixActor {
  static accepts = {
    'health.check': { description: 'Health check' },
  };

  onHealthCheck() {
    return { ok: true, healthy: true, mount: this.mount, ts: Date.now() };
  }
}

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

async function invoke<T = Record<string, unknown>>(
  transport: TestTransport,
  mount: string,
  op: string,
  payload?: unknown,
  timeoutMs = 3000,
): Promise<T> {
  return new Promise((resolve, reject) => {
    const cid = `test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
    const replyTopic = `$replies.${cid}`;
    const timer = setTimeout(() => {
      unsub();
      reject(new Error(`Timeout waiting for reply to ${op} on ${mount}`));
    }, timeoutMs);

    const unsub = transport.subscribe(replyTopic, (msg: unknown) => {
      clearTimeout(timer);
      unsub();
      const reply = typeof msg === 'object' && msg !== null ? msg as { ok?: boolean; result?: unknown } : null;
      if (reply?.ok === true) resolve(reply.result as T);
      else if (reply?.ok === false) reject(new Error(replyErrorMessage(msg, JSON.stringify(msg))));
      else resolve(unwrapReply(msg) as T);
    });

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

// ═══════════════════════════════════════════════════════════════════════════════
// SUITE 1: Runtime boots and root actor works
// ═══════════════════════════════════════════════════════════════════════════════

describe('Runtime integration — boot and root actor', () => {
  let runtime: MatrixRuntime;

  before(async () => {
    runtime = new MatrixRuntime({ logging: false });
    await tick(100);
  });

  after(async () => {
    if (runtime) await runtime.shutdown();
  });

  it('1. Root actor responds to $ping via transport', async () => {
    const result = await invoke(runtime.transport, '', '$ping');
    assert.equal(result.status, 'ok');
    assert.equal(result.type, 'runtime');
  });

  it('2. Root actor $introspect returns type=runtime', async () => {
    const result = await invoke(runtime.transport, '', '$introspect');
    assert.equal(result.type, 'runtime');
    assert.ok(typeof result.uptime === 'number');
    assert.ok(typeof result.realm === 'string');
  });

  it('3. runtime.status returns same data as $introspect', async () => {
    const introspect = await invoke(runtime.transport, '', '$introspect');
    const status = await invoke(runtime.transport, '', 'runtime.status');
    assert.equal(status.type, introspect.type);
    assert.equal(status.realm, introspect.realm);
  });

  it('4. Root actor mount is empty string', async () => {
    const result = await invoke(runtime.transport, '', '$introspect');
    assert.equal(result.mount, '');
  });
});

// ═══════════════════════════════════════════════════════════════════════════════
// SUITE 2: Multiple actors communicate through runtime
// ═══════════════════════════════════════════════════════════════════════════════

describe('Runtime integration — multi-actor communication', () => {
  let runtime: MatrixRuntime;

  before(async () => {
    runtime = new MatrixRuntime({ logging: false });
    await tick(100);

    // Create multiple actors at different mount paths
    await runtime.create(CounterActor, 'system.counter');
    await runtime.create(EchoActor, 'system.echo');
    await runtime.create(HealthActor, 'system.health');
    await tick(100);
  });

  after(async () => {
    if (runtime) await runtime.shutdown();
  });

  it('5. Counter actor responds to domain ops', async () => {
    const r1 = await invoke(runtime.transport, 'system.counter', 'counter.increment', { amount: 5 });
    assert.equal(r1.value, 5);
    const r2 = await invoke(runtime.transport, 'system.counter', 'counter.increment', { amount: 3 });
    assert.equal(r2.value, 8);
    const r3 = await invoke(runtime.transport, 'system.counter', 'counter.get');
    assert.equal(r3.value, 8);
  });

  it('6. Echo actor responds independently from counter', async () => {
    const result = await invoke(runtime.transport, 'system.echo', 'echo', { msg: 'hello' });
    assert.deepEqual(result.echo, { msg: 'hello' });
  });

  it('7. Health actor reports its own mount path', async () => {
    const result = await invoke(runtime.transport, 'system.health', 'health.check');
    assert.equal(result.healthy, true);
    assert.equal(result.mount, 'system.health');
  });

  it('8. All three actors respond to $ping (system command)', async () => {
    const [r1, r2, r3] = await Promise.all([
      invoke(runtime.transport, 'system.counter', '$ping'),
      invoke(runtime.transport, 'system.echo', '$ping'),
      invoke(runtime.transport, 'system.health', '$ping'),
    ]);
    assert.equal(r1.status, 'ok');
    assert.equal(r2.status, 'ok');
    assert.equal(r3.status, 'ok');
  });

  it('9. All three actors respond to $introspect', async () => {
    const [r1, r2, r3] = await Promise.all([
      invoke(runtime.transport, 'system.counter', '$introspect'),
      invoke(runtime.transport, 'system.echo', '$introspect'),
      invoke(runtime.transport, 'system.health', '$introspect'),
    ]);
    assert.equal(r1.componentClass, 'CounterActor');
    assert.equal(r2.componentClass, 'EchoActor');
    assert.equal(r3.componentClass, 'HealthActor');
  });

  it('10. Root actor sees all children', async () => {
    const result = await invoke<{ actors: number }>(runtime.transport, '', '$introspect');
    assert.ok(result.actors >= 3, `Expected >=3 actors, got ${result.actors}`);
  });

  it('11. Actors mount at system.* (no daemon prefix)', async () => {
    const result = await invoke(runtime.transport, 'system.counter', '$introspect');
    assert.equal(result.mount, 'system.counter');
    assert.ok(!result.mount.includes('daemon'), 'Mount should NOT contain daemon');
  });

  it('12. Counter reset works independently', async () => {
    await invoke(runtime.transport, 'system.counter', 'counter.reset');
    const result = await invoke(runtime.transport, 'system.counter', 'counter.get');
    assert.equal(result.value, 0);
  });
});

// ═══════════════════════════════════════════════════════════════════════════════
// SUITE 3: Membrane enforcement through real runtime
// ═══════════════════════════════════════════════════════════════════════════════

describe('Runtime integration — membrane enforcement', () => {
  let runtime: MatrixRuntime;

  before(async () => {
    runtime = new MatrixRuntime({ logging: false });
    await tick(100);
    await runtime.create(CounterActor, 'guarded.counter');
    await runtime.create(EchoActor, 'guarded.echo');
    await tick(100);
  });

  after(async () => {
    if (runtime) await runtime.shutdown();
  });

  it('13. Set deny-all membrane via $membrane — domain ops blocked', async () => {
    // Set deny-all membrane
    const setResult = await invoke(runtime.transport, 'guarded.counter', '$membrane', {
      action: 'set-local',
      source: '(effect membrane.deny "BLOCKED_BY_MEMBRANE")',
    });
    assert.equal(setResult.ok, true);
    await tick(50);

    // Domain op should be denied
    try {
      await invoke(runtime.transport, 'guarded.counter', 'counter.increment', { amount: 1 });
      assert.fail('Should have been denied by membrane');
    } catch (err: unknown) {
      assert.ok(err instanceof Error);
      assert.ok(err.message.includes('BLOCKED_BY_MEMBRANE') || err.message.includes('denied'),
        `Expected membrane deny, got: ${err.message}`);
    }
  });

  it('14. System commands bypass deny-all membrane', async () => {
    // $ping should still work through deny-all
    const ping = await invoke(runtime.transport, 'guarded.counter', '$ping');
    assert.equal(ping.status, 'ok');

    // $introspect should still work
    const intro = await invoke(runtime.transport, 'guarded.counter', '$introspect');
    assert.equal(intro.componentClass, 'CounterActor');
  });

  it('15. Clear membrane — domain ops resume', async () => {
    // Clear the membrane
    const clearResult = await invoke(runtime.transport, 'guarded.counter', '$membrane', {
      action: 'clear-local',
    });
    assert.equal(clearResult.ok, true);
    await tick(50);

    // Domain ops should work again
    const result = await invoke(runtime.transport, 'guarded.counter', 'counter.increment', { amount: 10 });
    assert.equal(result.value, 10);
  });

  it('16. Rate-limiting membrane allows first N, blocks rest', async () => {
    // Set rate-limit membrane (allow 3, then deny)
    const rateLimitSource = `
      (begin
        (effect membrane.rate-check "counter-ops" 3 10000)
        (effect membrane.allow op payload))
    `;
    await invoke(runtime.transport, 'guarded.echo', '$membrane', {
      action: 'set-local',
      source: rateLimitSource,
    });
    await tick(50);

    // First 3 should pass
    for (let i = 0; i < 3; i++) {
      const result = await invoke(runtime.transport, 'guarded.echo', 'echo', { n: i });
      assert.ok(result.echo, `Call ${i} should succeed`);
    }

    // 4th should be denied
    try {
      await invoke(runtime.transport, 'guarded.echo', 'echo', { n: 3 });
      assert.fail('4th call should have been rate-limited');
    } catch (err: unknown) {
      assert.ok(err instanceof Error);
      assert.ok(err.message.includes('rate') || err.message.includes('denied') || err.message.includes('Rate'),
        `Expected rate limit error, got: ${err.message}`);
    }
  });

  it('17. Membrane mutation rewrites payload before handler', async () => {
    // Clear rate limit first
    await invoke(runtime.transport, 'guarded.echo', '$membrane', { action: 'clear-local' });
    await tick(50);

    // Set mutating membrane that injects metadata
    await invoke(runtime.transport, 'guarded.echo', '$membrane', {
      action: 'set-local',
      source: '(effect membrane.mutate "{\\"injected\\":true}")',
    });
    await tick(50);

    const result = await invoke(runtime.transport, 'guarded.echo', 'echo', { original: true });
    // The payload should be the mutated one
    assert.deepEqual(result.echo, { injected: true });
  });
});

// ═══════════════════════════════════════════════════════════════════════════════
// SUITE 4: Cross-actor scenarios through runtime
// ═══════════════════════════════════════════════════════════════════════════════

describe('Runtime integration — cross-actor scenarios', () => {
  let runtime: MatrixRuntime;

  before(async () => {
    runtime = new MatrixRuntime({ logging: false });
    await tick(100);
    await runtime.create(CounterActor, 'app.counter-a');
    await runtime.create(CounterActor, 'app.counter-b');
    await runtime.create(HealthActor, 'app.health');
    await tick(100);
  });

  after(async () => {
    if (runtime) await runtime.shutdown();
  });

  it('18. Two counters are completely independent', async () => {
    await invoke(runtime.transport, 'app.counter-a', 'counter.increment', { amount: 10 });
    await invoke(runtime.transport, 'app.counter-b', 'counter.increment', { amount: 20 });

    const a = await invoke(runtime.transport, 'app.counter-a', 'counter.get');
    const b = await invoke(runtime.transport, 'app.counter-b', 'counter.get');

    assert.equal(a.value, 10);
    assert.equal(b.value, 20);
  });

  it('19. Concurrent invokes to different actors work correctly', async () => {
    const [r1, r2, r3] = await Promise.all([
      invoke<{ value: number }>(runtime.transport, 'app.counter-a', 'counter.increment', { amount: 1 }),
      invoke<{ value: number }>(runtime.transport, 'app.counter-b', 'counter.increment', { amount: 1 }),
      invoke<{ healthy: boolean }>(runtime.transport, 'app.health', 'health.check'),
    ]);
    assert.ok(r1.value > 0);
    assert.ok(r2.value > 0);
    assert.equal(r3.healthy, true);
  });

  it('20. Membrane on one actor does not affect siblings', async () => {
    // Deny all on counter-a
    await invoke(runtime.transport, 'app.counter-a', '$membrane', {
      action: 'set-local',
      source: '(effect membrane.deny "A_BLOCKED")',
    });
    await tick(50);

    // counter-b should still work fine
    const result = await invoke<{ value: number }>(
      runtime.transport,
      'app.counter-b',
      'counter.increment',
      { amount: 100 },
    );
    assert.ok(result.value > 0);

    // health should still work fine
    const health = await invoke(runtime.transport, 'app.health', 'health.check');
    assert.equal(health.healthy, true);

    // counter-a should be blocked
    try {
      await invoke(runtime.transport, 'app.counter-a', 'counter.increment', { amount: 1 });
      assert.fail('Should be blocked');
    } catch {
      // Expected
    }

    // Clean up
    await invoke(runtime.transport, 'app.counter-a', '$membrane', { action: 'clear-local' });
  });

  it('21. Rapid sequential invokes to same actor work correctly', async () => {
    await invoke(runtime.transport, 'app.counter-a', 'counter.reset');
    for (let i = 0; i < 20; i++) {
      await invoke(runtime.transport, 'app.counter-a', 'counter.increment', { amount: 1 });
    }
    const result = await invoke(runtime.transport, 'app.counter-a', 'counter.get');
    assert.equal(result.value, 20);
  });

  it('22. Runtime survives actor error gracefully', async () => {
    // Send a bad op to an actor — should not crash the runtime
    try {
      await invoke(runtime.transport, 'app.counter-a', 'nonexistent.op', {}, 1000);
    } catch {
      // Expected — no handler for this op
    }

    // Runtime should still be alive
    const ping = await invoke(runtime.transport, '', '$ping');
    assert.equal(ping.status, 'ok');

    // Other actors should still work
    const health = await invoke(runtime.transport, 'app.health', 'health.check');
    assert.equal(health.healthy, true);
  });
});
