# [DEF:test_candidate_manifest_services:Module]
# @TIER: STANDARD
# @PURPOSE: Test lifecycle and manifest versioning for release candidates.
# @LAYER: Tests

import pytest
from datetime import datetime, timezone
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.core.database import Base
from src.models.clean_release import ReleaseCandidate, DistributionManifest, CandidateArtifact
from src.services.clean_release.enums import CandidateStatus
from src.services.clean_release.candidate_service import register_candidate
from src.services.clean_release.manifest_service import build_manifest_snapshot
from src.services.clean_release.repository import CleanReleaseRepository

@pytest.fixture
def db_session():
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    session = Session()
    yield session
    session.close()

def test_candidate_lifecycle_transitions(db_session):
    """
    @PURPOSE: Verify legal state transitions for ReleaseCandidate.
    """
    candidate = ReleaseCandidate(
        id="test-candidate-1",
        name="Test Candidate",
        version="1.0.0",
        source_snapshot_ref="ref-1",
        created_by="operator",
        status=CandidateStatus.DRAFT
    )
    db_session.add(candidate)
    db_session.commit()

    # Valid transition: DRAFT -> PREPARED
    candidate.transition_to(CandidateStatus.PREPARED)
    assert candidate.status == CandidateStatus.PREPARED

    # Invalid transition: PREPARED -> DRAFT (should raise IllegalTransitionError)
    from src.services.clean_release.exceptions import IllegalTransitionError
    with pytest.raises(IllegalTransitionError, match="Forbidden transition"):
        candidate.transition_to(CandidateStatus.DRAFT)

def test_manifest_versioning_and_immutability(db_session):
    """
    @PURPOSE: Verify manifest versioning and immutability invariants.
    """
    candidate_id = "test-candidate-2"

    # Create version 1
    m1 = DistributionManifest(
        id="manifest-v1",
        candidate_id=candidate_id,
        manifest_version=1,
        manifest_digest="hash1",
        artifacts_digest="hash1",
        source_snapshot_ref="ref1",
        content_json={},
        created_at=datetime.now(timezone.utc),
        created_by="operator"
    )
    db_session.add(m1)

    # Create version 2
    m2 = DistributionManifest(
        id="manifest-v2",
        candidate_id=candidate_id,
        manifest_version=2,
        manifest_digest="hash2",
        artifacts_digest="hash2",
        source_snapshot_ref="ref1",
        content_json={},
        created_at=datetime.now(timezone.utc),
        created_by="operator"
    )
    db_session.add(m2)
    db_session.commit()

    latest = db_session.query(DistributionManifest).filter_by(candidate_id=candidate_id).order_by(DistributionManifest.manifest_version.desc()).first()
    assert latest.manifest_version == 2
    assert latest.id == "manifest-v2"

    all_manifests = db_session.query(DistributionManifest).filter_by(candidate_id=candidate_id).all()
    assert len(all_manifests) == 2


def _valid_artifacts():
    return [
        {
            "id": "art-1",
            "path": "bin/app",
            "sha256": "abc123",
            "size": 42,
        }
    ]


def test_register_candidate_rejects_duplicate_candidate_id():
    repository = CleanReleaseRepository()
    register_candidate(
        repository=repository,
        candidate_id="dup-1",
        version="1.0.0",
        source_snapshot_ref="git:sha1",
        created_by="operator",
        artifacts=_valid_artifacts(),
    )

    with pytest.raises(ValueError, match="already exists"):
        register_candidate(
            repository=repository,
            candidate_id="dup-1",
            version="1.0.0",
            source_snapshot_ref="git:sha1",
            created_by="operator",
            artifacts=_valid_artifacts(),
        )


def test_register_candidate_rejects_malformed_artifact_input():
    repository = CleanReleaseRepository()
    bad_artifacts = [{"id": "art-1", "path": "bin/app", "size": 42}]  # missing sha256

    with pytest.raises(ValueError, match="missing required field 'sha256'"):
        register_candidate(
            repository=repository,
            candidate_id="bad-art-1",
            version="1.0.0",
            source_snapshot_ref="git:sha2",
            created_by="operator",
            artifacts=bad_artifacts,
        )


def test_register_candidate_rejects_empty_artifact_set():
    repository = CleanReleaseRepository()

    with pytest.raises(ValueError, match="artifacts must not be empty"):
        register_candidate(
            repository=repository,
            candidate_id="empty-art-1",
            version="1.0.0",
            source_snapshot_ref="git:sha3",
            created_by="operator",
            artifacts=[],
        )


def test_manifest_service_rebuild_creates_new_version():
    repository = CleanReleaseRepository()
    register_candidate(
        repository=repository,
        candidate_id="manifest-version-1",
        version="1.0.0",
        source_snapshot_ref="git:sha10",
        created_by="operator",
        artifacts=_valid_artifacts(),
    )

    first = build_manifest_snapshot(repository=repository, candidate_id="manifest-version-1", created_by="operator")
    second = build_manifest_snapshot(repository=repository, candidate_id="manifest-version-1", created_by="operator")

    assert first.manifest_version == 1
    assert second.manifest_version == 2
    assert first.id != second.id


def test_manifest_service_existing_manifest_cannot_be_mutated():
    repository = CleanReleaseRepository()
    register_candidate(
        repository=repository,
        candidate_id="manifest-immutable-1",
        version="1.0.0",
        source_snapshot_ref="git:sha11",
        created_by="operator",
        artifacts=_valid_artifacts(),
    )

    created = build_manifest_snapshot(repository=repository, candidate_id="manifest-immutable-1", created_by="operator")
    original_digest = created.manifest_digest

    rebuilt = build_manifest_snapshot(repository=repository, candidate_id="manifest-immutable-1", created_by="operator")
    old_manifest = repository.get_manifest(created.id)

    assert old_manifest is not None
    assert old_manifest.manifest_digest == original_digest
    assert old_manifest.id == created.id
    assert rebuilt.id != created.id


def test_manifest_service_rejects_missing_candidate():
    repository = CleanReleaseRepository()

    with pytest.raises(ValueError, match="not found"):
        build_manifest_snapshot(repository=repository, candidate_id="missing-candidate", created_by="operator")

# [/DEF:test_candidate_manifest_services:Module]
