import { describe, expect, it } from 'vitest';
import { sanitizeContentBody } from './sanitize.js';

/**
 * Stored-XSS hard-floor regression guard for mod-cms's CHOSEN sanitizer engine (sanitize-html).
 * The content module owns the fail-closed CONTRACT (packages/content store.test.ts); this proves
 * the host ENGINE actually strips every executable vector while preserving the ui-editor vocabulary.
 *
 * DOMPurify (fail-open on Workers) and ultrahtml (leaks onclick/style/nested-script) were rejected
 * empirically before this engine was chosen — see lib/sanitize.ts.
 */
const XSS_VECTORS: Array<[name: string, raw: string]> = [
  ['script tag', '<p>hi</p><script>alert(1)</script>'],
  ['img onerror', '<img src=x onerror="alert(1)">'],
  ['svg onload', '<svg onload="alert(1)"></svg>'],
  ['javascript: href', '<a href="javascript:alert(1)">x</a>'],
  ['onclick handler', '<p onclick="alert(1)">click</p>'],
  ['iframe', '<iframe src="evil"></iframe>'],
  ['inline style expr', '<p style="background:url(javascript:alert(1))">x</p>'],
  ['nested obfuscation', '<scr<script>ipt>alert(1)</scr</script>ipt>'],
  ['data: uri', '<a href="data:text/html,<script>alert(1)</script>">x</a>'],
];

const EXECUTABLE = /<script|onerror=|onload=|onclick=|javascript:|<iframe|<svg|style=/i;

describe('sanitizeContentBody — stored-XSS hard floor', () => {
  for (const [name, raw] of XSS_VECTORS) {
    it(`strips ${name}`, () => {
      expect(sanitizeContentBody(raw)).not.toMatch(EXECUTABLE);
    });
  }

  it('preserves the ui-editor allowlist vocabulary (p/h2/strong/em/ul/li + dir)', () => {
    const out = sanitizeContentBody(
      '<p dir="rtl">shalom <strong>bold</strong> <em>italic</em></p><ul><li>one</li></ul><h2>head</h2>',
    );
    expect(out).toContain('<p dir="rtl">');
    expect(out).toContain('<strong>bold</strong>');
    expect(out).toContain('<em>italic</em>');
    expect(out).toContain('<ul><li>one</li></ul>');
    expect(out).toContain('<h2>head</h2>');
  });

  it('drops tags outside the editor vocabulary while keeping their text', () => {
    expect(sanitizeContentBody('<div><p>kept</p><span>text</span></div>')).toBe('<p>kept</p>text');
  });
});

describe('sanitizeContentBody — render-time defense-in-depth (host-templates spec §J)', () => {
  // The render sinks apply sanitizeContentBody a SECOND time over already-sanitized stored bodies.
  // That second pass MUST be idempotent (never corrupts good editor output) and MUST still strip a
  // vector that somehow reached storage unsanitized (legacy row / future write-path bypass).
  it('is idempotent on already-clean editor content', () => {
    const clean =
      '<h1>Title</h1><p dir="rtl">shalom <strong>bold</strong> <em>italic</em></p><ul><li>one</li><li>two</li></ul><h2>Section</h2>';
    const once = sanitizeContentBody(clean);
    expect(sanitizeContentBody(once)).toBe(once);
  });

  it('is idempotent (converges) even on a dirty input', () => {
    const once = sanitizeContentBody('<p>hi</p><script>alert(1)</script><img src=x onerror="alert(1)">');
    expect(sanitizeContentBody(once)).toBe(once);
  });

  it('does not double-encode entities across chained passes (no &amp;amp; corruption)', () => {
    // The render pass re-sanitizes the write pass's ALREADY entity-encoded output. sanitize-html
    // decodes entities on parse + re-encodes on serialize, so & and &amp; converge to one fixpoint —
    // this proves a literal ampersand never degrades to the visible text "&amp;" on a second pass.
    const authored = '<p>Tom & Jerry, 5 < 3 &amp; "quoted"</p>';
    const once = sanitizeContentBody(authored);
    expect(sanitizeContentBody(once)).toBe(once);
    expect(once).not.toContain('&amp;amp;');
  });

  it('strips a stored vector at render even if it bypassed write-time sanitize', () => {
    // Simulates a legacy/bypassed DB row reaching a set:html sink directly.
    const storedRaw = '<p>legit</p><script>steal(document.cookie)</script><a href="javascript:alert(1)">x</a>';
    const out = sanitizeContentBody(storedRaw);
    expect(out).toContain('<p>legit</p>');
    expect(out).not.toMatch(/<script|javascript:/i);
  });
});

describe('sanitizeContentBody — h1 (privacy policy title)', () => {
  it('keeps a structural <h1> heading', () => {
    expect(sanitizeContentBody('<h1>Privacy Policy</h1>')).toContain('<h1>');
  });
  it('still strips script even alongside h1', () => {
    const out = sanitizeContentBody('<h1>T</h1><script>alert(1)</script>');
    expect(out).toContain('<h1>');
    expect(out).not.toMatch(/<script/i);
  });
});

describe('sanitizeContentBody — img (media insert-into-content)', () => {
  it('keeps a media img with src/width/height', () => {
    const out = sanitizeContentBody('<img src="https://cdn/a.png" alt="" width="4" height="4" />');
    expect(out).toContain('src="https://cdn/a.png"');
    expect(out).toContain('width="4"');
    expect(out).toContain('height="4"');
  });

  it('strips on* handlers from img', () => {
    const out = sanitizeContentBody('<img src="https://x/a.png" onerror="alert(1)">');
    expect(out).not.toMatch(/onerror=/i);
  });

  it('drops non-https img src schemes', () => {
    expect(sanitizeContentBody('<img src="javascript:alert(1)">')).not.toMatch(/javascript:/i);
    expect(sanitizeContentBody('<img src="data:image/png;base64,abc">')).not.toMatch(/data:/i);
  });

  it('still strips script/iframe/href tags', () => {
    expect(sanitizeContentBody('<script>alert(1)</script>')).not.toMatch(/<script/i);
    expect(sanitizeContentBody('<iframe src="evil"></iframe>')).not.toMatch(/<iframe/i);
    expect(sanitizeContentBody('<a href="https://x.com">link</a>')).not.toMatch(/<a/i);
  });
});
