"""
Tests for Enhanced Date Activities System

Tests cover:
- Basic date execution
- Mini-game scoring (correct/wrong answers)
- Performance-based affinity gains
- Perfect score bonus (20% boost)
- Energy and diamond costs
- Premium date options
- Mini-game question flow
- Date history tracking
- Statistics
"""

import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

import pytest
from unittest.mock import Mock, patch, MagicMock
from decimal import Decimal

from dating.date_activities import (
    get_activity,
    get_all_activities,
    validate_date_prerequisites,
    calculate_affinity_gain,
    generate_date_feedback,
    execute_date,
    get_date_history,
    get_date_statistics,
    clear_activities_cache
)


@pytest.fixture(autouse=True)
def clear_cache():
    """Clear activities cache before each test."""
    clear_activities_cache()
    yield
    clear_activities_cache()


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


@pytest.fixture
def sample_activity():
    """Sample activity definition."""
    return {
        'activity_name': 'dinner',
        'base_cost': 0,
        'premium_version': False,
        'diamond_cost': 0,
        'affinity_gain_min': 3,
        'affinity_gain_max': 8,
        'energy_cost': 10,
        'minigame_questions': [
            {'text': 'Your date mentions loving concerts', 'correct': 'ask_about_music', 'wrong': 'change_subject'},
            {'text': 'Your date seems interested in your hobby', 'correct': 'share_more', 'wrong': 'talk_about_work'}
        ]
    }


@pytest.fixture
def premium_activity():
    """Sample premium activity definition."""
    return {
        'activity_name': 'luxury_restaurant',
        'base_cost': 0,
        'premium_version': True,
        'diamond_cost': 50,
        'affinity_gain_min': 15,
        'affinity_gain_max': 25,
        'energy_cost': 10,
        'minigame_questions': []
    }


class TestGetActivity:
    """Tests for get_activity function."""

    @patch('dating.date_activities.get_database_connection')
    def test_get_activity_success(self, mock_get_conn, sample_activity):
        """Test successfully retrieving an activity."""
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchone.return_value = sample_activity

        result = get_activity('dinner')

        assert result is not None
        assert result['activity_name'] == 'dinner'
        assert result['energy_cost'] == 10
        mock_cursor.execute.assert_called_once()

    @patch('dating.date_activities.get_database_connection')
    def test_get_activity_not_found(self, mock_get_conn):
        """Test retrieving non-existent activity."""
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchone.return_value = None

        result = get_activity('nonexistent')

        assert result is None

    @patch('dating.date_activities.get_database_connection')
    def test_get_activity_caching(self, mock_get_conn, sample_activity):
        """Test that activities are cached after first retrieval."""
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchone.return_value = sample_activity

        # First call
        result1 = get_activity('dinner')
        # Second call should use cache
        result2 = get_activity('dinner')

        assert result1 == result2
        # Should only call database once
        assert mock_cursor.execute.call_count == 1


class TestGetAllActivities:
    """Tests for get_all_activities function."""

    @patch('dating.date_activities.get_database_connection')
    def test_get_all_activities_with_premium(self, mock_get_conn, sample_activity, premium_activity):
        """Test getting all activities including premium."""
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchall.return_value = [sample_activity, premium_activity]

        results = get_all_activities(include_premium=True)

        assert len(results) == 2
        assert any(a['activity_name'] == 'luxury_restaurant' for a in results)

    @patch('dating.date_activities.get_database_connection')
    def test_get_all_activities_without_premium(self, mock_get_conn, sample_activity):
        """Test getting only non-premium activities."""
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchall.return_value = [sample_activity]

        results = get_all_activities(include_premium=False)

        assert len(results) == 1
        assert all(not a.get('premium_version') for a in results)


class TestValidatePrerequisites:
    """Tests for validate_date_prerequisites function."""

    @patch('dating.date_activities.get_activity')
    @patch('dating.date_activities.get_database_connection')
    def test_valid_prerequisites(self, mock_get_conn, mock_get_activity, sample_activity):
        """Test validation with all prerequisites met."""
        mock_get_activity.return_value = sample_activity
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchone.side_effect = [
            {'energy': 50, 'diamonds': 100},  # Player
            {'id': 2}  # NPC
        ]

        result = validate_date_prerequisites(1, 2, 'dinner')

        assert result['valid'] is True
        assert result['message'] == 'Prerequisites met'

    @patch('dating.date_activities.get_activity')
    def test_invalid_activity(self, mock_get_activity):
        """Test validation with non-existent activity."""
        mock_get_activity.return_value = None

        result = validate_date_prerequisites(1, 2, 'nonexistent')

        assert result['valid'] is False
        assert 'not found' in result['message']

    @patch('dating.date_activities.get_activity')
    @patch('dating.date_activities.get_database_connection')
    def test_insufficient_energy(self, mock_get_conn, mock_get_activity, sample_activity):
        """Test validation with insufficient energy."""
        mock_get_activity.return_value = sample_activity
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchone.return_value = {'energy': 5, 'diamonds': 100}

        result = validate_date_prerequisites(1, 2, 'dinner')

        assert result['valid'] is False
        assert 'Not enough energy' in result['message']

    @patch('dating.date_activities.get_activity')
    @patch('dating.date_activities.get_database_connection')
    def test_insufficient_diamonds(self, mock_get_conn, mock_get_activity, premium_activity):
        """Test validation with insufficient diamonds for premium activity."""
        mock_get_activity.return_value = premium_activity
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchone.return_value = {'energy': 50, 'diamonds': 10}

        result = validate_date_prerequisites(1, 2, 'luxury_restaurant')

        assert result['valid'] is False
        assert 'Not enough diamonds' in result['message']


class TestCalculateAffinityGain:
    """Tests for calculate_affinity_gain function."""

    def test_perfect_score(self, sample_activity):
        """Test affinity calculation with perfect score (20% bonus)."""
        minigame_results = [
            {'correct': True},
            {'correct': True},
            {'correct': True}
        ]

        affinity = calculate_affinity_gain(sample_activity, minigame_results)

        # Perfect score: max affinity (8) * 1.2 = 9.6 -> 9
        expected = int(8 * 1.2)
        assert affinity == expected
        assert affinity > sample_activity['affinity_gain_max']

    def test_75_percent_score(self, sample_activity):
        """Test affinity calculation with 75% correct."""
        minigame_results = [
            {'correct': True},
            {'correct': True},
            {'correct': True},
            {'correct': False}
        ]

        affinity = calculate_affinity_gain(sample_activity, minigame_results)

        # 75% performance: 3 + (8-3) * 0.75 = 6.75 -> 6
        expected = int(3 + (8 - 3) * 0.75)
        assert affinity == expected

    def test_50_percent_score(self, sample_activity):
        """Test affinity calculation with 50% correct."""
        minigame_results = [
            {'correct': True},
            {'correct': False}
        ]

        affinity = calculate_affinity_gain(sample_activity, minigame_results)

        # 50% performance: 3 + (8-3) * 0.5 = 5.5 -> 5
        expected = int(3 + (8 - 3) * 0.5)
        assert affinity == expected

    def test_zero_percent_score(self, sample_activity):
        """Test affinity calculation with all wrong answers."""
        minigame_results = [
            {'correct': False},
            {'correct': False}
        ]

        affinity = calculate_affinity_gain(sample_activity, minigame_results)

        # 0% performance: min affinity
        assert affinity == sample_activity['affinity_gain_min']

    def test_no_minigame_results(self, sample_activity):
        """Test affinity calculation with no mini-game (default 50%)."""
        minigame_results = []

        affinity = calculate_affinity_gain(sample_activity, minigame_results)

        # Default 50% performance
        expected = int(3 + (8 - 3) * 0.5)
        assert affinity == expected


class TestGenerateFeedback:
    """Tests for generate_date_feedback function."""

    def test_perfect_feedback(self):
        """Test feedback for perfect performance."""
        feedback = generate_date_feedback(1.0, 'dinner')

        assert 'Perfect' in feedback or 'Amazing' in feedback
        assert len(feedback) > 0

    def test_good_feedback(self):
        """Test feedback for good performance (75%)."""
        feedback = generate_date_feedback(0.75, 'dinner')

        assert 'Great' in feedback or 'Good' in feedback
        assert len(feedback) > 0

    def test_average_feedback(self):
        """Test feedback for average performance (50%)."""
        feedback = generate_date_feedback(0.5, 'dinner')

        assert 'Decent' in feedback or 'Okay' in feedback or 'Average' in feedback
        assert len(feedback) > 0

    def test_poor_feedback(self):
        """Test feedback for poor performance."""
        feedback = generate_date_feedback(0.25, 'dinner')

        assert 'Awkward' in feedback or 'Uncomfortable' in feedback
        assert len(feedback) > 0


class TestExecuteDate:
    """Tests for execute_date function."""

    @patch('dating.date_activities.validate_date_prerequisites')
    @patch('dating.date_activities.get_activity')
    @patch('dating.date_activities.transaction')
    @patch('dating.date_activities.deduct_diamonds')
    @patch('dating.date_activities.update_quest_progress')
    def test_execute_date_success(self, mock_update_quest, mock_deduct, mock_transaction,
                                   mock_get_activity, mock_validate, sample_activity):
        """Test successful date execution with basic activity."""
        mock_validate.return_value = {'valid': True, 'message': 'Prerequisites met'}
        mock_get_activity.return_value = sample_activity

        # Mock the transaction context manager
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_transaction.return_value.__enter__.return_value = (mock_conn, mock_cursor)

        minigame_results = [
            {'correct': True},
            {'correct': True}
        ]

        result = execute_date(1, 2, 'dinner', minigame_results)

        assert result['success'] is True
        assert result['affinity_gain'] > 0
        assert result['performance'] == 1.0
        assert 'feedback' in result
        assert result['energy_spent'] == 10
        assert result['diamonds_spent'] == 0

        # Verify transaction context manager was called
        mock_transaction.assert_called_once()

    @patch('dating.date_activities.validate_date_prerequisites')
    @patch('dating.date_activities.get_activity')
    @patch('dating.date_activities.transaction')
    @patch('dating.date_activities.deduct_diamonds')
    @patch('dating.date_activities.update_quest_progress')
    def test_execute_date_premium(self, mock_update_quest, mock_deduct, mock_transaction,
                                   mock_get_activity, mock_validate, premium_activity):
        """Test successful date execution with premium activity."""
        mock_validate.return_value = {'valid': True, 'message': 'Prerequisites met'}
        mock_get_activity.return_value = premium_activity
        mock_deduct.return_value = {'success': True, 'new_balance': 50}

        # Mock the transaction context manager
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_transaction.return_value.__enter__.return_value = (mock_conn, mock_cursor)

        minigame_results = [
            {'correct': True},
            {'correct': False}
        ]

        result = execute_date(1, 2, 'luxury_restaurant', minigame_results)

        assert result['success'] is True
        assert result['diamonds_spent'] == 50
        mock_deduct.assert_called_once()

    @patch('dating.date_activities.validate_date_prerequisites')
    def test_execute_date_invalid_prerequisites(self, mock_validate):
        """Test date execution with failed prerequisites."""
        mock_validate.return_value = {'valid': False, 'message': 'Not enough energy'}

        result = execute_date(1, 2, 'dinner', [])

        assert result['success'] is False
        assert 'message' in result

    @patch('dating.date_activities.validate_date_prerequisites')
    @patch('dating.date_activities.get_activity')
    @patch('dating.date_activities.deduct_diamonds')
    def test_execute_date_diamond_failure(self, mock_deduct,
                                          mock_get_activity, mock_validate, premium_activity):
        """Test date execution with diamond deduction failure."""
        mock_validate.return_value = {'valid': True, 'message': 'Prerequisites met'}
        mock_get_activity.return_value = premium_activity
        mock_deduct.return_value = {'success': False, 'message': 'Insufficient diamonds'}

        result = execute_date(1, 2, 'luxury_restaurant', [])

        assert result['success'] is False
        assert 'Insufficient diamonds' in result['message']
        # No transaction should occur if diamond deduction fails
        mock_deduct.assert_called_once()

    @patch('dating.date_activities.validate_date_prerequisites')
    @patch('dating.date_activities.get_activity')
    @patch('dating.date_activities.transaction')
    @patch('dating.date_activities.update_quest_progress')
    def test_execute_date_poor_performance(self, mock_update_quest, mock_transaction,
                                           mock_get_activity, mock_validate, sample_activity):
        """Test date execution with poor performance (all wrong answers)."""
        mock_validate.return_value = {'valid': True, 'message': 'Prerequisites met'}
        mock_get_activity.return_value = sample_activity

        # Mock the transaction context manager
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_transaction.return_value.__enter__.return_value = (mock_conn, mock_cursor)

        minigame_results = [
            {'correct': False},
            {'correct': False}
        ]

        result = execute_date(1, 2, 'dinner', minigame_results)

        assert result['success'] is True
        assert result['performance'] == 0.0
        assert result['affinity_gain'] == sample_activity['affinity_gain_min']
        assert 'Awkward' in result['feedback'] or 'Uncomfortable' in result['feedback']


class TestGetDateHistory:
    """Tests for get_date_history function."""

    @patch('dating.date_activities.get_database_connection')
    def test_get_date_history_success(self, mock_get_conn):
        """Test retrieving date history."""
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchall.return_value = [
            {
                'player_id': 1,
                'npc_id': 2,
                'activity_name': 'dinner',
                'performance_score': Decimal('0.75'),
                'affinity_gained': 6,
                'firstname': 'Jane',
                'lastname': 'Doe'
            }
        ]

        result = get_date_history(1, limit=10)

        assert len(result) == 1
        assert result[0]['activity_name'] == 'dinner'
        assert result[0]['firstname'] == 'Jane'

    @patch('dating.date_activities.get_database_connection')
    def test_get_date_history_empty(self, mock_get_conn):
        """Test retrieving date history with no dates."""
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchall.return_value = []

        result = get_date_history(1)

        assert result == []


class TestGetDateStatistics:
    """Tests for get_date_statistics function."""

    @patch('dating.date_activities.get_database_connection')
    def test_get_statistics_with_dates(self, mock_get_conn):
        """Test getting statistics with date history."""
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchone.return_value = {
            'total_dates': 5,
            'avg_performance': Decimal('0.75'),
            'total_affinity_gained': 30,
            'best_performance': Decimal('1.00'),
            'total_diamonds_spent': 50
        }

        result = get_date_statistics(1)

        assert result['total_dates'] == 5
        assert result['avg_performance'] == 0.75
        assert result['total_affinity_gained'] == 30
        assert result['best_performance'] == 1.00
        assert result['total_diamonds_spent'] == 50

    @patch('dating.date_activities.get_database_connection')
    def test_get_statistics_no_dates(self, mock_get_conn):
        """Test getting statistics with no date history."""
        mock_conn, mock_cursor = Mock(), Mock()
        mock_get_conn.return_value = mock_conn
        mock_conn.cursor.return_value = mock_cursor
        mock_cursor.fetchone.return_value = {
            'total_dates': 0,
            'avg_performance': None,
            'total_affinity_gained': None,
            'best_performance': None,
            'total_diamonds_spent': None
        }

        result = get_date_statistics(1)

        assert result['total_dates'] == 0
        assert result['avg_performance'] == 0.0
        assert result['total_affinity_gained'] == 0


class TestPerfectScoreBonus:
    """Tests specifically for the 20% perfect score bonus."""

    def test_perfect_score_bonus_applied(self, sample_activity):
        """Test that perfect score gives 20% bonus."""
        minigame_results = [{'correct': True}, {'correct': True}]

        affinity = calculate_affinity_gain(sample_activity, minigame_results)

        # Max affinity is 8, with 20% bonus = 8 * 1.2 = 9.6 -> 9
        expected = int(sample_activity['affinity_gain_max'] * 1.2)
        assert affinity == expected

    def test_imperfect_score_no_bonus(self, sample_activity):
        """Test that imperfect score doesn't get bonus."""
        minigame_results = [{'correct': True}, {'correct': False}]

        affinity = calculate_affinity_gain(sample_activity, minigame_results)

        # Should not exceed max affinity (no bonus)
        assert affinity <= sample_activity['affinity_gain_max']

    def test_perfect_score_bonus_premium(self, premium_activity):
        """Test perfect score bonus on premium activity."""
        minigame_results = [{'correct': True}]

        affinity = calculate_affinity_gain(premium_activity, minigame_results)

        # Max affinity is 25, with 20% bonus = 25 * 1.2 = 30
        expected = int(premium_activity['affinity_gain_max'] * 1.2)
        assert affinity == expected
        assert affinity > premium_activity['affinity_gain_max']


class TestEnergyCosts:
    """Tests for energy cost handling."""

    @patch('dating.date_activities.validate_date_prerequisites')
    @patch('dating.date_activities.get_activity')
    @patch('dating.date_activities.transaction')
    @patch('dating.date_activities.update_quest_progress')
    def test_energy_deduction(self, mock_update_quest, mock_transaction,
                              mock_get_activity, mock_validate, sample_activity):
        """Test that energy is correctly deducted."""
        mock_validate.return_value = {'valid': True, 'message': 'Prerequisites met'}
        mock_get_activity.return_value = sample_activity

        # Mock the transaction context manager
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_transaction.return_value.__enter__.return_value = (mock_conn, mock_cursor)

        result = execute_date(1, 2, 'dinner', [{'correct': True}])

        assert result['success'] is True
        assert result['energy_spent'] == 10

        # Verify energy update SQL was called
        calls = [str(call) for call in mock_cursor.execute.call_args_list]
        energy_update_found = any('energy - ' in str(call) for call in calls)
        assert energy_update_found


if __name__ == '__main__':
    pytest.main([__file__, '-v'])
