"""Tests for compliance report generation — NIS2, DORA, ISO 27001."""

from __future__ import annotations

import json
from pathlib import Path

import pytest
from click.testing import CliRunner

from certisigma_census import __version__
from certisigma_census.cli import main
from certisigma_census.compliance import (
    TEMPLATES,
    ComplianceReport,
    generate_compliance_report,
    render_compliance_html,
    render_compliance_json,
)
from certisigma_census.evidence import IntegrityReport, ModifiedFile
from certisigma_census.manifest import Manifest, ManifestEntry


@pytest.fixture
def populated_manifest(tmp_path: Path) -> Manifest:
    """Manifest with a mix of attested and pending entries."""
    m = Manifest(root_dir=str(tmp_path))
    m.add(ManifestEntry(
        hash_hex="a" * 64, path="file1.txt", size=100,
        mtime=1000.0, attested=True, attestation_id="att_1",
    ))
    m.add(ManifestEntry(
        hash_hex="b" * 64, path="file2.txt", size=200,
        mtime=1000.0, attested=True, attestation_id="att_2",
    ))
    m.add(ManifestEntry(
        hash_hex="c" * 64, path="file3.txt", size=300,
        mtime=1000.0, attested=False,
    ))
    yield m
    m.close()


@pytest.fixture
def integrity_ok() -> IntegrityReport:
    return IntegrityReport(
        root_dir="/tmp/test", total_in_manifest=3,
        total_checked=3, unchanged=3,
    )


@pytest.fixture
def integrity_bad() -> IntegrityReport:
    return IntegrityReport(
        root_dir="/tmp/test", total_in_manifest=3,
        total_checked=3, unchanged=1,
        modified=[ModifiedFile(
            path="file1.txt",
            expected_hash="a" * 64, actual_hash="d" * 64,
            expected_size=100, actual_size=150,
        )],
        missing=["file2.txt"],
    )


class TestGenerateComplianceReport:
    """Test generate_compliance_report() logic."""

    def test_nis2_template(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest, template="nis2")
        assert report.template == "nis2"
        assert report.total_files == 3
        assert report.attested_files == 2
        assert report.pending_files == 1
        assert len(report.requirements) == 10

    def test_dora_template(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest, template="dora")
        assert report.template == "dora"
        assert len(report.requirements) == 8

    def test_iso27001_template(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest, template="iso27001")
        assert report.template == "iso27001"
        assert len(report.requirements) == 9

    def test_invalid_template_raises(self, populated_manifest: Manifest) -> None:
        with pytest.raises(ValueError, match="Unknown template"):
            generate_compliance_report(populated_manifest, template="hipaa")

    def test_all_templates_valid(self) -> None:
        assert TEMPLATES == {"nis2", "dora", "iso27001"}

    def test_with_integrity_ok(
        self, populated_manifest: Manifest, integrity_ok: IntegrityReport,
    ) -> None:
        report = generate_compliance_report(
            populated_manifest, integrity=integrity_ok,
        )
        assert report.integrity_ok is True
        assert report.integrity_modified == 0

    def test_with_integrity_violations(
        self, populated_manifest: Manifest, integrity_bad: IntegrityReport,
    ) -> None:
        report = generate_compliance_report(
            populated_manifest, integrity=integrity_bad,
        )
        assert report.integrity_ok is False
        assert report.integrity_modified == 1
        assert report.integrity_missing == 1

    def test_scan_warning_on_pending(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest)
        scan_req = next(r for r in report.requirements if r.requirement == "Asset inventory")
        assert scan_req.status == "warn"
        assert "pending" in scan_req.detail.lower()

    def test_scan_pass_when_all_attested(self) -> None:
        m = Manifest(root_dir="/tmp")
        try:
            m.add(ManifestEntry(
                hash_hex="a" * 64, path="f.txt", size=1,
                mtime=1.0, attested=True, attestation_id="att_1",
            ))
            report = generate_compliance_report(m)
            scan_req = next(r for r in report.requirements if r.requirement == "Asset inventory")
            assert scan_req.status == "pass"
        finally:
            m.close()

    def test_scan_fail_when_empty(self) -> None:
        m = Manifest(root_dir="/tmp")
        try:
            report = generate_compliance_report(m)
            scan_req = next(r for r in report.requirements if r.requirement == "Asset inventory")
            assert scan_req.status == "fail"
        finally:
            m.close()

    def test_integrity_warn_when_not_run(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest)
        req = next(
            (r for r in report.requirements
             if "integrity" in r.requirement.lower() or "risk" in r.requirement.lower()),
            None,
        )
        if req:
            assert req.status == "warn"

    def test_counts_match(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest)
        assert report.passed + report.warnings + report.failures == len(report.requirements)

    def test_version_set(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest)
        assert report.census_version == __version__

    def test_generated_at_set(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest)
        assert report.generated_at.endswith("Z")


class TestRenderComplianceHtml:
    """Test HTML rendering of compliance reports."""

    def test_produces_valid_html(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest)
        html = render_compliance_html(report)
        assert "<!DOCTYPE html>" in html
        assert "</html>" in html

    def test_contains_framework_name(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest, template="dora")
        html = render_compliance_html(report)
        assert "DORA" in html

    def test_html_escape_on_dynamic_values(self) -> None:
        m = Manifest(root_dir='<script>alert("xss")</script>')
        try:
            report = generate_compliance_report(m)
            html = render_compliance_html(report)
            assert "<script>" not in html
            assert "&lt;script&gt;" in html
        finally:
            m.close()

    def test_all_requirements_in_html(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest)
        html = render_compliance_html(report)
        for req in report.requirements:
            assert req.requirement in html

    def test_each_template_renders(self, populated_manifest: Manifest) -> None:
        for tpl in TEMPLATES:
            report = generate_compliance_report(populated_manifest, template=tpl)
            html = render_compliance_html(report)
            assert "<!DOCTYPE html>" in html
            assert len(html) > 1000


class TestRenderComplianceJson:
    """Test JSON rendering of compliance reports."""

    def test_serializable(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest)
        data = render_compliance_json(report)
        serialized = json.dumps(data, default=str)
        assert isinstance(json.loads(serialized), dict)

    def test_has_requirements(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest)
        data = render_compliance_json(report)
        assert "requirements" in data
        assert len(data["requirements"]) == len(report.requirements)

    def test_has_summary_fields(self, populated_manifest: Manifest) -> None:
        report = generate_compliance_report(populated_manifest)
        data = render_compliance_json(report)
        for field in ("template", "total_files", "attested_files", "passed", "warnings", "failures"):
            assert field in data


class TestComplianceReportCli:
    """Test the CLI integration of compliance-report."""

    def test_json_output(self, tmp_path: Path) -> None:
        db = tmp_path / "test.db"
        m = Manifest(root_dir=str(tmp_path))
        m.add(ManifestEntry(
            hash_hex="a" * 64, path="f.txt", size=1,
            mtime=1.0, attested=True, attestation_id="att_1",
        ))
        m.save(db)
        m.close()

        runner = CliRunner()
        result = runner.invoke(main, ["compliance-report", str(db), "--json"])
        assert result.exit_code == 0, result.output
        data = json.loads(result.output)
        assert data["template"] == "nis2"
        assert "census_version" in data

    def test_html_to_file(self, tmp_path: Path) -> None:
        db = tmp_path / "test.db"
        m = Manifest(root_dir=str(tmp_path))
        m.add(ManifestEntry(
            hash_hex="a" * 64, path="f.txt", size=1,
            mtime=1.0, attested=True, attestation_id="att_1",
        ))
        m.save(db)
        m.close()

        out = tmp_path / "report.html"
        runner = CliRunner()
        result = runner.invoke(main, [
            "compliance-report", str(db), "-o", str(out),
        ])
        assert result.exit_code == 0, result.output
        assert out.exists()
        content = out.read_text()
        assert "<!DOCTYPE html>" in content
        assert "NIS2" in content

    def test_dora_template_cli(self, tmp_path: Path) -> None:
        db = tmp_path / "test.db"
        m = Manifest(root_dir=str(tmp_path))
        m.save(db)
        m.close()

        runner = CliRunner()
        result = runner.invoke(main, [
            "compliance-report", str(db), "--template", "dora", "--json",
        ])
        assert result.exit_code == 0, result.output
        data = json.loads(result.output)
        assert data["template"] == "dora"

    def test_with_integrity_flag(self, tmp_path: Path) -> None:
        db = tmp_path / "test.db"
        d = tmp_path / "data"
        d.mkdir()
        f = d / "file.txt"
        f.write_text("content")

        m = Manifest(root_dir=str(d))
        from certisigma import hash_file
        h = hash_file(str(f))
        m.add(ManifestEntry(
            hash_hex=h, path="file.txt", size=f.stat().st_size,
            mtime=f.stat().st_mtime, attested=True, attestation_id="att_1",
        ))
        m.save(db)
        m.close()

        runner = CliRunner()
        result = runner.invoke(main, [
            "compliance-report", str(db), "--integrity", "--json",
        ])
        assert result.exit_code == 0, result.output
        data = json.loads(result.output)
        assert data["integrity_ok"] is True
