from datetime import datetime, timezone import json import pytest from fan_passport.match_ingestion import ( ExternalMatchSnapshot, ExternalTeamRef, InMemoryMatchSnapshotRepository, JSONFileMatchProvider, MatchIngestionEngine, MatchIngestionError, MatchStatus, ) def make_snapshot( *, home_score: int | None = None, away_score: int | None = None, status: MatchStatus = MatchStatus.SCHEDULED, ) -> ExternalMatchSnapshot: return ExternalMatchSnapshot( provider="fixture-provider", provider_match_id="match-1", competition_slug="world-cup", season_label="2026", kickoff_at=datetime(2026, 6, 11, 19, 0, tzinfo=timezone.utc), updated_at=datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc), home_team=ExternalTeamRef(provider="fixture-provider", external_id="ENG", name="England", fifa_code="ENG"), away_team=ExternalTeamRef(provider="fixture-provider", external_id="USA", name="United States", fifa_code="USA"), status=status, home_score=home_score, away_score=away_score, group_name="Group A", ) class StaticProvider: provider_name = "fixture-provider" def __init__(self, snapshots): self.snapshots = tuple(snapshots) def fetch_snapshots(self, *, since=None): return self.snapshots def test_ingestion_is_idempotent_and_updates_on_changed_score() -> None: repository = InMemoryMatchSnapshotRepository() engine = MatchIngestionEngine(repository) scheduled = make_snapshot() first_report = engine.ingest(StaticProvider([scheduled])) second_report = engine.ingest(StaticProvider([scheduled])) assert first_report.created == 1 assert second_report.unchanged == 1 assert repository.version("fixture-provider", "match-1") == 1 final = make_snapshot(home_score=2, away_score=1, status=MatchStatus.FULL_TIME) third_report = engine.ingest(StaticProvider([final])) assert third_report.updated == 1 assert repository.version("fixture-provider", "match-1") == 2 assert repository.get_snapshot("fixture-provider", "match-1").home_score == 2 def test_ingestion_rejects_invalid_partial_score_without_crashing() -> None: repository = InMemoryMatchSnapshotRepository() engine = MatchIngestionEngine(repository) report = engine.ingest(StaticProvider([make_snapshot(home_score=1, away_score=None, status=MatchStatus.LIVE)])) assert report.rejected == 1 assert report.created == 0 assert report.rejected_snapshots[0].errors == ("score must include both home_score and away_score or neither",) def test_ingestion_strict_mode_raises_on_invalid_snapshot() -> None: repository = InMemoryMatchSnapshotRepository() engine = MatchIngestionEngine(repository) with pytest.raises(MatchIngestionError, match="invalid snapshot"): engine.ingest(StaticProvider([make_snapshot(home_score=1, away_score=None, status=MatchStatus.LIVE)]), strict=True) def test_json_file_provider_normalizes_status_aliases(tmp_path) -> None: path = tmp_path / "matches.json" path.write_text( json.dumps( { "matches": [ { "provider_match_id": "match-2", "competition_slug": "world-cup", "season_label": "2026", "kickoff_at": "2026-06-12T20:00:00Z", "updated_at": "2026-06-12T22:00:00Z", "status": "FT", "home_team": {"external_id": "ARG", "name": "Argentina"}, "away_team": {"external_id": "MEX", "name": "Mexico"}, "score": {"home": 3, "away": 2}, } ] } ), encoding="utf-8", ) provider = JSONFileMatchProvider(path, provider_name="json-provider") snapshots = tuple(provider.fetch_snapshots()) assert snapshots[0].status == MatchStatus.FULL_TIME assert snapshots[0].home_score == 3 assert snapshots[0].home_team.provider == "json-provider"