import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';

async function startNpmRegistryFixture(params: {
  readonly packageName: string;
  readonly version: string;
  readonly tarballPath: string;
  readonly expectedToken?: string;
}): Promise<{
  readonly registryUrl: string;
  readonly metadataRequests: () => number;
  readonly tarballRequests: () => number;
  readonly authorizationHeaders: () => readonly string[];
  close(): Promise<void>;
}> {
  const tarball = fs.readFileSync(params.tarballPath);
  const shasum = createHash('sha1').update(tarball).digest('hex');
  const integrity = `sha512-${createHash('sha512').update(tarball).digest('base64')}`;
  let metadataRequestCount = 0;
  let tarballRequestCount = 0;
  const authorizationHeaders: string[] = [];

  const server = http.createServer((request, response) => {
    const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
    const decodedPath = decodeURIComponent(requestUrl.pathname);
    const authorization = request.headers.authorization;
    if (typeof authorization === 'string') {
      authorizationHeaders.push(authorization);
    } else if (Array.isArray(authorization)) {
      authorizationHeaders.push(...authorization);
    }

    if (decodedPath === `/${params.packageName}`) {
      metadataRequestCount += 1;
      const registryUrl = `http://127.0.0.1:${addressPort(server)}/`;
      const metadata = {
        name: params.packageName,
        'dist-tags': { latest: params.version },
        versions: {
          [params.version]: {
            name: params.packageName,
            version: params.version,
            dist: {
              tarball: new URL('tarballs/package.tgz', registryUrl).toString(),
              shasum,
              integrity,
            },
          },
        },
      };
      response.writeHead(200, { 'content-type': 'application/json' });
      response.end(JSON.stringify(metadata));
      return;
    }

    if (decodedPath === '/tarballs/package.tgz') {
      tarballRequestCount += 1;
      response.writeHead(200, {
        'content-type': 'application/octet-stream',
        'content-length': String(tarball.byteLength),
      });
      response.end(tarball);
      return;
    }

    response.writeHead(404, { 'content-type': 'application/json' });
    response.end(JSON.stringify({ error: `Not found: ${decodedPath}` }));
  });

  await new Promise<void>((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => {
      server.off('error', reject);
      resolve();
    });
  });

  return {
    registryUrl: `http://127.0.0.1:${addressPort(server)}/`,
    metadataRequests: () => metadataRequestCount,
    tarballRequests: () => tarballRequestCount,
    authorizationHeaders: () => [...authorizationHeaders],
    async close(): Promise<void> {
      await new Promise<void>((resolve, reject) => {
        server.close((error) => (error ? reject(error) : resolve()));
      });
    },
  };
}

function addressPort(server: http.Server): number {
  const address = server.address();
  if (!address || typeof address === 'string') {
    throw new Error('Registry fixture is not listening on a TCP port');
  }
  return address.port;
}

function npmPackPackage(packageDir: string, outDir: string): string {
  fs.mkdirSync(outDir, { recursive: true });
  const result = spawnSync('npm', ['pack', packageDir, '--pack-destination', outDir], {
    encoding: 'utf8',
  });
  assert.equal(
    result.status,
    0,
    `npm pack failed\nstdout:\n${result.stdout ?? ''}\nstderr:\n${result.stderr ?? ''}`,
  );
  const fileName = result.stdout.trim().split(/\r?\n/).filter(Boolean).pop();
  assert.equal(Boolean(fileName), true);
  return path.join(outDir, fileName!);
}

describe('FP-30 mx install source resolution', () => {
  const tempDirs: string[] = [];
  const closeFns: Array<() => Promise<void>> = [];
  const restoreEnvFns: Array<() => void> = [];

  afterEach(async () => {
    while (restoreEnvFns.length > 0) {
      const restore = restoreEnvFns.pop();
      if (restore) {
        restore();
      }
    }
    while (closeFns.length > 0) {
      const close = closeFns.pop();
      if (close) {
        await close();
      }
    }
    while (tempDirs.length > 0) {
      const dir = tempDirs.pop();
      if (dir) {
        fs.rmSync(dir, { recursive: true, force: true });
      }
    }
  });

  function useMatrixHome(matrixHome: string): void {
    const previousMatrixHome = process.env.MATRIX_HOME;
    const previousCliConfig = process.env.MATRIX_CLI_CONFIG;
    const previousPackageRegistry = process.env.MATRIX_PACKAGE_REGISTRY;
    const previousRegistryUrl = process.env.MATRIX_REGISTRY_URL;
    process.env.MATRIX_HOME = matrixHome;
    delete process.env.MATRIX_CLI_CONFIG;
    delete process.env.MATRIX_PACKAGE_REGISTRY;
    delete process.env.MATRIX_REGISTRY_URL;
    restoreEnvFns.push(() => {
      restoreEnv('MATRIX_HOME', previousMatrixHome);
      restoreEnv('MATRIX_CLI_CONFIG', previousCliConfig);
      restoreEnv('MATRIX_PACKAGE_REGISTRY', previousPackageRegistry);
      restoreEnv('MATRIX_REGISTRY_URL', previousRegistryUrl);
    });
  }

  function restoreEnv(key: string, value: string | undefined): void {
    if (value === undefined) {
      delete process.env[key];
      return;
    }
    process.env[key] = value;
  }

  it('installs package from local directory, tarball, and registry name', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-sources-'));
    tempDirs.push(root);
    const workspaceDir = path.join(root, 'workspace');
    fs.mkdirSync(workspaceDir, { recursive: true });

    const packagesDir = path.join(root, '.matrix', 'packages');
    const registryDir = path.join(root, '.matrix', 'registry');
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');

    const fixture = createMatrixPackage(path.join(root, 'source'), {
      packageName: '@acme/install-source',
      version: '1.0.0',
      componentType: 'InstallSourceActor',
      mount: 'install.source',
    });

    const installFromDirectory = await runMxCli([
      'node',
      'mx',
      'install',
      fixture.packageDir,
      '--packages-dir',
      packagesDir,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(installFromDirectory, 0);
    const installedFromDir = parseLastJson(installFromDirectory.logs) as { targetDir: string };
    assert.equal(fs.existsSync(installedFromDir.targetDir), true);

    const uninstallAfterDirectory = await runMxCli([
      'node',
      'mx',
      'uninstall',
      fixture.packageName,
      '--packages-dir',
      packagesDir,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(uninstallAfterDirectory, 0);

    const packResult = await runMxCli([
      'node',
      'mx',
      'pack',
      '--out-dir',
      path.join(root, 'packed'),
      '--json',
    ], { cwd: fixture.packageDir });
    assertExitCode(packResult, 0);
    const packPayload = parseLastJson(packResult.logs) as { tarballPath: string };
    assert.equal(fs.existsSync(packPayload.tarballPath), true);

    const installFromTarball = await runMxCli([
      'node',
      'mx',
      'install',
      packPayload.tarballPath,
      '--packages-dir',
      packagesDir,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(installFromTarball, 0);
    const installedFromTarball = parseLastJson(installFromTarball.logs) as { targetDir: string };
    assert.equal(fs.existsSync(installedFromTarball.targetDir), true);

    const uninstallAfterTarball = await runMxCli([
      'node',
      'mx',
      'uninstall',
      fixture.packageName,
      '--packages-dir',
      packagesDir,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(uninstallAfterTarball, 0);

    const loginResult = await runMxCli([
      'node',
      'mx',
      'login',
      '--username',
      'alice',
      '--token',
      'token-install',
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(loginResult, 0);

    const publishResult = await runMxCli([
      'node',
      'mx',
      'publish',
      '--registry-dir',
      registryDir,
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: fixture.packageDir });
    assertExitCode(publishResult, 0);

    const installFromRegistry = await runMxCli([
      'node',
      'mx',
      'install',
      `${fixture.packageName}@${fixture.version}`,
      '--packages-dir',
      packagesDir,
      '--registry-dir',
      registryDir,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(installFromRegistry, 0);
    const installedFromRegistry = parseLastJson(installFromRegistry.logs) as { source: string; targetDir: string };
    assert.equal(installedFromRegistry.source, `${fixture.packageName}@${fixture.version}`);
    assert.equal(fs.existsSync(installedFromRegistry.targetDir), true);
  });

  it('installs package from an npm-compatible registry URL with stored registry credentials', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-npm-registry-'));
    tempDirs.push(root);
    const workspaceDir = path.join(root, 'workspace');
    fs.mkdirSync(workspaceDir, { recursive: true });

    const packagesDir = path.join(root, '.matrix', 'packages');
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');
    const fixture = createMatrixPackage(path.join(root, 'source'), {
      packageName: '@acme/remote-install',
      version: '1.2.3',
      componentType: 'RemoteInstallActor',
      mount: 'remote.install',
    });
    fs.mkdirSync(path.join(fixture.packageDir, 'vendor', 'runtime-helper'), { recursive: true });
    fs.writeFileSync(
      path.join(fixture.packageDir, 'vendor', 'runtime-helper', 'package.json'),
      JSON.stringify({
        name: '@acme/runtime-helper',
        version: '1.0.0',
        type: 'module',
        main: 'index.js',
      }, null, 2),
      'utf8',
    );
    fs.writeFileSync(
      path.join(fixture.packageDir, 'vendor', 'runtime-helper', 'index.js'),
      'export const helper = true;\n',
      'utf8',
    );
    const packageJsonPath = path.join(fixture.packageDir, 'package.json');
    const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as Record<string, unknown>;
    packageJson.dependencies = {
      '@acme/runtime-helper': 'file:./vendor/runtime-helper',
    };
    packageJson.devDependencies = {
      '@open-matrix/core': '*',
    };
    packageJson.peerDependencies = {
      '@open-matrix/core': '*',
    };
    packageJson.peerDependenciesMeta = {
      '@open-matrix/core': { optional: true },
    };
    fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');

    const packResult = await runMxCli([
      'node',
      'mx',
      'pack',
      '--out-dir',
      path.join(root, 'packed'),
      '--json',
    ], { cwd: fixture.packageDir });
    assertExitCode(packResult, 0);
    const packPayload = parseLastJson(packResult.logs) as { tarballPath: string };

    const registry = await startNpmRegistryFixture({
      packageName: fixture.packageName,
      version: fixture.version,
      tarballPath: packPayload.tarballPath,
      expectedToken: 'token-remote-install',
    });
    closeFns.push(registry.close);

    const loginResult = await runMxCli([
      'node',
      'mx',
      'login',
      '--username',
      'alice',
      '--token',
      'token-remote-install',
      '--registry',
      registry.registryUrl,
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(loginResult, 0);

    const installFromRemoteRegistry = await runMxCli([
      'node',
      'mx',
      'install',
      `${fixture.packageName}@${fixture.version}`,
      '--packages-dir',
      packagesDir,
      '--registry',
      registry.registryUrl,
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(installFromRemoteRegistry, 0);

    const installed = parseLastJson(installFromRemoteRegistry.logs) as {
      source: string;
      targetDir: string;
      version: string;
    };
    assert.equal(installed.source, `${fixture.packageName}@${fixture.version}`);
    assert.equal(installed.version, fixture.version);
    assert.equal(fs.existsSync(installed.targetDir), true);
    assert.equal(
      fs.existsSync(path.join(installed.targetDir, 'node_modules', '@acme', 'runtime-helper', 'package.json')),
      true,
    );
    assert.equal(
      fs.existsSync(path.join(installed.targetDir, 'node_modules', '@open-matrix', 'core', 'package.json')),
      false,
    );
    assert.equal(registry.metadataRequests() > 0, true);
    assert.equal(registry.tarballRequests() > 0, true);
    assert.equal(
      registry.authorizationHeaders().some((header) => header.includes('token-remote-install')),
      true,
    );
  });

  it('installs package names from the configured npm registry without a per-command registry flag', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-config-registry-'));
    tempDirs.push(root);
    const workspaceDir = path.join(root, 'workspace');
    fs.mkdirSync(workspaceDir, { recursive: true });
    useMatrixHome(path.join(root, '.matrix'));

    const packagesDir = path.join(root, '.matrix', 'packages');
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');
    const fixture = createMatrixPackage(path.join(root, 'source'), {
      packageName: '@acme/configured-remote-install',
      version: '1.2.3',
      componentType: 'ConfiguredRemoteInstallActor',
      mount: 'configured.remote.install',
    });

    const packResult = await runMxCli([
      'node',
      'mx',
      'pack',
      '--out-dir',
      path.join(root, 'packed'),
      '--json',
    ], { cwd: fixture.packageDir });
    assertExitCode(packResult, 0);
    const packPayload = parseLastJson(packResult.logs) as { tarballPath: string };

    const registry = await startNpmRegistryFixture({
      packageName: fixture.packageName,
      version: fixture.version,
      tarballPath: packPayload.tarballPath,
      expectedToken: 'token-configured-install',
    });
    closeFns.push(registry.close);

    const setConfig = await runMxCli([
      'node',
      'mx',
      'config',
      'set',
      'registry',
      registry.registryUrl,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(setConfig, 0);

    const loginResult = await runMxCli([
      'node',
      'mx',
      'login',
      '--username',
      'alice',
      '--token',
      'token-configured-install',
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(loginResult, 0);

    const installFromConfiguredRegistry = await runMxCli([
      'node',
      'mx',
      'install',
      `${fixture.packageName}@${fixture.version}`,
      '--packages-dir',
      packagesDir,
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(installFromConfiguredRegistry, 0);

    const installed = parseLastJson(installFromConfiguredRegistry.logs) as {
      source: string;
      targetDir: string;
      version: string;
    };
    assert.equal(installed.source, `${fixture.packageName}@${fixture.version}`);
    assert.equal(installed.version, fixture.version);
    assert.equal(fs.existsSync(installed.targetDir), true);
    assert.equal(registry.metadataRequests() > 0, true);
    assert.equal(registry.tarballRequests() > 0, true);
    assert.equal(
      registry.authorizationHeaders().some((header) => header.includes('token-configured-install')),
      true,
    );
  });

  it('rejects npm registry packages whose matrix.json and package.json versions disagree', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-version-mismatch-'));
    tempDirs.push(root);
    const workspaceDir = path.join(root, 'workspace');
    fs.mkdirSync(workspaceDir, { recursive: true });

    const packagesDir = path.join(root, '.matrix', 'packages');
    const fixture = createMatrixPackage(path.join(root, 'source'), {
      packageName: '@acme/version-mismatch',
      version: '1.0.0',
      componentType: 'VersionMismatchActor',
      mount: 'version.mismatch',
    });
    fs.writeFileSync(
      path.join(fixture.packageDir, 'package.json'),
      JSON.stringify({
        name: fixture.packageName,
        version: '9.9.9',
        type: 'module',
        main: 'src/index.ts',
      }, null, 2),
      'utf8',
    );
    const tarballPath = npmPackPackage(fixture.packageDir, path.join(root, 'packed'));
    const registry = await startNpmRegistryFixture({
      packageName: fixture.packageName,
      version: '9.9.9',
      tarballPath,
    });
    closeFns.push(registry.close);

    const installResult = await runMxCli([
      'node',
      'mx',
      'install',
      `${fixture.packageName}@9.9.9`,
      '--packages-dir',
      packagesDir,
      '--registry',
      registry.registryUrl,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(installResult, 1);
    assert.equal(
      installResult.errors.some((line) => line.includes('MX_PACKAGE_VERSION_MISMATCH')),
      true,
    );
  });

  it('normalizes whitespace-padded registry specifiers before install', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-specifier-trim-'));
    tempDirs.push(root);
    const workspaceDir = path.join(root, 'workspace');
    fs.mkdirSync(workspaceDir, { recursive: true });

    const packagesDir = path.join(root, '.matrix', 'packages');
    const registryDir = path.join(root, '.matrix', 'registry');
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');

    const fixture = createMatrixPackage(path.join(root, 'source'), {
      packageName: '@acme/install-source',
      version: '1.0.0',
      componentType: 'InstallSourceActor',
      mount: 'install.source',
    });

    const loginResult = await runMxCli([
      'node',
      'mx',
      'login',
      '--username',
      'alice',
      '--token',
      'token-install',
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(loginResult, 0);

    const publishResult = await runMxCli([
      'node',
      'mx',
      'publish',
      '--registry-dir',
      registryDir,
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: fixture.packageDir });
    assertExitCode(publishResult, 0);

    const installFromRegistry = await runMxCli([
      'node',
      'mx',
      'install',
      `  ${fixture.packageName}@${fixture.version}  `,
      '--packages-dir',
      packagesDir,
      '--registry-dir',
      registryDir,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(installFromRegistry, 0);

    const installedFromRegistry = parseLastJson(installFromRegistry.logs) as { source: string; targetDir: string };
    assert.equal(installedFromRegistry.source, `${fixture.packageName}@${fixture.version}`);
    assert.equal(fs.existsSync(installedFromRegistry.targetDir), true);
  });
});
