"""
Unit tests for monetization systems (ws/monetization/).

Tests energy refills, time skips, idempotency, and diamond economy.

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

import pytest
from unittest.mock import Mock, patch, MagicMock, call
from datetime import datetime, timedelta
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 monetization.energy_refills import (
    purchase_energy_refill,
    check_unlimited_energy,
    REFILL_TIERS,
)
from monetization.time_skips import (
    purchase_time_skip,
    simulate_time_period,
    SKIP_TIERS,
)
from monetization.validation import (
    IdempotencyManager,
    validate_iap_receipt,
    rate_limit,
)
from monetization.diamond_economy import (
    award_diamonds,
    deduct_diamonds,
    get_diamond_balance,
    get_diamond_transaction_history,
)


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

@pytest.fixture
def adult_player():
    """Create a mock adult player with money and diamonds"""
    return {
        'id': 1,
        'diamonds': 100,
        'energy': 50,
        'max_energy': 100,
        'money': 5000,
        'date': datetime.now(),
        'hourOfDay': 12,
        'minuteOfHour': 0,
        'unlimited_energy_until': None,
        'dead': False,
        'occupation': 'Software Engineer',
        'occupationSalary': 8000,
        'educationLevel': 'College',
        'age': 30,
        'health': 85,
        'happiness': 70,
        'relationships': []
    }


@pytest.fixture
def mock_db_cursor():
    """Create a mock database cursor"""
    cursor = Mock()
    cursor.fetchone = Mock()
    cursor.fetchall = Mock()
    cursor.execute = Mock()
    cursor.close = Mock()
    cursor.rowcount = 1
    return cursor


@pytest.fixture
def mock_db_connection(mock_db_cursor):
    """Create a mock database connection"""
    conn = Mock()
    conn.cursor = Mock(return_value=mock_db_cursor)
    conn.commit = Mock()
    conn.rollback = Mock()
    conn.close = Mock()
    return conn


# ============================================================================
# ENERGY REFILLS TESTS (3 tests)
# ============================================================================

@patch('monetization.energy_refills.get_database_connection')
def test_purchase_energy_refill_restores_energy(mock_get_db, adult_player, mock_db_connection, mock_db_cursor):
    """Energy restored to max on refill purchase"""
    # Arrange
    mock_get_db.return_value = mock_db_connection
    mock_db_cursor.fetchone.return_value = adult_player
    mock_db_cursor.rowcount = 1

    # Act
    result = purchase_energy_refill(adult_player['id'], 'full')

    # Assert
    assert result['success'] is True
    assert result['new_balance']['energy'] == adult_player['max_energy']
    assert result['message'] == 'Energy refilled successfully'


@patch('monetization.energy_refills.get_database_connection')
def test_purchase_energy_refill_deducts_diamonds(mock_get_db, adult_player, mock_db_connection, mock_db_cursor):
    """Diamonds deducted on energy refill"""
    # Arrange
    mock_get_db.return_value = mock_db_connection
    mock_db_cursor.fetchone.return_value = adult_player
    mock_db_cursor.rowcount = 1
    initial_diamonds = adult_player['diamonds']
    refill_cost = REFILL_TIERS['medium']['diamonds']

    # Act
    result = purchase_energy_refill(adult_player['id'], 'medium')

    # Assert
    assert result['success'] is True
    assert result['new_balance']['diamonds'] == initial_diamonds - refill_cost


@patch('monetization.energy_refills.get_database_connection')
def test_purchase_energy_refill_rate_limited(mock_get_db):
    """Cannot spam energy refills due to rate limiting"""
    # Arrange
    mock_conn = Mock()
    mock_cursor = Mock()
    mock_get_db.return_value = mock_conn
    mock_conn.cursor.return_value = mock_cursor

    player_data = {
        'id': 1,
        'diamonds': 1000,
        'energy': 50,
        'max_energy': 100,
        'unlimited_energy_until': None
    }
    mock_cursor.fetchone.return_value = player_data
    mock_cursor.rowcount = 1

    # Act - attempt to purchase 11 times (rate limit is 10 per minute)
    results = []
    for i in range(11):
        result = purchase_energy_refill(1, 'small')
        results.append(result)

    # Assert - 11th purchase should be rate limited
    assert results[10]['success'] is False
    assert 'rate' in results[10]['message'].lower() or 'many' in results[10]['message'].lower()


# ============================================================================
# TIME SKIPS TESTS (3 tests)
# ============================================================================

@patch('monetization.time_skips.get_database_connection')
@patch('monetization.time_skips.simulate_time_period')
def test_purchase_time_skip_advances_time(mock_simulate, mock_get_db, adult_player, mock_db_connection, mock_db_cursor):
    """Time advanced by skip amount"""
    # Arrange
    mock_get_db.return_value = mock_db_connection
    mock_db_cursor.fetchone.return_value = adult_player
    mock_db_cursor.rowcount = 1

    mock_simulate.return_value = {
        'events': [],
        'money_earned': 100,
        'energy_change': -10,
        'health_change': -2,
        'happiness_change': -5
    }

    initial_time = adult_player['date']

    # Act
    result = purchase_time_skip(adult_player['id'], '1day')

    # Assert
    assert result['success'] is True
    assert 'summary' in result
    assert result['summary']['duration_hours'] == 24  # 1 day = 24 hours


@patch('monetization.time_skips.get_database_connection')
@patch('monetization.time_skips.simulate_time_period')
def test_purchase_time_skip_deducts_diamonds(mock_simulate, mock_get_db, adult_player, mock_db_connection, mock_db_cursor):
    """Diamonds deducted on time skip"""
    # Arrange
    mock_get_db.return_value = mock_db_connection
    mock_db_cursor.fetchone.return_value = adult_player
    mock_db_cursor.rowcount = 1

    mock_simulate.return_value = {
        'events': [],
        'money_earned': 0,
        'energy_change': 0,
        'health_change': 0,
        'happiness_change': 0
    }

    initial_diamonds = adult_player['diamonds']
    skip_cost = SKIP_TIERS['1hour']['diamonds']

    # Act
    result = purchase_time_skip(adult_player['id'], '1hour')

    # Assert
    assert result['success'] is True
    assert result['summary']['diamonds'] == initial_diamonds - skip_cost


@patch('monetization.time_skips.get_database_connection')
def test_purchase_time_skip_processes_events(mock_get_db, adult_player, mock_db_connection, mock_db_cursor):
    """Events during skip processed"""
    # Arrange
    mock_get_db.return_value = mock_db_connection
    mock_db_cursor.fetchone.side_effect = [
        adult_player,  # First call for player data
        adult_player   # Second call for player state (simulate)
    ]
    mock_db_cursor.rowcount = 1

    # Act
    result = purchase_time_skip(adult_player['id'], '1day')

    # Assert
    assert result['success'] is True
    assert 'summary' in result
    assert 'events' in result['summary']
    assert 'stat_changes' in result['summary']


# ============================================================================
# IDEMPOTENCY TESTS (2 tests)
# ============================================================================

@patch('monetization.validation.get_database_connection')
def test_purchase_idempotency(mock_get_db, mock_db_connection, mock_db_cursor):
    """Duplicate purchase requests prevented"""
    # Arrange
    mock_get_db.return_value = mock_db_connection
    mock_db_cursor.fetchone.return_value = {'count': 1}  # Already processed

    player_id = 1
    transaction_id = "txn_123456"
    idempotency_key = IdempotencyManager.generate_key(player_id, transaction_id)

    # Act
    is_processed = IdempotencyManager.check_processed(idempotency_key)

    # Assert
    assert is_processed is True


@patch('monetization.validation.get_database_connection')
def test_transaction_validation_prevents_duplicates(mock_get_db, mock_db_connection, mock_db_cursor):
    """Validation manager prevents duplicate transactions"""
    # Arrange
    mock_get_db.return_value = mock_db_connection

    # First check: not processed (count = 0)
    mock_db_cursor.fetchone.return_value = {'count': 0}

    player_id = 1
    transaction_id = "unique_txn_789"
    idempotency_key = IdempotencyManager.generate_key(player_id, transaction_id)

    # Act - first check should return False (not processed)
    is_processed_first = IdempotencyManager.check_processed(idempotency_key)

    # Simulate marking as processed
    IdempotencyManager.mark_processed(idempotency_key, player_id, {'txn': transaction_id})

    # Second check: now processed (count = 1)
    mock_db_cursor.fetchone.return_value = {'count': 1}
    is_processed_second = IdempotencyManager.check_processed(idempotency_key)

    # Assert
    assert is_processed_first is False  # First check: not yet processed
    assert is_processed_second is True  # Second check: now processed


# ============================================================================
# DIAMOND ECONOMY TESTS (2 tests)
# ============================================================================

@patch('monetization.diamond_economy.get_database_connection')
def test_diamond_balance_tracking(mock_get_db, mock_db_connection, mock_db_cursor):
    """Diamond balance tracked correctly"""
    # Arrange
    mock_get_db.return_value = mock_db_connection
    mock_db_cursor.fetchone.return_value = {'diamonds': 250}

    # Act
    balance = get_diamond_balance(player_id=1)

    # Assert
    assert balance == 250
    mock_db_cursor.execute.assert_called_once()


@patch('monetization.diamond_economy.get_database_connection')
def test_diamond_purchases_logged(mock_get_db, mock_db_connection, mock_db_cursor):
    """All diamond transactions logged"""
    # Arrange
    mock_get_db.return_value = mock_db_connection

    # For award_diamonds: first SELECT to get current balance, then UPDATE
    mock_db_cursor.fetchone.return_value = {'diamonds': 150}

    # Act
    result = award_diamonds(player_id=1, reason='daily_login', amount=10)

    # Assert
    assert result is True
    # Should execute UPDATE and INSERT (for transaction log)
    assert mock_db_cursor.execute.call_count >= 2


# ============================================================================
# RATE LIMITING TESTS (2 additional tests)
# ============================================================================

def test_rate_limit_decorator_allows_valid_requests():
    """Rate limit decorator allows valid number of requests"""
    # Arrange
    @rate_limit(max_calls=3, time_window=60)
    def test_function(player_id: int):
        return {'success': True, 'message': 'OK'}

    # Act - make 3 requests (within limit)
    result1 = test_function(1)
    result2 = test_function(1)
    result3 = test_function(1)

    # Assert
    assert result1['success'] is True
    assert result2['success'] is True
    assert result3['success'] is True


def test_rate_limit_decorator_blocks_excessive_requests():
    """Rate limit decorator blocks excessive requests"""
    # Arrange
    @rate_limit(max_calls=2, time_window=60)
    def test_function(player_id: int):
        return {'success': True, 'message': 'OK'}

    # Act - make 3 requests (exceeds limit of 2)
    result1 = test_function(2)
    result2 = test_function(2)
    result3 = test_function(2)  # Should be blocked

    # Assert
    assert result1['success'] is True
    assert result2['success'] is True
    assert result3['success'] is False
    assert 'error_code' in result3
    assert result3['error_code'] == 'RATE_LIMIT_EXCEEDED'


# ============================================================================
# DIAMOND AWARD/DEDUCT TESTS (2 additional tests)
# ============================================================================

@patch('monetization.diamond_economy.get_database_connection')
def test_award_diamonds_increases_balance(mock_get_db, mock_db_connection, mock_db_cursor):
    """Award diamonds increases player balance"""
    # Arrange
    mock_get_db.return_value = mock_db_connection
    mock_db_cursor.fetchone.return_value = {'diamonds': 110}  # After award

    # Act
    result = award_diamonds(player_id=1, reason='quest_complete', amount=10)

    # Assert
    assert result is True


@patch('monetization.diamond_economy.get_database_connection')
def test_deduct_diamonds_validates_balance(mock_get_db, mock_db_connection, mock_db_cursor):
    """Deduct diamonds validates sufficient balance"""
    # Arrange
    mock_get_db.return_value = mock_db_connection

    # Case 1: Sufficient balance
    mock_db_cursor.fetchone.return_value = {'diamonds': 100}
    result_success = deduct_diamonds(player_id=1, reason='purchase', amount=50)

    # Assert
    assert result_success['success'] is True

    # Case 2: Insufficient balance
    mock_db_cursor.fetchone.return_value = {'diamonds': 10}
    result_fail = deduct_diamonds(player_id=1, reason='purchase', amount=50)

    # Assert
    assert result_fail['success'] is False
    assert 'insufficient' in result_fail['message'].lower()


# ============================================================================
# UNLIMITED ENERGY TESTS (1 additional test)
# ============================================================================

@patch('monetization.energy_refills.get_database_connection')
def test_unlimited_energy_24h_purchase(mock_get_db, adult_player, mock_db_connection, mock_db_cursor):
    """Unlimited energy for 24 hours purchase works"""
    # Arrange
    # Use different player ID to avoid rate limit from previous tests
    test_player_id = 999
    test_player = adult_player.copy()
    test_player['id'] = test_player_id

    mock_get_db.return_value = mock_db_connection
    mock_db_cursor.fetchone.return_value = test_player
    mock_db_cursor.rowcount = 1

    # Act
    result = purchase_energy_refill(test_player_id, 'unlimited_24h')

    # Assert
    assert result['success'] is True
    assert result['new_balance']['unlimited_until'] is not None
    # Energy should be filled to max immediately
    assert result['new_balance']['energy'] == test_player['max_energy']


# ============================================================================
# TIME SKIP SIMULATION TESTS (1 additional test)
# ============================================================================

def test_simulate_time_period_calculates_earnings(mock_db_cursor):
    """Time period simulation calculates work earnings correctly"""
    # Arrange
    player_state = {
        'occupation': 'Teacher',
        'occupationSalary': 4000,  # Monthly salary
        'educationLevel': None,
        'age': 28,
        'relationships': None
    }
    mock_db_cursor.fetchone.return_value = player_state

    start_time = datetime(2025, 1, 1, 9, 0)  # Monday 9am
    end_time = datetime(2025, 1, 1, 17, 0)   # Monday 5pm (8 work hours)

    # Act
    result = simulate_time_period(
        player_id=1,
        start_time=start_time,
        end_time=end_time,
        cursor=mock_db_cursor
    )

    # Assert
    assert 'money_earned' in result
    assert result['money_earned'] >= 0  # Should earn some money during work hours
    assert 'events' in result
    assert len(result['events']) > 0


# ============================================================================
# IAP VALIDATION TESTS (1 additional test)
# ============================================================================

@patch('monetization.validation.get_database_connection')
@patch('monetization.validation.IdempotencyManager.check_processed')
@patch('monetization.validation.IdempotencyManager.mark_processed')
@patch('monetization.diamond_economy.award_diamonds')
def test_validate_iap_receipt_awards_diamonds(mock_award, mock_mark, mock_check, mock_get_db):
    """IAP receipt validation awards correct diamond amount"""
    # Arrange
    mock_check.return_value = False  # Not already processed
    mock_award.return_value = True

    player_id = 1
    receipt_data = "base64_encoded_receipt"
    transaction_id = "apple_txn_123"
    product_id = "com.baolife.diamonds.small"

    # Act
    result = validate_iap_receipt(player_id, receipt_data, transaction_id, product_id)

    # Assert
    assert result['success'] is True
    assert result['diamonds_awarded'] == 100  # Small pack = 100 diamonds
    mock_mark.assert_called_once()
    mock_award.assert_called_once()
