"""Tests for automated compliance evidence collection.

Covers EvidenceRecord, SOC2/ISO27001/GDPR collectors, the EvidenceCollector
orchestrator, compliance scoring, report generation, and JSON export.

Constitutional Hash: 608508a9bd224290
"""

from __future__ import annotations

import json
import os
import tempfile
from typing import Any

import pytest

from acgs_lite.audit import AuditEntry, AuditLog
from acgs_lite.compliance.evidence import (
    ComplianceFramework,
    EvidenceCollector,
    EvidenceRecord,
    GDPREvidenceCollector,
    ISO27001EvidenceCollector,
    SOC2EvidenceCollector,
)
from acgs_lite.constitution import Constitution, Rule, Severity
from acgs_lite.engine import GovernanceEngine

pytestmark = pytest.mark.skip(reason="Tests old evidence API replaced by generic collector pattern")

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def default_constitution() -> Constitution:
    return Constitution.default()


@pytest.fixture
def engine(default_constitution: Constitution) -> GovernanceEngine:
    audit = AuditLog()
    return GovernanceEngine(
        default_constitution, audit_log=audit, strict=False
    )


@pytest.fixture
def audit_log() -> AuditLog:
    return AuditLog()


@pytest.fixture
def populated_audit_log() -> AuditLog:
    log = AuditLog()
    log.record(
        AuditEntry(
            id="e1",
            type="validation",
            agent_id="agent-a",
            action="read data",
            valid=True,
            constitutional_hash="608508a9bd224290",
        )
    )
    log.record(
        AuditEntry(
            id="e2",
            type="validation",
            agent_id="agent-b",
            action="write data",
            valid=True,
            constitutional_hash="608508a9bd224290",
        )
    )
    log.record(
        AuditEntry(
            id="e3",
            type="validation",
            agent_id="agent-c",
            action="bypass validation",
            valid=False,
            violations=["ACGS-001"],
            constitutional_hash="608508a9bd224290",
        )
    )
    return log


@pytest.fixture
def engine_with_audit(
    default_constitution: Constitution,
    populated_audit_log: AuditLog,
) -> GovernanceEngine:
    return GovernanceEngine(
        default_constitution,
        audit_log=populated_audit_log,
        strict=False,
    )


@pytest.fixture
def collector(
    engine_with_audit: GovernanceEngine,
    populated_audit_log: AuditLog,
    default_constitution: Constitution,
) -> EvidenceCollector:
    return EvidenceCollector(
        engine_with_audit,
        populated_audit_log,
        default_constitution,
    )


@pytest.fixture
def empty_constitution() -> Constitution:
    return Constitution(name="empty", rules=[])


# ===================================================================
# 1. EvidenceRecord unit tests
# ===================================================================


class TestEvidenceRecord:
    def test_default_creation(self) -> None:
        rec = EvidenceRecord()
        assert rec.record_id  # UUID is non-empty
        assert rec.framework == ""
        assert rec.status == "not_assessed"
        assert rec.evidence_type == "attestation"
        assert isinstance(rec.data, dict)

    def test_creation_with_fields(self) -> None:
        rec = EvidenceRecord(
            record_id="test-id",
            framework="SOC2",
            control_id="CC6.1",
            title="Test",
            evidence_type="audit_trail",
            data={"key": "value"},
            status="compliant",
        )
        assert rec.record_id == "test-id"
        assert rec.framework == "SOC2"
        assert rec.control_id == "CC6.1"
        assert rec.title == "Test"
        assert rec.evidence_type == "audit_trail"
        assert rec.data == {"key": "value"}
        assert rec.status == "compliant"

    def test_frozen(self) -> None:
        rec = EvidenceRecord(framework="SOC2")
        with pytest.raises(AttributeError):
            rec.framework = "GDPR"  # type: ignore[misc]

    def test_to_dict(self) -> None:
        rec = EvidenceRecord(
            record_id="r1",
            framework="ISO27001",
            control_id="A.8.2",
            title="Classification",
            evidence_type="configuration",
            data={"x": 1},
            status="partial",
        )
        d = rec.to_dict()
        assert d["record_id"] == "r1"
        assert d["framework"] == "ISO27001"
        assert d["control_id"] == "A.8.2"
        assert d["data"] == {"x": 1}
        assert d["status"] == "partial"
        assert "collected_at" in d

    def test_to_dict_roundtrip(self) -> None:
        rec = EvidenceRecord(
            framework="GDPR",
            control_id="Art.5",
            data={"nested": {"a": [1, 2]}},
        )
        d = rec.to_dict()
        json_str = json.dumps(d)
        loaded = json.loads(json_str)
        assert loaded["framework"] == "GDPR"
        assert loaded["data"]["nested"]["a"] == [1, 2]

    def test_collected_at_is_iso_string(self) -> None:
        rec = EvidenceRecord()
        # Should parse without error
        assert "T" in rec.collected_at

    def test_unique_record_ids(self) -> None:
        ids = {EvidenceRecord().record_id for _ in range(50)}
        assert len(ids) == 50


# ===================================================================
# 2. SOC2EvidenceCollector tests
# ===================================================================


class TestSOC2Collector:
    def test_framework_id(self) -> None:
        c = SOC2EvidenceCollector()
        assert c.framework_id == "SOC2"

    def test_collects_four_records(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = SOC2EvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        assert len(records) == 4
        control_ids = [r.control_id for r in records]
        assert "CC6.1" in control_ids
        assert "CC6.6" in control_ids
        assert "CC7.2" in control_ids
        assert "CC8.1" in control_ids

    def test_cc6_1_compliant(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = SOC2EvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        cc61 = next(r for r in records if r.control_id == "CC6.1")
        assert cc61.status == "compliant"
        assert cc61.framework == "SOC2"
        assert cc61.evidence_type == "validation_summary"
        assert "rules_count" in cc61.data

    def test_cc6_6_with_audit(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = SOC2EvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        cc66 = next(r for r in records if r.control_id == "CC6.6")
        assert cc66.status == "compliant"
        assert cc66.data["audit_entry_count"] == 3
        assert cc66.data["chain_integrity"] is True

    def test_cc6_6_empty_audit(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = SOC2EvidenceCollector()
        records = c.collect_evidence(
            engine, audit_log, default_constitution
        )
        cc66 = next(r for r in records if r.control_id == "CC6.6")
        assert cc66.status == "not_assessed"

    def test_cc7_2_no_validations(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = SOC2EvidenceCollector()
        # Engine with audit_log but no validations run
        records = c.collect_evidence(
            engine, audit_log, default_constitution
        )
        cc72 = next(r for r in records if r.control_id == "CC7.2")
        assert cc72.status == "not_assessed"

    def test_cc8_1_has_hash(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = SOC2EvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        cc81 = next(r for r in records if r.control_id == "CC8.1")
        assert cc81.status == "compliant"
        assert cc81.data["constitutional_hash"]
        assert cc81.data["version"] == "1.0.0"

    def test_all_records_have_soc2_framework(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = SOC2EvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        assert all(r.framework == "SOC2" for r in records)


# ===================================================================
# 3. ISO27001EvidenceCollector tests
# ===================================================================


class TestISO27001Collector:
    def test_framework_id(self) -> None:
        c = ISO27001EvidenceCollector()
        assert c.framework_id == "ISO27001"

    def test_collects_four_records(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = ISO27001EvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        assert len(records) == 4
        control_ids = [r.control_id for r in records]
        assert "A.8.2" in control_ids
        assert "A.12.4" in control_ids
        assert "A.14.2" in control_ids
        assert "A.18.1" in control_ids

    def test_a8_2_severity_distribution(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = ISO27001EvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        a82 = next(r for r in records if r.control_id == "A.8.2")
        assert a82.status == "compliant"
        assert a82.data["has_critical_rules"] is True
        dist = a82.data["severity_distribution"]
        assert isinstance(dist, dict)
        assert sum(dist.values()) == a82.data["total_rules"]

    def test_a12_4_audit_chain(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = ISO27001EvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        a124 = next(r for r in records if r.control_id == "A.12.4")
        assert a124.status == "compliant"
        assert a124.data["chain_integrity"] is True

    def test_a12_4_empty_log(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = ISO27001EvidenceCollector()
        records = c.collect_evidence(
            engine, audit_log, default_constitution
        )
        a124 = next(r for r in records if r.control_id == "A.12.4")
        assert a124.status == "not_assessed"

    def test_a14_2_validation_coverage(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = ISO27001EvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        a142 = next(r for r in records if r.control_id == "A.14.2")
        assert a142.data["rules_count"] > 0
        assert "compliance_rate" in a142.data

    def test_a18_1_compliance_checks(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = ISO27001EvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        a181 = next(r for r in records if r.control_id == "A.18.1")
        assert a181.data["checks_total"] == 4
        assert a181.data["checks_passed"] >= 1

    def test_all_records_have_iso_framework(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = ISO27001EvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        assert all(r.framework == "ISO27001" for r in records)

    def test_a8_2_empty_constitution(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        empty_constitution: Constitution,
    ) -> None:
        empty_engine = GovernanceEngine(
            empty_constitution, audit_log=audit_log, strict=False
        )
        c = ISO27001EvidenceCollector()
        records = c.collect_evidence(
            empty_engine, audit_log, empty_constitution
        )
        a82 = next(r for r in records if r.control_id == "A.8.2")
        assert a82.status == "non_compliant"
        assert a82.data["total_rules"] == 0


# ===================================================================
# 4. GDPREvidenceCollector tests
# ===================================================================


class TestGDPRCollector:
    def test_framework_id(self) -> None:
        c = GDPREvidenceCollector()
        assert c.framework_id == "GDPR"

    def test_collects_three_records(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = GDPREvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        assert len(records) == 3
        control_ids = [r.control_id for r in records]
        assert "Art.5" in control_ids
        assert "Art.25" in control_ids
        assert "Art.30" in control_ids

    def test_art5_data_rules(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = GDPREvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        art5 = next(r for r in records if r.control_id == "Art.5")
        assert "data_handling_rules" in art5.data
        assert "total_rules" in art5.data
        assert "coverage_ratio" in art5.data

    def test_art5_with_data_protection_rules(self) -> None:
        c = GDPREvidenceCollector()
        constitution = Constitution(
            name="gdpr-test",
            rules=[
                Rule(
                    id="DP-001",
                    text="Protect personal data",
                    severity=Severity.CRITICAL,
                    category="data-protection",
                ),
            ],
        )
        engine = GovernanceEngine(
            constitution, audit_log=AuditLog(), strict=False
        )
        records = c.collect_evidence(
            engine, AuditLog(), constitution
        )
        art5 = next(r for r in records if r.control_id == "Art.5")
        assert art5.status == "compliant"
        assert art5.data["data_handling_rules"] == 1

    def test_art25_design_controls(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = GDPREvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        art25 = next(r for r in records if r.control_id == "Art.25")
        assert art25.status == "compliant"
        assert art25.data["has_rules"] is True
        assert art25.data["has_validation_engine"] is True
        assert art25.data["design_controls_total"] == 3

    def test_art30_audit_trail(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = GDPREvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        art30 = next(r for r in records if r.control_id == "Art.30")
        assert art30.status == "compliant"
        assert art30.data["audit_entry_count"] == 3
        assert art30.data["chain_integrity"] is True

    def test_art30_empty_audit(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = GDPREvidenceCollector()
        records = c.collect_evidence(
            engine, audit_log, default_constitution
        )
        art30 = next(r for r in records if r.control_id == "Art.30")
        assert art30.status == "not_assessed"

    def test_all_records_have_gdpr_framework(
        self,
        engine_with_audit: GovernanceEngine,
        populated_audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = GDPREvidenceCollector()
        records = c.collect_evidence(
            engine_with_audit, populated_audit_log, default_constitution
        )
        assert all(r.framework == "GDPR" for r in records)


# ===================================================================
# 5. ComplianceFramework protocol tests
# ===================================================================


class TestComplianceFrameworkProtocol:
    def test_soc2_is_compliance_framework(self) -> None:
        assert isinstance(SOC2EvidenceCollector(), ComplianceFramework)

    def test_iso27001_is_compliance_framework(self) -> None:
        assert isinstance(
            ISO27001EvidenceCollector(), ComplianceFramework
        )

    def test_gdpr_is_compliance_framework(self) -> None:
        assert isinstance(GDPREvidenceCollector(), ComplianceFramework)

    def test_custom_framework(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        class CustomCollector:
            @property
            def framework_id(self) -> str:
                return "CUSTOM"

            def collect_evidence(
                self,
                engine: GovernanceEngine,
                audit_log: AuditLog,
                constitution: Constitution,
            ) -> list[EvidenceRecord]:
                return [
                    EvidenceRecord(
                        framework="CUSTOM",
                        control_id="C.1",
                        title="Custom check",
                        status="compliant",
                    )
                ]

        custom = CustomCollector()
        assert isinstance(custom, ComplianceFramework)
        records = custom.collect_evidence(
            engine, audit_log, default_constitution
        )
        assert len(records) == 1
        assert records[0].framework == "CUSTOM"


# ===================================================================
# 6. EvidenceCollector orchestrator tests
# ===================================================================


class TestEvidenceCollector:
    def test_default_frameworks_registered(
        self, collector: EvidenceCollector
    ) -> None:
        records = collector.collect_all()
        frameworks = {r.framework for r in records}
        assert "SOC2" in frameworks
        assert "ISO27001" in frameworks
        assert "GDPR" in frameworks

    def test_collect_all_count(
        self, collector: EvidenceCollector
    ) -> None:
        records = collector.collect_all()
        # SOC2=4, ISO27001=4, GDPR=3
        assert len(records) == 11

    def test_collect_framework_soc2(
        self, collector: EvidenceCollector
    ) -> None:
        records = collector.collect_framework("SOC2")
        assert len(records) == 4
        assert all(r.framework == "SOC2" for r in records)

    def test_collect_framework_iso27001(
        self, collector: EvidenceCollector
    ) -> None:
        records = collector.collect_framework("ISO27001")
        assert len(records) == 4
        assert all(r.framework == "ISO27001" for r in records)

    def test_collect_framework_gdpr(
        self, collector: EvidenceCollector
    ) -> None:
        records = collector.collect_framework("GDPR")
        assert len(records) == 3
        assert all(r.framework == "GDPR" for r in records)

    def test_collect_unknown_framework_raises(
        self, collector: EvidenceCollector
    ) -> None:
        with pytest.raises(KeyError, match="Unknown framework"):
            collector.collect_framework("HIPAA")

    def test_register_framework(
        self, collector: EvidenceCollector
    ) -> None:
        class Stub:
            @property
            def framework_id(self) -> str:
                return "STUB"

            def collect_evidence(
                self,
                engine: Any,
                audit_log: Any,
                constitution: Any,
            ) -> list[EvidenceRecord]:
                return [
                    EvidenceRecord(
                        framework="STUB",
                        control_id="S.1",
                        status="compliant",
                    )
                ]

        collector.register_framework(Stub())
        records = collector.collect_framework("STUB")
        assert len(records) == 1

    def test_custom_frameworks_only(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        """Pass explicit frameworks list to skip default registration."""

        class Mini:
            @property
            def framework_id(self) -> str:
                return "MINI"

            def collect_evidence(
                self,
                engine: Any,
                audit_log: Any,
                constitution: Any,
            ) -> list[EvidenceRecord]:
                return [
                    EvidenceRecord(
                        framework="MINI",
                        control_id="M.1",
                        status="compliant",
                    )
                ]

        c = EvidenceCollector(
            engine, audit_log, default_constitution, frameworks=[Mini()]
        )
        records = c.collect_all()
        assert len(records) == 1
        assert records[0].framework == "MINI"

    def test_empty_frameworks_list(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        c = EvidenceCollector(
            engine, audit_log, default_constitution, frameworks=[]
        )
        records = c.collect_all()
        assert records == []


# ===================================================================
# 7. Compliance scoring tests
# ===================================================================


class TestComplianceScoring:
    def test_overall_score_range(
        self, collector: EvidenceCollector
    ) -> None:
        score = collector.compliance_score()
        assert 0.0 <= score <= 1.0

    def test_framework_score_range(
        self, collector: EvidenceCollector
    ) -> None:
        for fw in ("SOC2", "ISO27001", "GDPR"):
            score = collector.compliance_score(fw)
            assert 0.0 <= score <= 1.0

    def test_perfect_score(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        class PerfectCollector:
            @property
            def framework_id(self) -> str:
                return "PERFECT"

            def collect_evidence(
                self,
                engine: Any,
                audit_log: Any,
                constitution: Any,
            ) -> list[EvidenceRecord]:
                return [
                    EvidenceRecord(
                        framework="PERFECT",
                        control_id="P.1",
                        status="compliant",
                    ),
                    EvidenceRecord(
                        framework="PERFECT",
                        control_id="P.2",
                        status="compliant",
                    ),
                ]

        c = EvidenceCollector(
            engine,
            audit_log,
            default_constitution,
            frameworks=[PerfectCollector()],
        )
        assert c.compliance_score("PERFECT") == 1.0

    def test_zero_score(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        class FailCollector:
            @property
            def framework_id(self) -> str:
                return "FAIL"

            def collect_evidence(
                self,
                engine: Any,
                audit_log: Any,
                constitution: Any,
            ) -> list[EvidenceRecord]:
                return [
                    EvidenceRecord(
                        framework="FAIL",
                        control_id="F.1",
                        status="non_compliant",
                    ),
                ]

        c = EvidenceCollector(
            engine,
            audit_log,
            default_constitution,
            frameworks=[FailCollector()],
        )
        assert c.compliance_score("FAIL") == 0.0

    def test_partial_score(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        class MixedCollector:
            @property
            def framework_id(self) -> str:
                return "MIXED"

            def collect_evidence(
                self,
                engine: Any,
                audit_log: Any,
                constitution: Any,
            ) -> list[EvidenceRecord]:
                return [
                    EvidenceRecord(
                        framework="MIXED",
                        control_id="M.1",
                        status="compliant",
                    ),
                    EvidenceRecord(
                        framework="MIXED",
                        control_id="M.2",
                        status="non_compliant",
                    ),
                ]

        c = EvidenceCollector(
            engine,
            audit_log,
            default_constitution,
            frameworks=[MixedCollector()],
        )
        assert c.compliance_score("MIXED") == 0.5

    def test_score_with_no_records(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        class EmptyCollector:
            @property
            def framework_id(self) -> str:
                return "EMPTY"

            def collect_evidence(
                self,
                engine: Any,
                audit_log: Any,
                constitution: Any,
            ) -> list[EvidenceRecord]:
                return []

        c = EvidenceCollector(
            engine,
            audit_log,
            default_constitution,
            frameworks=[EmptyCollector()],
        )
        assert c.compliance_score("EMPTY") == 0.0

    def test_score_partial_status_value(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        class PartialCollector:
            @property
            def framework_id(self) -> str:
                return "PART"

            def collect_evidence(
                self,
                engine: Any,
                audit_log: Any,
                constitution: Any,
            ) -> list[EvidenceRecord]:
                return [
                    EvidenceRecord(
                        framework="PART",
                        control_id="P.1",
                        status="partial",
                    ),
                ]

        c = EvidenceCollector(
            engine,
            audit_log,
            default_constitution,
            frameworks=[PartialCollector()],
        )
        assert c.compliance_score("PART") == 0.5


# ===================================================================
# 8. Report generation tests
# ===================================================================


class TestReportGeneration:
    def test_report_has_summary(
        self, collector: EvidenceCollector
    ) -> None:
        report = collector.generate_report()
        assert "summary" in report
        assert "frameworks" in report

    def test_report_summary_fields(
        self, collector: EvidenceCollector
    ) -> None:
        summary = collector.generate_report()["summary"]
        assert "overall_score" in summary
        assert "framework_scores" in summary
        assert "total_evidence_records" in summary
        assert "status_counts" in summary
        assert "frameworks_assessed" in summary
        assert "collected_at" in summary

    def test_report_overall_score_range(
        self, collector: EvidenceCollector
    ) -> None:
        summary = collector.generate_report()["summary"]
        assert 0.0 <= summary["overall_score"] <= 1.0

    def test_report_framework_scores(
        self, collector: EvidenceCollector
    ) -> None:
        scores = collector.generate_report()["summary"][
            "framework_scores"
        ]
        assert "SOC2" in scores
        assert "ISO27001" in scores
        assert "GDPR" in scores

    def test_report_total_records(
        self, collector: EvidenceCollector
    ) -> None:
        summary = collector.generate_report()["summary"]
        assert summary["total_evidence_records"] == 11

    def test_report_frameworks_section(
        self, collector: EvidenceCollector
    ) -> None:
        frameworks = collector.generate_report()["frameworks"]
        assert "SOC2" in frameworks
        assert "ISO27001" in frameworks
        assert "GDPR" in frameworks
        # Each record serialized as dict
        for fw_records in frameworks.values():
            for rec_dict in fw_records:
                assert isinstance(rec_dict, dict)
                assert "record_id" in rec_dict
                assert "control_id" in rec_dict

    def test_report_status_counts(
        self, collector: EvidenceCollector
    ) -> None:
        counts = collector.generate_report()["summary"][
            "status_counts"
        ]
        assert isinstance(counts, dict)
        total = sum(counts.values())
        assert total == 11


# ===================================================================
# 9. JSON export tests
# ===================================================================


class TestJSONExport:
    def test_export_returns_string(
        self, collector: EvidenceCollector
    ) -> None:
        result = collector.export_json()
        assert isinstance(result, str)

    def test_export_valid_json(
        self, collector: EvidenceCollector
    ) -> None:
        result = collector.export_json()
        parsed = json.loads(result)
        assert "summary" in parsed
        assert "frameworks" in parsed

    def test_export_to_file(
        self, collector: EvidenceCollector
    ) -> None:
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "report.json")
            result = collector.export_json(path)
            assert os.path.exists(path)
            with open(path) as f:
                on_disk = json.load(f)
            assert on_disk["summary"]["total_evidence_records"] == 11
            # Return value should match file content
            assert json.loads(result) == on_disk

    def test_export_creates_parent_dirs(
        self, collector: EvidenceCollector
    ) -> None:
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "sub", "dir", "report.json")
            collector.export_json(path)
            assert os.path.exists(path)

    def test_export_no_path_returns_json(
        self, collector: EvidenceCollector
    ) -> None:
        result = collector.export_json()
        parsed = json.loads(result)
        assert isinstance(parsed["summary"]["overall_score"], float)


# ===================================================================
# 10. Edge case and integration tests
# ===================================================================


class TestEdgeCases:
    def test_empty_audit_log_all_frameworks(
        self,
        engine: GovernanceEngine,
        audit_log: AuditLog,
        default_constitution: Constitution,
    ) -> None:
        """All frameworks handle empty audit log gracefully."""
        c = EvidenceCollector(engine, audit_log, default_constitution)
        records = c.collect_all()
        assert len(records) == 11
        # No exceptions raised

    def test_no_rules_constitution(
        self,
        audit_log: AuditLog,
    ) -> None:
        """Collectors handle empty constitution."""
        empty = Constitution(name="empty", rules=[])
        eng = GovernanceEngine(
            empty, audit_log=audit_log, strict=False
        )
        c = EvidenceCollector(eng, audit_log, empty)
        records = c.collect_all()
        assert len(records) == 11

    def test_engine_after_validations(
        self,
        default_constitution: Constitution,
    ) -> None:
        """Evidence reflects actual validation activity."""
        log = AuditLog()
        eng = GovernanceEngine(
            default_constitution, audit_log=log, strict=False
        )
        eng.validate("safe action", agent_id="test-agent")
        eng.validate("another safe action", agent_id="test-agent")

        c = EvidenceCollector(eng, log, default_constitution)
        soc2 = c.collect_framework("SOC2")
        cc72 = next(r for r in soc2 if r.control_id == "CC7.2")
        assert cc72.data["total_validations"] >= 2
        assert cc72.status == "compliant"

    def test_single_rule_constitution(self) -> None:
        """Single-rule constitution still produces valid evidence."""
        const = Constitution(
            name="single",
            rules=[
                Rule(
                    id="S-001",
                    text="Do no harm",
                    severity=Severity.CRITICAL,
                    category="safety",
                ),
            ],
        )
        log = AuditLog()
        eng = GovernanceEngine(const, audit_log=log, strict=False)
        c = EvidenceCollector(eng, log, const)
        records = c.collect_all()
        assert len(records) == 11
        score = c.compliance_score()
        assert 0.0 <= score <= 1.0

    def test_report_is_json_serializable(
        self, collector: EvidenceCollector
    ) -> None:
        report = collector.generate_report()
        # Must not raise
        json.dumps(report, default=str)

    def test_concurrent_collect_calls(
        self, collector: EvidenceCollector
    ) -> None:
        """Multiple collect calls return consistent results."""
        r1 = collector.collect_all()
        r2 = collector.collect_all()
        assert len(r1) == len(r2)
        for a, b in zip(r1, r2, strict=True):
            assert a.framework == b.framework
            assert a.control_id == b.control_id
            assert a.status == b.status

    def test_cc8_1_empty_constitution_non_compliant(self) -> None:
        """CC8.1 with no hash or version is non_compliant."""
        # Constitution with no rules still has a hash from Pydantic
        # so we just check the collector logic
        const = Constitution(name="", version="", rules=[])
        log = AuditLog()
        eng = GovernanceEngine(const, audit_log=log, strict=False)
        c = SOC2EvidenceCollector()
        records = c.collect_evidence(eng, log, const)
        cc81 = next(r for r in records if r.control_id == "CC8.1")
        # Even empty constitution gets a hash from pydantic model
        # so hash is always present
        assert cc81.status in ("compliant", "partial")

    def test_a18_1_non_compliant_scenario(self) -> None:
        """A.18.1 with poor compliance rate."""
        const = Constitution(name="empty", rules=[])
        log = AuditLog()
        # Add only invalid entries
        log.record(
            AuditEntry(
                id="x1", type="validation", valid=False,
                violations=["V1"],
            )
        )
        eng = GovernanceEngine(const, audit_log=log, strict=False)
        c = ISO27001EvidenceCollector()
        records = c.collect_evidence(eng, log, const)
        a181 = next(r for r in records if r.control_id == "A.18.1")
        # compliance_rate is 0% (1 invalid), chain valid, no rules,
        # has hash -> checks_passed = 2 -> partial
        assert a181.status in ("partial", "non_compliant")
