"""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 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()
        try:
            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
        finally:
            m.close()

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

    def test_contains_evidence_section(self):
        m = _sample_manifest()
        try:
            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
        finally:
            m.close()

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

    def test_xss_prevention(self):
        """File paths with HTML special chars are escaped."""
        m = Manifest(root_dir="/data")
        try:
            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
        finally:
            m.close()

    def test_javascript_url_scheme_blocked(self):
        """Malicious javascript: URLs in evidence are stripped (defense-in-depth)."""
        m = _sample_manifest()
        try:
            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
        finally:
            m.close()

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

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


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

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

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

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


class TestEvidenceBundle:
    def test_bundle_contains_expected_files(self):
        m = _sample_manifest()
        try:
            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
        finally:
            m.close()

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

    def test_bundle_evidence_json_valid(self):
        m = _sample_manifest()
        try:
            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
        finally:
            m.close()

    def test_bundle_sha256sums_correct(self):
        """SHA256SUMS file contains correct checksums for all files."""
        m = _sample_manifest()
        try:
            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}"
        finally:
            m.close()

    @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()
        try:
            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()
        finally:
            m.close()

    def test_bundle_readme_content(self):
        m = _sample_manifest()
        try:
            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
        finally:
            m.close()
