"""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 (
    attest_report,
    generate_evidence_bundle,
    generate_html_report,
    generate_pdf_report,
    verify_report_attestation,
)


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()


class TestAttestReport:
    def test_attest_report_creates_receipt(self, tmp_path: Path):
        report = tmp_path / "report.html"
        report.write_text("<html>test</html>", encoding="utf-8")

        mock_client = MagicMock()
        mock_result = MagicMock()
        mock_result.id = "att_report_123"
        mock_result.timestamp = "2026-03-21T12:00:00Z"
        mock_client.attest.return_value = mock_result

        receipt = attest_report(report, mock_client)

        assert receipt["report_file"] == "report.html"
        assert receipt["attestation_id"] == "att_report_123"
        assert receipt["report_hash"]
        assert len(receipt["report_hash"]) == 64
        assert "certisigma.ch" in receipt["verification_url"]
        mock_client.attest.assert_called_once()
        call_args = mock_client.attest.call_args
        assert call_args[0][0] == receipt["report_hash"]
        assert call_args[1]["source"] == "census-report"

    def test_verify_report_matching(self, tmp_path: Path):
        report = tmp_path / "report.pdf"
        report.write_bytes(b"fake pdf content")

        from certisigma import hash_file
        h = hash_file(str(report))

        sidecar = tmp_path / "report.pdf.attestation.json"
        sidecar.write_text(json.dumps({
            "report_hash": h,
            "attestation_id": "att_abc",
            "verification_url": "https://app.certisigma.ch/verify/" + h,
            "timestamp": "2026-01-01T00:00:00Z",
        }), encoding="utf-8")

        result = verify_report_attestation(str(report))
        assert result["match"] is True
        assert result["attestation_id"] == "att_abc"

    def test_verify_report_tampered(self, tmp_path: Path):
        report = tmp_path / "report.html"
        report.write_text("<html>original</html>", encoding="utf-8")

        sidecar = tmp_path / "report.html.attestation.json"
        sidecar.write_text(json.dumps({
            "report_hash": "00" * 32,
            "attestation_id": "att_old",
            "verification_url": "https://app.certisigma.ch/verify/0000",
            "timestamp": "2026-01-01T00:00:00Z",
        }), encoding="utf-8")

        result = verify_report_attestation(str(report))
        assert result["match"] is False

    def test_verify_report_missing_sidecar(self, tmp_path: Path):
        report = tmp_path / "report.html"
        report.write_text("<html>test</html>", encoding="utf-8")

        with pytest.raises(FileNotFoundError, match="sidecar not found"):
            verify_report_attestation(str(report))

    def test_verify_report_missing_report(self, tmp_path: Path):
        with pytest.raises(FileNotFoundError, match="Report not found"):
            verify_report_attestation(str(tmp_path / "nonexistent.html"))

    def test_verify_report_invalid_sidecar_json(self, tmp_path: Path):
        report = tmp_path / "report.html"
        report.write_text("<html>test</html>", encoding="utf-8")
        sidecar = tmp_path / "report.html.attestation.json"
        sidecar.write_text("not json", encoding="utf-8")

        with pytest.raises(ValueError, match="Invalid attestation sidecar"):
            verify_report_attestation(str(report))

    def test_verify_report_sidecar_not_dict(self, tmp_path: Path):
        report = tmp_path / "report.html"
        report.write_text("<html>test</html>", encoding="utf-8")
        sidecar = tmp_path / "report.html.attestation.json"
        sidecar.write_text('["list"]', encoding="utf-8")

        with pytest.raises(ValueError, match="not a JSON object"):
            verify_report_attestation(str(report))

    def test_verify_report_invalid_hash_in_sidecar(self, tmp_path: Path):
        report = tmp_path / "report.html"
        report.write_text("<html>test</html>", encoding="utf-8")
        sidecar = tmp_path / "report.html.attestation.json"
        sidecar.write_text(json.dumps({"report_hash": "tooshort"}), encoding="utf-8")

        with pytest.raises(ValueError, match="invalid report_hash"):
            verify_report_attestation(str(report))

    def test_attest_report_rejects_directory(self, tmp_path: Path):
        with pytest.raises(FileNotFoundError, match="not a regular file"):
            attest_report(tmp_path, MagicMock())
