# #region Test.ComplianceExecution.Full [C:3] [TYPE Module] [SEMANTICS test,clean-release,compliance,execution]
# @BRIEF Full tests for ComplianceExecutionService — manifest resolution, run execution, stage persistence, violation tracking, error handling, report generation.
# @RELATION BINDS_TO -> [ComplianceExecutionService]

from __future__ import annotations

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

from src.models.clean_release import (
    CheckFinalStatus,
    CleanPolicySnapshot,
    ComplianceRun,
    ComplianceStageRun,
    ComplianceViolation,
    DistributionManifest,
    ReleaseCandidate,
    SourceRegistrySnapshot,
)
from src.services.clean_release.compliance_execution_service import (
    ComplianceExecutionResult,
    ComplianceExecutionService,
)
from src.services.clean_release.enums import CandidateStatus, ComplianceDecision, RunStatus
from src.services.clean_release.exceptions import ComplianceRunError
from src.services.clean_release.repository import CleanReleaseRepository
from src.services.clean_release.stages.base import ComplianceStage, ComplianceStageContext, StageExecutionResult
from src.services.clean_release.enums import ComplianceStageName


class _MockStage(ComplianceStage):
    """Deterministic mock stage for test isolation."""
    def __init__(self, name: str = "DATA_PURITY", decision: ComplianceDecision = ComplianceDecision.PASSED):
        self.stage_name = ComplianceStageName(name)
        self._decision = decision

    def execute(self, context: ComplianceStageContext) -> StageExecutionResult:
        return StageExecutionResult(
            decision=self._decision,
            details_json={"checked": True},
            violations=[],
        )


def _make_repo_with_candidate(
    candidate_id: str = "cand-exe-1",
    status: str = CandidateStatus.MANIFEST_BUILT.value,
    policy_id: str = "policy-exe-1",
    registry_id: str = "registry-exe-1",
    manifest_id: str = "manifest-exe-1",
) -> CleanReleaseRepository:
    repo = CleanReleaseRepository()
    repo.save_candidate(ReleaseCandidate(
        id=candidate_id, version="1.0.0", source_snapshot_ref="git:sha",
        created_by="tester", created_at=datetime.now(UTC), status=status,
    ))
    repo.save_policy(CleanPolicySnapshot(
        id=policy_id, policy_id=policy_id, policy_version="1",
        content_json={}, registry_snapshot_id=registry_id, immutable=True,
    ))
    repo.save_registry(SourceRegistrySnapshot(
        id=registry_id, registry_id=registry_id, registry_version="1",
        allowed_hosts=["internal.local"], allowed_schemes=["https"],
        immutable=True,
    ))
    repo.save_manifest(DistributionManifest(
        id=manifest_id, candidate_id=candidate_id, manifest_version=1,
        manifest_digest="digest1", artifacts_digest="digest1",
        source_snapshot_ref="git:sha",
        content_json={"summary": {"included_count": 1, "excluded_count": 0, "prohibited_detected_count": 0}},
        created_by="tester", created_at=datetime.now(UTC), immutable=True,
    ))
    return repo


class TestComplianceExecutionServiceBasic:
    """ComplianceExecutionService — basic error paths."""

    # #region test_missing_candidate_raises [C:2] [TYPE Function]
    def test_missing_candidate_raises(self):
        """Missing candidate → ComplianceRunError."""
        repo = CleanReleaseRepository()
        config = MagicMock()
        svc = ComplianceExecutionService(repository=repo, config_manager=config)
        with pytest.raises(ComplianceRunError, match="not found"):
            svc.execute_run(candidate_id="nonexistent", requested_by="tester")
    # #endregion test_missing_candidate_raises

    # #region test_resolve_manifest_by_id [C:2] [TYPE Function]
    def test_resolve_manifest_by_id(self):
        """Explicit manifest_id resolves correctly."""
        repo = _make_repo_with_candidate()
        config = MagicMock()
        svc = ComplianceExecutionService(repository=repo, config_manager=config)
        manifest = svc._resolve_manifest("cand-exe-1", "manifest-exe-1")
        assert manifest is not None
        assert manifest.id == "manifest-exe-1"
    # #endregion test_resolve_manifest_by_id

    # #region test_resolve_manifest_not_found_raises [C:2] [TYPE Function]
    def test_resolve_manifest_not_found_raises(self):
        """Nonexistent manifest_id → ComplianceRunError."""
        repo = _make_repo_with_candidate()
        config = MagicMock()
        svc = ComplianceExecutionService(repository=repo, config_manager=config)
        with pytest.raises(ComplianceRunError, match="not found"):
            svc._resolve_manifest("cand-exe-1", "nonexistent")
    # #endregion test_resolve_manifest_not_found_raises

    # #region test_resolve_manifest_not_belonging_raises [C:2] [TYPE Function]
    def test_resolve_manifest_not_belonging_raises(self):
        """Manifest not belonging to candidate → ComplianceRunError."""
        repo = _make_repo_with_candidate()
        repo.save_manifest(DistributionManifest(
            id="manifest-other", candidate_id="cand-other", manifest_version=1,
            manifest_digest="d", artifacts_digest="d",
            source_snapshot_ref="ref", content_json={},
            created_by="tester", created_at=datetime.now(UTC), immutable=True,
        ))
        config = MagicMock()
        svc = ComplianceExecutionService(repository=repo, config_manager=config)
        with pytest.raises(ComplianceRunError, match="does not belong"):
            svc._resolve_manifest("cand-exe-1", "manifest-other")
    # #endregion test_resolve_manifest_not_belonging_raises

    # #region test_resolve_manifest_no_manifests_raises [C:2] [TYPE Function]
    def test_resolve_manifest_no_manifests_raises(self):
        """Candidate has no manifests → ComplianceRunError."""
        repo = _make_repo_with_candidate()
        repo.manifests.clear()
        config = MagicMock()
        svc = ComplianceExecutionService(repository=repo, config_manager=config)
        with pytest.raises(ComplianceRunError, match="no manifest"):
            svc._resolve_manifest("cand-exe-1", None)
    # #endregion test_resolve_manifest_no_manifests_raises

    # #region test_persist_stage_run [C:2] [TYPE Function]
    def test_persist_stage_run(self):
        """Stage run is persisted via repository."""
        repo = _make_repo_with_candidate()
        config = MagicMock()
        svc = ComplianceExecutionService(repository=repo, config_manager=config)
        stage_run = ComplianceStageRun(
            id="stg-test-1", run_id="run-test-1", stage_name="DATA_PURITY",
            status="SUCCEEDED", decision="PASSED", details_json={},
        )
        svc._persist_stage_run(stage_run)
        assert repo.stage_runs.get("stg-test-1") is not None
    # #endregion test_persist_stage_run

    # #region test_persist_violations [C:2] [TYPE Function]
    def test_persist_violations(self):
        """Violations are persisted via repository."""
        repo = _make_repo_with_candidate()
        config = MagicMock()
        svc = ComplianceExecutionService(repository=repo, config_manager=config)
        violations = [
            ComplianceViolation(
                id="viol-1", run_id="run-1", stage_name="DATA_PURITY",
                code="EXT", severity="MAJOR", message="test",
            )
        ]
        svc._persist_violations(violations)
        assert repo.violations.get("viol-1") is not None
    # #endregion test_persist_violations


class TestComplianceExecutionServiceRun:
    """ComplianceExecutionService.execute_run — full execution flow."""

    # #region test_execute_run_success [C:2] [TYPE Function]
    @patch("src.services.clean_release.compliance_execution_service.resolve_trusted_policy_snapshots")
    def test_execute_run_success(self, mock_resolve):
        """Full successful run returns ComplianceExecutionResult."""
        repo = _make_repo_with_candidate()
        mock_resolve.return_value = (
            repo.policies["policy-exe-1"],
            repo.registries["registry-exe-1"],
        )
        config = MagicMock()
        config.get_config.return_value.settings.clean_release.active_policy_id = "policy-exe-1"
        config.get_config.return_value.settings.clean_release.active_registry_id = "registry-exe-1"

        stage = _MockStage(name="DATA_PURITY")
        svc = ComplianceExecutionService(repository=repo, config_manager=config, stages=[stage])
        result = svc.execute_run(candidate_id="cand-exe-1", requested_by="tester")

        assert isinstance(result, ComplianceExecutionResult)
        assert result.run is not None
        assert result.run.candidate_id == "cand-exe-1"
        assert result.run.manifest_id == "manifest-exe-1"
        assert len(result.stage_runs) == 1
        assert result.stage_runs[0].stage_name == "DATA_PURITY"
        assert result.report is not None
    # #endregion test_execute_run_success

    # #region test_execute_run_stage_failure [C:2] [TYPE Function]
    @patch("src.services.clean_release.compliance_execution_service.resolve_trusted_policy_snapshots")
    def test_execute_run_stage_failure(self, mock_resolve):
        """Stage execution error → run marked FAILED."""
        repo = _make_repo_with_candidate()
        mock_resolve.return_value = (
            repo.policies["policy-exe-1"],
            repo.registries["registry-exe-1"],
        )
        config = MagicMock()
        config.get_config.return_value.settings.clean_release.active_policy_id = "policy-exe-1"
        config.get_config.return_value.settings.clean_release.active_registry_id = "registry-exe-1"

        failing_stage = _MockStage(name="DATA_PURITY", decision=ComplianceDecision.ERROR)
        svc = ComplianceExecutionService(repository=repo, config_manager=config, stages=[failing_stage])
        result = svc.execute_run(candidate_id="cand-exe-1", requested_by="tester")

        assert result.run.status == RunStatus.SUCCEEDED.value
        assert result.run.final_status == CheckFinalStatus.FAILED.value
    # #endregion test_execute_run_stage_failure

    # #region test_execute_run_policy_resolution_error [C:2] [TYPE Function]
    @patch("src.services.clean_release.compliance_execution_service.resolve_trusted_policy_snapshots")
    def test_execute_run_policy_resolution_error(self, mock_resolve):
        """Policy resolution failure → ComplianceRunError."""
        from src.services.clean_release.exceptions import PolicyResolutionError
        repo = _make_repo_with_candidate()
        mock_resolve.side_effect = PolicyResolutionError("policy missing")
        config = MagicMock()
        svc = ComplianceExecutionService(repository=repo, config_manager=config)
        with pytest.raises(ComplianceRunError, match="policy missing"):
            svc.execute_run(candidate_id="cand-exe-1", requested_by="tester")
    # #endregion test_execute_run_policy_resolution_error

    # #region test_execute_result_dataclass [C:2] [TYPE Function]
    def test_execute_result_dataclass(self):
        """ComplianceExecutionResult dataclass works as expected."""
        run = MagicMock(spec=ComplianceRun)
        stage_runs = [MagicMock(spec=ComplianceStageRun)]
        violations = [MagicMock(spec=ComplianceViolation)]
        result = ComplianceExecutionResult(
            run=run, report=None, stage_runs=stage_runs, violations=violations,
        )
        assert result.run == run
        assert result.report is None
        assert result.stage_runs == stage_runs
        assert result.violations == violations
    # #endregion test_execute_result_dataclass

    # #region test_execute_run_stage_raises_exception [C:2] [TYPE Function]
    @patch("src.services.clean_release.compliance_execution_service.resolve_trusted_policy_snapshots")
    def test_execute_run_stage_raises_exception(self, mock_resolve):
        """Stage that raises an exception triggers the except handler (lines 211-217)."""
        repo = _make_repo_with_candidate()
        mock_resolve.return_value = (
            repo.policies["policy-exe-1"],
            repo.registries["registry-exe-1"],
        )
        config = MagicMock()
        config.get_config.return_value.settings.clean_release.active_policy_id = "policy-exe-1"
        config.get_config.return_value.settings.clean_release.active_registry_id = "registry-exe-1"

        class CrashingStage(ComplianceStage):
            stage_name = ComplianceStageName("DATA_PURITY")
            def execute(self, context):
                raise RuntimeError("stage crashed unexpectedly")

        svc = ComplianceExecutionService(repository=repo, config_manager=config, stages=[CrashingStage()])
        result = svc.execute_run(candidate_id="cand-exe-1", requested_by="tester")

        assert result.run.status == RunStatus.FAILED.value
        assert result.run.final_status == ComplianceDecision.ERROR.value
        assert "stage crashed" in result.run.failure_reason
    # #endregion test_execute_run_stage_raises_exception
