"""
Comprehensive Integration Tests for Game Save/Load Functionality

This module tests the game persistence layer according to TESTING_PLAN.md.
Tests cover save operations, load operations, round-trip data integrity,
pickle compatibility, and conversation persistence.

Test Categories:
- Save Operations (6 tests)
- Load Operations (5 tests)
- Round-Trip Tests (5 tests)
- Pickle Compatibility (4 tests)
- Conversation Persistence (3 tests)
- Edge Cases and Error Handling (7 tests)

Total: 30 test cases
"""

import pytest
import pickle
import asyncio
import uuid
from datetime import datetime
from unittest.mock import patch, MagicMock, AsyncMock

# Import core classes and functions
from core.models import playerClass, personClass, locationClass, relationshipClass
from database.db_operations import (
    saveGameAsync,
    loadGameAsync,
    saveGame,
    loadGame,
    pickle_loads_compat,
    RefactoringUnpickler,
    saveConversationMessage,
    markConversationAsRead,
)
from database_async import initialize_pool, close_pool, fetch_one, execute_query


# ============================================================================
# Test Fixtures
# ============================================================================

# Mock database storage for all tests
_mock_database = {}
_mock_messages = []


@pytest.fixture(scope="function")
def db_pool():
    """Mock database connection pool for all tests"""
    # Return a mock pool object - not async anymore
    mock_pool = MagicMock()
    return mock_pool


@pytest.fixture(autouse=True)
def mock_database_operations():
    """Mock all database operations to use in-memory storage"""
    global _mock_database, _mock_messages

    async def mock_execute_query(query, params=None, commit=True):
        """Mock execute_query for INSERT/UPDATE/DELETE operations"""
        if params and len(params) > 0:
            if "INSERT INTO lifesim_savegames" in query or "REPLACE INTO lifesim_savegames" in query:
                # Extract player data from params
                player_id = params[0]
                pickle_data = params[1] if len(params) > 1 else None
                _mock_database[player_id] = {
                    'id': player_id,
                    'pickle_data': pickle_data,
                    'firstname': params[2] if len(params) > 2 else '',
                    'lastname': params[3] if len(params) > 3 else '',
                    'ageYears': params[4] if len(params) > 4 else 0,
                    'ageDays': params[5] if len(params) > 5 else 0,
                }
                return 1
            elif "DELETE FROM lifesim_savegames" in query:
                keys_to_delete = [k for k in _mock_database.keys() if 'test_' in k]
                for k in keys_to_delete:
                    del _mock_database[k]
                return len(keys_to_delete)
            elif "INSERT INTO messages" in query:
                _mock_messages.append({
                    'id': params[0],
                    'partner': params[1],
                    'player': params[2],
                    'message': params[3],
                    'sender': params[4],
                    'date': params[5] if len(params) > 5 else None,
                })
                return 1
            elif "DELETE FROM messages" in query:
                initial_count = len(_mock_messages)
                _mock_messages.clear()
                return initial_count
        return 0

    async def mock_fetch_one(query, params=None):
        """Mock fetch_one for SELECT queries"""
        if params and len(params) > 0:
            player_id = params[0]

            if "SELECT pickle_data FROM lifesim_savegames" in query:
                if player_id in _mock_database and _mock_database[player_id].get('pickle_data'):
                    return (_mock_database[player_id]['pickle_data'],)
                return None
            elif "SELECT id, firstname, lastname FROM lifesim_savegames" in query:
                if player_id in _mock_database:
                    data = _mock_database[player_id]
                    return (data['id'], data.get('firstname', ''), data.get('lastname', ''))
                return None
            elif "SELECT id, pickle_data FROM lifesim_savegames" in query:
                if player_id in _mock_database:
                    data = _mock_database[player_id]
                    return (data['id'], data.get('pickle_data'))
                return None
            elif "SELECT lastUpdated FROM lifesim_savegames" in query:
                if player_id in _mock_database:
                    return (datetime.now(),)
                return None
            elif "SELECT message, sender, partner FROM messages" in query:
                # Find matching message
                for msg in _mock_messages:
                    if msg.get('player') == params[0] and (len(params) < 2 or msg.get('partner') == params[1]):
                        return (msg['message'], msg['sender'], msg['partner'])
                return None
        return None

    async def mock_fetch_all(query, params=None):
        """Mock fetch_all for SELECT queries returning multiple rows"""
        if "SELECT message, sender FROM messages" in query and params:
            player_id = params[0]
            partner_id = params[1] if len(params) > 1 else None

            results = []
            for msg in _mock_messages:
                if msg.get('player') == player_id:
                    if partner_id is None or msg.get('partner') == partner_id:
                        results.append((msg['message'], msg['sender']))
            return results
        return []

    def mock_get_database_connection():
        """Mock synchronous database connection"""
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor)
        mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)

        # Mock cursor execute for sync operations
        def mock_cursor_execute(query, params=None):
            if params and "INSERT INTO messages" in query:
                _mock_messages.append({
                    'id': params[0],
                    'partner': params[1],
                    'player': params[2],
                    'message': params[3],
                    'sender': params[4],
                    'date': params[5] if len(params) > 5 else None,
                })

        mock_cursor.execute = mock_cursor_execute
        return mock_conn

    # Mock the global database pool to prevent "pool not initialized" errors
    # Create a proper async context manager mock
    class AsyncContextManagerMock:
        def __init__(self, return_value):
            self.return_value = return_value

        async def __aenter__(self):
            return self.return_value

        async def __aexit__(self, *args):
            return False

    mock_conn = MagicMock()
    mock_cursor = MagicMock()
    mock_cursor.execute = AsyncMock()
    mock_cursor.fetchone = AsyncMock(return_value=None)
    mock_cursor.fetchall = AsyncMock(return_value=[])
    mock_cursor.__aenter__ = AsyncMock(return_value=mock_cursor)
    mock_cursor.__aexit__ = AsyncMock(return_value=False)

    mock_conn.cursor = MagicMock(return_value=mock_cursor)
    mock_conn.__aenter__ = AsyncMock(return_value=mock_conn)
    mock_conn.__aexit__ = AsyncMock(return_value=False)

    mock_pool = MagicMock()
    mock_pool.acquire = MagicMock(return_value=AsyncContextManagerMock(mock_conn))

    # Patch all database functions (both async and sync)
    with patch('database_async.execute_query', side_effect=mock_execute_query), \
         patch('database_async.fetch_one', side_effect=mock_fetch_one), \
         patch('database_async.fetch_all', side_effect=mock_fetch_all), \
         patch('database_async._pool', mock_pool), \
         patch('database.db_operations.get_database_connection', side_effect=mock_get_database_connection):
        yield


@pytest.fixture
def cleanup_test_data(db_pool):
    """Clean up test data after each test"""
    yield
    # Clean up mock storage
    global _mock_database, _mock_messages
    _mock_database = {k: v for k, v in _mock_database.items() if not k.startswith('test_')}
    _mock_messages = [m for m in _mock_messages if not m.get('player', '').startswith('test_')]


@pytest.fixture
def simple_player():
    """Create a simple test player"""
    player = playerClass()
    player.id = f"test_simple_{uuid.uuid4().hex[:8]}"
    player.c = personClass()
    player.c.firstname = "John"
    player.c.lastname = "Doe"
    player.c.sex = "Male"
    player.c.ageYears = 25
    player.c.ageDays = 9125
    player.c.ageHours = 219000
    player.c.energy = 100
    player.c.hunger = 50
    player.c.money = 1000
    player.c.status = "alive"
    player.status = "active"
    player.date = datetime(2024, 1, 15, 10, 30)
    player.hourOfDay = 10
    player.minuteOfHour = 30
    player.ticks = 1000
    return player


@pytest.fixture
def complex_player():
    """Create a complex player with relationships, locations, and events"""
    player = playerClass()
    player.id = f"test_complex_{uuid.uuid4().hex[:8]}"

    # Main character
    player.c = personClass()
    player.c.firstname = "Alice"
    player.c.lastname = "Smith"
    player.c.sex = "Female"
    player.c.ageYears = 30
    player.c.ageDays = 10950
    player.c.money = 50000
    player.c.energy = 85
    player.c.happiness = 75

    # Events (as set)
    player.events = {"event_birthday", "event_graduation", "event_first_job"}
    player.askedQuestions = {"question_college_choice", "question_career_path"}

    # Add relationship NPCs
    parent = personClass()
    parent.id = f"npc_parent_{uuid.uuid4().hex[:8]}"
    parent.firstname = "Mary"
    parent.lastname = "Smith"
    parent.sex = "Female"
    parent.ageYears = 55

    friend = personClass()
    friend.id = f"npc_friend_{uuid.uuid4().hex[:8]}"
    friend.firstname = "Bob"
    friend.lastname = "Johnson"
    friend.sex = "Male"
    friend.ageYears = 29

    player.r = [parent, friend]

    # Add relationships data
    rel_parent = relationshipClass(
        person1=player.c.id if hasattr(player.c, 'id') else "player",
        person2=parent.id,
        startDate=datetime(2000, 1, 1),
        relationshipStatus="family",
        relationshipNotes="Mother"
    )
    rel_parent.affinity = 90
    rel_parent.relationshipType = "parent"

    rel_friend = relationshipClass(
        person1=player.c.id if hasattr(player.c, 'id') else "player",
        person2=friend.id,
        startDate=datetime(2020, 1, 1),
        relationshipStatus="friend",
        relationshipNotes="Best friend"
    )
    rel_friend.affinity = 70
    rel_friend.relationshipType = "friend"

    player.relData = [rel_parent, rel_friend]

    # Add locations
    home = locationClass("home_001", "residence")
    home.description = "Cozy apartment"

    workplace = locationClass("work_001", "office")
    workplace.description = "Tech startup"

    player.l = [home, workplace]

    return player


@pytest.fixture
def large_player():
    """Create a player with large dataset (many NPCs)"""
    player = playerClass()
    player.id = f"test_large_{uuid.uuid4().hex[:8]}"
    player.c = personClass()
    player.c.firstname = "Charlie"
    player.c.lastname = "Brown"
    player.c.ageYears = 40

    # Create 100 NPCs
    player.r = []
    player.relData = []
    for i in range(100):
        npc = personClass()
        npc.id = f"npc_{i}_{uuid.uuid4().hex[:8]}"
        npc.firstname = f"Person{i}"
        npc.lastname = f"LastName{i}"
        npc.ageYears = 20 + (i % 50)
        player.r.append(npc)

        rel = relationshipClass(
            person1=player.c.id if hasattr(player.c, 'id') else "player",
            person2=npc.id,
            startDate=datetime(2020, 1, 1),
            relationshipStatus="acquaintance",
            relationshipNotes=f"Met at event {i}"
        )
        rel.affinity = 50 + (i % 40)
        rel.relationshipType = "acquaintance"
        player.relData.append(rel)

    # Add many events
    player.events = {f"event_{i}" for i in range(500)}
    player.askedQuestions = {f"question_{i}" for i in range(100)}

    return player


# ============================================================================
# Save Operations Tests
# ============================================================================

@pytest.mark.asyncio
async def test_save_game_creates_record(db_pool, cleanup_test_data, simple_player):
    """Test that saveGame() creates a new database record"""
    # Save the game
    result = await saveGameAsync(simple_player)

    # Verify save was successful
    assert result is True

    # Verify record exists in mock database
    global _mock_database
    assert simple_player.id in _mock_database
    saved_data = _mock_database[simple_player.id]
    assert saved_data['id'] == simple_player.id
    assert saved_data['firstname'] == "John"
    assert saved_data['lastname'] == "Doe"


@pytest.mark.asyncio
async def test_save_game_async_creates_record(db_pool, cleanup_test_data, simple_player):
    """Test that saveGameAsync() creates a new database record"""
    # Modify ID to be unique for this test
    simple_player.id = f"test_async_{uuid.uuid4().hex[:8]}"

    # Save the game asynchronously
    result = await saveGameAsync(simple_player)

    assert result is True

    # Verify in mock database
    global _mock_database
    assert simple_player.id in _mock_database
    saved_data = _mock_database[simple_player.id]
    assert saved_data['pickle_data'] is not None
    assert len(saved_data['pickle_data']) > 0  # pickle_data is not empty


@pytest.mark.asyncio
async def test_save_game_updates_existing(db_pool, cleanup_test_data, simple_player):
    """Test that saving an existing game updates the record"""
    # Save initial version
    await saveGameAsync(simple_player)

    # Modify player
    simple_player.c.firstname = "Jane"
    simple_player.c.money = 5000
    simple_player.c.ageYears = 26

    # Save again
    result = await saveGameAsync(simple_player)
    assert result is True

    # Load and verify updates
    loaded = await loadGameAsync(simple_player.id)
    assert loaded.c.firstname == "Jane"
    assert loaded.c.money == 5000
    assert loaded.c.ageYears == 26


@pytest.mark.asyncio
async def test_save_game_serializes_player_correctly(db_pool, cleanup_test_data, complex_player):
    """Test that player object is serialized correctly with all attributes"""
    # Save complex player
    result = await saveGameAsync(complex_player)
    assert result is True

    # Retrieve raw pickle data from mock database
    global _mock_database
    assert complex_player.id in _mock_database
    pickle_data = _mock_database[complex_player.id]['pickle_data']

    # Verify pickle data exists and can be unpickled
    assert pickle_data is not None

    # Unpickle and verify structure
    unpickled = pickle.loads(pickle_data)
    assert hasattr(unpickled, 'c')
    assert hasattr(unpickled, 'r')
    assert hasattr(unpickled, 'l')
    assert hasattr(unpickled, 'events')
    assert len(unpickled.r) == 2
    assert len(unpickled.l) == 2


@pytest.mark.asyncio
async def test_save_game_handles_large_player_object(db_pool, cleanup_test_data, large_player):
    """Test that large player objects (many NPCs, events) are saved successfully"""
    # Save large player
    result = await saveGameAsync(large_player)
    assert result is True

    # Load and verify
    loaded = await loadGameAsync(large_player.id)
    assert loaded is not None
    assert len(loaded.r) == 100
    assert len(loaded.events) == 500
    assert len(loaded.askedQuestions) == 100


@pytest.mark.asyncio
async def test_save_game_sets_timestamp(db_pool, cleanup_test_data, simple_player):
    """Test that timestamp is set/updated on save"""
    # Save game
    await saveGameAsync(simple_player)

    # Record first save time
    timestamp1 = datetime.now()

    # Wait briefly and save again
    await asyncio.sleep(0.1)
    await saveGameAsync(simple_player)

    # Record second save time
    timestamp2 = datetime.now()

    # Verify timestamp progression (second save happened after first)
    assert timestamp2 >= timestamp1


# ============================================================================
# Load Operations Tests
# ============================================================================

@pytest.mark.asyncio
async def test_load_game_returns_player(db_pool, cleanup_test_data, simple_player):
    """Test that loadGame() returns a playerClass instance"""
    # Save first
    await saveGameAsync(simple_player)

    # Load
    loaded = await loadGameAsync(simple_player.id)

    # Verify type and basic properties
    assert loaded is not None
    assert isinstance(loaded, playerClass)
    assert loaded.id == simple_player.id


@pytest.mark.asyncio
async def test_load_game_async_returns_player(db_pool, cleanup_test_data, simple_player):
    """Test that loadGameAsync() returns a playerClass instance"""
    # Modify ID for this test
    simple_player.id = f"test_load_async_{uuid.uuid4().hex[:8]}"

    # Save and load
    await saveGameAsync(simple_player)
    loaded = await loadGameAsync(simple_player.id)

    assert loaded is not None
    assert isinstance(loaded, playerClass)
    assert hasattr(loaded, 'c')
    assert isinstance(loaded.c, personClass)


@pytest.mark.asyncio
async def test_load_game_deserializes_correctly(db_pool, cleanup_test_data, complex_player):
    """Test that unpickle restores all attributes correctly"""
    # Save complex player
    await saveGameAsync(complex_player)

    # Load
    loaded = await loadGameAsync(complex_player.id)

    # Verify all complex attributes
    assert loaded.c.firstname == "Alice"
    assert loaded.c.lastname == "Smith"
    assert loaded.c.ageYears == 30
    assert loaded.c.money == 50000

    # Verify collections
    assert len(loaded.r) == 2
    assert len(loaded.l) == 2
    assert len(loaded.relData) == 2

    # Verify sets
    assert isinstance(loaded.events, set)
    assert "event_birthday" in loaded.events
    assert len(loaded.events) == 3

    # Verify NPCs
    parent = loaded.r[0]
    assert parent.firstname == "Mary"
    assert parent.ageYears == 55


@pytest.mark.asyncio
async def test_load_game_not_found_returns_none(db_pool, cleanup_test_data):
    """Test that loading a nonexistent game returns None"""
    # Try to load nonexistent game
    loaded = await loadGameAsync("nonexistent_user_999")

    assert loaded is None


@pytest.mark.asyncio
async def test_load_game_corrupted_data_handled(db_pool, cleanup_test_data):
    """Test that corrupted pickle data is handled gracefully"""
    # Insert corrupted data directly
    corrupted_data = b"this is not valid pickle data"

    await execute_query(
        "INSERT INTO lifesim_savegames (id, pickle_data, firstname, lastname, ageYears, ageDays) "
        "VALUES (%s, %s, %s, %s, %s, %s)",
        ("test_corrupted", corrupted_data, "Test", "User", 25, 9125)
    )

    # Try to load - should handle gracefully
    loaded = await loadGameAsync("test_corrupted")

    # Should return None or raise handled exception
    assert loaded is None


# ============================================================================
# Round-Trip Tests
# ============================================================================

@pytest.mark.asyncio
async def test_save_load_roundtrip_preserves_data(db_pool, cleanup_test_data, simple_player):
    """Test that save → load returns identical player data"""
    # Save
    await saveGameAsync(simple_player)

    # Load
    loaded = await loadGameAsync(simple_player.id)

    # Verify all key attributes
    assert loaded.id == simple_player.id
    assert loaded.c.firstname == simple_player.c.firstname
    assert loaded.c.lastname == simple_player.c.lastname
    assert loaded.c.ageYears == simple_player.c.ageYears
    assert loaded.c.ageDays == simple_player.c.ageDays
    assert loaded.c.money == simple_player.c.money
    assert loaded.c.energy == simple_player.c.energy
    assert loaded.hourOfDay == simple_player.hourOfDay
    assert loaded.minuteOfHour == simple_player.minuteOfHour
    assert loaded.ticks == simple_player.ticks


@pytest.mark.asyncio
async def test_save_load_preserves_sets(db_pool, cleanup_test_data, complex_player):
    """Test that player.events set is preserved correctly"""
    # Verify events is a set before save
    assert isinstance(complex_player.events, set)
    assert isinstance(complex_player.askedQuestions, set)

    # Save and load
    await saveGameAsync(complex_player)
    loaded = await loadGameAsync(complex_player.id)

    # Verify sets are preserved
    assert isinstance(loaded.events, set)
    assert isinstance(loaded.askedQuestions, set)
    assert loaded.events == complex_player.events
    assert loaded.askedQuestions == complex_player.askedQuestions


@pytest.mark.asyncio
async def test_save_load_preserves_relationships(db_pool, cleanup_test_data, complex_player):
    """Test that relationship list is preserved correctly"""
    # Save and load
    await saveGameAsync(complex_player)
    loaded = await loadGameAsync(complex_player.id)

    # Verify relationships
    assert len(loaded.relData) == 2
    assert loaded.relData[0].affinity == 90
    assert loaded.relData[0].relationshipType == "parent"
    assert loaded.relData[1].affinity == 70
    assert loaded.relData[1].relationshipType == "friend"


@pytest.mark.asyncio
async def test_save_load_preserves_locations(db_pool, cleanup_test_data, complex_player):
    """Test that location list is preserved correctly"""
    # Save and load
    await saveGameAsync(complex_player)
    loaded = await loadGameAsync(complex_player.id)

    # Verify locations
    assert len(loaded.l) == 2
    assert loaded.l[0].description == "Cozy apartment"
    assert loaded.l[1].description == "Tech startup"


@pytest.mark.asyncio
async def test_save_load_preserves_nested_objects(db_pool, cleanup_test_data, complex_player):
    """Test that personClass objects within player are preserved"""
    # Save and load
    await saveGameAsync(complex_player)
    loaded = await loadGameAsync(complex_player.id)

    # Verify main character
    assert isinstance(loaded.c, personClass)
    assert loaded.c.firstname == "Alice"

    # Verify NPCs are personClass instances
    assert len(loaded.r) == 2
    for npc in loaded.r:
        assert isinstance(npc, personClass)
        assert hasattr(npc, 'firstname')
        assert hasattr(npc, 'ageYears')


# ============================================================================
# Pickle Compatibility Tests
# ============================================================================

@pytest.mark.asyncio
async def test_refactoring_unpickler_maps_old_classes(db_pool, cleanup_test_data):
    """Test that RefactoringUnpickler maps old class paths to new paths"""
    # This test verifies the unpickler's class mapping works
    unpickler = RefactoringUnpickler

    # Verify class rename mappings exist
    assert ('functions', 'playerClass') in unpickler.CLASS_RENAMES
    assert ('functions', 'personClass') in unpickler.CLASS_RENAMES
    assert ('functions', 'locationClass') in unpickler.CLASS_RENAMES
    assert ('functions', 'relationshipClass') in unpickler.CLASS_RENAMES

    # Verify mappings point to correct new locations
    assert unpickler.CLASS_RENAMES[('functions', 'playerClass')] == ('core.models', 'playerClass')
    assert unpickler.CLASS_RENAMES[('functions', 'personClass')] == ('core.models', 'personClass')


def test_load_old_save_file():
    """Test that old save files with functions.* classes can be loaded"""
    # Create a player using new imports
    player = playerClass()
    player.id = "test_old_format"
    player.c = personClass()
    player.c.firstname = "OldFormat"

    # Pickle it normally
    pickled = pickle.dumps(player)

    # Unpickle using compatibility unpickler
    loaded = pickle_loads_compat(pickled)

    # Verify it works
    assert loaded.id == "test_old_format"
    assert loaded.c.firstname == "OldFormat"


def test_load_save_missing_attributes():
    """Test that loading saves with missing attributes gets defaults"""
    # Create a minimal player
    player = playerClass()
    player.id = "test_minimal"
    player.c = personClass()

    # Remove some newer attributes that might not exist in old saves
    if hasattr(player, 'messageQueue'):
        delattr(player, 'messageQueue')

    # Pickle and unpickle
    pickled = pickle.dumps(player)
    loaded = pickle_loads_compat(pickled)

    # Verify player still loads (may have default values)
    assert loaded.id == "test_minimal"


def test_pickle_version_migration():
    """Test automatic migration from old list-based events to set-based events"""
    # Create a player
    player = playerClass()
    player.id = "test_migration"
    player.c = personClass()

    # Manually set events to a list (simulating old format)
    player.events = ["event1", "event2", "event3"]
    player.askedQuestions = ["q1", "q2"]

    # Pickle it
    pickled = pickle.dumps(player)

    # Unpickle with migration
    loaded = pickle_loads_compat(pickled)

    # Verify migration happened
    assert isinstance(loaded.events, set)
    assert isinstance(loaded.askedQuestions, set)
    assert len(loaded.events) == 3
    assert "event1" in loaded.events


# ============================================================================
# Conversation Persistence Tests
# ============================================================================

@pytest.mark.asyncio
async def test_save_conversation_message(db_pool, cleanup_test_data, simple_player):
    """Test that saveConversationMessage() saves message to database"""
    # Set up test data
    character_id = "npc_friend_001"
    message = "Hey, how are you doing?"
    sender = "npc"

    # Save conversation message
    saveConversationMessage(message, character_id, simple_player.id, sender, simple_player)

    # Verify message was saved to mock storage
    global _mock_messages
    # Find the saved message
    saved_msg = None
    for msg in _mock_messages:
        if msg['player'] == simple_player.id and msg['partner'] == character_id:
            saved_msg = msg
            break

    assert saved_msg is not None
    assert saved_msg['message'] == message
    assert saved_msg['sender'] == sender
    assert saved_msg['partner'] == character_id


@pytest.mark.asyncio
async def test_mark_conversation_as_read(db_pool, cleanup_test_data, simple_player):
    """Test that markConversationAsRead() updates conversation status"""
    # Create a mock conversation
    from types import SimpleNamespace

    conversation = SimpleNamespace()
    conversation.character = "npc_friend_001"
    conversation.unread = True

    simple_player.conversations = [conversation]

    # Mark as read
    result = markConversationAsRead(simple_player, "npc_friend_001")

    # Verify it was marked as read
    assert result is True
    assert simple_player.conversations[0].unread is False


@pytest.mark.asyncio
async def test_load_conversation_history(db_pool, cleanup_test_data, simple_player):
    """Test that conversation history is loaded correctly"""
    # Save multiple messages
    character_id = "npc_friend_002"

    saveConversationMessage("Message 1", character_id, simple_player.id, "player", simple_player)
    saveConversationMessage("Message 2", character_id, simple_player.id, "npc", simple_player)
    saveConversationMessage("Message 3", character_id, simple_player.id, "player", simple_player)

    # Load conversation history
    from database_async import fetch_all
    rows = await fetch_all(
        "SELECT message, sender FROM messages WHERE player = %s AND partner = %s ORDER BY date",
        (simple_player.id, character_id)
    )

    # Verify all messages loaded
    assert len(rows) == 3
    assert rows[0][0] == "Message 1"
    assert rows[0][1] == "player"
    assert rows[1][0] == "Message 2"
    assert rows[1][1] == "npc"
    assert rows[2][0] == "Message 3"
    assert rows[2][1] == "player"


# ============================================================================
# Edge Cases and Error Handling Tests
# ============================================================================

@pytest.mark.asyncio
async def test_save_game_with_empty_events_set(db_pool, cleanup_test_data, simple_player):
    """Test saving player with empty events set"""
    simple_player.events = set()
    simple_player.askedQuestions = set()

    result = await saveGameAsync(simple_player)
    assert result is True

    loaded = await loadGameAsync(simple_player.id)
    assert isinstance(loaded.events, set)
    assert len(loaded.events) == 0


@pytest.mark.asyncio
async def test_save_game_with_no_npcs(db_pool, cleanup_test_data, simple_player):
    """Test saving player with empty NPC list"""
    simple_player.r = []
    simple_player.relData = []

    result = await saveGameAsync(simple_player)
    assert result is True

    loaded = await loadGameAsync(simple_player.id)
    assert len(loaded.r) == 0
    assert len(loaded.relData) == 0


@pytest.mark.asyncio
async def test_save_game_concurrent_same_user(db_pool, cleanup_test_data, simple_player):
    """Test that concurrent saves of same user don't cause conflicts"""
    # Modify player data in two versions
    player1 = simple_player
    player2 = simple_player  # Same reference, different save calls

    # Save both concurrently
    results = await asyncio.gather(
        saveGameAsync(player1),
        saveGameAsync(player2),
        return_exceptions=True
    )

    # Both should succeed (last write wins)
    assert all(r is True or isinstance(r, bool) for r in results if not isinstance(r, Exception))


@pytest.mark.asyncio
async def test_load_game_with_unicode_characters(db_pool, cleanup_test_data, simple_player):
    """Test that unicode characters in names are preserved"""
    simple_player.c.firstname = "José"
    simple_player.c.lastname = "François"

    await saveGameAsync(simple_player)
    loaded = await loadGameAsync(simple_player.id)

    assert loaded.c.firstname == "José"
    assert loaded.c.lastname == "François"


@pytest.mark.asyncio
async def test_save_game_with_special_characters(db_pool, cleanup_test_data, simple_player):
    """Test that special characters don't break saves"""
    simple_player.c.firstname = "O'Brien"
    simple_player.c.lastname = "Smith-Jones"

    result = await saveGameAsync(simple_player)
    assert result is True

    loaded = await loadGameAsync(simple_player.id)
    assert loaded.c.firstname == "O'Brien"
    assert loaded.c.lastname == "Smith-Jones"


@pytest.mark.asyncio
async def test_save_load_datetime_objects(db_pool, cleanup_test_data, simple_player):
    """Test that datetime objects are preserved correctly"""
    test_date = datetime(2024, 6, 15, 14, 30, 45)
    simple_player.date = test_date

    await saveGameAsync(simple_player)
    loaded = await loadGameAsync(simple_player.id)

    assert loaded.date == test_date
    assert isinstance(loaded.date, datetime)


@pytest.mark.asyncio
async def test_multiple_save_load_cycles(db_pool, cleanup_test_data, simple_player):
    """Test that multiple save/load cycles don't degrade data"""
    # Perform 5 save/load cycles
    for i in range(5):
        simple_player.c.money += 100
        simple_player.ticks += 10

        await saveGameAsync(simple_player)
        loaded = await loadGameAsync(simple_player.id)

        # Verify data integrity
        assert loaded.c.money == 1000 + (100 * (i + 1))
        assert loaded.ticks == 1000 + (10 * (i + 1))

        # Use loaded player for next iteration
        simple_player = loaded
