# #region Test.CleanRelease.Stages [C:3] [TYPE Module] [SEMANTICS test,clean-release,stage,compliance,pipeline]
# @BRIEF Tests for compliance stage pipeline — build_default_stages, stage_result_map, missing_mandatory_stages, derive_final_status, and all individual stages.
# @RELATION BINDS_TO -> [ComplianceStages]
# @TEST_EDGE: all_pass -> COMPLIANT
# @TEST_EDGE: mandatory_fail -> BLOCKED
# @TEST_EDGE: missing_mandatory -> FAILED
# @TEST_EDGE: skipped_stage -> FAILED

from datetime import UTC, datetime
from unittest.mock import MagicMock
import pytest

from src.models.clean_release import (
    CheckFinalStatus,
    CheckStageName,
    CheckStageResult,
    CheckStageStatus,
    CleanPolicySnapshot,
    ComplianceStageRun,
    DistributionManifest,
    ReleaseCandidate,
    SourceRegistrySnapshot,
)
from src.services.clean_release.compliance_execution_service import ComplianceExecutionService
from src.services.clean_release.enums import ComplianceDecision, ComplianceStageName, RunStatus
from src.services.clean_release.repository import CleanReleaseRepository
from src.services.clean_release.stages import (
    MANDATORY_STAGE_ORDER,
    build_default_stages,
    derive_final_status,
    missing_mandatory_stages,
    stage_result_map,
)
from src.services.clean_release.stages.base import (
    ComplianceStage,
    ComplianceStageContext,
    StageExecutionResult,
    build_stage_run_record,
    build_violation,
)
from src.services.clean_release.stages.data_purity import DataPurityStage
from src.services.clean_release.stages.internal_sources_only import InternalSourcesOnlyStage
from src.services.clean_release.stages.no_external_endpoints import NoExternalEndpointsStage
from src.services.clean_release.stages.manifest_consistency import ManifestConsistencyStage
from src.services.clean_release.enums import ViolationSeverity


def _make_context() -> ComplianceStageContext:
    """Create a minimal context for stage testing."""
    registry = SourceRegistrySnapshot(
        id="reg-1", registry_id="reg-1", registry_version="1",
        allowed_hosts=["internal.local"], allowed_schemes=["https"],
        allowed_source_types=["repo"], immutable=True,
    )
    policy = CleanPolicySnapshot(
        id="policy-1", policy_id="policy-1", policy_version="1",
        content_json={
            "profile": "enterprise-clean",
            "prohibited_artifact_categories": ["malware"],
            "required_system_categories": ["system-lib"],
            "external_source_forbidden": True,
        },
        registry_snapshot_id="reg-1", immutable=True,
    )
    manifest = DistributionManifest(
        id="man-1", candidate_id="cand-1", manifest_version=1,
        manifest_digest="d1", artifacts_digest="d1",
        source_snapshot_ref="ref",
        content_json={
            "summary": {"included_count": 1, "excluded_count": 0, "prohibited_detected_count": 0},
            "items": [],
        },
        created_by="tester", created_at=datetime.now(UTC), immutable=True,
    )
    candidate = ReleaseCandidate(
        id="cand-1", version="1.0.0", source_snapshot_ref="ref",
        created_by="tester", created_at=datetime.now(UTC), status="PREPARED",
    )
    run = MagicMock()
    run.id = "run-1"
    run.candidate_id = "cand-1"
    run.manifest_digest = "d1"
    return ComplianceStageContext(
        run=run, candidate=candidate, manifest=manifest,
        policy=policy, registry=registry,
    )


# === build_default_stages ===

class TestBuildDefaultStages:
    """build_default_stages — returns stages in mandatory order."""

    # #region test_default_stages_order [C:2] [TYPE Function]
    def test_default_stages_order(self):
        """Default stages match MANDATORY_STAGE_ORDER."""
        stages = build_default_stages()
        assert len(stages) == 4
        for i, stage in enumerate(stages):
            assert stage.stage_name == MANDATORY_STAGE_ORDER[i]
    # #endregion test_default_stages_order

    # #region test_default_stages_are_instances [C:2] [TYPE Function]
    def test_default_stages_are_instances(self):
        """Default stages are proper ComplianceStage instances."""
        stages = build_default_stages()
        assert isinstance(stages[0], DataPurityStage)
        assert isinstance(stages[1], InternalSourcesOnlyStage)
        assert isinstance(stages[2], NoExternalEndpointsStage)
        assert isinstance(stages[3], ManifestConsistencyStage)
    # #endregion test_default_stages_are_instances


# === stage_result_map ===

class TestStageResultMap:
    """stage_result_map — convert stage results to status map."""

    # #region test_map_empty [C:2] [TYPE Function]
    def test_map_empty(self):
        """Empty results → empty map."""
        assert stage_result_map([]) == {}
    # #endregion test_map_empty

    # #region test_map_clean_release_stage_run [C:2] [TYPE Function]
    def test_map_clean_release_stage_run(self):
        """ComplianceStageRun with PASSED decision → PASS."""
        run = ComplianceStageRun(
            id="stg-1", run_id="r1", stage_name="DATA_PURITY",
            status="SUCCEEDED", decision=ComplianceDecision.PASSED.value,
            details_json={},
        )
        result = stage_result_map([run])
        assert result[ComplianceStageName.DATA_PURITY] == CheckStageStatus.PASS
    # #endregion test_map_clean_release_stage_run

    # #region test_map_blocked_decision [C:2] [TYPE Function]
    def test_map_blocked_decision(self):
        """BLOCKED decision → FAIL."""
        run = ComplianceStageRun(
            id="stg-1", run_id="r1", stage_name="DATA_PURITY",
            status="SUCCEEDED", decision=ComplianceDecision.BLOCKED.value,
            details_json={},
        )
        result = stage_result_map([run])
        assert result[ComplianceStageName.DATA_PURITY] == CheckStageStatus.FAIL
    # #endregion test_map_blocked_decision

    # #region test_map_error_decision [C:2] [TYPE Function]
    def test_map_error_decision(self):
        """ERROR decision → SKIPPED."""
        run = ComplianceStageRun(
            id="stg-1", run_id="r1", stage_name="DATA_PURITY",
            status="FAILED", decision=ComplianceDecision.ERROR.value,
            details_json={},
        )
        result = stage_result_map([run])
        assert result[ComplianceStageName.DATA_PURITY] == CheckStageStatus.SKIPPED
    # #endregion test_map_error_decision

    # #region test_map_check_stage_result [C:2] [TYPE Function]
    def test_map_check_stage_result(self):
        """CheckStageResult mapped correctly."""
        result = stage_result_map([CheckStageResult(
            stage=CheckStageName.DATA_PURITY, status=CheckStageStatus.FAIL,
        )])
        assert ComplianceStageName.DATA_PURITY in result
    # #endregion test_map_check_stage_result

    # #region test_map_missing_stage_name [C:2] [TYPE Function]
    def test_map_missing_stage_name(self):
        """StageRun without stage_name → skipped."""
        run = MagicMock(spec=ComplianceStageRun)
        run.stage_name = None
        run.decision = ComplianceDecision.PASSED.value
        result = stage_result_map([run])
        assert result == {}
    # #endregion test_map_missing_stage_name

    # #region test_map_unknown_decision_falls_through [C:2] [TYPE Function]
    def test_map_unknown_decision_falls_through(self):
        """Custom decision that is truthy but not recognized → mapped as string (line 85).
        Uses a valid CheckStageStatus value (PASS/FAIL/SKIPPED/RUNNING) for the enum."""
        run = MagicMock(spec=ComplianceStageRun)
        run.stage_name = "DATA_PURITY"
        # "FAIL" is truthy and not one of ComplianceDecision values (PASSED/BLOCKED/ERROR)
        run.decision = "PASS"  # truthy, valid CheckStageStatus value
        run.status = "irrelevant_status"
        result = stage_result_map([run])
        assert ComplianceStageName.DATA_PURITY in result
        assert result[ComplianceStageName.DATA_PURITY] == CheckStageStatus.PASS
    # #endregion test_map_unknown_decision_falls_through

    # #region test_map_no_decision_uses_status [C:2] [TYPE Function]
    def test_map_no_decision_uses_status(self):
        """No decision, only status available → mapped from status (line 87)."""
        run = MagicMock(spec=ComplianceStageRun)
        run.stage_name = "DATA_PURITY"
        run.decision = None  # falsy
        run.status = "FAIL"  # truthy, valid CheckStageStatus value
        result = stage_result_map([run])
        assert ComplianceStageName.DATA_PURITY in result
        assert result[ComplianceStageName.DATA_PURITY] == CheckStageStatus.FAIL
    # #endregion test_map_no_decision_uses_status


# === missing_mandatory_stages ===

class TestMissingMandatoryStages:
    """missing_mandatory_stages — identify missing stages."""

    # #region test_no_missing [C:2] [TYPE Function]
    def test_no_missing(self):
        """All mandatory stages present → empty list."""
        status_map = {stage: CheckStageStatus.PASS for stage in MANDATORY_STAGE_ORDER}
        assert missing_mandatory_stages(status_map) == []
    # #endregion test_no_missing

    # #region test_one_missing [C:2] [TYPE Function]
    def test_one_missing(self):
        """One present → three missing."""
        status_map = {ComplianceStageName.DATA_PURITY: CheckStageStatus.PASS}
        missing = missing_mandatory_stages(status_map)
        assert len(missing) == 3
    # #endregion test_one_missing

    # #region test_all_missing [C:2] [TYPE Function]
    def test_all_missing(self):
        """Empty map → all mandatory stages missing."""
        missing = missing_mandatory_stages({})
        assert missing == MANDATORY_STAGE_ORDER
    # #endregion test_all_missing


# === derive_final_status ===

class TestDeriveFinalStatus:
    """derive_final_status — final status from stage results."""

    # #region test_all_pass_compliant [C:2] [TYPE Function]
    def test_all_pass_compliant(self):
        """All pass → COMPLIANT."""
        runs = [
            ComplianceStageRun(id="s1", run_id="r1", stage_name="DATA_PURITY", status="SUCCEEDED", decision=ComplianceDecision.PASSED.value, details_json={}),
            ComplianceStageRun(id="s2", run_id="r1", stage_name="INTERNAL_SOURCES_ONLY", status="SUCCEEDED", decision=ComplianceDecision.PASSED.value, details_json={}),
            ComplianceStageRun(id="s3", run_id="r1", stage_name="NO_EXTERNAL_ENDPOINTS", status="SUCCEEDED", decision=ComplianceDecision.PASSED.value, details_json={}),
            ComplianceStageRun(id="s4", run_id="r1", stage_name="MANIFEST_CONSISTENCY", status="SUCCEEDED", decision=ComplianceDecision.PASSED.value, details_json={}),
        ]
        assert derive_final_status(runs) == CheckFinalStatus.COMPLIANT
    # #endregion test_all_pass_compliant

    # #region test_blocked_on_fail [C:2] [TYPE Function]
    def test_blocked_on_fail(self):
        """One FAIL → BLOCKED."""
        runs = [
            ComplianceStageRun(id="s1", run_id="r1", stage_name="DATA_PURITY", status="SUCCEEDED", decision=ComplianceDecision.PASSED.value, details_json={}),
            ComplianceStageRun(id="s2", run_id="r1", stage_name="INTERNAL_SOURCES_ONLY", status="SUCCEEDED", decision=ComplianceDecision.PASSED.value, details_json={}),
            ComplianceStageRun(id="s3", run_id="r1", stage_name="NO_EXTERNAL_ENDPOINTS", status="SUCCEEDED", decision=ComplianceDecision.PASSED.value, details_json={}),
            ComplianceStageRun(id="s4", run_id="r1", stage_name="MANIFEST_CONSISTENCY", status="SUCCEEDED", decision=ComplianceDecision.BLOCKED.value, details_json={}),
        ]
        assert derive_final_status(runs) == CheckFinalStatus.BLOCKED
    # #endregion test_blocked_on_fail

    # #region test_missing_stage_failed [C:2] [TYPE Function]
    def test_missing_stage_failed(self):
        """Missing mandatory stage → FAILED."""
        runs = [
            ComplianceStageRun(id="s1", run_id="r1", stage_name="DATA_PURITY", status="SUCCEEDED", decision=ComplianceDecision.PASSED.value, details_json={}),
        ]
        assert derive_final_status(runs) == CheckFinalStatus.FAILED
    # #endregion test_missing_stage_failed

    # #region test_skipped_stage_failed [C:2] [TYPE Function]
    def test_skipped_stage_failed(self):
        """Skipped mandatory stage → FAILED."""
        runs = [
            ComplianceStageRun(id="s1", run_id="r1", stage_name="DATA_PURITY", status="FAILED", decision=ComplianceDecision.ERROR.value, details_json={}),
            ComplianceStageRun(id="s2", run_id="r1", stage_name="INTERNAL_SOURCES_ONLY", status="SUCCEEDED", decision=ComplianceDecision.PASSED.value, details_json={}),
            ComplianceStageRun(id="s3", run_id="r1", stage_name="NO_EXTERNAL_ENDPOINTS", status="SUCCEEDED", decision=ComplianceDecision.PASSED.value, details_json={}),
            ComplianceStageRun(id="s4", run_id="r1", stage_name="MANIFEST_CONSISTENCY", status="SUCCEEDED", decision=ComplianceDecision.PASSED.value, details_json={}),
        ]
        assert derive_final_status(runs) == CheckFinalStatus.FAILED
    # #endregion test_skipped_stage_failed

    # #region test_empty_results_failed [C:2] [TYPE Function]
    def test_empty_results_failed(self):
        """Empty results → FAILED."""
        assert derive_final_status([]) == CheckFinalStatus.FAILED
    # #endregion test_empty_results_failed


# === build_stage_run_record ===

class TestBuildStageRunRecord:
    """build_stage_run_record — create stage run record from result."""

    # #region test_build_success [C:2] [TYPE Function]
    def test_build_success(self):
        """PASSED result → SUCCEEDED status."""
        result = StageExecutionResult(decision=ComplianceDecision.PASSED, details_json={"ok": True})
        record = build_stage_run_record(run_id="run-1", stage_name=ComplianceStageName.DATA_PURITY, result=result)
        assert record.status == "SUCCEEDED"
        assert record.decision == ComplianceDecision.PASSED.value
        assert record.stage_name == "DATA_PURITY"
    # #endregion test_build_success

    # #region test_build_error [C:2] [TYPE Function]
    def test_build_error(self):
        """ERROR result → FAILED status."""
        result = StageExecutionResult(decision=ComplianceDecision.ERROR, details_json={"error": "fail"})
        record = build_stage_run_record(run_id="run-1", stage_name=ComplianceStageName.DATA_PURITY, result=result)
        assert record.status == "FAILED"
        assert record.decision == ComplianceDecision.ERROR.value
    # #endregion test_build_error

    # #region test_build_with_timestamps [C:2] [TYPE Function]
    def test_build_with_timestamps(self):
        """Custom timestamps preserved."""
        started = datetime.now(UTC)
        finished = datetime.now(UTC)
        result = StageExecutionResult(decision=ComplianceDecision.PASSED)
        record = build_stage_run_record(run_id="r1", stage_name=ComplianceStageName.DATA_PURITY, result=result, started_at=started, finished_at=finished)
        assert record.started_at == started
        assert record.finished_at == finished
    # #endregion test_build_with_timestamps


# === build_violation ===

class TestBuildViolation:
    """build_violation — create compliance violation."""

    # #region test_build_violation_basic [C:2] [TYPE Function]
    def test_build_violation_basic(self):
        """Basic violation constructed correctly."""
        viol = build_violation(
            run_id="run-1", stage_name=ComplianceStageName.DATA_PURITY,
            code="EXT001", message="External source detected",
        )
        assert viol.run_id == "run-1"
        assert viol.code == "EXT001"
        assert viol.severity == ViolationSeverity.MAJOR.value
        assert viol.evidence_json["blocked_release"] is True
    # #endregion test_build_violation_basic

    # #region test_build_violation_all_fields [C:2] [TYPE Function]
    def test_build_violation_all_fields(self):
        """All optional fields populated."""
        viol = build_violation(
            run_id="run-1", stage_name=ComplianceStageName.INTERNAL_SOURCES_ONLY,
            code="INT001", message="test", artifact_path="bin/app",
            severity=ViolationSeverity.CRITICAL, evidence_json={"detail": "info"}, blocked_release=False,
        )
        assert viol.artifact_path == "bin/app"
        assert viol.severity == ViolationSeverity.CRITICAL.value
        assert viol.evidence_json["blocked_release"] is False
    # #endregion test_build_violation_all_fields


# === Individual Stage Implementations ===

class TestDataPurityStage:
    """DataPurityStage — checks prohibited artifacts in manifest."""

    # #region test_data_purity_pass [C:2] [TYPE Function]
    def test_data_purity_pass(self):
        """No prohibited artifacts → PASSED."""
        stage = DataPurityStage()
        ctx = _make_context()
        result = stage.execute(ctx)
        assert result.decision == ComplianceDecision.PASSED
    # #endregion test_data_purity_pass

    # #region test_data_purity_fail [C:2] [TYPE Function]
    def test_data_purity_fail(self):
        """Prohibited artifacts → BLOCKED with violations."""
        ctx = _make_context()
        ctx.manifest.content_json["summary"]["prohibited_detected_count"] = 2
        stage = DataPurityStage()
        result = stage.execute(ctx)
        assert result.decision == ComplianceDecision.BLOCKED
        assert len(result.violations) == 1
    # #endregion test_data_purity_fail


class TestInternalSourcesOnlyStage:
    """InternalSourcesOnlyStage — validates internal registry consistency."""

    # #region test_internal_sources_pass [C:2] [TYPE Function]
    def test_internal_sources_pass(self):
        """No sources → PASSED."""
        stage = InternalSourcesOnlyStage()
        ctx = _make_context()
        result = stage.execute(ctx)
        assert result.decision == ComplianceDecision.PASSED
    # #endregion test_internal_sources_pass

    # #region test_internal_sources_blocked [C:2] [TYPE Function]
    def test_internal_sources_blocked(self):
        """External source → BLOCKED."""
        stage = InternalSourcesOnlyStage()
        ctx = _make_context()
        ctx.manifest.content_json["sources"] = [{"host": "external.bad.com", "path": "lib/bad.so"}]
        result = stage.execute(ctx)
        assert result.decision == ComplianceDecision.BLOCKED
        assert len(result.violations) == 1
    # #endregion test_internal_sources_blocked


class TestNoExternalEndpointsStage:
    """NoExternalEndpointsStage — checks endpoints against registry."""

    # #region test_no_external_pass [C:2] [TYPE Function]
    def test_no_external_pass(self):
        """No endpoints → PASSED."""
        stage = NoExternalEndpointsStage()
        ctx = _make_context()
        result = stage.execute(ctx)
        assert result.decision == ComplianceDecision.PASSED
    # #endregion test_no_external_pass

    # #region test_no_external_allowed_endpoint [C:2] [TYPE Function]
    def test_no_external_allowed_endpoint(self):
        """Allowed endpoint → PASSED."""
        stage = NoExternalEndpointsStage()
        ctx = _make_context()
        ctx.manifest.content_json["endpoints"] = ["https://internal.local/api"]
        result = stage.execute(ctx)
        assert result.decision == ComplianceDecision.PASSED
    # #endregion test_no_external_allowed_endpoint

    # #region test_no_external_blocked [C:2] [TYPE Function]
    def test_no_external_blocked(self):
        """External endpoint → BLOCKED."""
        stage = NoExternalEndpointsStage()
        ctx = _make_context()
        ctx.manifest.content_json["endpoints"] = ["https://external.bad.com/api"]
        result = stage.execute(ctx)
        assert result.decision == ComplianceDecision.BLOCKED
        assert len(result.violations) == 1
    # #endregion test_no_external_blocked


class TestManifestConsistencyStage:
    """ManifestConsistencyStage — validates manifest content consistency."""

    # #region test_consistency_pass [C:2] [TYPE Function]
    def test_consistency_pass(self):
        """Matching digests → PASSED."""
        stage = ManifestConsistencyStage()
        ctx = _make_context()
        result = stage.execute(ctx)
        assert result.decision == ComplianceDecision.PASSED
    # #endregion test_consistency_pass

    # #region test_consistency_mismatch [C:2] [TYPE Function]
    def test_consistency_mismatch(self):
        """Digest mismatch → ERROR."""
        stage = ManifestConsistencyStage()
        ctx = _make_context()
        ctx.run.manifest_digest = "wrong-digest"
        result = stage.execute(ctx)
        assert result.decision == ComplianceDecision.ERROR
        assert len(result.violations) == 1
    # #endregion test_consistency_mismatch


# === ComplianceStageContext ===

class TestComplianceStageContext:
    """ComplianceStageContext — frozen dataclass."""

    # #region test_context_immutable [C:2] [TYPE Function]
    def test_context_immutable(self):
        """Context is frozen (immutable)."""
        ctx = _make_context()
        with pytest.raises(AttributeError):
            ctx.candidate = None  # type: ignore
    # #endregion test_context_immutable
