from datetime import UTC, datetime from typing import Any import pytest from ai_platform.lead_scoring.application.admin import ( AdminScoringUseCases, AIProfileVersionConflict, AIProviderUnavailable, ) from ai_platform.lead_scoring.application.repository import ( AIProfileRecord, NewAIProfile, ) class FakeScoringRepository: def __init__(self) -> None: self.profiles: dict[str, AIProfileRecord] = {} self.audit_events: list[tuple[str, str, str, datetime]] = [] async def active_profile_version(self) -> str | None: return "profile-v1" async def list_profiles(self): return list(self.profiles.values()) async def find_profile(self, version: str): return self.profiles.get(version) async def activate_profile(self, profile: NewAIProfile): for version, current in list(self.profiles.items()): self.profiles[version] = AIProfileRecord( version=current.version, model=current.model, system_prompt=current.system_prompt, timeout_seconds=current.timeout_seconds, limits=current.limits, active=False, provider=current.provider, base_url=current.base_url, ) active = AIProfileRecord( version=profile.version, model=profile.model, system_prompt=profile.system_prompt, timeout_seconds=profile.timeout_seconds, limits=profile.limits, active=True, provider=profile.provider, base_url=profile.base_url, ) self.profiles[profile.version] = active return active async def audit_profile_activation(self, actor, version, provider, occurred_at): self.audit_events.append((actor, version, provider, occurred_at)) async def list_recent_runs(self, limit: int): return [] async def find_run(self, run_id: str): return None async def find_by_request(self, *_: Any, **__: Any): return None async def save_run(self, *_: Any, **__: Any): raise NotImplementedError class Transaction: def __init__(self) -> None: self.commits = 0 self.rollbacks = 0 async def commit(self) -> None: self.commits += 1 async def rollback(self) -> None: self.rollbacks += 1 async def test_admin_profile_versions_are_immutable(): repository = FakeScoringRepository() repository.profiles["profile-v1"] = AIProfileRecord( version="profile-v1", model="model-old", system_prompt="prompt", timeout_seconds=30, limits={}, active=True, ) transaction = Transaction() use_cases = AdminScoringUseCases( repository, transaction.commit, transaction.rollback, frozenset({"aihub", "openai_compatible"}), lambda: datetime(2026, 9, 25, tzinfo=UTC), ) with pytest.raises(AIProfileVersionConflict): await use_cases.activate_profile( NewAIProfile( version="profile-v1", model="model-new", system_prompt="prompt", timeout_seconds=30, limits={}, ), actor="admin@example.invalid", ) assert transaction.commits == 0 assert transaction.rollbacks == 1 assert repository.profiles["profile-v1"].model == "model-old" async def test_admin_profile_activation_commits_through_use_case(): repository = FakeScoringRepository() transaction = Transaction() use_cases = AdminScoringUseCases( repository, transaction.commit, transaction.rollback, frozenset({"aihub", "openai_compatible"}), lambda: datetime(2026, 9, 25, tzinfo=UTC), ) requested = NewAIProfile( version="profile-v2", model="model", system_prompt="prompt", timeout_seconds=30, limits={"max_tokens": 100}, ) activated = await use_cases.activate_profile(requested, actor="admin@example.invalid") assert activated.active is True assert activated.version == requested.version assert repository.audit_events == [ ("admin@example.invalid", requested.version, "aihub", datetime(2026, 9, 25, tzinfo=UTC)) ] assert transaction.commits == 1 assert transaction.rollbacks == 0 async def test_default_openai_url_is_frozen_in_profile_version(): repository = FakeScoringRepository() transaction = Transaction() cases = AdminScoringUseCases( repository, transaction.commit, transaction.rollback, frozenset({"openai_compatible"}), lambda: datetime(2026, 9, 28, tzinfo=UTC), openai_base_url="http://127.0.0.1:8317", ) requested = NewAIProfile("v1", "model", "prompt", 30, {}, "openai_compatible") activated = await cases.activate_profile(requested, "admin@example.invalid") assert activated.base_url == "http://127.0.0.1:8317" assert repository.profiles["v1"].base_url == activated.base_url async def test_provider_change_requires_new_profile_version_and_configured_adapter(): repository = FakeScoringRepository() transaction = Transaction() use_cases = AdminScoringUseCases( repository, transaction.commit, transaction.rollback, frozenset({"aihub"}), lambda: datetime(2026, 9, 28, tzinfo=UTC), ) requested = NewAIProfile( "v1", "gemini-3.5-flash-lite", "prompt", 30, {}, "openai_compatible", "http://127.0.0.1:8317", ) with pytest.raises(AIProviderUnavailable): await use_cases.activate_profile(requested, "admin@example.invalid") assert transaction.commits == 0 assert repository.audit_events == [] use_cases = AdminScoringUseCases( repository, transaction.commit, transaction.rollback, frozenset({"aihub", "openai_compatible"}), lambda: datetime(2026, 9, 28, tzinfo=UTC), ) await use_cases.activate_profile(requested, "admin@example.invalid") with pytest.raises(AIProfileVersionConflict): await use_cases.activate_profile( NewAIProfile("v1", requested.model, requested.system_prompt, 30, {}, "aihub"), "admin@example.invalid", ) assert transaction.commits == 1 assert len(repository.audit_events) == 1 assert repository.profiles["v1"].provider == "openai_compatible"