import { mkdirSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { normalizeGate, countAIFindings, runGate, runGates } from '../src/gate-runner.js';

function makeTmpDir(): string {
  const dir = join(tmpdir(), `macp-gate-${randomUUID()}`);
  mkdirSync(dir, { recursive: true });
  return dir;
}

describe('normalizeGate', () => {
  it('normalizes a string to mechanical gate', () => {
    expect(normalizeGate('echo test')).toEqual({
      command: 'echo test',
      type: 'mechanical',
      fail_on: 'blocker',
    });
  });

  it('normalizes an object gate with defaults', () => {
    expect(normalizeGate({ command: 'lint' })).toEqual({
      command: 'lint',
      type: 'mechanical',
      fail_on: 'blocker',
    });
  });

  it('preserves explicit type and fail_on', () => {
    expect(normalizeGate({ command: 'review', type: 'ai-review', fail_on: 'any' })).toEqual({
      command: 'review',
      type: 'ai-review',
      fail_on: 'any',
    });
  });

  it('handles non-string/non-object input', () => {
    expect(normalizeGate(42)).toEqual({ command: '', type: 'mechanical', fail_on: 'blocker' });
    expect(normalizeGate(null)).toEqual({ command: '', type: 'mechanical', fail_on: 'blocker' });
  });
});

describe('countAIFindings', () => {
  it('returns zeros for non-object', () => {
    expect(countAIFindings(null)).toEqual({ blockers: 0, total: 0 });
    expect(countAIFindings('string')).toEqual({ blockers: 0, total: 0 });
    expect(countAIFindings([])).toEqual({ blockers: 0, total: 0 });
  });

  it('counts from stats block', () => {
    const output = { stats: { blockers: 2, should_fix: 3, suggestions: 1 } };
    expect(countAIFindings(output)).toEqual({ blockers: 2, total: 6 });
  });

  it('counts from findings array when stats has no blockers', () => {
    const output = {
      stats: { blockers: 0 },
      findings: [{ severity: 'blocker' }, { severity: 'warning' }, { severity: 'blocker' }],
    };
    expect(countAIFindings(output)).toEqual({ blockers: 2, total: 3 });
  });

  it('uses stats blockers over findings array when stats has blockers', () => {
    const output = {
      stats: { blockers: 5 },
      findings: [{ severity: 'blocker' }, { severity: 'warning' }],
    };
    // stats.blockers = 5, total from stats = 5+0+0 = 5, findings not used for total since stats total is non-zero
    expect(countAIFindings(output)).toEqual({ blockers: 5, total: 5 });
  });

  it('counts findings length as total when stats has zero total', () => {
    const output = {
      findings: [{ severity: 'warning' }, { severity: 'info' }],
    };
    expect(countAIFindings(output)).toEqual({ blockers: 0, total: 2 });
  });
});

describe('runGate', () => {
  let tmp: string;
  let logPath: string;

  beforeEach(() => {
    tmp = makeTmpDir();
    logPath = join(tmp, 'gate.log');
  });

  afterEach(() => {
    rmSync(tmp, { recursive: true, force: true });
  });

  it('passes mechanical gate on exit 0', () => {
    const result = runGate('echo hello', tmp, logPath, 30);
    expect(result.passed).toBe(true);
    expect(result.exit_code).toBe(0);
    expect(result.type).toBe('mechanical');
    expect(result.output).toContain('hello');
  });

  it('fails mechanical gate on non-zero exit', () => {
    const result = runGate('exit 1', tmp, logPath, 30);
    expect(result.passed).toBe(false);
    expect(result.exit_code).toBe(1);
  });

  it('ci-pipeline always passes', () => {
    const result = runGate({ command: 'anything', type: 'ci-pipeline' }, tmp, logPath, 30);
    expect(result.passed).toBe(true);
    expect(result.type).toBe('ci-pipeline');
    expect(result.output).toBe('CI pipeline gate placeholder');
  });

  it('empty command passes', () => {
    const result = runGate({ command: '' }, tmp, logPath, 30);
    expect(result.passed).toBe(true);
  });

  it('ai-review gate parses JSON output', () => {
    const json = JSON.stringify({ stats: { blockers: 0, should_fix: 1 } });
    const result = runGate({ command: `echo '${json}'`, type: 'ai-review' }, tmp, logPath, 30);
    expect(result.passed).toBe(true);
    expect(result.blockers).toBe(0);
    expect(result.findings).toBe(1);
  });

  it('ai-review gate fails on blockers', () => {
    const json = JSON.stringify({ stats: { blockers: 2 } });
    const result = runGate({ command: `echo '${json}'`, type: 'ai-review' }, tmp, logPath, 30);
    expect(result.passed).toBe(false);
    expect(result.blockers).toBe(2);
  });

  it('ai-review gate with fail_on=any fails on any findings', () => {
    const json = JSON.stringify({ stats: { blockers: 0, should_fix: 1 } });
    const result = runGate(
      { command: `echo '${json}'`, type: 'ai-review', fail_on: 'any' },
      tmp,
      logPath,
      30,
    );
    expect(result.passed).toBe(false);
    expect(result.fail_on).toBe('any');
  });

  it('ai-review gate fails on invalid JSON output', () => {
    const result = runGate({ command: 'echo "not json"', type: 'ai-review' }, tmp, logPath, 30);
    expect(result.passed).toBe(false);
    expect(result.parse_error).toBeDefined();
  });

  it('writes to log file', () => {
    runGate('echo logged', tmp, logPath, 30);
    const log = readFileSync(logPath, 'utf-8');
    expect(log).toContain('COMMAND: echo logged');
    expect(log).toContain('logged');
    expect(log).toContain('EXIT:');
  });
});

describe('runGates', () => {
  let tmp: string;
  let logPath: string;
  let eventsPath: string;

  beforeEach(() => {
    tmp = makeTmpDir();
    logPath = join(tmp, 'gates.log');
    eventsPath = join(tmp, 'events.ndjson');
  });

  afterEach(() => {
    rmSync(tmp, { recursive: true, force: true });
  });

  it('runs multiple gates and returns results', () => {
    const { allPassed, gateResults } = runGates(
      ['echo one', 'echo two'],
      tmp,
      logPath,
      30,
      eventsPath,
      'task-1',
    );
    expect(allPassed).toBe(true);
    expect(gateResults).toHaveLength(2);
  });

  it('reports failure when any gate fails', () => {
    const { allPassed, gateResults } = runGates(
      ['echo ok', 'exit 1'],
      tmp,
      logPath,
      30,
      eventsPath,
      'task-2',
    );
    expect(allPassed).toBe(false);
    expect(gateResults[0]!.passed).toBe(true);
    expect(gateResults[1]!.passed).toBe(false);
  });

  it('emits events for each gate', () => {
    runGates(['echo test'], tmp, logPath, 30, eventsPath, 'task-3');
    const events = readFileSync(eventsPath, 'utf-8')
      .trim()
      .split('\n')
      .map((l) => JSON.parse(l));
    expect(events).toHaveLength(2); // started + passed
    expect(events[0].event_type).toBe('rail.check.started');
    expect(events[1].event_type).toBe('rail.check.passed');
  });

  it('skips gates with empty command (non ci-pipeline)', () => {
    const { gateResults } = runGates(
      [{ command: '', type: 'mechanical' }, 'echo real'],
      tmp,
      logPath,
      30,
      eventsPath,
      'task-4',
    );
    expect(gateResults).toHaveLength(1);
  });

  it('does not skip ci-pipeline even with empty command', () => {
    const { gateResults } = runGates(
      [{ command: '', type: 'ci-pipeline' }],
      tmp,
      logPath,
      30,
      eventsPath,
      'task-5',
    );
    expect(gateResults).toHaveLength(1);
    expect(gateResults[0]!.passed).toBe(true);
  });

  it('emits failed event with correct message', () => {
    runGates(['exit 42'], tmp, logPath, 30, eventsPath, 'task-6');
    const events = readFileSync(eventsPath, 'utf-8')
      .trim()
      .split('\n')
      .map((l) => JSON.parse(l));
    const failEvent = events.find(
      (e: Record<string, unknown>) => e.event_type === 'rail.check.failed',
    );
    expect(failEvent).toBeDefined();
    expect(failEvent.message).toContain('Gate failed (');
  });
});
