"""
Unit tests for achievement system (ws/retention/achievements.py).

Tests achievement unlocking, progress tracking, and rewards.
Includes 40+ achievements across multiple categories.

Run with: pytest tests/unit/test_achievements.py -v
"""

import pytest
from unittest.mock import Mock, patch, MagicMock
import sys
from pathlib import Path

# Add ws directory to path
ws_dir = Path(__file__).parent.parent.parent / 'ws'
sys.path.insert(0, str(ws_dir))

from retention.achievements import (
    ACHIEVEMENT_DEFINITIONS,
    check_achievements,
    check_and_unlock,
    get_player_achievements,
)


# ============================================================================
# FIXTURES
# ============================================================================

@pytest.fixture
def adult_player():
    """Create an adult test player for achievement tests"""
    player = Mock()
    player.userID = "test_user_achievements"
    player.id = 12345
    player.c = Mock()
    player.c.ageYears = 25
    player.c.diamonds = 50
    player.c.money = 10000
    player.c.education_records = []
    player.c.job = None
    return player


@pytest.fixture
def mock_db_connection():
    """Mock database connection and cursor"""
    mock_conn = MagicMock()
    mock_cursor = MagicMock()
    mock_conn.cursor.return_value = mock_cursor

    with patch('retention.achievements.get_database_connection', return_value=mock_conn):
        yield mock_conn, mock_cursor


# ============================================================================
# ACHIEVEMENT TESTS (6 tests)
# ============================================================================

def test_achievement_unlock_conditions(adult_player, mock_db_connection):
    """Test that achievement unlocks when conditions are met"""
    # Arrange
    mock_conn, mock_cursor = mock_db_connection

    # Player has completed high school
    adult_player.c.education_records = [
        {'level': 'high_school', 'graduated': True}
    ]

    # Mock no existing achievement
    mock_cursor.fetchone.return_value = None

    # Act - Check achievements with high school graduation event
    result = check_achievements(
        adult_player.id,
        'graduate_high_school',
        {'education_level': 'high_school'}
    )

    # Assert
    assert result is not None
    # Result should be a list of unlocked achievements
    assert isinstance(result, list)


def test_achievement_awards_reward(adult_player, mock_db_connection):
    """Test that achievement gives diamond reward when unlocked"""
    # Arrange
    mock_conn, mock_cursor = mock_db_connection
    achievement_key = 'first_job'

    # Find achievement reward
    achievement = next((a for a in ACHIEVEMENT_DEFINITIONS if a['key'] == achievement_key), None)
    assert achievement is not None
    expected_reward = achievement['reward']

    mock_cursor.fetchone.return_value = None  # Achievement not yet unlocked

    # Act
    result = check_and_unlock(adult_player.id, achievement_key, mock_cursor, mock_conn)

    # Assert
    # Result should contain unlocked achievement info
    assert result is not None


def test_achievement_progress_tracked(adult_player, mock_db_connection):
    """Test that progress toward achievement is saved"""
    # Arrange
    mock_conn, mock_cursor = mock_db_connection
    achievement_key = 'earn_1m_lifetime'

    # Mock existing progress
    mock_cursor.fetchone.return_value = {
        'achievement_key': achievement_key,
        'progress_current': 500000,
        'progress_required': 1000000,
        'unlocked': False
    }

    # Act - Check achievements with money earned event
    result = check_achievements(
        adult_player.id,
        'earn_money',
        {'amount': 100000, 'lifetime_total': 600000}
    )

    # Assert
    # Achievement system tracks progress internally
    assert result is not None
    assert isinstance(result, list)


def test_achievement_not_re_awarded(adult_player, mock_db_connection):
    """Test that achievement is only awarded once"""
    # Arrange
    mock_conn, mock_cursor = mock_db_connection
    achievement_key = 'first_job'

    # Mock achievement already unlocked
    mock_cursor.fetchone.return_value = {
        'achievement_key': achievement_key,
        'unlocked': True,
        'unlock_date': '2024-01-01'
    }

    # Act - Try to unlock again via check_and_unlock
    result = check_and_unlock(adult_player.id, achievement_key, mock_cursor, mock_conn)

    # Assert
    # Should return empty list or not unlock again
    assert result is not None or isinstance(result, list)


def test_multiple_achievements(adult_player, mock_db_connection):
    """Test that multiple achievements can be active/tracked simultaneously"""
    # Arrange
    mock_conn, mock_cursor = mock_db_connection

    # Mock multiple achievements in progress
    mock_cursor.fetchall.return_value = [
        {
            'achievement_key': 'earn_1m_lifetime',
            'progress_current': 500000,
            'progress_required': 1000000,
            'unlocked': False
        },
        {
            'achievement_key': 'popular',
            'progress_current': 5,
            'progress_required': 10,
            'unlocked': False
        },
        {
            'achievement_key': 'first_job',
            'unlocked': True,
            'unlock_date': '2024-01-01'
        }
    ]

    # Act
    achievements = get_player_achievements(adult_player.id)

    # Assert
    assert len(achievements) >= 3
    unlocked = [a for a in achievements if a.get('unlocked')]
    in_progress = [a for a in achievements if not a.get('unlocked')]
    assert len(unlocked) >= 1
    assert len(in_progress) >= 2


def test_achievement_categories(adult_player):
    """Test that different categories exist (education, job, relationships)"""
    # Arrange & Assert
    categories = set()
    for achievement in ACHIEVEMENT_DEFINITIONS:
        categories.add(achievement['category'])

    # Verify multiple categories exist
    assert 'life_milestone' in categories
    assert 'career' in categories
    assert 'relationship' in categories
    assert len(categories) >= 4  # At least 4 categories


# ============================================================================
# ACHIEVEMENT DEFINITION TESTS
# ============================================================================

def test_all_achievements_have_required_fields():
    """Test that all achievement definitions have required fields"""
    # Assert
    for achievement in ACHIEVEMENT_DEFINITIONS:
        assert 'key' in achievement, f"Achievement missing 'key': {achievement}"
        assert 'name' in achievement, f"Achievement missing 'name': {achievement}"
        assert 'desc' in achievement, f"Achievement missing 'desc': {achievement}"
        assert 'category' in achievement, f"Achievement missing 'category': {achievement}"
        assert 'reward' in achievement, f"Achievement missing 'reward': {achievement}"
        assert 'icon' in achievement, f"Achievement missing 'icon': {achievement}"
        assert 'hidden' in achievement, f"Achievement missing 'hidden': {achievement}"


def test_achievement_rewards_are_positive():
    """Test that all achievement rewards are positive values"""
    # Assert
    for achievement in ACHIEVEMENT_DEFINITIONS:
        assert achievement['reward'] > 0, f"Achievement {achievement['key']} has non-positive reward"


def test_hidden_achievements_exist():
    """Test that secret/hidden achievements exist"""
    # Assert
    hidden = [a for a in ACHIEVEMENT_DEFINITIONS if a['hidden']]
    visible = [a for a in ACHIEVEMENT_DEFINITIONS if not a['hidden']]

    assert len(hidden) > 0, "Should have at least some hidden achievements"
    assert len(visible) > 0, "Should have visible achievements"


# ============================================================================
# ACHIEVEMENT UNLOCKING LOGIC TESTS
# ============================================================================

def test_check_achievement_unlock_evaluates_conditions(adult_player, mock_db_connection):
    """Test that check achievements properly evaluates unlock conditions"""
    # Arrange
    mock_conn, mock_cursor = mock_db_connection

    # Player reaches age 50
    adult_player.c.ageYears = 50

    # Mock no existing achievement
    mock_cursor.fetchone.return_value = None

    # Act - Check with age milestone event
    result = check_achievements(
        adult_player.id,
        'age_milestone',
        {'age': 50}
    )

    # Assert
    # Should check age-based achievements
    assert result is not None
    assert isinstance(result, list)


def test_achievement_progress_auto_unlocks_at_100_percent(adult_player, mock_db_connection):
    """Test that achievement auto-unlocks when progress reaches 100%"""
    # Arrange
    mock_conn, mock_cursor = mock_db_connection
    achievement_key = 'popular'

    # Mock progress at completion point
    mock_cursor.fetchone.return_value = {
        'achievement_key': achievement_key,
        'progress_current': 10,
        'progress_required': 10,
        'unlocked': False
    }

    # Act - Check with friend milestone event
    result = check_achievements(
        adult_player.id,
        'friend_milestone',
        {'friend_count': 10}
    )

    # Assert - when progress reaches required, should trigger unlock
    assert result is not None
    assert isinstance(result, list)


# ============================================================================
# CATEGORY-SPECIFIC TESTS
# ============================================================================

def test_life_milestone_achievements():
    """Test that life milestone achievements are properly defined"""
    # Arrange
    milestones = [a for a in ACHIEVEMENT_DEFINITIONS if a['category'] == 'life_milestone']

    # Assert
    assert len(milestones) > 0
    # Check for common milestones
    milestone_keys = [m['key'] for m in milestones]
    assert any('school' in key or 'graduate' in key for key in milestone_keys)


def test_career_achievements():
    """Test that career achievements are properly defined"""
    # Arrange
    career = [a for a in ACHIEVEMENT_DEFINITIONS if a['category'] == 'career']

    # Assert
    assert len(career) > 0
    # Check for career-related achievements
    career_keys = [c['key'] for c in career]
    assert any('job' in key or 'earn' in key or 'ceo' in key for key in career_keys)


def test_relationship_achievements():
    """Test that relationship achievements are properly defined"""
    # Arrange
    relationships = [a for a in ACHIEVEMENT_DEFINITIONS if a['category'] == 'relationship']

    # Assert
    assert len(relationships) > 0
    # Check for relationship-related achievements
    rel_keys = [r['key'] for r in relationships]
    assert any('friend' in key or 'marry' in key or 'date' in key for key in rel_keys)
