/**
 * root-actor.test.ts — Tests for runtime-as-actor (RootActor at mount "").
 *
 * Verifies VISION.md Section 2: "The Runtime Is an Actor"
 * and CONSTRAINTS.md Invariant 2: "Runtime IS the root actor."
 */
// DOM stubs for Node.js (MatrixRuntime transitively imports MatrixActorHtmlElement)
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));

interface RuntimeIntrospectResult {
  type: string;
  mount: string;
  runtimeType?: string;
  realm?: string;
  uptime?: number;
  actors?: number;
  children?: unknown[];
  services?: unknown;
  componentClass?: string;
}

interface RuntimeActorEntry {
  id: string;
  mount: string;
}

interface RuntimeRegisteredResult {
  componentTags?: string[];
  supervisedRoots?: string[];
  requiredPackages?: Array<{
    packageName: string;
    revision?: string;
    sourceOverride?: string;
  }>;
  deploymentInstances?: Array<{
    id: string;
    packageName: string;
    mount: string;
    rootKind: string;
    class: string;
    state: string;
    packageRevision?: string;
    componentType?: string;
  }>;
}

interface RuntimeStatusResult {
  ok?: boolean;
  type?: string;
  running?: boolean;
  actors?: number;
  registeredComponents?: number;
  supervisedRoots?: string[];
  services?: string[];
}

/** Helper: send op to actor at mount via transport and collect reply */
async function invokeViaTransport<T = unknown>(
  transport: TestTransport,
  mount: string,
  op: string,
  payload?: unknown,
  timeoutMs = 2000
): 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?: T } : null;
      if (reply?.ok === true) {
        resolve(reply.result as T);
      } else if (reply?.ok === false) {
        reject(new Error(replyErrorMessage(msg)));
      } else {
        resolve(unwrapReply(msg) as T);
      }
    });

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

describe('RootActor — runtime-as-actor at mount ""', () => {
  let runtime: MatrixRuntime;

  before(async () => {
    runtime = new MatrixRuntime({ logging: false });
    // Give root actor time to initialize (async from constructor)
    await tick(100);
  });

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

  // ─── $introspect ─────────────────────────────────────────────────

  it('1. Root actor responds to $introspect', async () => {
    const result = await invokeViaTransport<RuntimeIntrospectResult>(runtime.transport, '', '$introspect');
    assert.ok(result, 'Should return a result');
    assert.equal(result.type, 'runtime', 'Should report type=runtime');
    assert.ok('uptime' in result, 'Should include uptime');
    assert.ok('actors' in result, 'Should include actor count');
    assert.ok('children' in result, 'Should include children');
    assert.ok('services' in result, 'Should include services');
  });

  it('2. Root actor reports mount ""', async () => {
    const result = await invokeViaTransport<RuntimeIntrospectResult>(runtime.transport, '', '$introspect');
    assert.equal(result.mount, '', 'Mount should be empty string (root)');
  });

  it('3. $introspect includes realm', async () => {
    const result = await invokeViaTransport<RuntimeIntrospectResult>(runtime.transport, '', '$introspect');
    assert.ok('realm' in result, 'Should include realm');
  });

  it('4. $introspect includes runtimeType', async () => {
    const result = await invokeViaTransport<RuntimeIntrospectResult>(runtime.transport, '', '$introspect');
    assert.equal(result.runtimeType, 'headless', 'Should report headless type');
  });

  // ─── $ping ───────────────────────────────────────────────────────

  it('5. Root actor responds to $ping', async () => {
    const result = await invokeViaTransport<{ status?: string; type?: string }>(runtime.transport, '', '$ping');
    assert.equal(result.status, 'ok', 'Should return status ok');
    assert.equal(result.type, 'runtime', 'Should identify as runtime');
  });

  // ─── runtime.status ──────────────────────────────────────────────

  it('6. runtime.status returns full introspect data', async () => {
    const result = await invokeViaTransport<RuntimeIntrospectResult>(runtime.transport, '', 'runtime.status');
    assert.equal(result.type, 'runtime');
    assert.ok(typeof result.uptime === 'number');
  });

  // ─── runtime.services ────────────────────────────────────────────

  it('7. runtime.services returns service map', async () => {
    const result = await invokeViaTransport(runtime.transport, '', 'runtime.services');
    assert.ok(result !== null && result !== undefined, 'Should return services');
  });

  // ─── runtime.actors ──────────────────────────────────────────────

  it('8. runtime.actors returns component list', async () => {
    const result = await invokeViaTransport(runtime.transport, '', 'runtime.actors');
    assert.ok(Array.isArray(result), 'Should return an array');
  });

  it('8b. runtime.registered returns runtime registration inventory', async () => {
    const result = await invokeViaTransport<RuntimeRegisteredResult>(runtime.transport, '', 'runtime.registered');
    assert.ok(Array.isArray(result.componentTags), 'Should include registered component tags');
    assert.ok(Array.isArray(result.supervisedRoots), 'Should include supervised roots');
  });

  it('8c. $children returns running root children', async () => {
    const result = await invokeViaTransport<{ ok?: boolean; children?: string[]; count?: number }>(runtime.transport, '', '$children');
    assert.equal(result.ok, true, 'Should report success');
    assert.ok(Array.isArray(result.children), 'Should return child mounts');
    assert.equal(result.count, result.children?.length, 'Count should match child list length');
  });

  it('8d. $status returns runtime summary', async () => {
    const result = await invokeViaTransport<RuntimeStatusResult>(runtime.transport, '', '$status');
    assert.equal(result.ok, true, 'Should report success');
    assert.equal(result.type, 'runtime', 'Should report runtime type');
    assert.equal(result.running, true, 'Runtime should report running');
    assert.ok(typeof result.actors === 'number', 'Should include actor count');
    assert.ok(typeof result.registeredComponents === 'number', 'Should include registered component count');
    assert.ok(Array.isArray(result.services), 'Should include service keys');
  });

  // ─── Uptime increases ────────────────────────────────────────────

  it('9. Uptime increases over time', async () => {
    const r1 = await invokeViaTransport<RuntimeIntrospectResult>(runtime.transport, '', '$introspect');
    await tick(50);
    const r2 = await invokeViaTransport<RuntimeIntrospectResult>(runtime.transport, '', '$introspect');
    const uptime1 = r1.uptime;
    const uptime2 = r2.uptime;
    if (typeof uptime1 !== 'number' || typeof uptime2 !== 'number') {
      assert.fail('Root introspect should include numeric uptime');
    }
    assert.ok(uptime2 > uptime1, 'Uptime should increase');
  });

  // ─── Children reflect created components ─────────────────────────

  it('10. Children list reflects created components', async () => {
    class TestService extends MatrixActor {
      static accepts = { 'test.op': {} };
    }

    await runtime.create(TestService, 'test-svc-1');
    await tick(100); // Wait for $join handshake

    const result = await invokeViaTransport<RuntimeActorEntry[]>(runtime.transport, '', 'runtime.actors');
    const found = result.some((child) => child.id === 'test-svc-1');
    assert.ok(found, 'Created component should appear in actors list');
  });

  // ─── Rapid $introspect doesn't crash ─────────────────────────────

  it('11. Survives rapid $introspect (burst test)', async () => {
    const promises: Promise<unknown>[] = [];
    for (let i = 0; i < 20; i++) {
      promises.push(invokeViaTransport(runtime.transport, '', '$introspect'));
    }
    const results = await Promise.all(promises);
    assert.equal(results.length, 20, 'All 20 introspect calls should return');
    for (const r of results) {
      assert.equal((r as RuntimeIntrospectResult).type, 'runtime');
    }
  });

  // ─── Root actor has correct componentClass ───────────────────────

  it('12. Root actor reports componentClass=RootActor', async () => {
    const result = await invokeViaTransport<RuntimeIntrospectResult>(runtime.transport, '', '$introspect');
    assert.equal(result.componentClass, 'RootActor', 'Should report class name');
  });

  it('12b. runtime.registered reports registered tags and supervised roots', async () => {
    class DemoElement extends HTMLElement {
      connectedCallback() {}
      disconnectedCallback() {}
    }
    class SupervisedSvc extends MatrixActor {}

    runtime.registerComponent('mx-demo-root-actor', DemoElement);
    await runtime.createSupervised(SupervisedSvc, 'supervised-root');
    await tick(100);

    const result = await invokeViaTransport<RuntimeRegisteredResult>(runtime.transport, '', 'runtime.registered');
    assert.ok(result.componentTags?.includes('mx-demo-root-actor'), 'Registered tag should be visible');
    assert.ok(result.supervisedRoots?.includes('supervised-root'), 'Supervised root should be visible');

    const status = await invokeViaTransport<RuntimeStatusResult>(runtime.transport, '', '$status');
    assert.ok((status.registeredComponents ?? 0) >= 1, 'Status should count registered components');
    assert.ok(status.supervisedRoots?.includes('supervised-root'), 'Status should include supervised roots');
  });

  it('12c. runtime.registered includes deployment instance inventory when provided by the runtime', async () => {
    const rootActor = runtime.getRootActor();
    assert.ok(rootActor, 'Root actor should exist');
    const baseRegisteredSnapshot = rootActor._registeredSnapshot;
    rootActor._registeredSnapshot = () => ({
      ...baseRegisteredSnapshot(),
      requiredPackages: [
        {
          packageName: '@acme/runtime-package',
          revision: '1.2.3',
        },
      ],
      deploymentInstances: [
        {
          id: 'workspace-blackboard',
          packageName: '@acme/runtime-registered',
          mount: 'workspace.blackboard',
          rootKind: 'package-root',
          class: 'service',
          state: 'ready',
        },
      ],
    });

    try {
      const result = await invokeViaTransport<RuntimeRegisteredResult>(runtime.transport, '', 'runtime.registered');
      assert.deepEqual(result.requiredPackages, [
        {
          packageName: '@acme/runtime-package',
          revision: '1.2.3',
        },
      ]);
      assert.deepEqual(result.deploymentInstances, [
        {
          id: 'workspace-blackboard',
          packageName: '@acme/runtime-registered',
          mount: 'workspace.blackboard',
          rootKind: 'package-root',
          class: 'service',
          state: 'ready',
        },
      ]);
    } finally {
      rootActor._registeredSnapshot = baseRegisteredSnapshot;
    }
  });

  it('12d. runtime.remove stops a supervised root without dropping its registration', async () => {
    class RemovableSvc extends MatrixActor {}

    await runtime.createSupervised(RemovableSvc, 'removable-root');
    await tick(100);

    const before = await invokeViaTransport<RuntimeActorEntry[]>(runtime.transport, '', 'runtime.actors');
    assert.ok(before.some((child) => child.mount === 'removable-root'), 'Supervised root should start running');

    const removeResult = await invokeViaTransport<{ ok?: boolean; removed?: boolean; mount?: string }>(
      runtime.transport,
      '',
      'runtime.remove',
      { mount: 'removable-root' },
    );
    assert.equal(removeResult.ok, true, 'runtime.remove should report success');
    assert.equal(removeResult.removed, true, 'runtime.remove should report removal');

    const after = await invokeViaTransport<RuntimeActorEntry[]>(runtime.transport, '', 'runtime.actors');
    assert.ok(!after.some((child) => child.mount === 'removable-root'), 'Removed supervised root should not be running');

    const registered = await invokeViaTransport<RuntimeRegisteredResult>(runtime.transport, '', 'runtime.registered');
    assert.ok(
      registered.supervisedRoots?.includes('removable-root'),
      'runtime.remove should not drop the supervised root registration',
    );
  });

  it('12e. runtime.restart restores a supervised root from its existing registration', async () => {
    const result = await invokeViaTransport<{ ok?: boolean; restarted?: boolean; mount?: string }>(
      runtime.transport,
      '',
      'runtime.restart',
      { mount: 'removable-root' },
    );
    assert.equal(result.ok, true, 'runtime.restart should report success');
    assert.equal(result.restarted, true, 'runtime.restart should report restart');

    const actors = await invokeViaTransport<RuntimeActorEntry[]>(runtime.transport, '', 'runtime.actors');
    assert.ok(actors.some((child) => child.mount === 'removable-root'), 'Restarted supervised root should be running again');
  });

  it('12f. runtime.stop stops a supervised root without dropping its registration', async () => {
    const result = await invokeViaTransport<{ ok?: boolean; stopped?: boolean; alreadyStopped?: boolean; mount?: string }>(
      runtime.transport,
      '',
      'runtime.stop',
      { mount: 'removable-root' },
    );
    assert.equal(result.ok, true, 'runtime.stop should report success');
    assert.equal(result.stopped, true, 'runtime.stop should report a stop');
    assert.equal(result.alreadyStopped, false, 'runtime.stop should report the actor was running');

    const actors = await invokeViaTransport<RuntimeActorEntry[]>(runtime.transport, '', 'runtime.actors');
    assert.ok(!actors.some((child) => child.mount === 'removable-root'), 'Stopped supervised root should no longer be running');

    const registered = await invokeViaTransport<RuntimeRegisteredResult>(runtime.transport, '', 'runtime.registered');
    assert.ok(
      registered.supervisedRoots?.includes('removable-root'),
      'runtime.stop should not drop the supervised root registration',
    );
  });

  it('12g. runtime.start starts a stopped supervised root without forcing a restart', async () => {
    const result = await invokeViaTransport<{ ok?: boolean; started?: boolean; alreadyRunning?: boolean; mount?: string }>(
      runtime.transport,
      '',
      'runtime.start',
      { mount: 'removable-root' },
    );
    assert.equal(result.ok, true, 'runtime.start should report success');
    assert.equal(result.started, true, 'runtime.start should report a start');
    assert.equal(result.alreadyRunning, false, 'runtime.start should report the actor was stopped');

    const actors = await invokeViaTransport<RuntimeActorEntry[]>(runtime.transport, '', 'runtime.actors');
    assert.ok(actors.some((child) => child.mount === 'removable-root'), 'Started supervised root should be running again');
  });

  it('12h. runtime.reload reloads a supervised root', async () => {
    const result = await invokeViaTransport<{ ok?: boolean; reloaded?: boolean; mount?: string }>(
      runtime.transport,
      '',
      'runtime.reload',
      { mount: 'removable-root' },
    );
    assert.equal(result.ok, true, 'runtime.reload should report success');
    assert.equal(result.reloaded, true, 'runtime.reload should report a reload');

    const actors = await invokeViaTransport<RuntimeActorEntry[]>(runtime.transport, '', 'runtime.actors');
    assert.ok(actors.some((child) => child.mount === 'removable-root'), 'Reloaded supervised root should still be running');
  });
});

describe('RootActor — with child actors', () => {
  let runtime: MatrixRuntime;

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

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

  it('13. Multiple children visible in introspect', async () => {
    class SvcA extends MatrixActor { static accepts = { 'a.op': {} }; }
    class SvcB extends MatrixActor { static accepts = { 'b.op': {} }; }

    await runtime.create(SvcA, 'svc-a');
    await runtime.create(SvcB, 'svc-b');
    await tick(100);

    const result = await invokeViaTransport<RuntimeActorEntry[]>(runtime.transport, '', 'runtime.actors');
    const ids = result.map((child) => child.id);
    assert.ok(ids.includes('svc-a'), 'svc-a should be listed');
    assert.ok(ids.includes('svc-b'), 'svc-b should be listed');
  });

  it('14. Actor count matches component count', async () => {
    const result = await invokeViaTransport<RuntimeIntrospectResult>(runtime.transport, '', '$introspect');
    const actors = await invokeViaTransport<RuntimeActorEntry[]>(runtime.transport, '', 'runtime.actors');
    assert.equal(result.actors, actors.length, 'Actor count should match actors list length');
  });
});

describe('RootActor — membrane compatibility', () => {
  let runtime: MatrixRuntime;

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

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

  it('15. Can set membrane on root actor', async () => {
    // Send $membrane to root actor — should not crash
    const rootActor = runtime.getRootActor();
    assert.ok(rootActor, 'Root actor should exist');

    // The membrane accept handler is a system command, should work
    const result = await invokeViaTransport<RuntimeIntrospectResult>(runtime.transport, '', '$introspect');
    assert.equal(result.type, 'runtime', 'Root actor still works after membrane check');
  });
});
