import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
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';

interface INpmPublishRegistryFixture {
  readonly registryUrl: string;
  readonly close: () => Promise<void>;
  readonly publishRequests: () => number;
  readonly tarballRequests: () => number;
  readonly authorizationHeaders: () => readonly string[];
}

interface IPublishedPackage {
  readonly name: string;
  readonly version: string;
  readonly fileName: string;
  readonly tarball: Buffer;
}

function asRecord(value: unknown): Record<string, unknown> | null {
  return value !== null && typeof value === 'object' && !Array.isArray(value)
    ? value as Record<string, unknown>
    : null;
}

function readString(value: unknown): string | null {
  return typeof value === 'string' && value.trim().length > 0 ? value : null;
}

function addressPort(server: http.Server): number {
  const address = server.address();
  if (!address || typeof address === 'string') {
    throw new Error('Fixture registry did not bind a TCP port');
  }
  return address.port;
}

async function readRequestBody(req: http.IncomingMessage): Promise<string> {
  const chunks: Buffer[] = [];
  for await (const chunk of req) {
    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
  }
  return Buffer.concat(chunks).toString('utf8');
}

async function startNpmPublishRegistryFixture(): Promise<INpmPublishRegistryFixture> {
  let publishRequests = 0;
  let tarballRequests = 0;
  let published: IPublishedPackage | null = null;
  const authorizationHeaders: string[] = [];

  const server = http.createServer((req, res) => {
    void (async () => {
      const auth = req.headers.authorization;
      if (typeof auth === 'string') {
        authorizationHeaders.push(auth);
      }
      const registryUrl = `http://127.0.0.1:${addressPort(server)}/`;
      const url = new URL(req.url ?? '/', registryUrl);
      if (req.method === 'PUT') {
        publishRequests += 1;
        const body = asRecord(JSON.parse(await readRequestBody(req)) as unknown);
        const packageName = readString(body?.name);
        const distTags = asRecord(body?.['dist-tags']);
        const version = readString(distTags?.latest);
        const attachments = asRecord(body?._attachments);
        const attachmentEntry = attachments ? Object.entries(attachments)[0] : undefined;
        const attachment = asRecord(attachmentEntry?.[1]);
        const data = readString(attachment?.data);
        if (!packageName || !version || !attachmentEntry || !data) {
          res.writeHead(400, { 'content-type': 'application/json' });
          res.end(JSON.stringify({ error: 'invalid publish payload' }));
          return;
        }
        published = {
          name: packageName,
          version,
          fileName: attachmentEntry[0],
          tarball: Buffer.from(data, 'base64'),
        };
        res.writeHead(201, { 'content-type': 'application/json' });
        res.end(JSON.stringify({ ok: true }));
        return;
      }

      if (req.method === 'GET' && url.pathname.endsWith('.tgz')) {
        tarballRequests += 1;
        if (!published) {
          res.writeHead(404, { 'content-type': 'application/json' });
          res.end(JSON.stringify({ error: 'no tarball published' }));
          return;
        }
        res.writeHead(200, { 'content-type': 'application/octet-stream' });
        res.end(published.tarball);
        return;
      }

      const decodedPath = decodeURIComponent(url.pathname);
      if (req.method === 'GET' && published && decodedPath === `/${published.name}`) {
        res.writeHead(200, { 'content-type': 'application/json' });
        res.end(JSON.stringify({
          name: published.name,
          'dist-tags': { latest: published.version },
          versions: {
            [published.version]: {
              name: published.name,
              version: published.version,
              dist: {
                tarball: new URL(`tarballs/${published.fileName}`, registryUrl).toString(),
              },
            },
          },
        }));
        return;
      }

      res.writeHead(404, { 'content-type': 'application/json' });
      res.end(JSON.stringify({ error: 'not found', path: url.pathname }));
    })().catch((error: unknown) => {
      res.writeHead(500, { 'content-type': 'application/json' });
      res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
    });
  });

  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)}/`,
    close: async () => {
      await new Promise<void>((resolve, reject) => {
        server.close((error) => (error ? reject(error) : resolve()));
      });
    },
    publishRequests: () => publishRequests,
    tarballRequests: () => tarballRequests,
    authorizationHeaders: () => authorizationHeaders,
  };
}

describe('FP-30 mx auth + publish integration', { concurrency: false }, () => {
  const tempDirs: string[] = [];
  const restoreEnvFns: Array<() => void> = [];

  afterEach(() => {
    while (restoreEnvFns.length > 0) {
      const restore = restoreEnvFns.pop();
      if (restore) {
        restore();
      }
    }
    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;
    const previousNpmUserConfig = process.env.NPM_CONFIG_USERCONFIG;
    const previousNodeAuthToken = process.env.NODE_AUTH_TOKEN;
    const previousNpmToken = process.env.NPM_TOKEN;
    process.env.MATRIX_HOME = matrixHome;
    delete process.env.MATRIX_CLI_CONFIG;
    delete process.env.MATRIX_PACKAGE_REGISTRY;
    delete process.env.MATRIX_REGISTRY_URL;
    delete process.env.NPM_CONFIG_USERCONFIG;
    delete process.env.NODE_AUTH_TOKEN;
    delete process.env.NPM_TOKEN;
    restoreEnvFns.push(() => {
      restoreEnv('MATRIX_HOME', previousMatrixHome);
      restoreEnv('MATRIX_CLI_CONFIG', previousCliConfig);
      restoreEnv('MATRIX_PACKAGE_REGISTRY', previousPackageRegistry);
      restoreEnv('MATRIX_REGISTRY_URL', previousRegistryUrl);
      restoreEnv('NPM_CONFIG_USERCONFIG', previousNpmUserConfig);
      restoreEnv('NODE_AUTH_TOKEN', previousNodeAuthToken);
      restoreEnv('NPM_TOKEN', previousNpmToken);
    });
  }

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

  it('requires login before publish and enables registry install after publish', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-auth-publish-'));
    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 homeDir = path.join(root, 'home');
    const previousHome = process.env.HOME;
    fs.mkdirSync(homeDir, { recursive: true });
    process.env.HOME = homeDir;

    try {
      const fixture = createMatrixPackage(path.join(root, 'source'), {
        packageName: '@acme/fp30-auth-publish',
        version: '1.0.0',
        componentType: 'AuthPublishActor',
        mount: 'auth.publish',
      });

      const publishWithoutLogin = await runMxCli([
        'node',
        'mx',
        'publish',
        '--registry-dir',
        registryDir,
        '--credentials-file',
        credentialsFile,
        '--json',
      ], { cwd: fixture.packageDir });
      assertExitCode(publishWithoutLogin, 1);
      assert.equal(publishWithoutLogin.errors.some((line) => line.includes('MX_AUTH_REQUIRED')), true);

      const loginResult = await runMxCli([
        'node',
        'mx',
        'login',
        '--username',
        'alice',
        '--token',
        'token-auth-publish',
        '--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 payload = parseLastJson(installFromRegistry.logs) as { targetDir: string };
      assert.equal(fs.existsSync(payload.targetDir), true);
    } finally {
      restoreEnv('HOME', previousHome);
    }
  });

  it('publishes to an npm registry API and installs from the published tarball', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-npm-publish-'));
    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 registry = await startNpmPublishRegistryFixture();

    try {
      const fixture = createMatrixPackage(path.join(root, 'source'), {
        packageName: '@acme/fp30-npm-publish',
        version: '1.0.0',
        componentType: 'NpmPublishActor',
        mount: 'npm.publish',
      });

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

      const publishResult = await runMxCli([
        'node',
        'mx',
        'publish',
        '--registry',
        registry.registryUrl,
        '--credentials-file',
        credentialsFile,
        '--json',
      ], { cwd: fixture.packageDir });
      assertExitCode(publishResult, 0);
      const publishPayload = parseLastJson(publishResult.logs) as {
        packageName: string;
        version: string;
        registryUrl?: string;
      };
      assert.equal(publishPayload.packageName, fixture.packageName);
      assert.equal(publishPayload.version, fixture.version);
      assert.equal(publishPayload.registryUrl, registry.registryUrl);
      assert.equal(registry.publishRequests(), 1);
      assert.equal(
        registry.authorizationHeaders().some((header) => header.includes('token-npm-publish')),
        true
      );

      const installResult = await runMxCli([
        'node',
        'mx',
        'install',
        `${fixture.packageName}@${fixture.version}`,
        '--packages-dir',
        packagesDir,
        '--registry',
        registry.registryUrl,
        '--credentials-file',
        credentialsFile,
        '--json',
      ], { cwd: workspaceDir });
      assertExitCode(installResult, 0);
      const installPayload = parseLastJson(installResult.logs) as { targetDir: string };
      assert.equal(fs.existsSync(path.join(installPayload.targetDir, 'matrix.json')), true);
      assert.equal(registry.tarballRequests(), 1);
    } finally {
      await registry.close();
    }
  });

  it('keeps Matrix scoped packages on the requested npm registry API', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-scoped-npm-publish-'));
    tempDirs.push(root);
    const workspaceDir = path.join(root, 'workspace');
    const homeDir = path.join(root, 'home');
    fs.mkdirSync(workspaceDir, { recursive: true });
    fs.mkdirSync(homeDir, { recursive: true });
    const packagesDir = path.join(root, '.matrix', 'packages');
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');
    const registry = await startNpmPublishRegistryFixture();
    const previousHome = process.env.HOME;
    const previousGlobalConfig = process.env.NPM_CONFIG_GLOBALCONFIG;
    const wrongRegistry = 'http://127.0.0.1:9/wrong/npm/';
    const poisonedGlobalConfig = path.join(root, 'poisoned-global-npmrc');

    fs.writeFileSync(
      path.join(homeDir, '.npmrc'),
      [
        `@matrix:registry=${wrongRegistry}`,
        `@open-matrix:registry=${wrongRegistry}`,
        `@omega:registry=${wrongRegistry}`,
        '',
      ].join('\n'),
      'utf8',
    );
    fs.writeFileSync(
      poisonedGlobalConfig,
      [
        `@matrix:registry=${wrongRegistry}`,
        `@open-matrix:registry=${wrongRegistry}`,
        `@omega:registry=${wrongRegistry}`,
        '',
      ].join('\n'),
      'utf8',
    );
    process.env.HOME = homeDir;
    process.env.NPM_CONFIG_GLOBALCONFIG = poisonedGlobalConfig;

    try {
      const fixture = createMatrixPackage(path.join(root, 'source'), {
        packageName: '@matrix/fp30-scoped-npm-publish',
        version: '1.0.0',
        componentType: 'ScopedNpmPublishActor',
        mount: 'scoped.npm.publish',
      });

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

      const publishResult = await runMxCli([
        'node',
        'mx',
        'publish',
        '--registry',
        registry.registryUrl,
        '--credentials-file',
        credentialsFile,
        '--json',
      ], { cwd: fixture.packageDir });
      assertExitCode(publishResult, 0);
      assert.equal(registry.publishRequests(), 1);
      assert.equal(
        registry.authorizationHeaders().some((header) => header.includes('token-scoped-npm-publish')),
        true,
      );

      const installResult = await runMxCli([
        'node',
        'mx',
        'install',
        `${fixture.packageName}@${fixture.version}`,
        '--packages-dir',
        packagesDir,
        '--registry',
        registry.registryUrl,
        '--credentials-file',
        credentialsFile,
        '--json',
      ], { cwd: workspaceDir });
      assertExitCode(installResult, 0);
      const installPayload = parseLastJson(installResult.logs) as { targetDir: string };
      assert.equal(fs.existsSync(path.join(installPayload.targetDir, 'matrix.json')), true);
      assert.equal(registry.tarballRequests(), 1);
    } finally {
      restoreEnv('HOME', previousHome);
      restoreEnv('NPM_CONFIG_GLOBALCONFIG', previousGlobalConfig);
      await registry.close();
    }
  });

  it('publishes to the configured npm registry API without a per-command registry flag', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-configured-npm-publish-'));
    tempDirs.push(root);
    useMatrixHome(path.join(root, '.matrix'));
    const workspaceDir = path.join(root, 'workspace');
    fs.mkdirSync(workspaceDir, { recursive: true });
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');
    const registry = await startNpmPublishRegistryFixture();

    try {
      const fixture = createMatrixPackage(path.join(root, 'source'), {
        packageName: '@acme/fp30-configured-npm-publish',
        version: '1.0.0',
        componentType: 'ConfiguredNpmPublishActor',
        mount: 'configured.npm.publish',
      });

      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-npm-publish',
        '--credentials-file',
        credentialsFile,
        '--json',
      ], { cwd: workspaceDir });
      assertExitCode(loginResult, 0);

      const publishResult = await runMxCli([
        'node',
        'mx',
        'publish',
        '--credentials-file',
        credentialsFile,
        '--json',
      ], { cwd: fixture.packageDir });
      assertExitCode(publishResult, 0);
      const publishPayload = parseLastJson(publishResult.logs) as {
        packageName: string;
        version: string;
        registryUrl?: string;
      };
      assert.equal(publishPayload.packageName, fixture.packageName);
      assert.equal(publishPayload.version, fixture.version);
      assert.equal(publishPayload.registryUrl, registry.registryUrl);
      assert.equal(registry.publishRequests(), 1);
      assert.equal(
        registry.authorizationHeaders().some((header) => header.includes('token-configured-npm-publish')),
        true,
      );
    } finally {
      await registry.close();
    }
  });

  it('publishes to an npm registry API using an npmrc token', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-npmrc-publish-'));
    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/fp30-npmrc-publish',
      version: '1.0.0',
      componentType: 'NpmrcPublishActor',
      mount: 'npmrc.publish',
    });
    const registry = await startNpmPublishRegistryFixture();
    const npmrcPath = path.join(root, '.npmrc');
    const registryKey = registry.registryUrl.replace(/^https?:/, '');
    const previousNpmUserConfig = process.env.NPM_CONFIG_USERCONFIG;
    restoreEnvFns.push(() => restoreEnv('NPM_CONFIG_USERCONFIG', previousNpmUserConfig));
    fs.writeFileSync(
      npmrcPath,
      `${registryKey}:_authToken=npmrc-token-publish\n`,
      'utf8',
    );
    process.env.NPM_CONFIG_USERCONFIG = npmrcPath;
    try {
      const publishResult = await runMxCli([
        'node',
        'mx',
        'publish',
        '--registry',
        registry.registryUrl,
        '--json',
      ], { cwd: fixture.packageDir });
      assertExitCode(publishResult, 0);

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

      assert.equal(registry.publishRequests(), 1);
      assert.equal(registry.tarballRequests(), 1);
      assert.equal(registry.authorizationHeaders().includes('Bearer npmrc-token-publish'), true);
    } finally {
      await registry.close();
    }
  });
});
