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

import { resolveNatsBinary } from '../../src/utils/runner-transport.js';
import { assertExitCode, parseLastJson, runMxCli } from '../helpers/cli-harness.js';

interface IWebSocketLike {
  addEventListener(
    type: 'open' | 'error' | 'close',
    listener: (event: unknown) => void,
    options?: { once?: boolean },
  ): void;
  close(): void;
}

async function getAvailablePorts(count: number): Promise<number[]> {
  const servers = await Promise.all(
    Array.from({ length: count }, () => new Promise<net.Server>((resolve, reject) => {
      const server = net.createServer();
      server.once('error', reject);
      server.listen(0, '127.0.0.1', () => resolve(server));
    })),
  );
  const ports = servers.map((server) => {
    const address = server.address();
    assert.ok(address && typeof address === 'object');
    return address.port;
  });
  await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => {
    server.close((error) => (error ? reject(error) : resolve()));
  })));
  return ports;
}

async function createRunInPlaceFixture(rootDir: string): Promise<string> {
  const packageDir = path.join(rootDir, 'sample-run-package');
  const runtimeDir = path.join(packageDir, 'dist', 'runtime');
  fs.mkdirSync(runtimeDir, { recursive: true });
  fs.mkdirSync(path.join(packageDir, '.matrix'), { recursive: true });
  const [natsPort, wsPort] = await getAvailablePorts(2);

  fs.writeFileSync(
    path.join(packageDir, 'package.json'),
    JSON.stringify({
      name: '@acme/sample-run-package',
      version: '1.0.0',
      type: 'module',
      main: './dist/runtime/index.js',
    }, null, 2),
    'utf8',
  );

  fs.writeFileSync(
    path.join(packageDir, 'matrix.json'),
    JSON.stringify({
      name: '@acme/sample-run-package',
      version: '1.0.0',
      runtime: {
        language: 'typescript',
        entry: './dist/runtime/index.js',
        factory: {
          kind: 'factory',
          export: 'createSampleRunService',
          bootstrap: {
            overrides: {
              mode: 'run-in-place',
            },
          },
          instance: {
            packageName: '@acme/sample-run-package',
            packageRevision: '1.0.0',
            class: 'service',
            rootKind: 'package-root',
            mount: 'sample-run.local',
            autoStart: true,
          },
        },
      },
      components: [
        {
          type: 'SampleRunService',
          export: 'SampleRunService',
          mount: 'sample-run',
          autoStart: true,
        },
      ],
      permissions: {
        fsPolicy: 'none',
      },
    }, null, 2),
    'utf8',
  );

  fs.writeFileSync(
    path.join(packageDir, '.matrix', 'dev.environment.json'),
    JSON.stringify({
      name: 'dev',
      runtime: {
        root: 'COM.TEST.RUNNER',
        runtimeId: 'sample-run-dev',
      },
      nats: {
        mode: 'embedded',
        port: natsPort,
        wsPort,
        binaryPath: resolveNatsBinary(process.cwd()),
      },
    }, null, 2),
    'utf8',
  );

  fs.writeFileSync(
    path.join(runtimeDir, 'index.js'),
    `export async function createSampleRunService(overrides = {}, _services, _configResolution, bootstrapContext) {
      return {
        ok: true,
        overrides,
        bootContext: bootstrapContext,
        runtime: {
          async shutdown() {
            return undefined;
          }
        }
      };
    }
    `,
    'utf8',
  );

  return packageDir;
}

async function createServeFixture(rootDir: string): Promise<{
  packageDir: string;
  httpPort: number;
}> {
  const packageDir = path.join(rootDir, 'sample-serve-package');
  const distDir = path.join(packageDir, 'dist');
  fs.mkdirSync(path.join(packageDir, '.matrix'), { recursive: true });
  fs.mkdirSync(distDir, { recursive: true });
  fs.mkdirSync(path.join(packageDir, 'branding'), { recursive: true });
  const [httpPort, natsPort, wsPort] = await getAvailablePorts(3);

  fs.writeFileSync(
    path.join(packageDir, 'package.json'),
    JSON.stringify({
      name: '@acme/sample-serve-package',
      version: '1.0.0',
      type: 'module',
    }, null, 2),
    'utf8',
  );

  fs.writeFileSync(
    path.join(packageDir, 'matrix.json'),
    JSON.stringify({
      name: '@acme/sample-serve-package',
      version: '1.0.0',
      webapp: {
        distDir: 'dist',
        entry: 'index.html',
        appName: 'sample-serve',
      },
      permissions: {
        fsPolicy: 'none',
      },
    }, null, 2),
    'utf8',
  );

  fs.writeFileSync(
    path.join(packageDir, '.matrix', 'dev.environment.json'),
    JSON.stringify({
      name: 'dev',
      runtime: {
        root: 'COM.TEST.SERVE',
        runtimeId: 'sample-serve-dev',
      },
      nats: {
        mode: 'embedded',
        port: natsPort,
        wsPort,
        binaryPath: resolveNatsBinary(process.cwd()),
      },
      http: {
        enabled: true,
        port: httpPort,
      },
    }, null, 2),
    'utf8',
  );

  fs.writeFileSync(
    path.join(distDir, 'index.html'),
    '<!DOCTYPE html><html><head><title>Sample Serve</title><link rel="manifest" href="/manifest.json"></head><body><div id="app">Sample Serve Fixture</div></body></html>\n',
    'utf8',
  );

  fs.writeFileSync(
    path.join(distDir, 'app.js'),
    'console.log("sample-serve");\n',
    'utf8',
  );

  fs.writeFileSync(
    path.join(packageDir, 'branding', 'manifest.json'),
    JSON.stringify({
      name: 'Sample Serve',
      short_name: 'Sample',
      start_url: '/',
      display: 'standalone',
      icons: [{ src: '/branding/favicon.svg', sizes: '16x16', type: 'image/svg+xml' }],
    }, null, 2),
    'utf8',
  );

  fs.writeFileSync(
    path.join(packageDir, 'branding', 'favicon.svg'),
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" fill="#111827"/><path d="M4 11h8v2H4zM5 3h6v2H5zm1 4h4v2H6z" fill="#f8fafc"/></svg>\n',
    'utf8',
  );

  return { packageDir, httpPort };
}

async function waitForHttpReady(
  url: string,
  child: ReturnType<typeof spawn>,
  timeoutMs = 15_000,
): Promise<void> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (child.exitCode !== null) {
      throw new Error(`mx run exited early with code ${child.exitCode}`);
    }
    try {
      const response = await fetch(url);
      if (response.ok) {
        return;
      }
    } catch {
      // Retry until the deadline expires.
    }
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
  throw new Error(`Timed out waiting for ${url}`);
}

describe('mx run package front door', () => {
  it('runs a package from the current folder via matrix.json runtime.factory and package-local environment', async () => {
    const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-package-'));
    const packageDir = await createRunInPlaceFixture(tempRoot);

    assert.equal(fs.existsSync(path.join(packageDir, '.matrix', 'host.status.json')), false);

    const result = await runMxCli(
      ['node', 'mx', 'run', '.', '--env', 'dev', '--check', '--json'],
      { cwd: packageDir },
    );

    assertExitCode(result, 0);
    const payload = parseLastJson(result.logs) as Record<string, unknown>;
    assert.equal(payload.packageName, '@acme/sample-run-package');
    assert.equal(payload.packageDir, packageDir);
    assert.equal(payload.environmentName, 'dev');
    assert.equal(payload.root, 'COM.TEST.RUNNER');
    assert.equal(payload.mount, 'sample-run.local');
    assert.equal(payload.entryPath, path.join(packageDir, 'dist', 'runtime', 'index.js'));
    assert.equal(payload.runnerTransport?.mode, 'embedded');
    assert.equal(payload.runnerTransport?.root, 'COM.TEST.RUNNER');
    assert.match(String(payload.runnerTransport?.url), /^nats:\/\/127\.0\.0\.1:\d+$/);
    assert.match(String(payload.runnerTransport?.wsUrl), /^ws:\/\/127\.0\.0\.1:\d+$/);
  });

  it('fails clearly when a directory target does not declare a runtime factory', async () => {
    const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-missing-service-'));
    const packageDir = path.join(tempRoot, 'missing-service');
    fs.mkdirSync(packageDir, { recursive: true });

    const result = await runMxCli(
      ['node', 'mx', 'run', '.'],
      { cwd: packageDir },
    );

    assertExitCode(result, 1);
    assert.match(
      result.errors.join('\n'),
      /does not declare a matrix\.json runtime\.factory/,
    );
  });

  it('runs a webapp package as a Matrix asset runtime without a package HTTP server', async () => {
    const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-webapp-assets-'));
    const { packageDir } = await createServeFixture(tempRoot);

    const result = await runMxCli(
      ['node', 'mx', 'run', '.', '--env', 'dev', '--check', '--json'],
      { cwd: packageDir },
    );

    assertExitCode(result, 0);
    const payload = parseLastJson(result.logs) as Record<string, unknown>;
    assert.equal(payload.packageName, '@acme/sample-serve-package');
    assert.equal(payload.mode, 'webapp-assets');
    assert.equal(payload.assetMount, 'system.runtimes.sample-serve-dev.http');
    assert.equal(payload.baseUrl, undefined);
    assert.equal(payload.runnerTransport?.mode, 'embedded');
  });

  it('publishes a served webapp HTTP origin in run metadata', async () => {
    const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-webapp-origin-'));
    const { packageDir, httpPort } = await createServeFixture(tempRoot);

    const result = await runMxCli(
      ['node', 'mx', 'run', '.', '--env', 'dev', '--serve', '--check', '--json'],
      { cwd: packageDir },
    );

    assertExitCode(result, 0);
    const payload = parseLastJson(result.logs) as Record<string, unknown>;
    assert.equal(payload.packageName, '@acme/sample-serve-package');
    assert.equal(payload.mode, 'standalone-webapp');
    assert.equal(payload.assetMount, 'system.runtimes.sample-serve-dev.http');
    assert.equal(payload.baseUrl, `http://127.0.0.1:${httpPort}`);
    assert.equal(payload.httpPort, httpPort);
    assert.equal(payload.runnerTransport?.mode, 'embedded');
  });

  it('serves built webapp assets with same-origin bootstrap and nats-ws routes', async () => {
    const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-serve-'));
    const { packageDir, httpPort } = await createServeFixture(tempRoot);
    const mxCliDir = path.resolve(import.meta.dirname, '../..');
    const cliEntry = path.resolve(import.meta.dirname, '../../src/index.ts');
    const tsxLoader = path.resolve(import.meta.dirname, '../../../../node_modules/tsx/dist/loader.mjs');
    const child = spawn(
      process.execPath,
      ['--import', tsxLoader, cliEntry, 'run', packageDir, '--env', 'dev', '--serve'],
      {
        cwd: mxCliDir,
        stdio: ['ignore', 'pipe', 'pipe'],
      },
    );

    const stdout: string[] = [];
    const stderr: string[] = [];
    child.stdout.on('data', (chunk: Buffer) => {
      stdout.push(chunk.toString('utf8'));
    });
    child.stderr.on('data', (chunk: Buffer) => {
      stderr.push(chunk.toString('utf8'));
    });

    try {
      const baseUrl = `http://127.0.0.1:${httpPort}`;
      await waitForHttpReady(`${baseUrl}/healthz`, child);

      const html = await fetch(`${baseUrl}/`);
      assert.equal(html.status, 200);
      assert.match(await html.text(), /Sample Serve Fixture/);

      const bootstrap = await fetch(`${baseUrl}/api/bootstrap`);
      assert.equal(bootstrap.status, 200);
      const payload = await bootstrap.json() as {
        root?: string;
        transportPlan?: { wsPath?: string; authMode?: string; sessionMode?: string };
      };
      assert.equal(payload.root, 'COM.TEST.SERVE');
      assert.equal(payload.transportPlan?.wsPath, '/nats-ws');
      assert.equal(payload.transportPlan?.authMode, 'anonymous');
      assert.equal(payload.transportPlan?.sessionMode, 'local-client');

      const manifest = await fetch(`${baseUrl}/manifest.json`);
      assert.equal(manifest.status, 200);
      const branding = await fetch(`${baseUrl}/branding/favicon.svg`);
      assert.equal(branding.status, 200);

      const WebSocketCtor = (globalThis as typeof globalThis & {
        WebSocket?: new (url: string) => IWebSocketLike;
      }).WebSocket;
      assert.ok(WebSocketCtor, 'Node runtime must expose WebSocket for same-origin proxy proof');
      const ws = new WebSocketCtor(`ws://127.0.0.1:${httpPort}/nats-ws`);
      await new Promise<void>((resolve, reject) => {
        ws.addEventListener('open', () => {
          ws.close();
        }, { once: true });
        ws.addEventListener('error', (event) => {
          reject(new Error(`WebSocket upgrade failed: ${String(event)}`));
        }, { once: true });
        ws.addEventListener('close', () => {
          resolve();
        }, { once: true });
      });
    } finally {
      child.kill('SIGTERM');
      await new Promise<void>((resolve) => {
        child.once('exit', () => resolve());
      });
    }

    assert.match(stdout.join(''), /serving @acme\/sample-serve-package/);
    assert.doesNotMatch(stderr.join(''), /Run target not found|does not declare|Error:/);
  });
});
