"""
Unit tests for shop manager (ws/shop/shop_manager.py).

Tests store items, in-app purchases, and edge cases for the shop system.

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

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

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

from shop.shop_manager import (
    StoreItem,
    InAppPurchaseItem,
    getStoreItems,
    purchaseItem,
    getInAppPurchaseItems,
    purchaseInAppItem,
)


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

@pytest.fixture
def adult_player():
    """Create a test player with money and diamonds (adult character)"""
    player = Mock()
    player.userID = "test_user_adult"

    # Create mock character with money and diamonds
    player.c = Mock()
    player.c.money = 100000  # Enough money to buy expensive items
    player.c.diamonds = 100
    player.c.energy = 75
    player.c.prestige = 50
    player.c.items = []
    player.c.habits = []  # Required for getPeakEnergy function
    player.c.activities = []  # Required for getPeakEnergy function
    player.c.activityRecords = []  # Required for getPeakEnergy function
    player.c.ageYears = 30  # Required for getPeakEnergy function

    # Store items (will be populated from getStoreItems())
    player.storeItems = getStoreItems()

    return player


@pytest.fixture
def poor_player():
    """Create a test player with minimal money"""
    player = Mock()
    player.userID = "test_user_poor"

    player.c = Mock()
    player.c.money = 50  # Very little money
    player.c.diamonds = 0
    player.c.energy = 100
    player.c.prestige = 0
    player.c.items = []
    player.c.habits = []  # Required for getPeakEnergy function
    player.c.activities = []  # Required for getPeakEnergy function
    player.c.activityRecords = []  # Required for getPeakEnergy function
    player.c.ageYears = 30  # Required for getPeakEnergy function

    player.storeItems = getStoreItems()

    return player


@pytest.fixture
def sample_store_item():
    """Create a sample store item for testing"""
    return StoreItem(
        name="Test Item",
        price=500,
        description="A test item",
        prestigeBoost=10,
        image="test.png",
        energyBoost=0
    )


# ============================================================================
# STORE ITEMS TESTS (4 tests)
# ============================================================================

def test_get_store_items_returns_catalog():
    """Store catalog returned with multiple items"""
    # Arrange & Act
    items = getStoreItems()

    # Assert
    assert isinstance(items, list)
    assert len(items) > 0
    assert all(isinstance(item, StoreItem) for item in items)
    # Check that items have required properties
    assert all(hasattr(item, 'name') for item in items)
    assert all(hasattr(item, 'price') for item in items)
    assert all(hasattr(item, 'description') for item in items)


def test_purchase_item_deducts_money(adult_player):
    """Money deducted on successful purchase"""
    # Arrange
    initial_money = adult_player.c.money
    item = adult_player.storeItems[0]  # Get first item
    item_price = item.price

    # Act
    result = purchaseItem(adult_player, item.id)

    # Assert
    assert result is True
    assert adult_player.c.money == initial_money - item_price


def test_purchase_item_adds_to_inventory(adult_player):
    """Item added to player inventory after purchase"""
    # Arrange
    initial_items_count = len(adult_player.c.items)
    # Find an item that's not an energy boost (permanent item)
    # Skip first item (energy drinks) and get second item (sports car)
    permanent_item = adult_player.storeItems[1]  # Sports Car - permanent item
    assert permanent_item.energyBoost == 0  # Verify it's a permanent item

    # Act
    result = purchaseItem(adult_player, permanent_item.id)

    # Assert
    assert result is True
    assert len(adult_player.c.items) == initial_items_count + 1
    assert adult_player.c.items[-1] == permanent_item


def test_purchase_item_insufficient_funds(poor_player):
    """Purchase fails if not enough money"""
    # Arrange
    initial_money = poor_player.c.money
    # Find an expensive item
    expensive_item = next(item for item in poor_player.storeItems if item.price > poor_player.c.money)

    # Act
    result = purchaseItem(poor_player, expensive_item.id)

    # Assert
    assert result is False
    assert poor_player.c.money == initial_money  # Money unchanged
    assert len(poor_player.c.items) == 0  # No items added


# ============================================================================
# IN-APP PURCHASES TESTS (3 tests)
# ============================================================================

def test_get_iap_items_returns_catalog():
    """IAP catalog returned with diamond packages"""
    # Arrange & Act
    items = getInAppPurchaseItems()

    # Assert
    assert isinstance(items, list)
    assert len(items) > 0
    assert all(isinstance(item, InAppPurchaseItem) for item in items)
    # Check that items have required properties
    assert all(hasattr(item, 'id') for item in items)
    assert all(hasattr(item, 'name') for item in items)
    assert all(hasattr(item, 'price') for item in items)
    assert all(hasattr(item, 'diamonds') for item in items)
    # All items should award positive diamonds
    assert all(item.diamonds > 0 for item in items)


def test_purchase_iap_item_adds_diamonds(adult_player):
    """Diamonds added on in-app purchase"""
    # Arrange
    initial_diamonds = adult_player.c.diamonds
    iap_items = getInAppPurchaseItems()
    test_item = iap_items[0]  # Get first IAP item
    expected_diamonds = test_item.diamonds

    # Act
    result = purchaseInAppItem(adult_player, test_item.id)

    # Assert
    # Note: purchaseInAppItem returns False (legacy behavior)
    assert result is False
    assert adult_player.c.diamonds == initial_diamonds + expected_diamonds


def test_purchase_iap_item_validates_receipt():
    """Receipt validation required (tests structure, actual validation happens elsewhere)"""
    # Arrange
    player = Mock()
    player.c = Mock()
    player.c.diamonds = 0

    iap_items = getInAppPurchaseItems()

    # Act - purchase with valid item ID
    result = purchaseInAppItem(player, iap_items[0].id)

    # Assert
    # The function should process the purchase (validation happens in separate validation module)
    assert player.c.diamonds > 0

    # Act - purchase with invalid item ID
    player.c.diamonds = 0
    result_invalid = purchaseInAppItem(player, "invalid_item_id")

    # Assert - no diamonds awarded for invalid ID
    assert player.c.diamonds == 0


# ============================================================================
# EDGE CASES TESTS (3 tests)
# ============================================================================

def test_purchase_duplicate_item(adult_player):
    """Can buy same item multiple times"""
    # Arrange
    item = adult_player.storeItems[0]
    initial_money = adult_player.c.money

    # Act - purchase same item twice
    result1 = purchaseItem(adult_player, item.id)
    money_after_first = adult_player.c.money
    result2 = purchaseItem(adult_player, item.id)

    # Assert
    assert result1 is True
    assert result2 is True
    assert adult_player.c.money == initial_money - (item.price * 2)


def test_purchase_with_exact_amount(adult_player):
    """Purchase with exact money amount works"""
    # Arrange
    # Find an item we can afford exactly
    target_price = 100
    adult_player.c.money = target_price
    item = StoreItem(
        name="Exact Price Item",
        price=target_price,
        description="Test item",
        prestigeBoost=5
    )
    adult_player.storeItems.append(item)

    # Act
    result = purchaseItem(adult_player, item.id)

    # Assert
    assert result is True
    assert adult_player.c.money == 0  # Exactly zero after purchase


def test_store_item_properties(sample_store_item):
    """Items have name, price, description"""
    # Arrange & Act
    item = sample_store_item

    # Assert
    assert hasattr(item, 'id')
    assert hasattr(item, 'name')
    assert hasattr(item, 'price')
    assert hasattr(item, 'description')
    assert hasattr(item, 'prestigeBoost')
    assert hasattr(item, 'image')
    assert hasattr(item, 'energyBoost')

    assert item.name == "Test Item"
    assert item.price == 500
    assert item.description == "A test item"
    assert item.prestigeBoost == 10
    assert item.energyBoost == 0


# ============================================================================
# PRESTIGE AND ENERGY BOOST TESTS (2 additional tests)
# ============================================================================

def test_purchase_item_adds_prestige(adult_player):
    """Prestige boost applied on purchase"""
    # Arrange
    initial_prestige = adult_player.c.prestige
    item = adult_player.storeItems[0]
    expected_prestige_boost = item.prestigeBoost

    # Act
    result = purchaseItem(adult_player, item.id)

    # Assert
    assert result is True
    assert adult_player.c.prestige == initial_prestige + expected_prestige_boost


@patch('stats.stats_manager.getPeakEnergy')
def test_purchase_energy_item_restores_energy(mock_getPeakEnergy, adult_player):
    """Energy boost items restore energy"""
    # Arrange
    initial_energy = adult_player.c.energy
    # Find an energy boost item
    energy_item = next(item for item in adult_player.storeItems if item.energyBoost > 0)
    expected_energy_boost = energy_item.energyBoost

    # Mock getPeakEnergy to prevent energy cap errors
    mock_getPeakEnergy.return_value = None

    # Act
    result = purchaseItem(adult_player, energy_item.id)

    # Assert
    assert result is True
    assert adult_player.c.energy == initial_energy + expected_energy_boost
    mock_getPeakEnergy.assert_called_once()


# ============================================================================
# STORE ITEM VALIDATION TESTS (2 additional tests)
# ============================================================================

def test_purchase_invalid_item_id_returns_false(adult_player):
    """Purchase with invalid item ID returns False"""
    # Arrange
    invalid_item_id = "nonexistent_item_id"
    initial_money = adult_player.c.money

    # Act
    result = purchaseItem(adult_player, invalid_item_id)

    # Assert
    assert result is False
    assert adult_player.c.money == initial_money  # Money unchanged


def test_store_items_have_unique_ids():
    """All store items have unique IDs"""
    # Arrange & Act
    items = getStoreItems()
    item_ids = [item.id for item in items]

    # Assert
    assert len(item_ids) == len(set(item_ids))  # No duplicates
