# region TestPreparationService [TYPE Module]
# @SEMANTICS: tests, clean-release, preparation, flow
# @PURPOSE: Validate release candidate preparation flow, including policy evaluation and manifest persisting.
# @LAYER Domain
# @RELATION DEPENDS_ON ->[PreparationService]
# @INVARIANT  Candidate preparation always persists manifest and candidate status deterministically.

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

from src.models.clean_release import (
    CleanProfilePolicy,
    ProfileType,
    ReleaseCandidate,
    ReleaseCandidateStatus,
    ResourceSourceEntry,
    ResourceSourceRegistry,
)
from src.services.clean_release.preparation_service import prepare_candidate


# region _mock_policy [TYPE Function]
# @RELATION  BINDS_TO -> TestPreparationService
# @PURPOSE: Build a valid clean profile policy fixture for preparation tests.
def _mock_policy() -> CleanProfilePolicy:
    return CleanProfilePolicy(
        policy_id="pol-1",
        policy_version="1.0.0",
        active=True,
        prohibited_artifact_categories=["prohibited"],
        required_system_categories=["system"],
        external_source_forbidden=True,
        internal_source_registry_ref="reg-1",
        effective_from=datetime.now(UTC),
        profile=ProfileType.ENTERPRISE_CLEAN,
    )


# endregion _mock_policy


# region _mock_registry [TYPE Function]
# @RELATION  BINDS_TO -> TestPreparationService
# @PURPOSE: Build an internal-only source registry fixture for preparation tests.
def _mock_registry() -> ResourceSourceRegistry:
    return ResourceSourceRegistry(
        registry_id="reg-1",
        name="Reg",
        entries=[
            ResourceSourceEntry(
                source_id="s1",
                host="nexus.internal",
                protocol="https",
                purpose="pkg",
                enabled=True,
            )
        ],
        updated_at=datetime.now(UTC),
        updated_by="tester",
    )


# endregion _mock_registry


# region _mock_candidate [TYPE Function]
# @RELATION  BINDS_TO -> TestPreparationService
# @PURPOSE: Build a draft release candidate fixture with provided identifier.
def _mock_candidate(candidate_id: str) -> ReleaseCandidate:
    return ReleaseCandidate(
        candidate_id=candidate_id,
        version="1.0.0",
        profile=ProfileType.ENTERPRISE_CLEAN,
        created_at=datetime.now(UTC),
        status=ReleaseCandidateStatus.DRAFT,
        created_by="tester",
        source_snapshot_ref="v1.0.0-snapshot",
    )


# endregion _mock_candidate


# region test_prepare_candidate_success [TYPE Function]
# @RELATION  BINDS_TO -> TestPreparationService
# @PURPOSE: Verify candidate transitions to PREPARED when evaluation returns no violations.
# @TEST_CONTRACT  [valid_candidate + active_policy + internal_sources + no_violations] -> [status=PREPARED, manifest_persisted, candidate_saved]
# @TEST_SCENARIO  [prepare_success] -> [prepared status and persistence side effects are produced]
# @TEST_FIXTURE  [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE  [external_fail] -> [none; dependency interactions mocked and successful]
# @TEST_INVARIANT  [prepared_flow_persists_state] -> VERIFIED_BY: [prepare_success]
def test_prepare_candidate_success():
    # Setup
    repository = MagicMock()
    candidate_id = "cand-1"
    candidate = _mock_candidate(candidate_id)
    repository.get_candidate.return_value = candidate
    repository.get_active_policy.return_value = _mock_policy()
    repository.get_registry.return_value = _mock_registry()

    artifacts = [{"path": "file1.txt", "category": "system"}]
    sources = ["nexus.internal"]

    # Execute
    with patch(
        "src.services.clean_release.preparation_service.CleanPolicyEngine"
    ) as MockEngine:
        mock_engine_instance = MockEngine.return_value
        mock_engine_instance.validate_policy.return_value.ok = True
        mock_engine_instance.evaluate_candidate.return_value = (
            [
                {
                    "path": "file1.txt",
                    "category": "system",
                    "classification": "required-system",
                    "reason": "system-core",
                }
            ],
            [],
        )

        result = prepare_candidate(
            repository, candidate_id, artifacts, sources, "operator-1"
        )

    # Verify
    assert result["status"] == ReleaseCandidateStatus.PREPARED.value
    assert candidate.status == ReleaseCandidateStatus.PREPARED
    repository.save_manifest.assert_called_once()
    repository.save_candidate.assert_called_with(candidate)


# endregion test_prepare_candidate_success


# region test_prepare_candidate_with_violations [TYPE Function]
# @RELATION  BINDS_TO -> TestPreparationService
# @PURPOSE: Verify candidate transitions to BLOCKED when evaluation returns blocking violations.
# @TEST_CONTRACT  [valid_candidate + active_policy + evaluation_with_violations] -> [status=BLOCKED, violations_exposed]
# @TEST_SCENARIO  [prepare_blocked_due_to_policy] -> [blocked status and violation list are produced]
# @TEST_FIXTURE  [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE  [external_fail] -> [none; dependency interactions mocked and successful]
# @TEST_INVARIANT  [blocked_flow_reports_violations] -> VERIFIED_BY: [prepare_blocked_due_to_policy]
def test_prepare_candidate_with_violations():
    # Setup
    repository = MagicMock()
    candidate_id = "cand-1"
    candidate = _mock_candidate(candidate_id)
    repository.get_candidate.return_value = candidate
    repository.get_active_policy.return_value = _mock_policy()
    repository.get_registry.return_value = _mock_registry()

    artifacts = [{"path": "bad.txt", "category": "prohibited"}]
    sources = []

    # Execute
    with patch(
        "src.services.clean_release.preparation_service.CleanPolicyEngine"
    ) as MockEngine:
        mock_engine_instance = MockEngine.return_value
        mock_engine_instance.validate_policy.return_value.ok = True
        mock_engine_instance.evaluate_candidate.return_value = (
            [
                {
                    "path": "bad.txt",
                    "category": "prohibited",
                    "classification": "excluded-prohibited",
                    "reason": "test-data",
                }
            ],
            [{"category": "data-purity", "blocked_release": True}],
        )

        result = prepare_candidate(
            repository, candidate_id, artifacts, sources, "operator-1"
        )

    # Verify
    assert result["status"] == ReleaseCandidateStatus.BLOCKED.value
    assert candidate.status == ReleaseCandidateStatus.BLOCKED
    assert len(result["violations"]) == 1


# endregion test_prepare_candidate_with_violations


# region test_prepare_candidate_not_found [TYPE Function]
# @RELATION  BINDS_TO -> TestPreparationService
# @PURPOSE: Verify preparation raises ValueError when candidate does not exist.
# @TEST_CONTRACT  [missing_candidate] -> [ValueError('Candidate not found')]
# @TEST_SCENARIO  [prepare_missing_candidate] -> [raises candidate not found error]
# @TEST_FIXTURE  [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE  [missing_field] -> [candidate lookup returns None]
# @TEST_INVARIANT  [missing_candidate_is_rejected] -> VERIFIED_BY: [prepare_missing_candidate]
def test_prepare_candidate_not_found():
    repository = MagicMock()
    repository.get_candidate.return_value = None

    with pytest.raises(ValueError, match="Candidate not found"):
        prepare_candidate(repository, "non-existent", [], [], "op")


# endregion test_prepare_candidate_not_found


# region test_prepare_candidate_no_active_policy [TYPE Function]
# @RELATION  BINDS_TO -> TestPreparationService
# @PURPOSE: Verify preparation raises ValueError when no active policy is available.
# @TEST_CONTRACT  [candidate_present + missing_active_policy] -> [ValueError('Active clean policy not found')]
# @TEST_SCENARIO  [prepare_missing_policy] -> [raises active policy missing error]
# @TEST_FIXTURE  [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE  [invalid_type] -> [policy dependency resolves to None]
# @TEST_INVARIANT  [active_policy_required] -> VERIFIED_BY: [prepare_missing_policy]
def test_prepare_candidate_no_active_policy():
    repository = MagicMock()
    repository.get_candidate.return_value = _mock_candidate("cand-1")
    repository.get_active_policy.return_value = None

    with pytest.raises(ValueError, match="Active clean policy not found"):
        prepare_candidate(repository, "cand-1", [], [], "op")


# endregion test_prepare_candidate_no_active_policy


# endregion TestPreparationService
