"""Tests for apps.core.storage.validators.

Covers the two assessment fixes:

* CORE-STORE-04 — SVG sanitisation strips ``<script>``, ``on*`` handlers, and
  ``javascript:`` hrefs instead of rejecting the upload outright.
* CORE-STORE-05 / FRREP-DCI007 — ClamAV is invoked for every module
  (Repository / MEL / RIMS / Alumni / SME-Hub / REP), not just ``module="rep"``.
"""
from __future__ import annotations

import io
from unittest import mock

import pytest
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile

from apps.core.storage import validators as v
from apps.core.storage.exceptions import MaliciousFile


# ── SVG sanitisation ────────────────────────────────────────────────────────


def test_sanitise_svg_strips_script_tag():
    raw = '<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script><circle cx="5" cy="5" r="4"/></svg>'
    out = v._sanitise_svg(raw)
    assert "<script" not in out.lower()
    assert "alert" not in out.lower()
    assert "<circle" in out


def test_sanitise_svg_strips_event_handlers():
    raw = '<svg xmlns="http://www.w3.org/2000/svg"><circle cx="1" cy="1" r="1" onclick="alert(1)" onload="x()"/></svg>'
    out = v._sanitise_svg(raw)
    assert "onclick" not in out.lower()
    assert "onload" not in out.lower()
    assert "<circle" in out


def test_sanitise_svg_strips_javascript_href():
    raw = '<svg xmlns="http://www.w3.org/2000/svg"><a xlink:href="javascript:alert(1)"><text>hi</text></a></svg>'
    out = v._sanitise_svg(raw)
    assert "javascript:" not in out.lower()


def test_sanitise_svg_keeps_safe_markup():
    raw = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect x="0" y="0" width="10" height="10" fill="red"/></svg>'
    out = v._sanitise_svg(raw)
    assert "<rect" in out
    assert 'fill="red"' in out


def test_sanitise_svg_rejects_xxe_construct():
    # External entity reference must be blocked — defusedxml raises and we
    # surface the rejection as MaliciousFile.
    xxe = (
        '<?xml version="1.0"?>'
        '<!DOCTYPE svg [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>'
        '<svg xmlns="http://www.w3.org/2000/svg"><text>&xxe;</text></svg>'
    )
    with pytest.raises(MaliciousFile):
        v._sanitise_svg(xxe)


def test_sanitise_svg_tolerates_loose_markup():
    # Missing xlink namespace is malformed XML but not dangerous; bleach
    # handles the scrub. The function should not raise.
    raw = '<svg xmlns="http://www.w3.org/2000/svg"><a xlink:href="#x"><text>hi</text></a></svg>'
    out = v._sanitise_svg(raw)
    assert "<text" in out


def test_magic_bytes_validator_rewrites_svg_with_clean_bytes():
    raw = b'<svg xmlns="http://www.w3.org/2000/svg"><script>x()</script><circle cx="1" cy="1" r="1"/></svg>'
    f = SimpleUploadedFile("logo.svg", raw, content_type="image/svg+xml")
    v.MagicBytesValidator(".svg", ["image/svg+xml"])(f)
    f.seek(0)
    cleaned = f.read()
    assert b"<script" not in cleaned.lower()
    assert b"<circle" in cleaned


# ── ClamAV ungating ────────────────────────────────────────────────────────


@pytest.fixture
def _png_upload():
    # 8-byte PNG signature + IHDR is enough for libmagic to detect "image/png".
    png_header = (
        b"\x89PNG\r\n\x1a\n"
        b"\x00\x00\x00\rIHDR"
        b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89"
        b"\x00\x00\x00\x0aIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\x0d\n-\xb4"
        b"\x00\x00\x00\x00IEND\xaeB`\x82"
    )
    return SimpleUploadedFile("test.png", png_header, content_type="image/png")


@pytest.mark.parametrize("module", ["repository", "mel", "rims", "alumni", "smehub", "rep"])
def test_clamav_called_for_every_module(settings, _png_upload, module):
    settings.CLAMD_TCP_HOST = "clamd.local"
    settings.CLAMD_ENABLED = True
    with mock.patch("apps.core.storage.validators.scan_stream_optional") as scan:
        v.FileValidator(module=module)(_png_upload)
    assert scan.called, f"ClamAV must be invoked for module={module}"


def test_clamav_skipped_when_disabled(settings, _png_upload):
    settings.CLAMD_TCP_HOST = "clamd.local"
    settings.CLAMD_ENABLED = False
    with mock.patch("apps.core.storage.validators.scan_stream_optional") as scan:
        v.FileValidator(module="repository")(_png_upload)
    assert not scan.called


def test_clamav_skipped_when_host_unset(settings, _png_upload):
    settings.CLAMD_TCP_HOST = ""
    settings.CLAMD_ENABLED = True
    with mock.patch("apps.core.storage.validators.scan_stream_optional") as scan:
        v.FileValidator(module="repository")(_png_upload)
    assert not scan.called
