import { afterEach, 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 { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';

describe('FP-30 mx publish preflight', () => {
  const tempDirs: string[] = [];

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

  it('requires login and enforces immutable version publish', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-'));
    tempDirs.push(root);

    const fixture = createMatrixPackage(root, {
      packageName: '@acme/fp30-publish',
      version: '1.0.0',
      componentType: 'PublishActor',
      mount: 'fp30.publish',
    });
    const registryDir = path.join(root, 'registry');
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');

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

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

    const publishResult = await runMxCli([
      'node',
      'mx',
      'publish',
      '--registry-dir',
      registryDir,
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: fixture.packageDir });
    assertExitCode(publishResult, 0);
    const publishPayload = parseLastJson(publishResult.logs) as {
      packageName: string;
      version: string;
      registryTarballPath?: string;
    };
    assert.equal(publishPayload.packageName, fixture.packageName);
    assert.equal(publishPayload.version, fixture.version);
    assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);

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

  it('allows webapp-only packages without actor components', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-webapp-'));
    tempDirs.push(root);

    const packageDir = path.join(root, 'webapp-only');
    const registryDir = path.join(root, 'registry');
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');
    fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
    fs.writeFileSync(path.join(packageDir, 'dist', 'index.html'), '<!doctype html><title>Webapp only</title>\n', 'utf8');
    fs.writeFileSync(
      path.join(packageDir, 'matrix.json'),
      JSON.stringify(
        {
          name: '@acme/fp30-webapp-only',
          version: '1.0.0',
          runtime: { language: 'typescript', entry: 'dist/index.html' },
          webapp: { distDir: 'dist', entry: 'index.html', appName: 'webapp-only' },
          components: [],
          permissions: { fsPolicy: 'none' },
        },
        null,
        2,
      ),
      'utf8',
    );
    fs.writeFileSync(
      path.join(packageDir, 'package.json'),
      JSON.stringify(
        {
          name: '@acme/fp30-webapp-only',
          version: '1.0.0',
          type: 'module',
          files: ['dist', 'matrix.json'],
        },
        null,
        2,
      ),
      'utf8',
    );

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

    const publishResult = await runMxCli([
      'node',
      'mx',
      'publish',
      '--registry-dir',
      registryDir,
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: packageDir });
    assertExitCode(publishResult, 0);
    const publishPayload = parseLastJson(publishResult.logs) as {
      packageName: string;
      version: string;
      registryTarballPath?: string;
    };
    assert.equal(publishPayload.packageName, '@acme/fp30-webapp-only');
    assert.equal(publishPayload.version, '1.0.0');
    assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
  });

  it('allows runtime-only packages with a root actor and no components', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-runtime-only-'));
    tempDirs.push(root);

    const packageDir = path.join(root, 'runtime-only');
    const registryDir = path.join(root, 'registry');
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');
    fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
    fs.writeFileSync(path.join(packageDir, 'dist', 'index.js'), 'export class RuntimeOnlyActor {}\n', 'utf8');
    fs.writeFileSync(
      path.join(packageDir, 'matrix.json'),
      JSON.stringify(
        {
          name: '@acme/fp30-runtime-only',
          version: '1.0.0',
          root: {
            type: 'RuntimeOnlyActor',
            export: 'RuntimeOnlyActor',
            description: 'Runtime-only root actor',
          },
          runtime: { language: 'typescript', entry: 'dist/index.js' },
          components: [],
          permissions: { fsPolicy: 'none' },
        },
        null,
        2,
      ),
      'utf8',
    );
    fs.writeFileSync(
      path.join(packageDir, 'package.json'),
      JSON.stringify(
        {
          name: '@acme/fp30-runtime-only',
          version: '1.0.0',
          type: 'module',
          files: ['dist', 'matrix.json'],
        },
        null,
        2,
      ),
      'utf8',
    );

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

    const publishResult = await runMxCli([
      'node',
      'mx',
      'publish',
      '--registry-dir',
      registryDir,
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: packageDir });
    assertExitCode(publishResult, 0);
    const publishPayload = parseLastJson(publishResult.logs) as {
      packageName: string;
      version: string;
      registryTarballPath?: string;
    };
    assert.equal(publishPayload.packageName, '@acme/fp30-runtime-only');
    assert.equal(publishPayload.version, '1.0.0');
    assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
  });

  it('allows service factory packages declared by matrix.json runtime.factory', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-service-'));
    tempDirs.push(root);

    const packageDir = path.join(root, 'service-factory');
    const registryDir = path.join(root, 'registry');
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');
    fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
    fs.writeFileSync(
      path.join(packageDir, 'dist', 'index.js'),
      'export async function createServiceFactory() { return { runtime: { shutdown() {} } }; }\n',
      'utf8',
    );
    fs.writeFileSync(
      path.join(packageDir, 'matrix.json'),
      JSON.stringify(
        {
          name: '@acme/fp30-service-factory',
          version: '1.0.0',
          runtime: {
            language: 'typescript',
            entry: 'dist/index.js',
            factory: {
              kind: 'factory',
              export: 'createServiceFactory',
              instance: {
                id: 'RUNTIME-SERVICE-FACTORY',
                class: 'service',
                rootKind: 'package-root',
                autoStart: true,
              },
            },
          },
          components: [],
          permissions: { fsPolicy: 'none' },
        },
        null,
        2,
      ),
      'utf8',
    );
    fs.writeFileSync(
      path.join(packageDir, 'package.json'),
      JSON.stringify(
        {
          name: '@acme/fp30-service-factory',
          version: '1.0.0',
          type: 'module',
          files: ['dist', 'matrix.json'],
        },
        null,
        2,
      ),
      'utf8',
    );

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

    const publishResult = await runMxCli([
      'node',
      'mx',
      'publish',
      '--registry-dir',
      registryDir,
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: packageDir });
    assertExitCode(publishResult, 0);
    const publishPayload = parseLastJson(publishResult.logs) as {
      packageName: string;
      version: string;
      registryTarballPath?: string;
    };
    assert.equal(publishPayload.packageName, '@acme/fp30-service-factory');
    assert.equal(publishPayload.version, '1.0.0');
    assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
  });
});
