"""
tests/test_pdf_exporter.py — End-to-end tests for PDFExporter.

Tests:
  - HTML rendering (no errors, expected content present)
  - PDF generation (mocked weasyprint)
  - wkhtmltopdf fallback (mocked)
  - Error when no renderer available
  - Different finding counts (0, 1, 10, 100)
  - With/without differential report
  - With/without attestation
  - With/without AIBoM
  - Output file creation (JSON, HTML artefacts)
"""
from __future__ import annotations

import json
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from sichgate.technical_file_builder import (
    TechnicalFile,
    TechnicalFileBuilder,
    TechnicalFileSection,
)
from sichgate.pdf_exporter import PDFExporter


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_finding(
    attack_type: str = "prompt_injection",
    status: str = "fail",
    severity: str = "high",
) -> dict:
    return {
        "attack_name": f"test_{attack_type}",
        "attack_type": attack_type,
        "status": status,
        "severity": severity if status == "fail" else None,
        "notes": f"Finding for {attack_type}",
        "remediation": "Apply controls",
        "compliance_tags": [],
        "success_rate": 0.7 if status == "fail" else 0.1,
    }


def _sample_aibom() -> dict:
    return {
        "bomFormat": "CycloneDX",
        "specVersion": "1.6",
        "serialNumber": "urn:uuid:pdf-test-001",
        "version": 1,
        "metadata": {
            "timestamp": "2026-03-02T12:00:00+00:00",
            "tools": [{"vendor": "SichGate", "name": "SichGate Pro", "version": "2.1.0"}],
            "component": {
                "type": "machine-learning-model",
                "name": "pdf-test-model",
                "version": "2.0",
                "hashes": [{"alg": "SHA-256", "content": "cafebabe" * 8}],
            },
        },
        "properties": [],
    }


def _sample_attestation() -> dict:
    return {
        "attestation_id": "att-pdf-001",
        "algorithm": "ed25519",
        "signature": "mockedsignature==",
        "public_key_b64": "mockedpublickey==",
        "public_key_pem": "-----BEGIN PUBLIC KEY-----\nmocked\n-----END PUBLIC KEY-----\n",
        "payload_hash": "sha256mockedhash",
        "timestamp": "2026-03-02T12:00:00+00:00",
    }


def _sample_differential() -> dict:
    return {
        "run_id": "diff-pdf-001",
        "base_model_id": "base-model",
        "custom_model_id": "custom-model",
        "overall_risk_delta": 0.15,
        "critical_alerts": ["CRITICAL Article 15"],
        "differential_analysis": {
            "prompt_injection": {
                "base_success_rate": 0.3,
                "custom_success_rate": 0.5,
                "delta": 0.2,
                "trend": "WORSENED",
                "compliance_alert": "CRITICAL Article 15",
                "annex_iv_sections": ["2g", "2h"],
            }
        },
    }


def _build_technical_file(
    n_findings: int = 3,
    with_aibom: bool = True,
    with_differential: bool = False,
    with_attestation: bool = False,
) -> TechnicalFile:
    findings = [_make_finding("prompt_injection") for _ in range(n_findings)]
    for i, f in enumerate(findings):
        f["attack_name"] = f"finding_{i}"

    builder = TechnicalFileBuilder(
        findings=findings,
        aibom=_sample_aibom() if with_aibom else None,
        differential_report=_sample_differential() if with_differential else None,
        attestation=_sample_attestation() if with_attestation else None,
    )
    return builder.build()


@pytest.fixture
def exporter() -> PDFExporter:
    return PDFExporter()


@pytest.fixture
def basic_tf() -> TechnicalFile:
    return _build_technical_file(n_findings=3)


# ---------------------------------------------------------------------------
# 1. HTML rendering
# ---------------------------------------------------------------------------

class TestHTMLRendering:
    def test_render_html_returns_string(self, exporter, basic_tf):
        html = exporter.render_html(basic_tf)
        assert isinstance(html, str)
        assert len(html) > 100

    def test_html_contains_doctype(self, exporter, basic_tf):
        html = exporter.render_html(basic_tf)
        assert "<!DOCTYPE html>" in html

    def test_html_contains_model_name(self, exporter, basic_tf):
        html = exporter.render_html(basic_tf)
        assert "pdf-test-model" in html

    def test_html_contains_technical_file_heading(self, exporter, basic_tf):
        html = exporter.render_html(basic_tf)
        assert "Technical File" in html

    def test_html_contains_annex_iv_reference(self, exporter, basic_tf):
        html = exporter.render_html(basic_tf)
        assert "Annex IV" in html

    def test_html_contains_executive_summary(self, exporter, basic_tf):
        html = exporter.render_html(basic_tf)
        assert "Executive Summary" in html

    def test_html_contains_legal_disclaimer(self, exporter, basic_tf):
        html = exporter.render_html(basic_tf)
        assert "Legal Disclaimer" in html

    def test_html_no_jinja_errors(self, exporter, basic_tf):
        """Template should render without raising Jinja2 errors."""
        # If no exception, test passes
        html = exporter.render_html(basic_tf)
        # Should not contain unresolved Jinja2 tags
        assert "{{" not in html
        assert "}}" not in html

    def test_html_with_attestation(self, exporter):
        tf = _build_technical_file(with_attestation=True)
        html = exporter.render_html(tf)
        assert "att-pdf-001" in html
        assert "ed25519" in html

    def test_html_without_attestation_shows_placeholder(self, exporter, basic_tf):
        html = exporter.render_html(basic_tf)
        assert "No cryptographic attestation" in html

    def test_html_with_differential(self, exporter):
        tf = _build_technical_file(with_differential=True)
        html = exporter.render_html(tf)
        assert "diff-pdf-001" in html
        assert "WORSENED" in html

    def test_html_without_differential_shows_placeholder(self, exporter, basic_tf):
        html = exporter.render_html(basic_tf)
        assert "No differential redteaming" in html

    def test_html_with_aibom_shows_model_hash(self, exporter, basic_tf):
        html = exporter.render_html(basic_tf)
        assert "cafebabe" in html

    def test_html_without_aibom_shows_placeholder(self, exporter):
        tf = _build_technical_file(with_aibom=False)
        html = exporter.render_html(tf)
        assert "AIBoM was not captured" in html


# ---------------------------------------------------------------------------
# 2. PDF generation (mocked weasyprint)
# ---------------------------------------------------------------------------

class TestPDFGenerationWeasyprint:
    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    def test_export_pdf_calls_weasyprint(self, mock_render, mock_avail, exporter, basic_tf):
        with tempfile.TemporaryDirectory() as tmpdir:
            output = str(Path(tmpdir) / "output.pdf")
            exporter.export_pdf(basic_tf, output)
            mock_render.assert_called_once()

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    def test_export_pdf_returns_output_path(self, mock_render, mock_avail, exporter, basic_tf):
        with tempfile.TemporaryDirectory() as tmpdir:
            output = str(Path(tmpdir) / "output.pdf")
            result = exporter.export_pdf(basic_tf, output)
            assert result == output

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    def test_export_pdf_saves_html_artefact(self, mock_render, mock_avail, exporter, basic_tf):
        with tempfile.TemporaryDirectory() as tmpdir:
            output = str(Path(tmpdir) / "output.pdf")
            exporter.export_pdf(basic_tf, output)
            html_path = Path(output).with_suffix(".html")
            assert html_path.exists()
            assert html_path.stat().st_size > 100

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    def test_export_pdf_html_artefact_has_content(self, mock_render, mock_avail, exporter, basic_tf):
        with tempfile.TemporaryDirectory() as tmpdir:
            output = str(Path(tmpdir) / "output.pdf")
            exporter.export_pdf(basic_tf, output)
            html_content = Path(output).with_suffix(".html").read_text()
            assert "Technical File" in html_content

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    def test_export_pdf_creates_parent_directories(self, mock_render, mock_avail, exporter, basic_tf):
        with tempfile.TemporaryDirectory() as tmpdir:
            output = str(Path(tmpdir) / "subdir" / "output.pdf")
            exporter.export_pdf(basic_tf, output)
            assert Path(output).parent.exists()


# ---------------------------------------------------------------------------
# 3. wkhtmltopdf fallback
# ---------------------------------------------------------------------------

class TestPDFGenerationWkhtmltopdf:
    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=False)
    @patch("sichgate.pdf_exporter._wkhtmltopdf_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_wkhtmltopdf")
    def test_falls_back_to_wkhtmltopdf(self, mock_render, mock_wk, mock_wp, exporter, basic_tf):
        with tempfile.TemporaryDirectory() as tmpdir:
            output = str(Path(tmpdir) / "output.pdf")
            exporter.export_pdf(basic_tf, output)
            mock_render.assert_called_once()

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=False)
    @patch("sichgate.pdf_exporter._wkhtmltopdf_available", return_value=False)
    def test_raises_when_no_renderer(self, mock_wk, mock_wp, exporter, basic_tf):
        with tempfile.TemporaryDirectory() as tmpdir:
            output = str(Path(tmpdir) / "output.pdf")
            with pytest.raises(RuntimeError, match="No PDF renderer"):
                exporter.export_pdf(basic_tf, output)


# ---------------------------------------------------------------------------
# 4. Different finding counts
# ---------------------------------------------------------------------------

class TestDifferentFindingCounts:
    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    def test_zero_findings(self, mock_render, mock_avail, exporter):
        tf = _build_technical_file(n_findings=0)
        html = exporter.render_html(tf)
        assert "Technical File" in html
        assert "0" in html  # total findings

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    def test_single_finding(self, mock_render, mock_avail, exporter):
        tf = _build_technical_file(n_findings=1)
        html = exporter.render_html(tf)
        assert "Technical File" in html

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    def test_ten_findings(self, mock_render, mock_avail, exporter):
        tf = _build_technical_file(n_findings=10)
        html = exporter.render_html(tf)
        assert "Technical File" in html

    @patch("sichgate.pdf_exporter._weasyprint_available", return_value=True)
    @patch("sichgate.pdf_exporter.PDFExporter._render_weasyprint")
    def test_hundred_findings(self, mock_render, mock_avail, exporter):
        tf = _build_technical_file(n_findings=100)
        html = exporter.render_html(tf)
        assert "Technical File" in html
        # All 100 should be represented
        assert "100" in html
