# #region Test.Models.CleanRelease.Ext [C:2] [TYPE Module] [SEMANTICS test,models,clean-release,extended]
# @BRIEF Extended tests for models/clean_release.py — edge cases for enums, ResourceSourceRegistry,
#        CleanProfilePolicy, ComplianceCheckRun, ReleaseCandidate, DistributionManifest,
#        ComplianceRun, ComplianceViolation, ComplianceReport, ApprovalDecision, PublicationRecord,
#        CleanReleaseAuditLog, SourceRegistrySnapshot, CleanPolicySnapshot, ComplianceStageRun.
# @RELATION BINDS_TO -> [CleanReleaseModels]

from datetime import datetime, timezone
import pytest
from pydantic import ValidationError


class TestCleanReleaseLegacyEnums:
    """Legacy backward-compatible enums."""

    def test_execution_mode(self):
        from src.models.clean_release import ExecutionMode

        assert ExecutionMode.TUI == "TUI"
        assert ExecutionMode.API == "API"
        assert ExecutionMode.SCHEDULER == "SCHEDULER"

    def test_check_final_status(self):
        from src.models.clean_release import CheckFinalStatus

        assert CheckFinalStatus.COMPLIANT == "COMPLIANT"
        assert CheckFinalStatus.BLOCKED == "BLOCKED"
        assert CheckFinalStatus.FAILED == "FAILED"
        assert CheckFinalStatus.RUNNING == "RUNNING"

    def test_check_stage_name(self):
        from src.models.clean_release import CheckStageName

        assert CheckStageName.DATA_PURITY == "DATA_PURITY"
        assert CheckStageName.INTERNAL_SOURCES_ONLY == "INTERNAL_SOURCES_ONLY"
        assert CheckStageName.NO_EXTERNAL_ENDPOINTS == "NO_EXTERNAL_ENDPOINTS"
        assert CheckStageName.MANIFEST_CONSISTENCY == "MANIFEST_CONSISTENCY"

    def test_check_stage_status(self):
        from src.models.clean_release import CheckStageStatus

        assert CheckStageStatus.PASS == "PASS"
        assert CheckStageStatus.FAIL == "FAIL"
        assert CheckStageStatus.SKIPPED == "SKIPPED"
        assert CheckStageStatus.RUNNING == "RUNNING"

    def test_profile_type(self):
        from src.models.clean_release import ProfileType

        assert ProfileType.ENTERPRISE_CLEAN == "enterprise-clean"

    def test_registry_status(self):
        from src.models.clean_release import RegistryStatus

        assert RegistryStatus.ACTIVE == "ACTIVE"
        assert RegistryStatus.INACTIVE == "INACTIVE"

    def test_release_candidate_status(self):
        from src.models.clean_release import ReleaseCandidateStatus

        assert ReleaseCandidateStatus.DRAFT == "DRAFT"
        assert ReleaseCandidateStatus.PUBLISHED == "PUBLISHED"
        assert ReleaseCandidateStatus.REVOKED == "REVOKED"
        assert ReleaseCandidateStatus.BLOCKED == "CHECK_BLOCKED"  # alias

    def test_violation_severity(self):
        from src.models.clean_release import ViolationSeverity

        assert ViolationSeverity.CRITICAL == "CRITICAL"
        assert ViolationSeverity.MAJOR == "MAJOR"
        assert ViolationSeverity.MINOR == "MINOR"

    def test_violation_category(self):
        from src.models.clean_release import ViolationCategory

        assert ViolationCategory.DATA_PURITY == "DATA_PURITY"
        assert ViolationCategory.EXTERNAL_SOURCE == "EXTERNAL_SOURCE"
        assert ViolationCategory.SOURCE_ISOLATION == "SOURCE_ISOLATION"
        assert ViolationCategory.MANIFEST_CONSISTENCY == "MANIFEST_CONSISTENCY"
        assert ViolationCategory.EXTERNAL_ENDPOINT == "EXTERNAL_ENDPOINT"


class TestCheckStageResult:
    """CheckStageResult — pydantic dataclass."""

    def test_minimal(self):
        from src.models.clean_release import CheckStageName, CheckStageResult, CheckStageStatus

        result = CheckStageResult(stage=CheckStageName.DATA_PURITY, status=CheckStageStatus.PASS)
        assert result.stage == CheckStageName.DATA_PURITY
        assert result.status == CheckStageStatus.PASS
        assert result.details == ""

    def test_with_details(self):
        from src.models.clean_release import CheckStageName, CheckStageResult, CheckStageStatus

        result = CheckStageResult(stage=CheckStageName.DATA_PURITY, status=CheckStageStatus.FAIL, details="Data issues")
        assert result.details == "Data issues"


class TestResourceSourceEntry:
    """ResourceSourceEntry — pydantic dataclass."""

    def test_minimal(self):
        from src.models.clean_release import ResourceSourceEntry

        entry = ResourceSourceEntry(source_id="s-1", host="example.com", protocol="https", purpose="api")
        assert entry.host == "example.com"
        assert entry.enabled is True

    def test_disabled(self):
        from src.models.clean_release import ResourceSourceEntry

        entry = ResourceSourceEntry(source_id="s-1", host="example.com", protocol="https", purpose="api", enabled=False)
        assert entry.enabled is False


class TestResourceSourceRegistry:
    """ResourceSourceRegistry — registry with populate_legacy_allowlists."""

    def test_minimal(self):
        from src.models.clean_release import ResourceSourceEntry, ResourceSourceRegistry

        ts = datetime.now(timezone.utc)
        entry = ResourceSourceEntry(source_id="s-1", host="example.com", protocol="https", purpose="api")
        registry = ResourceSourceRegistry(
            registry_id="reg-1", name="My Registry",
            entries=[entry], updated_at=ts, updated_by="admin",
        )
        assert registry.registry_id == "reg-1"
        assert registry.allowed_hosts == ["example.com"]  # auto-populated
        assert registry.allowed_schemes == ["https"]
        assert registry.allowed_source_types == ["api"]
        assert registry.id == "reg-1"
        assert registry.immutable is True

    def test_with_some_disabled_entries(self):
        from src.models.clean_release import ResourceSourceEntry, ResourceSourceRegistry

        ts = datetime.now(timezone.utc)
        e1 = ResourceSourceEntry(source_id="s-1", host="good.com", protocol="https", purpose="api", enabled=True)
        e2 = ResourceSourceEntry(source_id="s-2", host="bad.com", protocol="http", purpose="old", enabled=False)
        registry = ResourceSourceRegistry(
            registry_id="reg-1", name="Registry",
            entries=[e1, e2], updated_at=ts, updated_by="admin",
        )
        # Only enabled entries should be included in allow lists
        assert "good.com" in registry.allowed_hosts
        assert "bad.com" not in registry.allowed_hosts
        assert registry.allowed_schemes == ["https"]  # only e1's protocol

    def test_preserves_explicit_allowlists(self):
        from src.models.clean_release import ResourceSourceEntry, ResourceSourceRegistry

        ts = datetime.now(timezone.utc)
        entry = ResourceSourceEntry(source_id="s-1", host="example.com", protocol="https", purpose="api")
        registry = ResourceSourceRegistry(
            registry_id="reg-1", name="Registry",
            entries=[entry], updated_at=ts, updated_by="admin",
            allowed_hosts=["explicit.com"], allowed_schemes=["ftp"],
            allowed_source_types=["explicit"],
        )
        assert registry.allowed_hosts == ["explicit.com"]
        assert registry.allowed_schemes == ["ftp"]
        assert registry.allowed_source_types == ["explicit"]


class TestCleanProfilePolicy:
    """CleanProfilePolicy — enterprise policy validation."""

    def test_non_enterprise_no_validation(self):
        from src.models.clean_release import CleanProfilePolicy, ProfileType

        ts = datetime.now(timezone.utc)
        # For non-enterprise profile, prohibited_artifact_categories are not required by the validator
        policy = CleanProfilePolicy(
            policy_id="p-1", policy_version="1",
            profile=ProfileType.ENTERPRISE_CLEAN,
            active=True,
            prohibited_artifact_categories=["test"],
            internal_source_registry_ref="reg-1",
            effective_from=ts,
            external_source_forbidden=True,
        )
        assert policy.profile == ProfileType.ENTERPRISE_CLEAN
        assert policy.id == "p-1"
        assert policy.registry_snapshot_id == "reg-1"
        assert policy.content_json is not None
        assert policy.content_json["profile"] == "enterprise-clean"

    def test_property_access(self):
        from src.models.clean_release import CleanProfilePolicy, ProfileType

        ts = datetime.now(timezone.utc)
        policy = CleanProfilePolicy(
            policy_id="p-1", policy_version="1",
            profile=ProfileType.ENTERPRISE_CLEAN,
            active=True,
            prohibited_artifact_categories=["test-data"],
            internal_source_registry_ref="REG-1",
            effective_from=ts,
        )
        assert policy.id == "p-1"
        assert policy.registry_snapshot_id == "REG-1"

    # ── Lines 172, 174: enterprise-clean validation errors ──

    def test_enterprise_requires_prohibited_artifact_categories(self):
        """ENTERPRISE_CLEAN profile with empty prohibited_artifact_categories raises (line 172)."""
        from src.models.clean_release import CleanProfilePolicy, ProfileType

        ts = datetime.now(timezone.utc)
        with pytest.raises(ValueError, match="enterprise-clean policy requires prohibited_artifact_categories"):
            CleanProfilePolicy(
                policy_id="p-err", policy_version="1",
                profile=ProfileType.ENTERPRISE_CLEAN,
                active=True,
                prohibited_artifact_categories=[],
                internal_source_registry_ref="reg-1",
                effective_from=ts,
                external_source_forbidden=True,
            )

    def test_enterprise_requires_external_forbidden(self):
        """ENTERPRISE_CLEAN profile with external_source_forbidden=False raises (line 174)."""
        from src.models.clean_release import CleanProfilePolicy, ProfileType

        ts = datetime.now(timezone.utc)
        with pytest.raises(ValueError, match="enterprise-clean policy requires external_source_forbidden=true"):
            CleanProfilePolicy(
                policy_id="p-err2", policy_version="1",
                profile=ProfileType.ENTERPRISE_CLEAN,
                active=True,
                prohibited_artifact_categories=["test-data"],
                internal_source_registry_ref="reg-1",
                effective_from=ts,
                external_source_forbidden=False,
            )


class TestReleaseCandidate:
    """ReleaseCandidate — lifecycle and transitions."""

    def test_with_status_enum(self):
        from src.models.clean_release import ReleaseCandidate, ReleaseCandidateStatus

        rc = ReleaseCandidate(
            candidate_id="RC-002",
            version="2.0.0",
            source_snapshot_ref="snap-2",
            created_by="admin",
            status=ReleaseCandidateStatus.DRAFT,
        )
        assert rc.status == "DRAFT"
        assert rc.candidate_id == "RC-002"

    def test_with_candidate_status_enum(self):
        from src.models.clean_release import CandidateStatus, ReleaseCandidate

        rc = ReleaseCandidate(
            candidate_id="RC-003",
            version="3.0.0",
            source_snapshot_ref="snap-3",
            created_by="admin",
            status=CandidateStatus.DRAFT,
        )
        assert rc.status == "DRAFT"

    def test_transition_draft_to_prepared(self):
        from src.models.clean_release import CandidateStatus, ReleaseCandidate

        rc = ReleaseCandidate(candidate_id="RC-010", version="1.0.0",
                              source_snapshot_ref="snap", created_by="u",
                              status=CandidateStatus.DRAFT)
        rc.transition_to(CandidateStatus.PREPARED)
        assert rc.status == "PREPARED"

    def test_transition_full_flow(self):
        from src.models.clean_release import CandidateStatus, ReleaseCandidate

        rc = ReleaseCandidate(candidate_id="RC-020", version="1.0.0",
                              source_snapshot_ref="snap", created_by="u",
                              status=CandidateStatus.DRAFT)
        rc.transition_to(CandidateStatus.PREPARED)
        rc.transition_to(CandidateStatus.MANIFEST_BUILT)
        rc.transition_to(CandidateStatus.CHECK_PENDING)
        rc.transition_to(CandidateStatus.CHECK_RUNNING)
        rc.transition_to(CandidateStatus.CHECK_PASSED)
        rc.transition_to(CandidateStatus.APPROVED)
        rc.transition_to(CandidateStatus.PUBLISHED)
        rc.transition_to(CandidateStatus.REVOKED)
        assert rc.status == "REVOKED"

    def test_transition_illegal_raises(self):
        from src.models.clean_release import CandidateStatus, IllegalTransitionError, ReleaseCandidate

        rc = ReleaseCandidate(candidate_id="RC-030", version="1.0.0",
                              source_snapshot_ref="snap", created_by="u",
                              status=CandidateStatus.DRAFT)
        with pytest.raises(IllegalTransitionError, match="Forbidden transition"):
            rc.transition_to(CandidateStatus.APPROVED)

    def test_transition_revoked_has_no_transitions(self):
        from src.models.clean_release import CandidateStatus, IllegalTransitionError, ReleaseCandidate

        rc = ReleaseCandidate(candidate_id="RC-040", version="1.0.0",
                              source_snapshot_ref="snap", created_by="u",
                              status=CandidateStatus.REVOKED)
        with pytest.raises(IllegalTransitionError):
            rc.transition_to(CandidateStatus.DRAFT)

    def test_transition_check_running_to_blocked(self):
        from src.models.clean_release import CandidateStatus, ReleaseCandidate

        rc = ReleaseCandidate(candidate_id="RC-050", version="1.0.0",
                              source_snapshot_ref="snap", created_by="u",
                              status=CandidateStatus.CHECK_RUNNING)
        rc.transition_to(CandidateStatus.CHECK_BLOCKED)
        assert rc.status == "CHECK_BLOCKED"

    def test_transition_check_running_to_error(self):
        from src.models.clean_release import CandidateStatus, ReleaseCandidate

        rc = ReleaseCandidate(candidate_id="RC-060", version="1.0.0",
                              source_snapshot_ref="snap", created_by="u",
                              status=CandidateStatus.CHECK_RUNNING)
        rc.transition_to(CandidateStatus.CHECK_ERROR)
        assert rc.status == "CHECK_ERROR"

    def test_transition_from_blocked_back_to_pending(self):
        from src.models.clean_release import CandidateStatus, ReleaseCandidate

        rc = ReleaseCandidate(candidate_id="RC-070", version="1.0.0",
                              source_snapshot_ref="snap", created_by="u",
                              status=CandidateStatus.CHECK_BLOCKED)
        rc.transition_to(CandidateStatus.CHECK_PENDING)
        assert rc.status == "CHECK_PENDING"

    def test_transition_from_passed_to_pending(self):
        from src.models.clean_release import CandidateStatus, ReleaseCandidate

        rc = ReleaseCandidate(candidate_id="RC-080", version="1.0.0",
                              source_snapshot_ref="snap", created_by="u",
                              status=CandidateStatus.CHECK_PASSED)
        rc.transition_to(CandidateStatus.CHECK_PENDING)
        assert rc.status == "CHECK_PENDING"

    # ── Lines 262, 265, 269: ReleaseCandidate.__init__ edge cases ──

    def test_init_pops_profile_kwarg(self):
        """ReleaseCandidate.__init__ silently pops profile kwarg (line 262)."""
        from src.models.clean_release import CandidateStatus, ReleaseCandidate

        rc = ReleaseCandidate(
            candidate_id="RC-PROF",
            version="1.0.0",
            source_snapshot_ref="snap",
            created_by="u",
            profile="should_be_removed",
            status=CandidateStatus.DRAFT,
        )
        assert rc.candidate_id == "RC-PROF"
        assert not hasattr(rc, "profile")

    def test_init_defaults_status_when_none(self):
        """ReleaseCandidate.__init__ defaults to DRAFT when no status given (line 265)."""
        from src.models.clean_release import CandidateStatus, ReleaseCandidate

        rc = ReleaseCandidate(
            candidate_id="RC-NOSTAT",
            version="1.0.0",
            source_snapshot_ref="snap",
            created_by="u",
        )
        assert rc.status == CandidateStatus.DRAFT.value

    def test_init_raises_on_empty_id(self):
        """ReleaseCandidate.__init__ raises ValueError on empty candidate_id (line 269)."""
        from src.models.clean_release import ReleaseCandidate

        with pytest.raises(ValueError, match="candidate_id must be non-empty"):
            ReleaseCandidate(
                candidate_id="",
                version="1.0.0",
                source_snapshot_ref="snap",
                created_by="u",
            )

    def test_init_raises_on_blank_id(self):
        """ReleaseCandidate.__init__ raises ValueError on whitespace-only candidate_id."""
        from src.models.clean_release import ReleaseCandidate

        with pytest.raises(ValueError, match="candidate_id must be non-empty"):
            ReleaseCandidate(
                candidate_id="   ",
                version="1.0.0",
                source_snapshot_ref="snap",
                created_by="u",
            )


class TestDistributionManifest:
    """DistributionManifest — immutable manifest model."""

    def test_minimal(self):
        from src.models.clean_release import DistributionManifest

        manifest = DistributionManifest(
            id="m-1", candidate_id="c-1",
            manifest_digest="digest1", artifacts_digest="ad1",
            created_by="admin", source_snapshot_ref="snap-1",
            content_json={"key": "value"},
            manifest_version=1, immutable=True,
        )
        assert manifest.manifest_version == 1
        assert manifest.manifest_id == "m-1"
        assert manifest.deterministic_hash == "digest1"
        assert manifest.summary.included_count == 0

    def test_with_items_and_summary(self):
        from src.models.clean_release import ClassificationType, DistributionManifest, ManifestItem, ManifestSummary

        items = [ManifestItem(path="/file", category="bin", classification=ClassificationType.ALLOWED, reason="ok")]
        summary = ManifestSummary(included_count=1, excluded_count=0, prohibited_detected_count=0)
        manifest = DistributionManifest(
            manifest_id="m-2", candidate_id="c-1",
            deterministic_hash="hash1",
            generated_at=datetime.now(timezone.utc),
            generated_by="admin",
            items=items, summary=summary,
        )
        assert manifest.manifest_digest == "hash1"
        assert manifest.summary.included_count == 1

    def test_id_mapping(self):
        from src.models.clean_release import DistributionManifest

        manifest = DistributionManifest(
            manifest_id="m-1", candidate_id="c-1",
            manifest_digest="d1", artifacts_digest="ad1",
            created_by="admin", source_snapshot_ref="snap-1",
            content_json={},
        )
        assert manifest.id == "m-1"

    def test_immutable_default(self):
        from src.models.clean_release import DistributionManifest

        manifest = DistributionManifest(
            id="m-imm", candidate_id="c-1",
            manifest_digest="d1", artifacts_digest="ad1",
            created_by="admin", source_snapshot_ref="snap-1",
            content_json={}, immutable=True,
        )
        assert manifest.immutable is True

    # ── Line 375: DistributionManifest.__init__ pops policy_id ──

    def test_init_pops_policy_id_kwarg(self):
        """DistributionManifest.__init__ silently pops policy_id (line 375)."""
        from src.models.clean_release import DistributionManifest

        manifest = DistributionManifest(
            manifest_id="m-pol", candidate_id="c-1",
            deterministic_hash="hash1",
            generated_at=datetime.now(timezone.utc),
            generated_by="admin",
            policy_id="should-be-popped",
        )
        assert manifest.manifest_id == "m-pol"
        assert not hasattr(manifest, "policy_id")

    # ── Line 380: DistributionManifest.__init__ validates items vs summary ──

    def test_init_raises_on_mismatched_items_summary(self):
        """DistributionManifest raises ValueError when summary counts don't match items (line 380)."""
        from src.models.clean_release import ClassificationType, DistributionManifest, ManifestItem, ManifestSummary

        items = [ManifestItem(path="/f1", category="bin", classification=ClassificationType.ALLOWED, reason="ok")]
        summary = ManifestSummary(included_count=2, excluded_count=0, prohibited_detected_count=0)

        with pytest.raises(ValueError, match="manifest summary counts must match items size"):
            DistributionManifest(
                manifest_id="m-mismatch", candidate_id="c-1",
                deterministic_hash="hash1",
                generated_at=datetime.now(timezone.utc),
                generated_by="admin",
                items=items, summary=summary,
            )


class TestComplianceRun:
    """ComplianceRun — operational record."""

    def test_minimal(self):
        from src.models.clean_release import ComplianceRun

        run = ComplianceRun(
            id="r-1", candidate_id="c-1",
            manifest_id="m-1", manifest_digest="d1",
            policy_snapshot_id="ps-1", registry_snapshot_id="rs-1",
            requested_by="admin", status="PENDING",
        )
        assert run.check_run_id == "r-1"
        assert run.status == "PENDING"
        assert run.final_status is None

    def test_with_final_status(self):
        from src.models.clean_release import ComplianceRun

        run = ComplianceRun(
            id="r-2", candidate_id="c-1",
            manifest_id="m-1", manifest_digest="d1",
            policy_snapshot_id="ps-1", registry_snapshot_id="rs-1",
            requested_by="admin", final_status="PASSED",
        )
        assert run.final_status == "PASSED"
        assert run.task_id is None


class TestComplianceStageRun:
    """ComplianceStageRun — stage-level execution."""

    def test_minimal(self):
        from src.models.clean_release import ComplianceStageRun

        stage = ComplianceStageRun(id="s-1", run_id="r-1", stage_name="DATA_PURITY", status="PASS")
        assert stage.stage_name == "DATA_PURITY"
        assert stage.status == "PASS"


class TestComplianceViolation:
    """ComplianceViolation — violation with legacy property mapping."""

    def test_minimal(self):
        from src.models.clean_release import ComplianceViolation

        violation = ComplianceViolation(
            id="v-1", run_id="r-1", stage_name="DATA_PURITY",
            code="DP001", severity="MAJOR", message="Data issue",
        )
        assert violation.violation_id == "v-1"
        assert violation.check_run_id == "r-1"
        assert violation.code == "DP001"
        assert violation.artifact_path is None
        assert violation.remediation is None
        assert violation.blocked_release is False

    def test_with_legacy_kwargs(self):
        from src.models.clean_release import ComplianceViolation

        violation = ComplianceViolation(
            violation_id="v-1",
            check_run_id="r-1",
            category="DATA_PURITY",
            code="DP002",
            severity="CRITICAL",
            message="NPE",
            location="/path/to/file",
            remediation="Fix it",
            blocked_release=True,
        )
        assert violation.id == "v-1"
        assert violation.run_id == "r-1"
        assert violation.stage_name == "DATA_PURITY"
        assert violation.artifact_path == "/path/to/file"
        assert violation.remediation == "Fix it"
        assert violation.blocked_release is True
        assert violation.category.value == "DATA_PURITY"
        assert violation.location == "/path/to/file"

    def test_category_setter(self):
        from src.models.clean_release import ComplianceViolation, ViolationCategory

        violation = ComplianceViolation(
            id="v-1", run_id="r-1", stage_name="DATA_PURITY",
            code="DP001", severity="MAJOR", message="Data issue",
        )
        violation.category = ViolationCategory.EXTERNAL_SOURCE
        assert violation.stage_name == "EXTERNAL_SOURCE"

    def test_violation_id_setter(self):
        from src.models.clean_release import ComplianceViolation

        violation = ComplianceViolation(
            id="v-1", run_id="r-1", stage_name="DATA_PURITY",
            code="DP001", severity="MAJOR", message="Data issue",
        )
        violation.violation_id = "v-2"
        assert violation.id == "v-2"

    def test_default_code_and_message(self):
        from src.models.clean_release import ComplianceViolation

        violation = ComplianceViolation(
            id="v-3", run_id="r-1", stage_name="DATA_PURITY",
            severity="MINOR", message="",
        )
        # When code not provided, defaults to LEGACY_VIOLATION
        assert violation.code == "LEGACY_VIOLATION"

    def test_detected_at_popped(self):
        from src.models.clean_release import ComplianceViolation

        # detected_at is popped (removed) in __init__, so it shouldn't be an attribute
        violation = ComplianceViolation(
            id="v-4", run_id="r-1", stage_name="DATA_PURITY",
            severity="MINOR", message="test",
            detected_at="2024-01-01",  # should be popped
        )
        assert not hasattr(violation, "detected_at")

    def test_message_defaults_to_stage_name(self):
        """When message is not provided, it defaults to stage_name."""
        from src.models.clean_release import ComplianceViolation

        violation = ComplianceViolation(
            id="v-5", run_id="r-1", stage_name="SOURCE_ISOLATION",
            severity="MAJOR", code="SI001",
            # no message kwarg
        )
        # message should default to stage_name
        assert violation.message == "SOURCE_ISOLATION"

    def test_message_defaults_fallback(self):
        """When message is not provided, defaults to stage_name value."""
        from src.models.clean_release import ComplianceViolation

        violation = ComplianceViolation(
            id="v-6", run_id="r-1", stage_name="SOURCE_ISOLATION",
            severity="MAJOR", code="GEN001",
            # no message kwarg
        )
        # Without 'message' kwarg, defaults to stage_name value
        assert violation.message == "SOURCE_ISOLATION"


class TestComplianceReport:
    """ComplianceReport — immutable report with property access."""

    def test_minimal(self):
        from src.models.clean_release import ComplianceReport

        report = ComplianceReport(
            id="rep-1", run_id="r-1", candidate_id="c-1",
            final_status="PASSED", summary_json={"key": "val"},
            immutable=True,
        )
        assert report.report_id == "rep-1"
        assert report.check_run_id == "r-1"
        assert report.final_status == "PASSED"
        assert report.immutable is True

    def test_with_legacy_kwargs(self):
        from src.models.clean_release import ComplianceReport

        report = ComplianceReport(
            report_id="rep-1",
            check_run_id="r-1",
            candidate_id="c-1",
            final_status="PASSED",
            operator_summary="All good",
            structured_payload_ref="ref-1",
            violations_count=0,
            blocking_violations_count=0,
        )
        assert report.report_id == "rep-1"
        assert report.check_run_id == "r-1"
        assert report.operator_summary == "All good"
        assert report.structured_payload_ref == "ref-1"
        assert report.violations_count == 0
        assert report.blocking_violations_count == 0

    def test_blocked_without_violations_raises(self):
        from src.models.clean_release import CheckFinalStatus, ComplianceReport

        with pytest.raises(ValueError, match="blocked report requires blocking violations"):
            ComplianceReport(
                report_id="rep-2",
                check_run_id="r-2",
                candidate_id="c-2",
                final_status=CheckFinalStatus.BLOCKED,
                violations_count=0,
                blocking_violations_count=0,
            )

    def test_blocked_with_violations_ok(self):
        from src.models.clean_release import ComplianceDecision, ComplianceReport

        report = ComplianceReport(
            report_id="rep-3",
            check_run_id="r-3",
            candidate_id="c-3",
            final_status=ComplianceDecision.BLOCKED,
            operator_summary="Blocked",
            violations_count=3,
            blocking_violations_count=2,
        )
        assert report.final_status == "BLOCKED"
        assert report.violations_count == 3


class TestApprovalDecision:
    """ApprovalDecision — approval record."""

    def test_minimal(self):
        from src.models.clean_release import ApprovalDecision

        decision = ApprovalDecision(
            id="a-1", candidate_id="c-1", report_id="r-1",
            decision="APPROVED", decided_by="admin",
        )
        assert decision.decision == "APPROVED"
        assert decision.comment is None

    def test_with_comment(self):
        from src.models.clean_release import ApprovalDecision

        decision = ApprovalDecision(
            id="a-2", candidate_id="c-1", report_id="r-1",
            decision="REJECTED", decided_by="admin",
            comment="Missing required artifacts",
        )
        assert decision.comment == "Missing required artifacts"


class TestPublicationRecord:
    """PublicationRecord — publication or revocation."""

    def test_minimal(self):
        from src.models.clean_release import PublicationRecord

        record = PublicationRecord(
            id="p-1", candidate_id="c-1", report_id="r-1",
            published_by="admin", target_channel="registry",
            status="ACTIVE",
        )
        assert record.status == "ACTIVE"
        assert record.publication_ref is None

    def test_with_all_fields(self):
        from src.models.clean_release import PublicationRecord

        record = PublicationRecord(
            id="p-2", candidate_id="c-1", report_id="r-1",
            published_by="admin", target_channel="registry",
            publication_ref="pub-ref-1", status="REVOKED",
        )
        assert record.publication_ref == "pub-ref-1"
        assert record.status == "REVOKED"


class TestCleanReleaseAuditLog:
    """CleanReleaseAuditLog — persistent audit log."""

    def test_minimal(self):
        from src.models.clean_release import CleanReleaseAuditLog

        log = CleanReleaseAuditLog(
            id="al-1", candidate_id="c-1",
            action="TRANSITION", actor="admin",
        )
        assert log.id == "al-1"
        assert log.action == "TRANSITION"
        assert log.actor == "admin"

    def test_with_details(self):
        from src.models.clean_release import CleanReleaseAuditLog

        log = CleanReleaseAuditLog(
            id="al-2", candidate_id="c-1",
            action="APPROVE", actor="admin",
            details_json={"reason": "All checks passed"},
        )
        assert log.details_json["reason"] == "All checks passed"

    def test_nullable_candidate_id(self):
        from src.models.clean_release import CleanReleaseAuditLog

        log = CleanReleaseAuditLog(id="al-3", action="SYSTEM", actor="system")
        assert log.candidate_id is None


class TestSourceRegistrySnapshot:
    """SourceRegistrySnapshot — immutable registry snapshot."""

    def test_minimal(self):
        from src.models.clean_release import SourceRegistrySnapshot

        snap = SourceRegistrySnapshot(
            id="rs-1", registry_id="reg-1", registry_version="1.0",
            allowed_hosts=["host1"], allowed_schemes=["https"],
            allowed_source_types=["api"], immutable=True,
        )
        assert snap.registry_id == "reg-1"
        assert snap.immutable is True


class TestCleanPolicySnapshot:
    """CleanPolicySnapshot — immutable policy snapshot."""

    def test_minimal(self):
        from src.models.clean_release import CleanPolicySnapshot

        snap = CleanPolicySnapshot(
            id="ps-1", policy_id="pol-1", policy_version="1",
            content_json={"rules": []}, registry_snapshot_id="rs-1",
            immutable=True,
        )
        assert snap.policy_id == "pol-1"
        assert snap.immutable is True


class TestComplianceCheckRunProperties:
    """ComplianceCheckRun — property access and status mappings."""

    def test_id_and_run_id(self):
        from src.models.clean_release import (
            CheckFinalStatus, CheckStageName, CheckStageResult, CheckStageStatus,
            ComplianceCheckRun, ExecutionMode,
        )

        ts = datetime.now(timezone.utc)
        run = ComplianceCheckRun(
            check_run_id="run-1",
            candidate_id="c-1",
            policy_id="p-1",
            started_at=ts,
            triggered_by="admin",
            execution_mode=ExecutionMode.TUI,
            final_status=CheckFinalStatus.RUNNING,
            checks=[
                CheckStageResult(stage=CheckStageName.DATA_PURITY, status=CheckStageStatus.PASS),
            ],
        )
        assert run.id == "run-1"
        assert run.run_id == "run-1"
        assert run.status == "RUNNING"

    def test_status_mapping_blocked(self):
        from src.models.clean_release import (
            CheckFinalStatus, CheckStageName, CheckStageResult, CheckStageStatus,
            ComplianceCheckRun, ExecutionMode,
        )

        ts = datetime.now(timezone.utc)
        run = ComplianceCheckRun(
            check_run_id="run-2",
            candidate_id="c-1",
            policy_id="p-1",
            started_at=ts,
            triggered_by="admin",
            execution_mode=ExecutionMode.API,
            final_status=CheckFinalStatus.BLOCKED,
            checks=[
                CheckStageResult(stage=CheckStageName.DATA_PURITY, status=CheckStageStatus.PASS),
            ],
        )
        assert run.status == "FAILED"

    def test_status_succeeded(self):
        from src.models.clean_release import (
            CheckFinalStatus, CheckStageName, CheckStageResult, CheckStageStatus,
            ComplianceCheckRun, ExecutionMode,
        )

        ts = datetime.now(timezone.utc)
        run = ComplianceCheckRun(
            check_run_id="run-5",
            candidate_id="c-1",
            policy_id="p-1",
            started_at=ts,
            triggered_by="admin",
            execution_mode=ExecutionMode.TUI,
            final_status=CheckFinalStatus.COMPLIANT,
            checks=[
                CheckStageResult(stage=CheckStageName.DATA_PURITY, status=CheckStageStatus.PASS),
                CheckStageResult(stage=CheckStageName.INTERNAL_SOURCES_ONLY, status=CheckStageStatus.PASS),
                CheckStageResult(stage=CheckStageName.NO_EXTERNAL_ENDPOINTS, status=CheckStageStatus.PASS),
                CheckStageResult(stage=CheckStageName.MANIFEST_CONSISTENCY, status=CheckStageStatus.PASS),
            ],
        )
        assert run.status == "SUCCEEDED"

    # ── Lines 219, 221: ComplianceCheckRun COMPLIANT validation errors ──

    def test_compliant_requires_all_stages(self):
        """COMPLIANT status with missing stages raises ValueError (line 219)."""
        from src.models.clean_release import (
            CheckFinalStatus, CheckStageName, CheckStageResult, CheckStageStatus,
            ComplianceCheckRun, ExecutionMode,
        )

        ts = datetime.now(timezone.utc)
        with pytest.raises(ValueError, match="compliant run requires all mandatory stages"):
            ComplianceCheckRun(
                check_run_id="run-err1",
                candidate_id="c-1",
                policy_id="p-1",
                started_at=ts,
                triggered_by="admin",
                execution_mode=ExecutionMode.TUI,
                final_status=CheckFinalStatus.COMPLIANT,
                checks=[
                    CheckStageResult(stage=CheckStageName.DATA_PURITY, status=CheckStageStatus.PASS),
                    # Missing: INTERNAL_SOURCES_ONLY, NO_EXTERNAL_ENDPOINTS, MANIFEST_CONSISTENCY
                ],
            )

    def test_compliant_requires_all_pass(self):
        """COMPLIANT status with FAIL checks raises ValueError (line 221)."""
        from src.models.clean_release import (
            CheckFinalStatus, CheckStageName, CheckStageResult, CheckStageStatus,
            ComplianceCheckRun, ExecutionMode,
        )

        ts = datetime.now(timezone.utc)
        with pytest.raises(ValueError, match="compliant run requires PASS on all mandatory stages"):
            ComplianceCheckRun(
                check_run_id="run-err2",
                candidate_id="c-1",
                policy_id="p-1",
                started_at=ts,
                triggered_by="admin",
                execution_mode=ExecutionMode.TUI,
                final_status=CheckFinalStatus.COMPLIANT,
                checks=[
                    CheckStageResult(stage=CheckStageName.DATA_PURITY, status=CheckStageStatus.PASS),
                    CheckStageResult(stage=CheckStageName.INTERNAL_SOURCES_ONLY, status=CheckStageStatus.PASS),
                    CheckStageResult(stage=CheckStageName.NO_EXTERNAL_ENDPOINTS, status=CheckStageStatus.PASS),
                    CheckStageResult(stage=CheckStageName.MANIFEST_CONSISTENCY, status=CheckStageStatus.FAIL),
                ],
            )


class TestManifestItem:
    """ManifestItem — pydantic dataclass."""

    def test_minimal(self):
        from src.models.clean_release import ClassificationType, ManifestItem

        item = ManifestItem(path="/file.txt", category="data", classification=ClassificationType.ALLOWED, reason="ok")
        assert item.path == "/file.txt"
        assert item.checksum is None

    def test_with_checksum(self):
        from src.models.clean_release import ClassificationType, ManifestItem

        item = ManifestItem(path="/file.txt", category="data", classification=ClassificationType.REQUIRED_SYSTEM, reason="core", checksum="sha256:abc")
        assert item.checksum == "sha256:abc"


class TestManifestSummary:
    """ManifestSummary — pydantic dataclass."""

    def test_minimal(self):
        from src.models.clean_release import ManifestSummary

        summary = ManifestSummary(included_count=1, excluded_count=2, prohibited_detected_count=0)
        assert summary.included_count == 1
        assert summary.excluded_count == 2
# #endregion Test.Models.CleanRelease.Ext
