"""Tests for the case data schema.""" import json from pathlib import Path import pytest from pydantic import ValidationError from turnabout.core.schema import ( CaseData, Character, Contradiction, CourtProceedings, CourtRound, DialogueLine, DialogueTree, EvidenceAcquisition, EvidenceItem, EvidencePresentResponse, Examinable, InvestigationGate, Location, Statement, Testimony, WinCondition, ) CASES_DIR = Path(__file__).parent.parent / "turnabout" / "cases" def _make_minimal_case(**overrides) -> dict: base = { "id": "test_001", "title": "Test Case", "difficulty": "easy", "description": "A test case.", "characters": [ {"id": "defendant", "name": "D", "description": "d", "role": "defendant"}, {"id": "witness1", "name": "W", "description": "w", "role": "witness"}, ], "evidence": [ { "id": "ev1", "name": "E1", "item_type": "document", "description": "desc", "detail": "detail", "source": "pre_given", } ], "court": { "penalty_limit": 5, "rounds": [ { "witness_id": "witness1", "order": 0, "initial_testimony_id": "t1", "testimonies": [ { "id": "t1", "title": "Testimony", "witness_id": "witness1", "preamble": "The witness testifies.", "statements": [ { "id": "s1", "text": "I saw something.", "press_response": "Well...", "contradiction": { "evidence_id": "ev1", "explanation": "This contradicts!", "is_primary": True, }, } ], } ], } ], "win_condition": {"required_contradiction_ids": ["s1"]}, }, } base.update(overrides) return base class TestMinimalCase: def test_parse_minimal(self): case = CaseData(**_make_minimal_case()) assert case.id == "test_001" assert case.difficulty == "easy" assert len(case.characters) == 2 assert len(case.evidence) == 1 assert case.court.penalty_limit == 5 def test_get_character(self): case = CaseData(**_make_minimal_case()) assert case.get_character("defendant") is not None assert case.get_character("nonexistent") is None def test_get_evidence(self): case = CaseData(**_make_minimal_case()) assert case.get_evidence("ev1") is not None assert case.get_evidence("nonexistent") is None def test_get_testimony(self): case = CaseData(**_make_minimal_case()) assert case.get_testimony("t1") is not None assert case.get_testimony("nonexistent") is None def test_get_statement(self): case = CaseData(**_make_minimal_case()) s = case.get_statement("s1") assert s is not None assert s.contradiction is not None assert s.contradiction.evidence_id == "ev1" def test_pre_given_evidence(self): case = CaseData(**_make_minimal_case()) pre = case.get_pre_given_evidence() assert len(pre) == 1 assert pre[0].id == "ev1" def test_required_contradictions(self): case = CaseData(**_make_minimal_case()) stmts = case.get_required_contradiction_statements() assert len(stmts) == 1 assert stmts[0].id == "s1" class TestValidation: def test_invalid_difficulty(self): with pytest.raises(ValidationError): CaseData(**_make_minimal_case(difficulty="medium")) def test_investigation_evidence_requires_acquisition(self): data = _make_minimal_case() data["evidence"].append( { "id": "ev2", "name": "E2", "item_type": "physical", "description": "d", "detail": "d", "source": "investigation", "acquisition": None, } ) with pytest.raises(ValidationError, match="investigation evidence must have acquisition"): CaseData(**data) def test_investigation_evidence_with_acquisition_ok(self): data = _make_minimal_case() data["locations"] = [ { "id": "loc1", "name": "L1", "description": "d", "examinables": [{"id": "obj1", "name": "O1", "description": "d"}], } ] data["evidence"].append( { "id": "ev2", "name": "E2", "item_type": "physical", "description": "d", "detail": "d", "source": "investigation", "acquisition": { "method": "examine", "location_id": "loc1", "target_id": "obj1", }, } ) case = CaseData(**data) assert len(case.evidence) == 2 class TestStolenPrototype: @pytest.fixture def case(self): path = CASES_DIR / "stolen_prototype.json" if not path.exists(): pytest.skip("stolen_prototype.json not yet created") with open(path) as f: data = json.load(f) return CaseData(**data) def test_loads(self, case): assert case.id == "stolen_prototype" assert case.title == "The Stolen Prototype" def test_characters(self, case): assert len(case.characters) == 5 roles = {c.role for c in case.characters} assert "defendant" in roles assert "witness" in roles def test_evidence_count(self, case): assert len(case.evidence) == 7 pre_given = case.get_pre_given_evidence() assert len(pre_given) == 2 investigation = [e for e in case.evidence if e.source == "investigation"] assert len(investigation) == 5 def test_locations(self, case): assert len(case.locations) == 5 hale_office = case.get_location("hale_office") assert hale_office is not None assert not hale_office.available_from_start assert "hale_suspicion" in hale_office.unlock_prerequisites def test_court_structure(self, case): assert case.court.penalty_limit == 5 assert len(case.court.rounds) == 1 rnd = case.court.rounds[0] assert rnd.witness_id == "morrison" assert len(rnd.testimonies) == 2 def test_win_condition(self, case): wc = case.court.win_condition assert len(wc.required_contradiction_ids) == 3 assert "patrol_1" in wc.required_contradiction_ids assert "revised_2b" in wc.required_contradiction_ids assert "revised_3" in wc.required_contradiction_ids def test_investigation_gate(self, case): gate = case.investigation_gate assert gate is not None assert len(gate.required_evidence) == 4 assert "cloning_device" in gate.required_evidence def test_all_contradictions_reference_valid_evidence(self, case): ev_ids = case.get_all_evidence_ids() for rnd in case.court.rounds: for t in rnd.testimonies: for s in t.statements: if s.contradiction: assert s.contradiction.evidence_id in ev_ids, ( f"Statement {s.id} contradiction references unknown evidence " f"{s.contradiction.evidence_id}" )