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 {
  exchangeActorCredential,
  loadActorConstructor,
  resolveActorRunProvider,
  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 API-key, cloud, and Space from an explicit home without Host state', () => {
    const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-provider-'));
    try {
      fs.mkdirSync(path.join(home, 'credentials'), { recursive: true });
      fs.writeFileSync(
        path.join(home, 'credentials', 'matrix-api-key.json'),
        `${JSON.stringify({
          apiKey: 'hc_home_key',
          cloud: 'https://dev.hivecast.ai/',
          authorityRoot: 'richard-santomauro-nimbletec-com',
        })}\n`,
        'utf8',
      );

      const resolved = resolveActorRunProvider({ home });
      assert.equal(resolved.key, 'hc_home_key');
      assert.equal(resolved.cloud, 'https://dev.hivecast.ai');
      assert.equal(resolved.space, 'richard-santomauro-nimbletec-com');
      assert.ok(resolved.checked.includes('<home>/credentials/matrix-api-key.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 provider options over environment and home configuration', () => {
    const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-provider-precedence-'));
    const previous = {
      MATRIX_API_KEY: process.env.MATRIX_API_KEY,
      HIVECAST_API_KEY: process.env.HIVECAST_API_KEY,
      MATRIX_CLOUD: process.env.MATRIX_CLOUD,
      HIVECAST_CLOUD: process.env.HIVECAST_CLOUD,
      MATRIX_SPACE: process.env.MATRIX_SPACE,
      HIVECAST_SPACE: process.env.HIVECAST_SPACE,
    };
    try {
      fs.mkdirSync(path.join(home, 'credentials'), { recursive: true });
      fs.writeFileSync(
        path.join(home, 'credentials', 'matrix-api-key.json'),
        `${JSON.stringify({
          apiKey: 'hc_home_key',
          cloud: 'https://home.hivecast.ai',
          space: 'home-space',
        })}\n`,
        'utf8',
      );
      process.env.MATRIX_API_KEY = 'hc_env_key';
      process.env.MATRIX_CLOUD = 'https://env.hivecast.ai';
      process.env.MATRIX_SPACE = 'env-space';

      const resolved = resolveActorRunProvider({
        home,
        key: 'hc_explicit_key',
        cloud: 'https://explicit.hivecast.ai/',
        space: 'explicit-space',
      });

      assert.equal(resolved.key, 'hc_explicit_key');
      assert.equal(resolved.cloud, 'https://explicit.hivecast.ai');
      assert.equal(resolved.space, 'explicit-space');
    } finally {
      restoreEnv(previous);
      fs.rmSync(home, { recursive: true, force: true });
    }
  });

  it('reports missing key, cloud, and Space before contacting the cloud', () => {
    const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-provider-missing-'));
    const previous = {
      MATRIX_API_KEY: process.env.MATRIX_API_KEY,
      HIVECAST_API_KEY: process.env.HIVECAST_API_KEY,
      MATRIX_CLOUD: process.env.MATRIX_CLOUD,
      HIVECAST_CLOUD: process.env.HIVECAST_CLOUD,
      MATRIX_SPACE: process.env.MATRIX_SPACE,
      HIVECAST_SPACE: process.env.HIVECAST_SPACE,
    };
    try {
      delete process.env.MATRIX_API_KEY;
      delete process.env.HIVECAST_API_KEY;
      delete process.env.MATRIX_CLOUD;
      delete process.env.HIVECAST_CLOUD;
      delete process.env.MATRIX_SPACE;
      delete process.env.HIVECAST_SPACE;

      assert.throws(
        () => resolveActorRunProvider({ home }),
        /MATRIX_API_KEY_REQUIRED/u,
      );
      assert.throws(
        () => resolveActorRunProvider({ home, key: 'hc_key' }),
        /MATRIX_CLOUD_REQUIRED/u,
      );
      assert.throws(
        () => resolveActorRunProvider({ home, key: 'hc_key', cloud: 'https://dev.hivecast.ai' }),
        /MATRIX_SPACE_REQUIRED/u,
      );
    } finally {
      restoreEnv(previous);
      fs.rmSync(home, { recursive: true, force: true });
    }
  });

  it('exchanges through /api/install/actor-credential without putting the key in the URL', async () => {
    const requests: Array<{ readonly url: string; readonly init: Record<string, unknown> }> = [];
    const provider = {
      key: 'hc_actor_key',
      cloud: 'https://dev.hivecast.ai',
      space: 'richard-santomauro-nimbletec-com',
      checked: [],
    };
    const fetchImpl = async (url: URL, init: RequestInit): Promise<Response> => {
      requests.push({ url: url.toString(), init: init as Record<string, unknown> });
      return new Response(JSON.stringify({
        ok: true,
        space: { authorityRoot: 'richard-santomauro-nimbletec-com' },
        transport: { natsUrl: 'nats://dev.hivecast.ai:4422' },
        mount: 'apps.ping',
        effectiveMount: 'richard-santomauro-nimbletec-com.apps.ping',
        credentials: { jwt: 'jwt', seed: 'SU' },
        source: 'standalone-actor',
        credentialPipeline: 'api-key-exchange',
      }), {
        status: 200,
        headers: { 'content-type': 'application/json' },
      });
    };

    const exchanged = await exchangeActorCredential({
      provider,
      mount: 'apps.ping',
      actorId: 'PingActor',
      packageId: '@acme/ping',
      runtimeId: 'standalone-actor:ping',
      accepts: { ping: {} },
      emits: {},
      subscribes: {},
      ttlSeconds: 90,
      fetchImpl,
    });

    assert.equal(exchanged.ok, true);
    assert.equal(requests.length, 1);
    assert.equal(requests[0]?.url, 'https://dev.hivecast.ai/api/install/actor-credential');
    assert.equal(JSON.stringify(requests[0]).includes('hc_actor_key'), true);
    assert.equal(requests[0]?.url.includes('hc_actor_key'), false);
    assert.equal((requests[0]?.init.headers as Record<string, string>).authorization, 'Bearer hc_actor_key');
    const body = JSON.parse(String(requests[0]?.init.body)) as Record<string, unknown>;
    assert.equal(body.space, 'richard-santomauro-nimbletec-com');
    assert.equal(body.mount, 'apps.ping');
    assert.equal(body.actorId, 'PingActor');
    assert.deepEqual(body.accepts, { ping: {} });
    assert.equal(body.ttlSeconds, 90);
  });

  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;
    }
  }
}
