import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';

import {
  loadActorConstructor,
  resolveActorRunBroker,
  resolvePackageRunSpec,
} from '../../src/commands/actor-run.js';

describe('matrix actor run', () => {
  it('loads a named actor export from an entry module', async () => {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-entry-'));
    try {
      const entry = path.join(dir, 'actors.mjs');
      fs.writeFileSync(entry, [
        'export class NamedActor {}',
        'export class OtherActor {}',
        '',
      ].join('\n'), 'utf8');

      const actor = await loadActorConstructor(entry, 'NamedActor');

      assert.equal(actor.name, 'NamedActor');
    } finally {
      fs.rmSync(dir, { recursive: true, force: true });
    }
  });

  it('loads a default actor export from an entry module', async () => {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-default-entry-'));
    try {
      const entry = path.join(dir, 'actor.mjs');
      fs.writeFileSync(entry, [
        'export default class DefaultActor {}',
        '',
      ].join('\n'), 'utf8');

      const actor = await loadActorConstructor(entry, 'default');

      assert.equal(actor.name, 'DefaultActor');
    } finally {
      fs.rmSync(dir, { recursive: true, force: true });
    }
  });

  it('reports available actor exports when the requested export is missing', async () => {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-missing-entry-'));
    try {
      const entry = path.join(dir, 'actor.mjs');
      fs.writeFileSync(entry, [
        'export class PresentActor {}',
        '',
      ].join('\n'), 'utf8');

      await assert.rejects(
        () => loadActorConstructor(entry, 'MissingActor'),
        /No actor class found as export "MissingActor".*PresentActor/u,
      );
    } finally {
      fs.rmSync(dir, { recursive: true, force: true });
    }
  });

  it('resolves broker URL, root, JWT, and seed from an explicit home without Host state', () => {
    const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-broker-'));
    try {
      fs.mkdirSync(path.join(home, 'credentials'), { recursive: true });
      fs.writeFileSync(
        path.join(home, 'credentials', 'matrix-broker.json'),
        `${JSON.stringify({
          natsUrl: 'nats://broker.example:4222',
          root: 'acme-dev',
          jwt: 'jwt-home',
          seed: 'seed-home',
        })}\n`,
        'utf8',
      );

      const resolved = resolveActorRunBroker({ home });
      assert.equal(resolved.natsUrl, 'nats://broker.example:4222');
      assert.equal(resolved.root, 'acme-dev');
      assert.equal(resolved.jwt, 'jwt-home');
      assert.equal(resolved.seed, 'seed-home');
      assert.ok(resolved.checked.includes(`${home}/credentials/matrix-broker.json`));
      assert.equal(fs.existsSync(path.join(home, 'host.json')), false);
      assert.equal(fs.existsSync(path.join(home, 'runtimes')), false);
      assert.equal(fs.existsSync(path.join(home, 'package-records')), false);
    } finally {
      fs.rmSync(home, { recursive: true, force: true });
    }
  });

  it('prefers explicit broker options over environment and home configuration', () => {
    const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-broker-precedence-'));
    const previous = {
      MATRIX_NATS_URL: process.env.MATRIX_NATS_URL,
      NATS_URL: process.env.NATS_URL,
      MATRIX_ROOT: process.env.MATRIX_ROOT,
      MATRIX_SPACE: process.env.MATRIX_SPACE,
      MATRIX_NATS_JWT: process.env.MATRIX_NATS_JWT,
      MATRIX_NATS_SEED: process.env.MATRIX_NATS_SEED,
    };
    try {
      fs.mkdirSync(path.join(home, 'credentials'), { recursive: true });
      fs.writeFileSync(
        path.join(home, 'credentials', 'matrix-broker.json'),
        `${JSON.stringify({
          natsUrl: 'nats://home.example:4222',
          root: 'home-root',
          jwt: 'home-jwt',
          seed: 'home-seed',
        })}\n`,
        'utf8',
      );
      process.env.MATRIX_NATS_URL = 'nats://env.example:4222';
      process.env.MATRIX_ROOT = 'env-root';
      process.env.MATRIX_NATS_JWT = 'env-jwt';
      process.env.MATRIX_NATS_SEED = 'env-seed';

      const resolved = resolveActorRunBroker({
        home,
        natsUrl: 'nats://explicit.example:4222',
        root: 'explicit-root',
        jwt: 'explicit-jwt',
        seed: 'explicit-seed',
      });

      assert.equal(resolved.natsUrl, 'nats://explicit.example:4222');
      assert.equal(resolved.root, 'explicit-root');
      assert.equal(resolved.jwt, 'explicit-jwt');
      assert.equal(resolved.seed, 'explicit-seed');
    } finally {
      restoreEnv(previous);
      fs.rmSync(home, { recursive: true, force: true });
    }
  });

  it('uses Space as a root alias for direct broker execution', () => {
    const resolved = resolveActorRunBroker({
      natsUrl: 'nats://127.0.0.1:4222',
      space: 'space-root',
    });

    assert.equal(resolved.natsUrl, 'nats://127.0.0.1:4222');
    assert.equal(resolved.root, 'space-root');
    assert.equal(resolved.jwt, undefined);
    assert.equal(resolved.seed, undefined);
  });

  it('reports missing broker URL/root and half credentials before connecting', () => {
    const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-broker-missing-'));
    const previous = {
      MATRIX_NATS_URL: process.env.MATRIX_NATS_URL,
      NATS_URL: process.env.NATS_URL,
      MATRIX_ROOT: process.env.MATRIX_ROOT,
      MATRIX_SPACE: process.env.MATRIX_SPACE,
      MATRIX_NATS_JWT: process.env.MATRIX_NATS_JWT,
      MATRIX_NATS_SEED: process.env.MATRIX_NATS_SEED,
      NATS_JWT: process.env.NATS_JWT,
      NATS_SEED: process.env.NATS_SEED,
    };
    try {
      delete process.env.MATRIX_NATS_URL;
      delete process.env.NATS_URL;
      delete process.env.MATRIX_ROOT;
      delete process.env.MATRIX_SPACE;
      delete process.env.MATRIX_NATS_JWT;
      delete process.env.MATRIX_NATS_SEED;
      delete process.env.NATS_JWT;
      delete process.env.NATS_SEED;

      assert.throws(
        () => resolveActorRunBroker({ home }),
        /MATRIX_NATS_URL_REQUIRED/u,
      );
      assert.throws(
        () => resolveActorRunBroker({ home, natsUrl: 'nats://127.0.0.1:4222' }),
        /MATRIX_ROOT_REQUIRED/u,
      );
      assert.throws(
        () => resolveActorRunBroker({ home, natsUrl: 'nats://127.0.0.1:4222', root: 'demo', jwt: 'jwt-only' }),
        /MATRIX_NATS_JWT_SEED_PAIR_REQUIRED/u,
      );
      assert.throws(
        () => resolveActorRunBroker({ home, natsUrl: 'nats://127.0.0.1:4222', root: 'demo', seed: 'seed-only' }),
        /MATRIX_NATS_JWT_SEED_PAIR_REQUIRED/u,
      );
    } finally {
      restoreEnv(previous);
      fs.rmSync(home, { recursive: true, force: true });
    }
  });

  it('resolves direct package run metadata from matrix.json runtime.factory without Host state', () => {
    const packageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-package-run-spec-'));
    try {
      fs.mkdirSync(path.join(packageDir, 'src'), { recursive: true });
      fs.writeFileSync(
        path.join(packageDir, 'package.json'),
        `${JSON.stringify({ name: '@example/direct-run', version: '0.1.0', type: 'module' }, null, 2)}\n`,
        'utf8',
      );
      fs.writeFileSync(
        path.join(packageDir, 'matrix.json'),
        `${JSON.stringify({
          name: '@example/direct-run',
          version: '0.1.0',
          runtime: {
            language: 'javascript',
            entry: 'src/index.mjs',
            factory: {
              kind: 'factory',
              export: 'createDirectRunActor',
              instance: {
                id: 'RUNTIME-HOST-DIRECT-RUN',
                class: 'service',
                rootKind: 'component',
                componentType: 'DirectRunActor',
                mount: 'actor-runner-smoke',
                autoStart: true,
              },
            },
          },
          components: [{
            type: 'DirectRunActor',
            mount: 'actor-runner-smoke',
            accepts: ['ping', 'getStatus'],
          }],
        }, null, 2)}\n`,
        'utf8',
      );
      fs.writeFileSync(path.join(packageDir, 'src', 'index.mjs'), 'export function createDirectRunActor() {}\n', 'utf8');

      const spec = resolvePackageRunSpec(packageDir, { mount: 'actor-runner-smoke' });

      assert.equal(spec.packageName, '@example/direct-run');
      assert.equal(spec.manifestPath, path.join(packageDir, 'matrix.json'));
      assert.equal(spec.exportName, 'createDirectRunActor');
      assert.equal(spec.mount, 'actor-runner-smoke');
      assert.equal(spec.actorId, 'DirectRunActor');
      assert.equal(spec.packageId, '@example/direct-run');
      assert.equal(spec.accepts.ping && typeof spec.accepts.ping, 'object');
      assert.equal(spec.accepts.getStatus && typeof spec.accepts.getStatus, 'object');
      assert.equal(fs.existsSync(path.join(packageDir, 'host.json')), false);
      assert.equal(fs.existsSync(path.join(packageDir, 'runtimes')), false);
      assert.equal(fs.existsSync(path.join(packageDir, 'package-records')), false);
    } finally {
      fs.rmSync(packageDir, { recursive: true, force: true });
    }
  });

});

function restoreEnv(previous: Record<string, string | undefined>): void {
  for (const [key, value] of Object.entries(previous)) {
    if (value === undefined) {
      delete process.env[key];
    } else {
      process.env[key] = value;
    }
  }
}
