import fs from 'fs';
import os from 'os';
import path from 'path';

import { describe, expect, it } from 'vitest';

import {
  buildVerificationCommand,
  computeVerificationSnapshot,
  isVerificationProfile,
  runVerificationRequest,
} from './verification.js';
import {
  buildWorkspaceCommandEnvironment,
  ensureWorkspaceDependenciesInstalled,
  hasInstalledNodeModules,
} from './workspace-package-manager.js';

describe('verification helpers', () => {
  it('recognizes only fixed verification profiles', () => {
    expect(isVerificationProfile('test')).toBe(true);
    expect(isVerificationProfile('typecheck')).toBe(true);
    expect(isVerificationProfile('build')).toBe(true);
    expect(isVerificationProfile('lint')).toBe(true);
  });

  it('builds deterministic commands for each profile', () => {
    const repoDir = fs.mkdtempSync(
      path.join(os.tmpdir(), 'ejclaw-verification-'),
    );
    fs.writeFileSync(path.join(repoDir, 'package.json'), JSON.stringify({}));

    expect(buildVerificationCommand('test', repoDir)).toMatchObject({
      file: 'npm',
      args: ['test'],
      requiredScript: 'test',
    });
    expect(buildVerificationCommand('typecheck', repoDir)).toMatchObject({
      file: 'npm',
      args: ['run', 'typecheck'],
      requiredScript: 'typecheck',
    });
    expect(buildVerificationCommand('build', repoDir)).toMatchObject({
      file: 'npm',
      args: ['run', 'build'],
      requiredScript: 'build',
    });
    expect(buildVerificationCommand('lint', repoDir)).toMatchObject({
      file: 'npm',
      args: ['run', 'lint'],
      requiredScript: 'lint',
    });
  });

  it('prefers lint:ci and lint:check before plain lint', () => {
    const repoDir = fs.mkdtempSync(
      path.join(os.tmpdir(), 'ejclaw-verification-lint-'),
    );
    fs.writeFileSync(
      path.join(repoDir, 'package.json'),
      JSON.stringify({
        scripts: {
          lint: 'eslint .',
          'lint:check': 'eslint --max-warnings 0 .',
          'lint:ci': 'eslint --format compact .',
        },
      }),
    );

    expect(buildVerificationCommand('lint', repoDir)).toMatchObject({
      file: 'npm',
      args: ['run', 'lint:ci'],
      requiredScript: 'lint:ci',
    });

    fs.writeFileSync(
      path.join(repoDir, 'package.json'),
      JSON.stringify({
        scripts: {
          lint: 'eslint .',
          'lint:check': 'eslint --max-warnings 0 .',
        },
      }),
    );

    expect(buildVerificationCommand('lint', repoDir)).toMatchObject({
      file: 'npm',
      args: ['run', 'lint:check'],
      requiredScript: 'lint:check',
    });
  });

  it('selects the workspace package manager for verification commands', () => {
    const repoDir = fs.mkdtempSync(
      path.join(os.tmpdir(), 'ejclaw-verification-pnpm-'),
    );
    fs.writeFileSync(
      path.join(repoDir, 'package.json'),
      JSON.stringify({ packageManager: 'pnpm@10.11.0' }),
    );

    expect(buildVerificationCommand('typecheck', repoDir)).toMatchObject({
      file: 'corepack',
      args: ['pnpm', 'run', 'typecheck'],
      commandText: 'corepack pnpm run typecheck',
      requiredScript: 'typecheck',
    });
  });

  it('builds nested pnpm verification commands without tripping corepack project specs', () => {
    const parentDir = fs.mkdtempSync(
      path.join(os.tmpdir(), 'ejclaw-verification-corepack-parent-'),
    );
    fs.writeFileSync(
      path.join(parentDir, 'package.json'),
      JSON.stringify({ packageManager: 'bun@1.3.11' }),
    );

    const repoDir = path.join(parentDir, 'owner');
    fs.mkdirSync(repoDir, { recursive: true });
    fs.writeFileSync(
      path.join(repoDir, 'package.json'),
      JSON.stringify({
        name: 'nested-pnpm-workspace',
        scripts: {
          typecheck: 'tsc --noEmit',
        },
      }),
    );
    fs.writeFileSync(
      path.join(repoDir, 'pnpm-lock.yaml'),
      'lockfileVersion: 9.0\n',
    );
    const command = buildVerificationCommand('typecheck', repoDir);

    expect(command).toMatchObject({
      file: 'corepack',
      args: ['pnpm', 'run', 'typecheck'],
      commandText: 'corepack pnpm run typecheck',
      packageManager: 'pnpm',
      requiredScript: 'typecheck',
    });
    expect(
      buildWorkspaceCommandEnvironment(repoDir, command.packageManager, {
        PATH: '/tmp/bin',
      }),
    ).toMatchObject({
      PATH: '/tmp/bin',
      COREPACK_ENABLE_PROJECT_SPEC: '0',
    });
  });

  it('computes a stable snapshot over the readable workspace inputs', () => {
    const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-snapshot-'));
    fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true });
    fs.mkdirSync(path.join(repoDir, 'node_modules'), { recursive: true });
    fs.writeFileSync(
      path.join(repoDir, 'src', 'index.ts'),
      'export const x = 1;\n',
    );
    fs.mkdirSync(path.join(repoDir, 'dist'), { recursive: true });
    fs.writeFileSync(path.join(repoDir, '.env'), 'SECRET=1\n');
    fs.writeFileSync(
      path.join(repoDir, 'dist', 'index.js'),
      'export const x = 1;\n',
    );

    const first = computeVerificationSnapshot(repoDir).snapshotId;
    fs.writeFileSync(path.join(repoDir, '.env'), 'SECRET=2\n');
    fs.writeFileSync(
      path.join(repoDir, 'dist', 'index.js'),
      'export const x = 2;\n',
    );
    const second = computeVerificationSnapshot(repoDir).snapshotId;
    fs.writeFileSync(
      path.join(repoDir, 'src', 'index.ts'),
      'export const x = 2;\n',
    );
    const third = computeVerificationSnapshot(repoDir).snapshotId;

    expect(second).toBe(first);
    expect(third).not.toBe(first);
  });

  it('ignores runtime state directories and local backup folders', () => {
    const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-snapshot-'));
    fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true });
    fs.mkdirSync(path.join(repoDir, 'data'), { recursive: true });
    fs.mkdirSync(path.join(repoDir, 'logs'), { recursive: true });
    fs.mkdirSync(path.join(repoDir, 'cache'), { recursive: true });
    fs.mkdirSync(path.join(repoDir, '.ejclaw-reviewer-runtime'), {
      recursive: true,
    });
    fs.mkdirSync(path.join(repoDir, 'store.local-backup-20260408_024500'), {
      recursive: true,
    });

    fs.writeFileSync(
      path.join(repoDir, 'src', 'index.ts'),
      'export const x = 1;\n',
    );
    fs.writeFileSync(path.join(repoDir, 'data', 'state.json'), '{"x":1}\n');
    fs.writeFileSync(path.join(repoDir, 'logs', 'service.log'), 'line-1\n');
    fs.writeFileSync(path.join(repoDir, 'cache', 'tmp.txt'), 'cache-a\n');
    fs.writeFileSync(
      path.join(repoDir, '.ejclaw-reviewer-runtime', 'runtime.json'),
      '{"ok":true}\n',
    );
    fs.writeFileSync(
      path.join(repoDir, 'store.local-backup-20260408_024500', 'db.sqlite'),
      'backup-a\n',
    );

    const first = computeVerificationSnapshot(repoDir).snapshotId;

    fs.writeFileSync(path.join(repoDir, 'data', 'state.json'), '{"x":2}\n');
    fs.writeFileSync(path.join(repoDir, 'logs', 'service.log'), 'line-2\n');
    fs.writeFileSync(path.join(repoDir, 'cache', 'tmp.txt'), 'cache-b\n');
    fs.writeFileSync(
      path.join(repoDir, '.ejclaw-reviewer-runtime', 'runtime.json'),
      '{"ok":false}\n',
    );
    fs.writeFileSync(
      path.join(repoDir, 'store.local-backup-20260408_024500', 'db.sqlite'),
      'backup-b\n',
    );

    const second = computeVerificationSnapshot(repoDir).snapshotId;

    expect(second).toBe(first);
  });

  it('still includes nested source directories named like excluded roots', () => {
    const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-snapshot-'));
    fs.mkdirSync(path.join(repoDir, 'src', 'fixtures', 'data'), {
      recursive: true,
    });
    fs.writeFileSync(
      path.join(repoDir, 'src', 'fixtures', 'data', 'sample.json'),
      '{"v":1}\n',
    );

    const first = computeVerificationSnapshot(repoDir).snapshotId;

    fs.writeFileSync(
      path.join(repoDir, 'src', 'fixtures', 'data', 'sample.json'),
      '{"v":2}\n',
    );

    const second = computeVerificationSnapshot(repoDir).snapshotId;

    expect(second).not.toBe(first);
  });

  it('backfills legacy node_modules state before verification', async () => {
    const repoDir = fs.mkdtempSync(
      path.join(os.tmpdir(), 'ejclaw-verification-legacy-'),
    );
    fs.mkdirSync(path.join(repoDir, 'node_modules', '.bin'), {
      recursive: true,
    });
    fs.writeFileSync(
      path.join(repoDir, 'package.json'),
      JSON.stringify({
        name: 'verification-legacy',
        packageManager: 'bun@1.3.11',
        scripts: {
          test: 'vitest run',
          typecheck: 'tsc --noEmit',
          build: 'tsc',
        },
      }),
    );
    fs.writeFileSync(path.join(repoDir, 'bun.lock'), '');
    fs.writeFileSync(path.join(repoDir, 'node_modules', '.bin', 'tsc'), '');

    expect(hasInstalledNodeModules(repoDir)).toBe(false);

    const result = await runVerificationRequest(
      {
        requestId: 'req-legacy-node-modules',
        profile: 'typecheck',
        expectedSnapshotId: 'fs:mismatch',
      },
      { repoDir },
    );

    expect(hasInstalledNodeModules(repoDir)).toBe(true);
    expect(result.ok).toBe(false);
    expect(result.error).toContain('Snapshot mismatch before verification');
  });

  it('runs typecheck directly on the host for non-build profiles', async () => {
    const repoDir = fs.mkdtempSync(
      path.join(os.tmpdir(), 'ejclaw-verification-direct-'),
    );
    fs.mkdirSync(path.join(repoDir, 'node_modules', '.bin'), {
      recursive: true,
    });
    fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true });
    fs.writeFileSync(
      path.join(repoDir, 'package.json'),
      JSON.stringify({
        name: 'verification-direct',
        packageManager: 'bun@1.3.11',
        scripts: {
          typecheck:
            "node -e \"process.stdout.write('typecheck:' + (process.env.CI || 'missing'))\"",
        },
      }),
    );
    fs.writeFileSync(path.join(repoDir, 'bun.lock'), '');
    fs.writeFileSync(
      path.join(repoDir, 'node_modules', '.bin', 'placeholder'),
      '',
    );
    fs.writeFileSync(
      path.join(repoDir, 'src', 'index.ts'),
      'export const value = 1;\n',
    );
    ensureWorkspaceDependenciesInstalled(repoDir);

    const expectedSnapshotId = computeVerificationSnapshot(repoDir).snapshotId;
    const result = await runVerificationRequest(
      {
        requestId: 'req-direct-typecheck',
        profile: 'typecheck',
        expectedSnapshotId,
      },
      { repoDir },
    );

    expect(result.ok).toBe(true);
    expect(result.exitCode).toBe(0);
    expect(result.stdout).toContain('typecheck:1');
    expect(result.runtimeVersion).toMatch(/^host:bun@/);
    expect(result.snapshotId).toBe(expectedSnapshotId);
  });

  it('runs lint verification using the preferred lint script', async () => {
    const repoDir = fs.mkdtempSync(
      path.join(os.tmpdir(), 'ejclaw-verification-direct-lint-'),
    );
    fs.mkdirSync(path.join(repoDir, 'node_modules', '.bin'), {
      recursive: true,
    });
    fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true });
    fs.writeFileSync(
      path.join(repoDir, 'package.json'),
      JSON.stringify({
        name: 'verification-direct-lint',
        packageManager: 'bun@1.3.11',
        scripts: {
          'lint:check':
            "node -e \"process.stdout.write('lint-check:' + (process.env.CI || 'missing'))\"",
        },
      }),
    );
    fs.writeFileSync(path.join(repoDir, 'bun.lock'), '');
    fs.writeFileSync(
      path.join(repoDir, 'node_modules', '.bin', 'placeholder'),
      '',
    );
    fs.writeFileSync(
      path.join(repoDir, 'src', 'index.ts'),
      'export const value = 1;\n',
    );
    ensureWorkspaceDependenciesInstalled(repoDir);

    const expectedSnapshotId = computeVerificationSnapshot(repoDir).snapshotId;
    const result = await runVerificationRequest(
      {
        requestId: 'req-direct-lint',
        profile: 'lint',
        expectedSnapshotId,
      },
      { repoDir },
    );

    expect(result.ok).toBe(true);
    expect(result.exitCode).toBe(0);
    expect(result.command).toContain('lint:check');
    expect(result.stdout).toContain('lint-check:1');
    expect(result.snapshotId).toBe(expectedSnapshotId);
  });

  it('does not leak host secrets into direct verification commands', async () => {
    const repoDir = fs.mkdtempSync(
      path.join(os.tmpdir(), 'ejclaw-verification-direct-secret-'),
    );
    fs.mkdirSync(path.join(repoDir, 'node_modules', '.bin'), {
      recursive: true,
    });
    fs.writeFileSync(
      path.join(repoDir, 'package.json'),
      JSON.stringify({
        name: 'verification-direct-secret',
        packageManager: 'bun@1.3.11',
        scripts: {
          typecheck:
            'node -e "process.stdout.write(process.env.EJCLAW_VERIFICATION_SECRET || \'missing\')"',
        },
      }),
    );
    fs.writeFileSync(path.join(repoDir, 'bun.lock'), '');
    fs.writeFileSync(
      path.join(repoDir, 'node_modules', '.bin', 'placeholder'),
      '',
    );
    ensureWorkspaceDependenciesInstalled(repoDir);

    const previousSecret = process.env.EJCLAW_VERIFICATION_SECRET;
    process.env.EJCLAW_VERIFICATION_SECRET = 'top-secret';

    try {
      const expectedSnapshotId =
        computeVerificationSnapshot(repoDir).snapshotId;
      const result = await runVerificationRequest(
        {
          requestId: 'req-direct-typecheck-secret',
          profile: 'typecheck',
          expectedSnapshotId,
        },
        { repoDir },
      );

      expect(result.ok).toBe(true);
      expect(result.stdout).toContain('missing');
      expect(result.stdout).not.toContain('top-secret');
    } finally {
      if (previousSecret == null) {
        delete process.env.EJCLAW_VERIFICATION_SECRET;
      } else {
        process.env.EJCLAW_VERIFICATION_SECRET = previousSecret;
      }
    }
  });

  it('fails direct verification if the command mutates the workspace', async () => {
    const repoDir = fs.mkdtempSync(
      path.join(os.tmpdir(), 'ejclaw-verification-direct-mutate-'),
    );
    fs.mkdirSync(path.join(repoDir, 'node_modules', '.bin'), {
      recursive: true,
    });
    fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true });
    fs.writeFileSync(
      path.join(repoDir, 'package.json'),
      JSON.stringify({
        name: 'verification-direct-mutate',
        packageManager: 'bun@1.3.11',
        scripts: {
          test: "node -e \"require('node:fs').writeFileSync('src/generated.txt', 'x\\\\n')\"",
        },
      }),
    );
    fs.writeFileSync(path.join(repoDir, 'bun.lock'), '');
    fs.writeFileSync(
      path.join(repoDir, 'node_modules', '.bin', 'placeholder'),
      '',
    );
    fs.writeFileSync(
      path.join(repoDir, 'src', 'index.ts'),
      'export const value = 1;\n',
    );
    ensureWorkspaceDependenciesInstalled(repoDir);

    const expectedSnapshotId = computeVerificationSnapshot(repoDir).snapshotId;
    const result = await runVerificationRequest(
      {
        requestId: 'req-direct-test-mutate',
        profile: 'test',
        expectedSnapshotId,
      },
      { repoDir },
    );

    expect(result.ok).toBe(false);
    expect(result.exitCode).toBe(1);
    expect(result.error).toContain('Workspace changed during verification');
    expect(result.snapshotId).not.toBe(expectedSnapshotId);
    expect(fs.existsSync(path.join(repoDir, 'src', 'generated.txt'))).toBe(
      true,
    );
  });

  it('allows build verification to write excluded output directories', async () => {
    const repoDir = fs.mkdtempSync(
      path.join(os.tmpdir(), 'ejclaw-verification-build-direct-'),
    );
    fs.mkdirSync(path.join(repoDir, 'node_modules', '.bin'), {
      recursive: true,
    });
    fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true });
    fs.writeFileSync(
      path.join(repoDir, 'package.json'),
      JSON.stringify({
        name: 'verification-build-direct',
        packageManager: 'bun@1.3.11',
        scripts: {
          build:
            "node -e \"require('node:fs').mkdirSync('dist', { recursive: true }); require('node:fs').writeFileSync('dist/output.js', 'ok\\\\n')\"",
        },
      }),
    );
    fs.writeFileSync(path.join(repoDir, 'bun.lock'), '');
    fs.writeFileSync(
      path.join(repoDir, 'node_modules', '.bin', 'placeholder'),
      '',
    );
    fs.writeFileSync(
      path.join(repoDir, 'src', 'index.ts'),
      'export const value = 1;\n',
    );
    ensureWorkspaceDependenciesInstalled(repoDir);

    const expectedSnapshotId = computeVerificationSnapshot(repoDir).snapshotId;
    const result = await runVerificationRequest(
      {
        requestId: 'req-direct-build',
        profile: 'build',
        expectedSnapshotId,
      },
      { repoDir },
    );

    expect(result.ok).toBe(true);
    expect(result.exitCode).toBe(0);
    expect(result.runtimeVersion).toMatch(/^host:bun@/);
    expect(result.snapshotId).toBe(expectedSnapshotId);
    expect(
      fs.readFileSync(path.join(repoDir, 'dist', 'output.js'), 'utf-8'),
    ).toBe('ok\n');
  });
});
