import { describe, test, expect } from 'vitest';
import { renderFindingsReport } from '../src/renderReport.js';
import type { SecurityCheckStep } from '@hover-dev/probe-engine';

function check(over: Partial<SecurityCheckStep> = {}): SecurityCheckStep {
  return {
    id: 1,
    sourceFlowId: 'a',
    replayId: 'b',
    intent: 'IDOR: read another order',
    expectStatus: 403,
    observed: { method: 'GET', url: 'https://app.test/api/orders/9', status: 200, statusMessage: 'OK', bodyExcerpt: null },
    matched: false,
    recordedAt: 0,
    ...over,
  };
}

describe('renderFindingsReport', () => {
  test('buckets findings vs verified controls and counts them', () => {
    const md = renderFindingsReport([
      check({ id: 1, intent: 'IDOR finding', matched: false }),
      check({ id: 2, intent: 'authz holds', matched: true, observed: { ...check().observed, status: 403 } }),
    ], { name: 'My App' });
    expect(md).toContain('# Security scan — My App');
    expect(md).toContain('1 finding(s), 1 verified control(s).');
    expect(md).toContain('## Findings');
    expect(md).toContain('IDOR finding');
    expect(md).toContain('## Verified controls');
    expect(md).toContain('authz holds');
  });

  test('rates access-granted-where-denial-expected as High', () => {
    const md = renderFindingsReport([check({ observed: { ...check().observed, status: 200 }, expectStatus: 403 })]);
    expect(md).toContain('### [High]');
  });

  test('rates a 5xx observation as Low', () => {
    const md = renderFindingsReport([check({ observed: { ...check().observed, status: 500 } })]);
    expect(md).toContain('### [Low]');
  });

  test('suppresses never-submit noise and notes the count', () => {
    const md = renderFindingsReport([
      check({ id: 1, intent: 'IDOR finding' }),
      check({ id: 2, intent: 'Self-XSS in profile' }),
    ]);
    expect(md).toContain('1 finding(s)'); // only the IDOR
    expect(md).toContain('1 noise check(s) suppressed');
    expect(md).not.toContain('### [High] Self-XSS'); // not a finding heading
  });

  test('notes the second identity for a cross-identity finding', () => {
    const md = renderFindingsReport([
      check({ crossIdentity: { identityB: 'state/userB.json' } }),
    ]);
    expect(md).toContain('second identity');
    expect(md).toContain('state/userB.json');
  });

  test('sanitizes a token in the URL — no secret in the report', () => {
    const md = renderFindingsReport([
      check({
        observed: { ...check().observed, url: 'https://app.test/api/x?token=supersecret' },
        request: { method: 'GET', url: 'https://app.test/api/x?token=supersecret', headers: {}, bodyText: null },
      }),
    ]);
    expect(md).not.toContain('supersecret');
  });

  test('enriches a finding with per-class Impact + Recommendation', () => {
    const md = renderFindingsReport([check({ class: 'sqli', intent: 'SQLi on /search' })]);
    expect(md).toContain('**Impact:**');
    expect(md).toContain('**Recommendation:**');
    expect(md).toContain('parameterized queries'); // from the sqli knowledge entry
  });

  test('always emits a "Not tested" section; lists the gaps when given', () => {
    const withGaps = renderFindingsReport([check()], {
      notTested: ['destructive probes (re-run with --allow-destructive)', 'the /admin area (out of scope)'],
    });
    expect(withGaps).toContain('## Not tested');
    expect(withGaps).toContain('destructive probes (re-run with --allow-destructive)');
    expect(withGaps).toContain('the /admin area (out of scope)');

    const noGaps = renderFindingsReport([check()]);
    expect(noGaps).toContain('## Not tested');
    expect(noGaps).toContain('No coverage gaps were reported');
  });

  test('renders a browser-confirmed finding (no replay) with its evidence + impact', () => {
    const md = renderFindingsReport([], {
      findings: [{
        id: 1,
        class: 'xss',
        intent: 'Reflected XSS in the search field',
        severity: 'High',
        evidence: "typed <script>alert('hov')</script> into ?q= ; reflected unencoded and the alert fired",
        location: 'https://app.test/search?q=',
        recordedAt: 0,
      }],
    });
    expect(md).toContain('## Findings');
    expect(md).toContain('[High] Reflected XSS in the search field');
    expect(md).toContain('**Type:** xss');
    expect(md).toContain('**Evidence:**');
    expect(md).toMatch(/\*\*Impact:\*\*.+/); // per-class impact from vulnInfo
    expect(md).toContain('1 finding(s)');
  });

  test('counts browser findings + replay findings together', () => {
    const md = renderFindingsReport(
      [check({ observed: { ...check().observed, status: 200 }, expectStatus: 403 })],
      { findings: [{ id: 1, intent: 'DOM XSS', severity: 'Medium', evidence: 'payload executed', recordedAt: 0 }] },
    );
    expect(md).toContain('2 finding(s)');
  });
});
