# #region TestApprovalService [C:2] [TYPE Module]
# @RELATION  BINDS_TO -> SrcRoot
# @SEMANTICS: tests, clean-release, approval, lifecycle, gate
# @PURPOSE: Define approval gate contracts for approve/reject operations over immutable compliance evidence.
# @LAYER Tests
# @INVARIANT: Approval is allowed only for PASSED report bound to candidate; duplicate approve and foreign report must be rejected.

from __future__ import annotations

from datetime import UTC, datetime
import pytest

from src.models.clean_release import ComplianceReport, ReleaseCandidate
from src.services.clean_release.enums import ApprovalDecisionType, CandidateStatus, ComplianceDecision
from src.services.clean_release.exceptions import ApprovalGateError
from src.services.clean_release.repository import CleanReleaseRepository


# #region _seed_candidate_with_report [C:2] [TYPE Function]
# @RELATION  BINDS_TO -> TestApprovalService
# @PURPOSE: Seed candidate and report fixtures for approval gate tests.
# @PRE: candidate_id and report_id are non-empty.
# @POST: Repository contains candidate and report linked by candidate_id.
def _seed_candidate_with_report(
    *,
    candidate_id: str = "cand-approve-1",
    report_id: str = "CCR-approve-1",
    report_status: ComplianceDecision = ComplianceDecision.PASSED,
) -> tuple[CleanReleaseRepository, str, str]:
    repository = CleanReleaseRepository()
    repository.save_candidate(
        ReleaseCandidate(
            id=candidate_id,
            version="1.0.0",
            source_snapshot_ref="git:sha-approve-1",
            created_by="tester",
            created_at=datetime.now(UTC),
            status=CandidateStatus.CHECK_PASSED.value,
        )
    )
    repository.save_report(
        ComplianceReport(
            id=report_id,
            run_id="run-approve-1",
            candidate_id=candidate_id,
            final_status=report_status.value,
            summary_json={
                "operator_summary": "seed",
                "violations_count": 0,
                "blocking_violations_count": 0 if report_status == ComplianceDecision.PASSED else 1,
            },
            generated_at=datetime.now(UTC),
            immutable=True,
        )
    )
    return repository, candidate_id, report_id
# #endregion _seed_candidate_with_report


# #region test_approve_rejects_blocked_report [C:2] [TYPE Function]
# @RELATION  BINDS_TO -> TestApprovalService
# @PURPOSE: Ensure approve is rejected when latest report final status is not PASSED.
# @PRE: Candidate has BLOCKED report.
# @POST: approve_candidate raises ApprovalGateError.
def test_approve_rejects_blocked_report():
    from src.services.clean_release.approval_service import approve_candidate

    repository, candidate_id, report_id = _seed_candidate_with_report(
        report_status=ComplianceDecision.BLOCKED,
    )

    with pytest.raises(ApprovalGateError, match="PASSED"):
        approve_candidate(
            repository=repository,
            candidate_id=candidate_id,
            report_id=report_id,
            decided_by="approver",
            comment="blocked report cannot be approved",
        )
# #endregion test_approve_rejects_blocked_report


# #region test_approve_rejects_foreign_report [C:2] [TYPE Function]
# @RELATION  BINDS_TO -> TestApprovalService
# @PURPOSE: Ensure approve is rejected when report belongs to another candidate.
# @PRE: Candidate exists, report candidate_id differs.
# @POST: approve_candidate raises ApprovalGateError.
def test_approve_rejects_foreign_report():
    from src.services.clean_release.approval_service import approve_candidate

    repository, candidate_id, _ = _seed_candidate_with_report()
    foreign_report = ComplianceReport(
        id="CCR-foreign-1",
        run_id="run-foreign-1",
        candidate_id="cand-foreign-1",
        final_status=ComplianceDecision.PASSED.value,
        summary_json={"operator_summary": "foreign", "violations_count": 0, "blocking_violations_count": 0},
        generated_at=datetime.now(UTC),
        immutable=True,
    )
    repository.save_report(foreign_report)

    with pytest.raises(ApprovalGateError, match="belongs to another candidate"):
        approve_candidate(
            repository=repository,
            candidate_id=candidate_id,
            report_id=foreign_report.id,
            decided_by="approver",
            comment="foreign report",
        )
# #endregion test_approve_rejects_foreign_report


# #region test_approve_rejects_duplicate_approve [C:2] [TYPE Function]
# @RELATION  BINDS_TO -> TestApprovalService
# @PURPOSE: Ensure repeated approve decision for same candidate is blocked.
# @PRE: Candidate has already been approved once.
# @POST: Second approve_candidate call raises ApprovalGateError.
def test_approve_rejects_duplicate_approve():
    from src.services.clean_release.approval_service import approve_candidate

    repository, candidate_id, report_id = _seed_candidate_with_report()

    first = approve_candidate(
        repository=repository,
        candidate_id=candidate_id,
        report_id=report_id,
        decided_by="approver",
        comment="first approval",
    )
    assert first.decision == ApprovalDecisionType.APPROVED.value
    assert repository.get_candidate(candidate_id).status == CandidateStatus.APPROVED.value

    with pytest.raises(ApprovalGateError, match="already approved"):
        approve_candidate(
            repository=repository,
            candidate_id=candidate_id,
            report_id=report_id,
            decided_by="approver",
            comment="duplicate approval",
        )
# #endregion test_approve_rejects_duplicate_approve


# #region test_reject_persists_decision_without_promoting_candidate_state [C:2] [TYPE Function]
# @RELATION  BINDS_TO -> TestApprovalService
# @PURPOSE: Ensure reject decision is immutable and does not promote candidate to APPROVED.
# @PRE: Candidate has PASSED report and CHECK_PASSED lifecycle state.
# @POST: reject_candidate persists REJECTED decision; candidate status remains unchanged.
def test_reject_persists_decision_without_promoting_candidate_state():
    from src.services.clean_release.approval_service import reject_candidate

    repository, candidate_id, report_id = _seed_candidate_with_report()

    decision = reject_candidate(
        repository=repository,
        candidate_id=candidate_id,
        report_id=report_id,
        decided_by="approver",
        comment="manual rejection",
    )

    candidate = repository.get_candidate(candidate_id)
    assert decision.decision == ApprovalDecisionType.REJECTED.value
    assert candidate is not None
    assert candidate.status == CandidateStatus.CHECK_PASSED.value
# #endregion test_reject_persists_decision_without_promoting_candidate_state


# #region test_reject_then_publish_is_blocked [C:2] [TYPE Function]
# @RELATION  BINDS_TO -> TestApprovalService
# @PURPOSE: Ensure latest REJECTED decision blocks publication gate.
# @PRE: Candidate is rejected for passed report.
# @POST: publish_candidate raises PublicationGateError.
def test_reject_then_publish_is_blocked():
    from src.services.clean_release.approval_service import reject_candidate
    from src.services.clean_release.exceptions import PublicationGateError
    from src.services.clean_release.publication_service import publish_candidate

    repository, candidate_id, report_id = _seed_candidate_with_report()

    reject_candidate(
        repository=repository,
        candidate_id=candidate_id,
        report_id=report_id,
        decided_by="approver",
        comment="rejected before publish",
    )

    with pytest.raises(PublicationGateError, match="APPROVED"):
        publish_candidate(
            repository=repository,
            candidate_id=candidate_id,
            report_id=report_id,
            published_by="publisher",
            target_channel="stable",
            publication_ref="rel-blocked",
        )
# #endregion test_reject_then_publish_is_blocked

# #endregion TestApprovalService
