/**
 * Developer Lifecycle — Scaffold & Validate Tests
 *
 * Tests the scaffold → validate → install pipeline WITHOUT requiring
 * the daemon runtime. These tests verify file generation, content
 * correctness, and manifest validation.
 *
 * For full daemon integration (discover → serve), see:
 *   tests/integration/daemon/e2e-developer-lifecycle.spec.ts
 */
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 { actorCommand } from '../../src/commands/actor.js';
import { webappCommand } from '../../src/commands/webapp.js';
import { validateCommand } from '../../src/commands/validate.js';
import { dependencyInstallNpmrcLines, installCommand } from '../../src/commands/install.js';

describe('developer lifecycle: scaffold + validate + install', () => {
  const tempDirs: string[] = [];

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

  // ---------------------------------------------------------------------------
  // Actor scaffold
  // ---------------------------------------------------------------------------
  describe('actor scaffold', () => {
    it('routes all Matrix runtime dependency scopes to the configured registry', () => {
      const lines = dependencyInstallNpmrcLines({
        registry: 'http://127.0.0.1:3000/api/packages/open-matrix/npm/',
        token: 'token-runtime-install',
      });
      assert.ok(lines.includes('@open-matrix:registry=http://127.0.0.1:3000/api/packages/open-matrix/npm/'));
      assert.ok(lines.includes('@matrix:registry=http://127.0.0.1:3000/api/packages/open-matrix/npm/'));
      assert.ok(lines.includes('@omega:registry=http://127.0.0.1:3000/api/packages/open-matrix/npm/'));
      assert.ok(lines.includes('always-auth=true'));
      assert.ok(lines.includes('//127.0.0.1:3000/api/packages/open-matrix/npm/:_authToken=token-runtime-install'));
    });

    it('generates all expected files with correct content', async () => {
      const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-actor-'));
      tempDirs.push(tempRoot);

      const actorDir = path.join(tempRoot, 'my-counter');
      const result = await actorCommand('my-counter', { outputDir: actorDir });

      // Return value shape
      assert.equal(result.rootDir, actorDir);
      assert.ok(result.className, 'className should be set');
      assert.equal(result.mount, 'my-counter');

      // All expected files
      const expectedFiles = [
        'matrix.json',
        'package.json',
        'tsconfig.json',
        path.join('src', `${result.className}.ts`),
        path.join('src', `create-standalone-${result.className}.ts`),
        path.join('src', 'index.ts'),
        path.join('examples', 'get-status.mxq'),
        path.join('skills', 'my-counter.skill.md'),
        path.join('tests', 'my-counter.spec.ts'),
      ];
      for (const f of expectedFiles) {
        assert.ok(fs.existsSync(path.join(actorDir, f)), `Missing: ${f}`);
      }

      // matrix.json
      const manifest = JSON.parse(fs.readFileSync(path.join(actorDir, 'matrix.json'), 'utf8'));
      assert.equal(manifest.name, '@matrix/my-counter');
      assert.ok(Array.isArray(manifest.components));
      assert.equal(manifest.components[0]?.mount, 'my-counter');
      assert.equal(manifest.components[0]?.type, result.className);
      assert.equal(manifest.runtime?.factory?.export, `createStandalone${result.className}`);
      assert.equal(manifest.runtime?.factory?.instance?.componentType, result.className);
      assert.equal(manifest.runtime?.factory?.instance?.autoStart, true);

      // package.json
      const pkg = JSON.parse(fs.readFileSync(path.join(actorDir, 'package.json'), 'utf8'));
      assert.equal(pkg.type, 'module');
      assert.equal(pkg.name, '@matrix/my-counter');
      assert.equal(pkg.scripts?.build, 'tsc -p tsconfig.json');
      assert.equal(pkg.scripts?.['build:watch'], 'tsc -w -p tsconfig.json --preserveWatchOutput');
      assert.equal(pkg.scripts?.dev, 'tsc -w -p tsconfig.json --preserveWatchOutput');
      assert.equal(pkg.scripts?.test, 'node --import tsx --test tests/**/*.spec.ts');
      assert.ok(pkg.dependencies?.['@open-matrix/core']);
      assert.equal(pkg.devDependencies?.['@open-matrix/core'], undefined);

      // tsconfig.json
      const tsconfig = JSON.parse(fs.readFileSync(path.join(actorDir, 'tsconfig.json'), 'utf8'));
      assert.equal(tsconfig.compilerOptions.target, 'ES2022');
      assert.equal(tsconfig.compilerOptions.module, 'NodeNext');
      assert.equal(tsconfig.compilerOptions.declaration, true);
      assert.equal(tsconfig.compilerOptions.incremental, true);
      assert.equal(tsconfig.compilerOptions.tsBuildInfoFile, '.tsbuildinfo');
      assert.equal(tsconfig.compilerOptions.sourceMap, true);
      assert.deepEqual(tsconfig.include, ['src']);

      // Actor source imports @open-matrix/core (not @open-matrix/core)
      const actorSrc = fs.readFileSync(path.join(actorDir, 'src', `${result.className}.ts`), 'utf8');
      assert.ok(actorSrc.includes('@open-matrix/core'), 'should import from @open-matrix/core');
      assert.ok(!actorSrc.includes("'@open-matrix/core'"), 'should NOT import from @open-matrix/core');
    });

    it('validates successfully after scaffolding', async () => {
      const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-actor-val-'));
      tempDirs.push(tempRoot);

      const actorDir = path.join(tempRoot, 'test-actor');
      await actorCommand('test-actor', { outputDir: actorDir });

      const validation = await validateCommand(actorDir);
      assert.equal(validation.valid, true, `Diagnostics: ${JSON.stringify(validation.diagnostics)}`);
      assert.equal(validation.packageName, '@matrix/test-actor');
    });

    it('installs into a packages directory', async () => {
      const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-actor-inst-'));
      tempDirs.push(tempRoot);

      const actorDir = path.join(tempRoot, 'my-service');
      await actorCommand('my-service', { outputDir: actorDir });

      const packagesRoot = path.join(tempRoot, '.matrix', 'packages');
      fs.mkdirSync(path.join(packagesRoot, 'node_modules'), { recursive: true });

      await installCommand(actorDir, {
        cwd: tempRoot,
        packagesDir: packagesRoot,
        force: true,
      });

      // Verify installed
      const installedManifest = path.join(packagesRoot, 'node_modules', '@matrix', 'my-service', 'matrix.json');
      assert.ok(fs.existsSync(installedManifest), 'Installed package should have matrix.json');

      const manifest = JSON.parse(fs.readFileSync(installedManifest, 'utf8'));
      assert.equal(manifest.name, '@matrix/my-service');
    });
  });

  // ---------------------------------------------------------------------------
  // Webapp scaffold
  // ---------------------------------------------------------------------------
  describe('webapp scaffold', () => {
    it('generates all expected files with correct content', async () => {
      const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-webapp-'));
      tempDirs.push(tempRoot);

      const appDir = path.join(tempRoot, 'my-dashboard');
      const result = await webappCommand('my-dashboard', { outputDir: appDir });

      // Return value shape
      assert.equal(result.rootDir, appDir);
      assert.ok(result.appClassName);
      assert.ok(result.shellClassName);
      assert.ok(result.tagName);
      assert.equal(result.appName, 'my-dashboard');

      // All expected webapp files
      const expectedFiles = [
        'matrix.json',
        'package.json',
        'tsconfig.json',
        'vite.config.ts',
        'index.html',
        path.join('src', 'index.ts'),
        path.join('src', 'init.ts'),
        path.join('src', `${result.appClassName}.ts`),
        path.join('src', 'shell', `${result.shellClassName}.ts`),
        path.join('src', 'template', 'app.template.ts'),
        path.join('src', 'styles', 'app.styles.ts'),
        path.join('tests', 'my-dashboard.spec.ts'),
        path.join('examples', 'introspect.mxq'),
      ];
      for (const f of expectedFiles) {
        assert.ok(fs.existsSync(path.join(appDir, f)), `Missing webapp file: ${f}`);
      }

      // matrix.json — webapp section (scoped name: @open-matrix/<name>)
      const manifest = JSON.parse(fs.readFileSync(path.join(appDir, 'matrix.json'), 'utf8'));
      assert.equal(manifest.name, '@open-matrix/my-dashboard');
      assert.ok(manifest.webapp, 'should have webapp section');
      assert.ok(manifest.webapp.distDir);
      assert.equal(manifest.webapp.appName, 'my-dashboard');

      // package.json — webapp deps
      const pkg = JSON.parse(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8'));
      assert.equal(pkg.type, 'module');
      assert.ok(pkg.scripts?.dev, 'should have vite dev script');
      assert.ok(pkg.scripts?.build, 'should have vite build script');
      assert.ok(pkg.devDependencies?.vite);
      assert.ok(pkg.devDependencies?.['@open-matrix/core']);
      assert.ok(pkg.devDependencies?.['@open-matrix/federation']);

      // tsconfig.json — DOM libs, noEmit
      const tsconfig = JSON.parse(fs.readFileSync(path.join(appDir, 'tsconfig.json'), 'utf8'));
      assert.ok(tsconfig.compilerOptions.lib?.includes('DOM'));
      assert.equal(tsconfig.compilerOptions.noEmit, true);
      assert.equal(tsconfig.compilerOptions.module, 'ESNext');
      assert.equal(tsconfig.compilerOptions.moduleResolution, 'Bundler');

      // vite.config.ts — base path
      const viteConfig = fs.readFileSync(path.join(appDir, 'vite.config.ts'), 'utf8');
      assert.ok(viteConfig.includes('/apps/my-dashboard/'), 'vite base should point to /apps/my-dashboard/');

      // index.html — custom element tag
      const indexHtml = fs.readFileSync(path.join(appDir, 'index.html'), 'utf8');
      assert.ok(indexHtml.includes(result.tagName), `HTML should contain custom element <${result.tagName}>`);

      // Shell imports federation
      const shellSrc = fs.readFileSync(
        path.join(appDir, 'src', 'shell', `${result.shellClassName}.ts`),
        'utf8',
      );
      assert.ok(shellSrc.includes('@open-matrix/core'), 'shell should import from @open-matrix/core');
    });

    it('validates successfully after scaffolding', async () => {
      const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-webapp-val-'));
      tempDirs.push(tempRoot);

      const appDir = path.join(tempRoot, 'test-app');
      await webappCommand('test-app', { outputDir: appDir });

      const validation = await validateCommand(appDir);
      assert.equal(validation.valid, true, `Diagnostics: ${JSON.stringify(validation.diagnostics)}`);
    });

    it('installs into a packages directory', async () => {
      const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-webapp-inst-'));
      tempDirs.push(tempRoot);

      const appDir = path.join(tempRoot, 'my-app');
      await webappCommand('my-app', { outputDir: appDir });

      const packagesRoot = path.join(tempRoot, '.matrix', 'packages');
      fs.mkdirSync(path.join(packagesRoot, 'node_modules'), { recursive: true });

      await installCommand(appDir, {
        cwd: tempRoot,
        packagesDir: packagesRoot,
        force: true,
      });

      // Scoped package: @open-matrix/my-app → node_modules/@open-matrix/my-app/
      const installedManifest = path.join(packagesRoot, 'node_modules', '@open-matrix', 'my-app', 'matrix.json');
      assert.ok(fs.existsSync(installedManifest), 'Installed webapp should have matrix.json');

      const manifest = JSON.parse(fs.readFileSync(installedManifest, 'utf8'));
      assert.equal(manifest.name, '@open-matrix/my-app');
      assert.ok(manifest.webapp, 'Installed manifest should preserve webapp section');
    });
  });

  // ---------------------------------------------------------------------------
  // Validation
  // ---------------------------------------------------------------------------
  describe('manifest validation', () => {
    it('rejects manifest with missing required fields', async () => {
      const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-bad-'));
      tempDirs.push(tempRoot);

      const badDir = path.join(tempRoot, 'bad-package');
      fs.mkdirSync(badDir, { recursive: true });
      fs.writeFileSync(
        path.join(badDir, 'matrix.json'),
        JSON.stringify({ name: 'bad-package' }, null, 2),
      );

      const validation = await validateCommand(badDir);
      assert.ok(validation.diagnostics.length > 0, 'Should have diagnostics for bad manifest');
    });
  });
});
