""" Tests for the GameSession model with solo play support. Tests cover: - Solo session creation and serialization - Multiplayer session creation and serialization - ConversationEntry with timestamps - GameState with location_type - Session type detection """ import pytest from datetime import datetime from app.models.session import ( GameSession, GameState, ConversationEntry, SessionConfig, ) from app.models.enums import ( SessionStatus, SessionType, LocationType, ) class TestGameState: """Tests for GameState dataclass.""" def test_default_values(self): """Test GameState has correct defaults.""" state = GameState() assert state.current_location == "Crossroads Village" assert state.location_type == LocationType.TOWN assert state.discovered_locations == [] assert state.active_quests == [] assert state.world_events == [] def test_to_dict_serializes_location_type(self): """Test location_type is serialized as string.""" state = GameState( current_location="The Rusty Anchor", location_type=LocationType.TAVERN, ) data = state.to_dict() assert data["current_location"] == "The Rusty Anchor" assert data["location_type"] == "tavern" def test_from_dict_deserializes_location_type(self): """Test location_type is deserialized from string.""" data = { "current_location": "Dark Forest", "location_type": "wilderness", "discovered_locations": ["Town A"], "active_quests": ["quest_1"], "world_events": [], } state = GameState.from_dict(data) assert state.current_location == "Dark Forest" assert state.location_type == LocationType.WILDERNESS assert state.discovered_locations == ["Town A"] def test_roundtrip_serialization(self): """Test GameState serializes and deserializes correctly.""" state = GameState( current_location="Ancient Library", location_type=LocationType.LIBRARY, discovered_locations=["Town", "Forest"], active_quests=["quest_1", "quest_2"], world_events=[{"type": "festival"}], ) data = state.to_dict() restored = GameState.from_dict(data) assert restored.current_location == state.current_location assert restored.location_type == state.location_type assert restored.discovered_locations == state.discovered_locations assert restored.active_quests == state.active_quests class TestConversationEntry: """Tests for ConversationEntry dataclass.""" def test_auto_timestamp(self): """Test timestamp is auto-generated.""" entry = ConversationEntry( turn=1, character_id="char_123", character_name="Hero", action="I explore", dm_response="You find a chest", ) assert entry.timestamp assert entry.timestamp.endswith("Z") def test_provided_timestamp_preserved(self): """Test provided timestamp is not overwritten.""" ts = "2025-11-21T10:30:00Z" entry = ConversationEntry( turn=1, character_id="char_123", character_name="Hero", action="I explore", dm_response="You find a chest", timestamp=ts, ) assert entry.timestamp == ts def test_to_dict_with_quest_offered(self): """Test serialization includes quest_offered when present.""" entry = ConversationEntry( turn=5, character_id="char_123", character_name="Hero", action="Talk to elder", dm_response="The elder offers you a quest", quest_offered={ "quest_id": "quest_goblin_cave", "quest_name": "Clear the Goblin Cave", }, ) data = entry.to_dict() assert "quest_offered" in data assert data["quest_offered"]["quest_id"] == "quest_goblin_cave" def test_to_dict_without_quest_offered(self): """Test serialization omits quest_offered when None.""" entry = ConversationEntry( turn=1, character_id="char_123", character_name="Hero", action="I explore", dm_response="You find nothing", ) data = entry.to_dict() assert "quest_offered" not in data def test_from_dict_roundtrip(self): """Test ConversationEntry roundtrip serialization.""" entry = ConversationEntry( turn=3, character_id="char_456", character_name="Wizard", action="Cast fireball", dm_response="The spell illuminates the cave", combat_log=[{"action": "attack", "damage": 15}], quest_offered={"quest_id": "q1"}, ) data = entry.to_dict() restored = ConversationEntry.from_dict(data) assert restored.turn == entry.turn assert restored.character_id == entry.character_id assert restored.action == entry.action assert restored.dm_response == entry.dm_response assert restored.timestamp == entry.timestamp assert restored.combat_log == entry.combat_log assert restored.quest_offered == entry.quest_offered class TestGameSessionSolo: """Tests for solo GameSession functionality.""" def test_create_solo_session(self): """Test creating a solo session.""" session = GameSession( session_id="sess_123", session_type=SessionType.SOLO, solo_character_id="char_456", user_id="user_789", ) assert session.session_type == SessionType.SOLO assert session.solo_character_id == "char_456" assert session.user_id == "user_789" assert session.is_solo() is True def test_is_solo_method(self): """Test is_solo returns correct values.""" solo = GameSession( session_id="s1", session_type=SessionType.SOLO, solo_character_id="c1", ) multi = GameSession( session_id="s2", session_type=SessionType.MULTIPLAYER, party_member_ids=["c1", "c2"], ) assert solo.is_solo() is True assert multi.is_solo() is False def test_get_character_id_solo(self): """Test get_character_id returns solo_character_id for solo sessions.""" session = GameSession( session_id="sess_123", session_type=SessionType.SOLO, solo_character_id="char_456", ) assert session.get_character_id() == "char_456" def test_get_character_id_multiplayer(self): """Test get_character_id returns current turn character for multiplayer.""" session = GameSession( session_id="sess_123", session_type=SessionType.MULTIPLAYER, party_member_ids=["c1", "c2", "c3"], turn_order=["c2", "c1", "c3"], current_turn=1, ) assert session.get_character_id() == "c1" def test_to_dict_includes_new_fields(self): """Test to_dict includes session_type, solo_character_id, user_id.""" session = GameSession( session_id="sess_123", session_type=SessionType.SOLO, solo_character_id="char_456", user_id="user_789", ) data = session.to_dict() assert data["session_id"] == "sess_123" assert data["session_type"] == "solo" assert data["solo_character_id"] == "char_456" assert data["user_id"] == "user_789" def test_from_dict_solo_session(self): """Test from_dict correctly deserializes solo session.""" data = { "session_id": "sess_123", "session_type": "solo", "solo_character_id": "char_456", "user_id": "user_789", "party_member_ids": [], "turn_number": 5, "game_state": { "current_location": "Town", "location_type": "town", }, } session = GameSession.from_dict(data) assert session.session_id == "sess_123" assert session.session_type == SessionType.SOLO assert session.solo_character_id == "char_456" assert session.user_id == "user_789" assert session.turn_number == 5 assert session.game_state.location_type == LocationType.TOWN def test_roundtrip_serialization(self): """Test complete roundtrip of solo session.""" session = GameSession( session_id="sess_test", session_type=SessionType.SOLO, solo_character_id="char_hero", user_id="user_player", turn_number=10, game_state=GameState( current_location="Dark Dungeon", location_type=LocationType.DUNGEON, active_quests=["quest_1"], ), conversation_history=[ ConversationEntry( turn=1, character_id="char_hero", character_name="Hero", action="Enter dungeon", dm_response="The darkness swallows you...", ) ], ) data = session.to_dict() restored = GameSession.from_dict(data) assert restored.session_id == session.session_id assert restored.session_type == session.session_type assert restored.solo_character_id == session.solo_character_id assert restored.user_id == session.user_id assert restored.turn_number == session.turn_number assert restored.game_state.current_location == session.game_state.current_location assert restored.game_state.location_type == session.game_state.location_type assert len(restored.conversation_history) == 1 assert restored.conversation_history[0].action == "Enter dungeon" def test_repr_solo(self): """Test __repr__ for solo session.""" session = GameSession( session_id="sess_123", session_type=SessionType.SOLO, solo_character_id="char_456", turn_number=5, ) repr_str = repr(session) assert "type=solo" in repr_str assert "char=char_456" in repr_str assert "turn=5" in repr_str def test_repr_multiplayer(self): """Test __repr__ for multiplayer session.""" session = GameSession( session_id="sess_123", session_type=SessionType.MULTIPLAYER, party_member_ids=["c1", "c2", "c3"], turn_number=10, ) repr_str = repr(session) assert "type=multiplayer" in repr_str assert "party=3" in repr_str assert "turn=10" in repr_str class TestGameSessionBackwardsCompatibility: """Tests for backwards compatibility with existing sessions.""" def test_default_session_type_is_solo(self): """Test new sessions default to solo type.""" session = GameSession(session_id="test") assert session.session_type == SessionType.SOLO def test_from_dict_without_session_type(self): """Test from_dict handles missing session_type (defaults to solo).""" data = { "session_id": "old_session", "party_member_ids": ["c1"], } session = GameSession.from_dict(data) assert session.session_type == SessionType.SOLO def test_from_dict_without_location_type(self): """Test from_dict handles missing location_type in game_state.""" data = { "session_id": "old_session", "game_state": { "current_location": "Old Town", }, } session = GameSession.from_dict(data) assert session.game_state.location_type == LocationType.TOWN def test_existing_methods_still_work(self): """Test existing session methods work with new fields.""" session = GameSession( session_id="test", session_type=SessionType.SOLO, solo_character_id="char_1", ) # Test existing methods assert session.is_in_combat() is False session.update_activity() assert session.last_activity # Add conversation entry entry = ConversationEntry( turn=1, character_id="char_1", character_name="Hero", action="test", dm_response="response", ) session.add_conversation_entry(entry) assert len(session.conversation_history) == 1 class TestLocationTypeEnum: """Tests for LocationType enum.""" def test_all_location_types_defined(self): """Test all expected location types exist.""" expected = ["town", "tavern", "wilderness", "dungeon", "ruins", "library", "safe_area"] actual = [lt.value for lt in LocationType] assert sorted(actual) == sorted(expected) def test_location_type_from_string(self): """Test LocationType can be created from string.""" assert LocationType("town") == LocationType.TOWN assert LocationType("wilderness") == LocationType.WILDERNESS assert LocationType("dungeon") == LocationType.DUNGEON class TestSessionTypeEnum: """Tests for SessionType enum.""" def test_session_types_defined(self): """Test session types are defined correctly.""" assert SessionType.SOLO.value == "solo" assert SessionType.MULTIPLAYER.value == "multiplayer"