"""Tests for report generation — HTML, PDF, and evidence bundles.

Covers all three output formats. PDF tests require fpdf2 (installed in dev).
"""

from __future__ import annotations

import hashlib
import json
import zipfile
from io import BytesIO
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from certisigma_census.evidence import IntegrityReport, ModifiedFile, VerifyDetail
from certisigma_census.manifest import Manifest, ManifestEntry
from certisigma_census.report import (
    generate_evidence_bundle,
    generate_html_report,
    generate_pdf_report,
)


def _sample_manifest() -> Manifest:
    m = Manifest(root_dir="/data/hr")
    m.add(ManifestEntry(hash_hex="aa" * 32, path="report.pdf", size=5120, mtime=1.0, attested=True, attestation_id="att_1", attested_at="2026-01-01T00:00:00Z"))
    m.add(ManifestEntry(hash_hex="bb" * 32, path="sheet.csv", size=2048, mtime=2.0, attested=True, attestation_id="att_2", attested_at="2026-01-02T00:00:00Z"))
    m.add(ManifestEntry(hash_hex="cc" * 32, path="notes.txt", size=1024, mtime=3.0))
    return m


def _sample_evidence() -> list[VerifyDetail]:
    return [
        VerifyDetail(
            hash_hex="aa" * 32, exists=True, attestation_id="att_1",
            level="T2", timestamp="2026-01-01T00:00:00Z", source="hr",
            t0={"algorithm": "ECDSA-P256-SHA256", "signature": "3045022100abc"},
            t1={"tsaProvider": "QuoVadis", "timestamp": "2026-01-01T12:00:00Z"},
            t2={"bitcoinBlock": 876543, "bitcoinBlockHash": "0000abc", "bitcoinTxid": "tx123"},
            blockchain_url="https://mempool.space/block/0000abc",
            blockchain_tx_url="https://mempool.space/tx/tx123",
        ),
        VerifyDetail(
            hash_hex="bb" * 32, exists=True, attestation_id="att_2",
            level="T1", timestamp="2026-01-02T00:00:00Z",
            t0={"algorithm": "ECDSA-P256-SHA256"}, t1={"tsaProvider": "QuoVadis"},
        ),
    ]


def _sample_integrity() -> IntegrityReport:
    return IntegrityReport(
        root_dir="/data/hr", total_in_manifest=3, total_checked=4,
        unchanged=2,
        modified=[ModifiedFile(path="report.pdf", expected_hash="aa" * 32, actual_hash="dd" * 32, expected_size=5120, actual_size=5248)],
        new_files=["intruder.exe"],
    )


class TestHTMLReport:
    def test_generates_valid_html(self):
        m = _sample_manifest()
        html = generate_html_report(m)
        assert "<!DOCTYPE html>" in html
        assert "CertiSigma Census" in html
        assert "report.pdf" in html
        assert "sheet.csv" in html

    def test_contains_inventory_summary(self):
        m = _sample_manifest()
        html = generate_html_report(m)
        assert "Total Files" in html
        assert "3" in html
        assert "Attested" in html
        assert "Pending" in html

    def test_contains_evidence_section(self):
        m = _sample_manifest()
        html = generate_html_report(m, evidence=_sample_evidence())
        assert "Evidence Chain" in html
        assert "T2" in html
        assert "QuoVadis" in html
        assert "mempool.space" in html

    def test_contains_integrity_section(self):
        m = _sample_manifest()
        html = generate_html_report(m, integrity=_sample_integrity())
        assert "Integrity Check" in html
        assert "VIOLATION" in html
        assert "report.pdf" in html

    def test_xss_prevention(self):
        """File paths with HTML special chars are escaped."""
        m = Manifest(root_dir="/data")
        m.add(ManifestEntry(
            hash_hex="ee" * 32,
            path='<script>alert("xss")</script>.txt',
            size=100, mtime=1.0,
        ))
        html = generate_html_report(m)
        assert "<script>" not in html
        assert "&lt;script&gt;" in html

    def test_javascript_url_scheme_blocked(self):
        """Malicious javascript: URLs in evidence are stripped (defense-in-depth)."""
        m = _sample_manifest()
        evil_evidence = [
            VerifyDetail(
                hash_hex="aa" * 32, exists=True, attestation_id="att_1",
                level="T2", t2={"bitcoinBlock": 1},
                blockchain_url='javascript:alert("xss")',
                blockchain_tx_url='data:text/html,<script>alert(1)</script>',
            ),
        ]
        html = generate_html_report(m, evidence=evil_evidence)
        assert "javascript:" not in html
        assert "data:text/html" not in html

    def test_contains_verification_instructions(self):
        m = _sample_manifest()
        html = generate_html_report(m)
        assert "sha256sum report.html" in html
        assert "(pending)" not in html

    def test_empty_manifest(self):
        m = Manifest(root_dir="/empty")
        html = generate_html_report(m)
        assert "<!DOCTYPE html>" in html
        assert "0" in html


class TestPDFReport:
    def test_generates_valid_pdf(self):
        m = _sample_manifest()
        pdf_bytes = generate_pdf_report(m)
        assert pdf_bytes[:5] == b"%PDF-"
        assert len(pdf_bytes) > 100

    def test_pdf_with_evidence(self):
        m = _sample_manifest()
        pdf_bytes = generate_pdf_report(m, evidence=_sample_evidence())
        assert pdf_bytes[:5] == b"%PDF-"
        assert len(pdf_bytes) > 200

    def test_pdf_with_integrity(self):
        m = _sample_manifest()
        pdf_bytes = generate_pdf_report(m, integrity=_sample_integrity())
        assert pdf_bytes[:5] == b"%PDF-"

    def test_empty_manifest_pdf(self):
        m = Manifest(root_dir="/empty")
        pdf_bytes = generate_pdf_report(m)
        assert pdf_bytes[:5] == b"%PDF-"


class TestEvidenceBundle:
    def test_bundle_contains_expected_files(self):
        m = _sample_manifest()
        bundle_bytes = generate_evidence_bundle(m)
        with zipfile.ZipFile(BytesIO(bundle_bytes)) as zf:
            names = zf.namelist()
            assert "report.html" in names
            assert "evidence.json" in names
            assert "SHA256SUMS" in names
            assert "README.txt" in names

    def test_bundle_includes_pdf_when_fpdf2_available(self):
        m = _sample_manifest()
        bundle_bytes = generate_evidence_bundle(m)
        with zipfile.ZipFile(BytesIO(bundle_bytes)) as zf:
            assert "report.pdf" in zf.namelist()

    def test_bundle_evidence_json_valid(self):
        m = _sample_manifest()
        ev = _sample_evidence()
        bundle_bytes = generate_evidence_bundle(m, evidence=ev)
        with zipfile.ZipFile(BytesIO(bundle_bytes)) as zf:
            data = json.loads(zf.read("evidence.json"))
            assert len(data) == 2
            assert data[0]["hash_hex"] == "aa" * 32

    def test_bundle_sha256sums_correct(self):
        """SHA256SUMS file contains correct checksums for all files."""
        m = _sample_manifest()
        bundle_bytes = generate_evidence_bundle(m)
        with zipfile.ZipFile(BytesIO(bundle_bytes)) as zf:
            sums_content = zf.read("SHA256SUMS").decode("utf-8")
            for line in sums_content.strip().split("\n"):
                expected_hash, filename = line.split("  ", 1)
                actual_hash = hashlib.sha256(zf.read(filename)).hexdigest()
                assert actual_hash == expected_hash, f"Checksum mismatch for {filename}"

    @patch("certisigma_census.report._fpdf2_available", return_value=False)
    def test_bundle_without_fpdf2_omits_pdf(self, _):
        """Bundle gracefully skips PDF when fpdf2 is not installed."""
        m = _sample_manifest()
        bundle_bytes = generate_evidence_bundle(m)
        with zipfile.ZipFile(BytesIO(bundle_bytes)) as zf:
            assert "report.pdf" not in zf.namelist()
            assert "report.html" in zf.namelist()

    def test_bundle_readme_content(self):
        m = _sample_manifest()
        bundle_bytes = generate_evidence_bundle(m)
        with zipfile.ZipFile(BytesIO(bundle_bytes)) as zf:
            readme = zf.read("README.txt").decode("utf-8")
            assert "CertiSigma Census" in readme
            assert "sha256sum" in readme
            assert "ots verify" in readme
