// sarif.test.mjs — Tests for SARIF 2.1.0 output formatter

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { toSARIF } from '../../scanners/lib/sarif-formatter.mjs';

// ---------------------------------------------------------------------------
// Fixture: minimal scan-orchestrator envelope
// ---------------------------------------------------------------------------

const EMPTY_ENVELOPE = {
  meta: { target: '/tmp/test', timestamp: '2026-04-10T12:00:00.000Z' },
  scanners: {},
  aggregate: { total_findings: 0, counts: { critical: 0, high: 0, medium: 0, low: 0, info: 0 } },
};

const ENVELOPE_WITH_FINDINGS = {
  meta: { target: '/tmp/test', timestamp: '2026-04-10T12:00:00.000Z' },
  scanners: {
    unicode: {
      scanner: 'unicode',
      status: 'ok',
      findings: [
        {
          id: 'DS-UNI-001',
          scanner: 'UNI',
          severity: 'critical',
          title: 'Invisible Unicode characters detected',
          description: 'File contains zero-width joiners that may hide malicious intent.',
          file: 'src/hook.mjs',
          line: 42,
          owasp: 'LLM01',
          recommendation: 'Remove invisible characters.',
        },
        {
          id: 'DS-UNI-002',
          scanner: 'UNI',
          severity: 'medium',
          title: 'Homoglyph characters detected',
          description: 'Cyrillic characters mixed with Latin.',
          file: 'src/config.mjs',
          line: null,
          owasp: 'LLM01',
          recommendation: 'Replace with ASCII equivalents.',
        },
      ],
      counts: { critical: 1, high: 0, medium: 1, low: 0, info: 0 },
    },
    entropy: {
      scanner: 'entropy',
      status: 'ok',
      findings: [
        {
          id: 'DS-ENT-001',
          scanner: 'ENT',
          severity: 'high',
          title: 'High-entropy string detected',
          description: 'Possible hardcoded API key.',
          file: '.env.example',
          line: 5,
          owasp: 'LLM03',
          recommendation: 'Move secrets to environment variables.',
        },
      ],
      counts: { critical: 0, high: 1, medium: 0, low: 0, info: 0 },
    },
  },
  aggregate: { total_findings: 3 },
};

// ---------------------------------------------------------------------------
// SARIF structure tests
// ---------------------------------------------------------------------------

describe('sarif-formatter: structure', () => {
  it('produces valid SARIF 2.1.0 shell', () => {
    const sarif = toSARIF(EMPTY_ENVELOPE);
    assert.equal(sarif.version, '2.1.0');
    assert.ok(sarif.$schema.includes('sarif-schema-2.1.0'));
    assert.ok(Array.isArray(sarif.runs));
    assert.equal(sarif.runs.length, 1);
  });

  it('has tool driver info', () => {
    const sarif = toSARIF(EMPTY_ENVELOPE);
    const driver = sarif.runs[0].tool.driver;
    assert.equal(driver.name, 'llm-security');
    assert.ok(driver.version);
    assert.ok(driver.informationUri);
  });

  it('empty findings produce empty results array', () => {
    const sarif = toSARIF(EMPTY_ENVELOPE);
    assert.deepEqual(sarif.runs[0].results, []);
    assert.deepEqual(sarif.runs[0].tool.driver.rules, []);
  });
});

// ---------------------------------------------------------------------------
// Findings conversion tests
// ---------------------------------------------------------------------------

describe('sarif-formatter: findings', () => {
  it('converts all findings to results', () => {
    const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
    assert.equal(sarif.runs[0].results.length, 3);
  });

  it('maps critical/high severity to error level', () => {
    const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
    const critResult = sarif.runs[0].results[0]; // critical
    const highResult = sarif.runs[0].results[2]; // high
    assert.equal(critResult.level, 'error');
    assert.equal(highResult.level, 'error');
  });

  it('maps medium severity to warning level', () => {
    const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
    const medResult = sarif.runs[0].results[1]; // medium
    assert.equal(medResult.level, 'warning');
  });

  it('includes file as artifact location URI', () => {
    const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
    const result = sarif.runs[0].results[0];
    assert.equal(result.locations[0].physicalLocation.artifactLocation.uri, 'src/hook.mjs');
  });

  it('includes line number as region startLine', () => {
    const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
    const result = sarif.runs[0].results[0];
    assert.equal(result.locations[0].physicalLocation.region.startLine, 42);
  });

  it('omits region when line is null', () => {
    const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
    const result = sarif.runs[0].results[1]; // line: null
    assert.ok(result.locations[0].physicalLocation.artifactLocation);
    assert.equal(result.locations[0].physicalLocation.region, undefined);
  });

  it('includes OWASP tags in properties', () => {
    const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
    const result = sarif.runs[0].results[0];
    assert.deepEqual(result.properties.tags, ['LLM01']);
  });

  it('generates unique rules from findings', () => {
    const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
    const rules = sarif.runs[0].tool.driver.rules;
    assert.equal(rules.length, 3); // 3 unique title+scanner combos
    assert.ok(rules[0].id.startsWith('UNI/'));
    assert.ok(rules[2].id.startsWith('ENT/'));
  });

  it('results reference correct rule index', () => {
    const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
    for (const result of sarif.runs[0].results) {
      assert.ok(typeof result.ruleIndex === 'number');
      assert.ok(result.ruleIndex >= 0);
      assert.ok(result.ruleIndex < sarif.runs[0].tool.driver.rules.length);
    }
  });

  it('accepts custom version', () => {
    const sarif = toSARIF(EMPTY_ENVELOPE, '7.0.0');
    assert.equal(sarif.runs[0].tool.driver.version, '7.0.0');
  });
});
