import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
  sanitizeEngineLogForDisplay,
  categorizeEngineLogMessage,
  categorizeConversionErrors,
  classifyConversionFailure,
  isStructuralFailureEngineLogLine,
  inferPrimaryFailureFromLogs,
  isAccountingSnapshot,
  labelAccountingStage,
  buildMigrationDecisionPaths,
} from './engine-integration.js';

// ── sanitizeEngineLogForDisplay ───────────────────────────────────────────────

describe('sanitizeEngineLogForDisplay', () => {
  it('returns empty string for falsy input', () => {
    assert.equal(sanitizeEngineLogForDisplay(''), '');
    assert.equal(sanitizeEngineLogForDisplay(null as unknown as string), '');
    assert.equal(sanitizeEngineLogForDisplay(undefined as unknown as string), '');
  });

  it('passes through clean messages unchanged', () => {
    assert.equal(sanitizeEngineLogForDisplay('Analysis complete — 12 files'), 'Analysis complete — 12 files');
    assert.equal(sanitizeEngineLogForDisplay('Translation pass 2 of 3'), 'Translation pass 2 of 3');
  });

  it('redacts openai references', () => {
    const result = sanitizeEngineLogForDisplay('Calling OpenAI for analysis');
    assert.ok(!result.toLowerCase().includes('openai'), `expected no "openai" in: ${result}`);
  });

  it('redacts anthropic references', () => {
    const result = sanitizeEngineLogForDisplay('Using Anthropic Claude for translation');
    assert.ok(!result.toLowerCase().includes('anthropic'), `expected no "anthropic" in: ${result}`);
    assert.ok(!result.toLowerCase().includes('claude'), `expected no "claude" in: ${result}`);
  });

  it('redacts Claude 4.x versioned model names', () => {
    for (const id of ['claude-opus-4-8', 'claude-sonnet-4-5', 'claude-haiku-4-5']) {
      const result = sanitizeEngineLogForDisplay(`LLM call via ${id} completed`);
      assert.ok(!result.toLowerCase().includes('claude'), `expected no "claude" for ${id}, got: ${result}`);
    }
  });

  it('redacts anthropic provider prefix in model strings', () => {
    const result = sanitizeEngineLogForDisplay('provider: anthropic:claude-opus-4-8 selected');
    assert.ok(!result.toLowerCase().includes('anthropic'), `expected no "anthropic" in: ${result}`);
    assert.ok(!result.toLowerCase().includes('claude'), `expected no "claude" in: ${result}`);
  });

  it('redacts gpt model names', () => {
    const result = sanitizeEngineLogForDisplay('Model gpt-4.1 returned 1200 tokens');
    assert.ok(!result.toLowerCase().includes('gpt'), `expected no "gpt" in: ${result}`);
  });

  it('redacts gemini references', () => {
    const result = sanitizeEngineLogForDisplay('gemini-pro finished processing');
    assert.ok(!result.toLowerCase().includes('gemini'), `expected no "gemini" in: ${result}`);
  });

  it('redacts provider: key=value pairs', () => {
    const result = sanitizeEngineLogForDisplay('retrying with provider: openai');
    assert.ok(!result.includes('provider: openai'));
  });

  it('replaces banned terms with em-dash placeholder', () => {
    const result = sanitizeEngineLogForDisplay('Done using OpenAI');
    assert.ok(result.includes('—'), 'should contain replacement dash');
  });

  it('replaces a bare vendor name with an em-dash placeholder', () => {
    const result = sanitizeEngineLogForDisplay('openai');
    assert.equal(result, '—');
  });

  it('collapses extra whitespace left after redaction', () => {
    const result = sanitizeEngineLogForDisplay('Using  OpenAI  today');
    assert.ok(!result.includes('  '), `double-space should be collapsed, got: ${result}`);
  });
});

// ── categorizeEngineLogMessage ────────────────────────────────────────────────

describe('categorizeEngineLogMessage', () => {
  it('returns generic for empty / non-string input', () => {
    assert.equal(categorizeEngineLogMessage(''), 'generic');
    assert.equal(categorizeEngineLogMessage(null as unknown as string), 'generic');
  });

  it('classifies preflight messages', () => {
    assert.equal(categorizeEngineLogMessage('Preflight check running'), 'preflight');
    assert.equal(categorizeEngineLogMessage('Source size limit exceeded'), 'preflight');
    assert.equal(categorizeEngineLogMessage('LOC exceeded for this plan'), 'preflight');
    assert.equal(categorizeEngineLogMessage('Too many files in source'), 'preflight');
  });

  it('classifies analysis messages', () => {
    assert.equal(categorizeEngineLogMessage('Running analysis pass on cobol sources'), 'analysis');
    assert.equal(categorizeEngineLogMessage('Analysis complete — 12 files'), 'analysis');
    assert.equal(categorizeEngineLogMessage('Scanning repository structure'), 'analysis');
  });

  it('classifies compile messages', () => {
    assert.equal(categorizeEngineLogMessage('compile-check: 0 errors'), 'compile');
    assert.equal(categorizeEngineLogMessage('Compiler invoked on output'), 'compile');
    assert.equal(categorizeEngineLogMessage('Sandbox running tests'), 'compile');
  });

  it('classifies translation messages', () => {
    assert.equal(categorizeEngineLogMessage('Translation pass starting'), 'translate');
    assert.equal(categorizeEngineLogMessage('Translating 14 files'), 'translate');
    assert.equal(categorizeEngineLogMessage('Transient backend issue, retrying'), 'translate');
  });

  it('classifies multi-stage translation as stage category', () => {
    assert.equal(categorizeEngineLogMessage('Using multi-stage translator'), 'stage');
    assert.equal(categorizeEngineLogMessage('BodiesStage complete'), 'stage');
  });

  it('classifies validation messages', () => {
    assert.equal(categorizeEngineLogMessage('Validation parity score: 0.87'), 'validate');
    assert.equal(categorizeEngineLogMessage('Parity check running'), 'validate');
  });

  it('classifies quality gate messages', () => {
    assert.equal(categorizeEngineLogMessage('Quality gate: index=87 / threshold=80 → pass'), 'quality');
    assert.equal(categorizeEngineLogMessage('Quality index: 92'), 'quality');
    assert.equal(categorizeEngineLogMessage('Marker gate passed'), 'quality');
  });

  it('classifies repair messages', () => {
    assert.equal(categorizeEngineLogMessage('Repair loop iteration 2'), 'repair');
  });

  it('classifies decision journal messages', () => {
    assert.equal(categorizeEngineLogMessage('Decision journal written'), 'decisions');
    assert.equal(categorizeEngineLogMessage('Writing migration decisions'), 'decisions');
  });

  it('classifies idiom feedback', () => {
    assert.equal(categorizeEngineLogMessage('Idioms: 3 applied'), 'idiom');
  });

  it('classifies anti-pattern feedback', () => {
    assert.equal(categorizeEngineLogMessage('Translator feedback: 2 anti-patterns detected'), 'antipattern');
  });

  it('classifies signing messages', () => {
    assert.equal(categorizeEngineLogMessage('Bundle signed successfully'), 'signing');
    assert.equal(categorizeEngineLogMessage('Signing failed — key not configured'), 'signing');
  });

  it('classifies timestamp messages', () => {
    assert.equal(categorizeEngineLogMessage('TSA timestamp applied'), 'timestamp');
  });

  it('classifies accounting/cost messages', () => {
    assert.equal(categorizeEngineLogMessage('Accounting snapshot written'), 'accounting');
    assert.equal(categorizeEngineLogMessage('Run cost: $0.042'), 'accounting');
  });

  it('classifies test-generation messages', () => {
    assert.equal(categorizeEngineLogMessage('Generated 14 tests for 5 files'), 'test-gen');
    assert.equal(categorizeEngineLogMessage('Generated 1 test'), 'test-gen');
  });

  it('falls back to info for unrecognised messages', () => {
    assert.equal(categorizeEngineLogMessage('Job started'), 'info');
    assert.equal(categorizeEngineLogMessage('Run queued'), 'info');
  });
});

// ── categorizeConversionErrors ────────────────────────────────────────────────

describe('categorizeConversionErrors', () => {
  it('returns null for empty array', () => {
    assert.equal(categorizeConversionErrors([]), null);
  });

  it('returns null for non-array input', () => {
    assert.equal(categorizeConversionErrors(null as unknown as string[]), null);
    assert.equal(categorizeConversionErrors(undefined as unknown as string[]), null);
  });

  it('classifies preflight grammar errors', () => {
    assert.equal(categorizeConversionErrors(['Preflight grammar check failed']), 'preflight');
  });

  it('classifies size-guard errors', () => {
    assert.equal(categorizeConversionErrors(['Source size limit exceeded']), 'size-guard');
    assert.equal(categorizeConversionErrors(['LOC exceeded by 20%']), 'size-guard');
  });

  it('classifies quality-gate errors', () => {
    assert.equal(categorizeConversionErrors(['Quality gate failed: index=65']), 'quality-gate');
  });

  it('classifies cancelled runs', () => {
    assert.equal(categorizeConversionErrors(['Cancelled']), 'cancelled');
    assert.equal(categorizeConversionErrors(['Cancelled by user']), 'cancelled');
  });

  it('classifies validation failures', () => {
    assert.equal(categorizeConversionErrors(['Validation failed: parity 0.45 < 0.80']), 'validation');
  });

  it('falls back to runtime for unrecognised errors', () => {
    assert.equal(categorizeConversionErrors(['Unexpected error: ECONNRESET']), 'runtime');
  });

  it('preflight takes priority over runtime when both present', () => {
    assert.equal(
      categorizeConversionErrors(['Preflight grammar check failed', 'also some runtime issue']),
      'preflight',
    );
  });
});

// ── classifyConversionFailure ─────────────────────────────────────────────────

describe('classifyConversionFailure', () => {
  it('returns unknown kind for undefined/empty error', () => {
    assert.equal(classifyConversionFailure(undefined).kind, 'unknown');
    assert.equal(classifyConversionFailure('').kind, 'unknown');
  });

  it('classifies cancelled', () => {
    const r = classifyConversionFailure('Cancelled');
    assert.equal(r.kind, 'user-cancelled');
  });

  it('classifies unsupported language pair as preflight', () => {
    const r = classifyConversionFailure('Unsupported language pair: cobol → ruby');
    assert.equal(r.kind, 'preflight');
  });

  it('classifies repository-too-large as preflight', () => {
    const r1 = classifyConversionFailure('Source size exceeds 1024 MB limit');
    assert.equal(r1.kind, 'preflight');
    const r2 = classifyConversionFailure('Too many files: 15000 > 10000');
    assert.equal(r2.kind, 'preflight');
  });

  it('classifies validation parity failure', () => {
    const r = classifyConversionFailure('Validation parity below threshold: 0.62 < 0.80');
    assert.equal(r.kind, 'validation');
  });

  it('classifies idiomatic gate failure', () => {
    const r = classifyConversionFailure('Idiomatic gate failed: score 72 < 85');
    assert.equal(r.kind, 'idiomatic-gate');
  });

  it('classifies quality gate failure', () => {
    const r = classifyConversionFailure('Quality gate failed: index=65 threshold=70');
    assert.equal(r.kind, 'quality-gate');
  });

  it('classifies compile failure', () => {
    const r = classifyConversionFailure('Compile-check failed: 3 errors');
    assert.equal(r.kind, 'compile');
  });

  it('classifies generic runtime errors', () => {
    const r = classifyConversionFailure('Unexpected error: ECONNRESET');
    assert.equal(r.kind, 'runtime');
  });

  it('title and detail are non-empty strings', () => {
    for (const msg of [
      'Cancelled',
      'Validation parity below threshold: 0.6',
      'Quality gate failed: index=60',
      'Compile-check failed: 1 error',
      undefined,
    ]) {
      const r = classifyConversionFailure(msg);
      assert.ok(typeof r.title === 'string' && r.title.length > 0, `title empty for: ${msg}`);
      assert.ok(typeof r.detail === 'string' && r.detail.length > 0, `detail empty for: ${msg}`);
    }
  });
});

// ── isStructuralFailureEngineLogLine ──────────────────────────────────────────

describe('isStructuralFailureEngineLogLine', () => {
  it('returns false for ordinary log lines', () => {
    assert.equal(isStructuralFailureEngineLogLine('Analysis complete'), false);
    assert.equal(isStructuralFailureEngineLogLine(''), false);
  });

  it('matches quality gate failure', () => {
    assert.equal(isStructuralFailureEngineLogLine('Quality gate failed: index=65'), true);
  });

  it('matches validation parity failure', () => {
    assert.equal(isStructuralFailureEngineLogLine('Validation parity below threshold'), true);
  });

  it('matches compile-check failure', () => {
    assert.equal(isStructuralFailureEngineLogLine('Compile-check failed: 2 errors'), true);
  });

  it('matches unsupported language pair', () => {
    assert.equal(isStructuralFailureEngineLogLine('Unsupported language pair: cobol → ruby'), true);
  });

  it('matches source size / too many files', () => {
    assert.equal(isStructuralFailureEngineLogLine('Source size exceeds 1024 MB'), true);
    assert.equal(isStructuralFailureEngineLogLine('Too many files in source'), true);
  });

  it('is case-insensitive', () => {
    assert.equal(isStructuralFailureEngineLogLine('QUALITY GATE FAILED'), true);
    assert.equal(isStructuralFailureEngineLogLine('compile-check failed'), true);
  });
});

// ── inferPrimaryFailureFromLogs ───────────────────────────────────────────────

describe('inferPrimaryFailureFromLogs', () => {
  it('returns undefined for empty log array', () => {
    assert.equal(inferPrimaryFailureFromLogs([]), undefined);
  });

  it('returns undefined when no structural failure lines present', () => {
    assert.equal(inferPrimaryFailureFromLogs([{ level: 'info', message: 'Analysis done' }]), undefined);
  });

  it('prefers error-level structural lines over lower levels', () => {
    const logs = [
      { level: 'info', message: 'Quality gate failed: index=60' },
      { level: 'error', message: 'Quality gate failed: index=55' },
      { level: 'info', message: 'Retrying' },
    ];
    assert.equal(inferPrimaryFailureFromLogs(logs), 'Quality gate failed: index=55');
  });

  it('falls back to non-error structural line when no error-level line present', () => {
    const logs = [
      { level: 'info', message: 'Analysis done' },
      { level: 'warn', message: 'Compile-check failed: 1 error' },
      { level: 'info', message: 'Retrying' },
    ];
    const result = inferPrimaryFailureFromLogs(logs);
    assert.equal(result, 'Compile-check failed: 1 error');
  });

  it('picks last error-level structural line when multiple present (iterates backwards)', () => {
    const logs = [
      { level: 'error', message: 'Quality gate failed: first' },
      { level: 'error', message: 'Quality gate failed: second' },
    ];
    assert.equal(inferPrimaryFailureFromLogs(logs), 'Quality gate failed: second');
  });
});

// ── isAccountingSnapshot ──────────────────────────────────────────────────────

describe('isAccountingSnapshot', () => {
  const validSnapshot = {
    totals: { calls: 3, promptTokens: 1000, completionTokens: 800, totalTokens: 1800, costUsd: 0.042 },
    byStage: [
      { stage: 'analyzer', calls: 1, promptTokens: 400, completionTokens: 300, totalTokens: 700, costUsd: 0.014 },
      { stage: 'translator', calls: 1, promptTokens: 400, completionTokens: 300, totalTokens: 700, costUsd: 0.014 },
      { stage: 'validator', calls: 1, promptTokens: 200, completionTokens: 200, totalTokens: 400, costUsd: 0.014 },
    ],
  };

  it('returns true for a valid snapshot', () => {
    assert.equal(isAccountingSnapshot(validSnapshot), true);
  });

  it('returns false for null / undefined', () => {
    assert.equal(isAccountingSnapshot(null), false);
    assert.equal(isAccountingSnapshot(undefined), false);
  });

  it('returns false when totals is missing', () => {
    assert.equal(isAccountingSnapshot({ byStage: [] }), false);
  });

  it('returns false when byStage is missing', () => {
    assert.equal(isAccountingSnapshot({ totals: { costUsd: 0 } }), false);
  });

  it('returns false when totals.costUsd is not a number', () => {
    assert.equal(isAccountingSnapshot({ totals: { costUsd: '0.04' }, byStage: [] }), false);
  });

  it('returns false for a primitive', () => {
    assert.equal(isAccountingSnapshot(42), false);
    assert.equal(isAccountingSnapshot('{}'), false);
  });
});

// ── labelAccountingStage ──────────────────────────────────────────────────────

describe('labelAccountingStage', () => {
  it('labels known engine stages', () => {
    assert.equal(labelAccountingStage('analyzer'), 'Analysis');
    assert.equal(labelAccountingStage('translator'), 'Translation');
    assert.equal(labelAccountingStage('validator'), 'Validation');
    assert.equal(labelAccountingStage('structure'), 'Translation (structure)');
    assert.equal(labelAccountingStage('bodies'), 'Translation (bodies)');
    assert.equal(labelAccountingStage('repair'), 'Repair');
  });

  it('returns the stage id for unknown stages', () => {
    assert.equal(labelAccountingStage('custom-stage'), 'custom-stage');
    assert.equal(labelAccountingStage(''), '');
  });
});

// ── buildMigrationDecisionPaths ───────────────────────────────────────────────

describe('buildMigrationDecisionPaths', () => {
  it('returns empty arrays for empty input', () => {
    const { sidecars, aggregateCandidates } = buildMigrationDecisionPaths([]);
    assert.deepEqual(sidecars, []);
    assert.deepEqual(aggregateCandidates, []);
  });

  it('builds sidecar paths by appending .scriba-decisions.md', () => {
    const { sidecars } = buildMigrationDecisionPaths(['src/main/java/Foo.java']);
    assert.ok(sidecars.includes('src/main/java/Foo.java.scriba-decisions.md'));
  });

  it('deduplicates sidecar paths', () => {
    const { sidecars } = buildMigrationDecisionPaths([
      'src/Foo.java',
      'src/Foo.java',
    ]);
    assert.equal(sidecars.length, 1);
  });

  it('builds aggregate candidate paths in parent directories', () => {
    const { aggregateCandidates } = buildMigrationDecisionPaths(['src/main/java/Foo.java']);
    assert.ok(aggregateCandidates.some(p => p.endsWith('MIGRATION-DECISIONS.md')));
    assert.ok(aggregateCandidates.some(p => p.includes('src/main/')));
  });

  it('normalises backslashes to forward slashes', () => {
    const { sidecars } = buildMigrationDecisionPaths(['src\\main\\java\\Foo.java']);
    assert.ok(sidecars.every(p => !p.includes('\\')));
  });

  it('filters out non-string or empty entries', () => {
    const { sidecars } = buildMigrationDecisionPaths([null as unknown as string, '', 'src/Foo.java']);
    assert.equal(sidecars.length, 1);
  });
});
