"""
Custom assertion helpers for BaoLife tests.

This module provides specialized assertion functions that make test code
more readable and provide better error messages.
"""
from typing import Any, List, Optional, Union


def assert_event_triggered(player, event_fname: str, message: str = None):
    """
    Assert that an event was triggered for the player.

    Args:
        player: The player object
        event_fname: The event function name to check
        message: Optional custom error message

    Raises:
        AssertionError: If event was not triggered
    """
    if not message:
        message = f"Event '{event_fname}' was not triggered"
    assert event_fname in player.events, message


def assert_event_not_triggered(player, event_fname: str, message: str = None):
    """
    Assert that an event was NOT triggered for the player.

    Args:
        player: The player object
        event_fname: The event function name to check
        message: Optional custom error message

    Raises:
        AssertionError: If event was triggered
    """
    if not message:
        message = f"Event '{event_fname}' should not have been triggered"
    assert event_fname not in player.events, message


def assert_stat_in_range(person, stat_name: str, min_val: float, max_val: float):
    """
    Assert that a stat is within the specified range.

    Args:
        person: The person/character object
        stat_name: Name of the stat attribute
        min_val: Minimum acceptable value
        max_val: Maximum acceptable value

    Raises:
        AssertionError: If stat is out of range
    """
    value = getattr(person, stat_name)
    assert min_val <= value <= max_val, \
        f"{stat_name} is {value}, expected between {min_val} and {max_val}"


def assert_stat_equals(person, stat_name: str, expected_value: Any):
    """
    Assert that a stat equals the expected value.

    Args:
        person: The person/character object
        stat_name: Name of the stat attribute
        expected_value: Expected value

    Raises:
        AssertionError: If stat doesn't match expected value
    """
    actual_value = getattr(person, stat_name)
    assert actual_value == expected_value, \
        f"{stat_name} is {actual_value}, expected {expected_value}"


def assert_stat_increased(person, stat_name: str, original_value: float):
    """
    Assert that a stat increased from its original value.

    Args:
        person: The person/character object
        stat_name: Name of the stat attribute
        original_value: The original value to compare against

    Raises:
        AssertionError: If stat did not increase
    """
    current_value = getattr(person, stat_name)
    assert current_value > original_value, \
        f"{stat_name} did not increase (was {original_value}, now {current_value})"


def assert_stat_decreased(person, stat_name: str, original_value: float):
    """
    Assert that a stat decreased from its original value.

    Args:
        person: The person/character object
        stat_name: Name of the stat attribute
        original_value: The original value to compare against

    Raises:
        AssertionError: If stat did not decrease
    """
    current_value = getattr(person, stat_name)
    assert current_value < original_value, \
        f"{stat_name} did not decrease (was {original_value}, now {current_value})"


def assert_relationship_exists(player, person_id: str, message: str = None):
    """
    Assert that a relationship exists with the specified person.

    Args:
        player: The player object
        person_id: ID of the person to check
        message: Optional custom error message

    Raises:
        AssertionError: If relationship doesn't exist
    """
    if not message:
        message = f"Relationship with person {person_id} not found"

    relationship_exists = any(
        hasattr(rel, 'id') and rel.id == person_id or
        hasattr(rel, 'personID') and rel.personID == person_id
        for rel in player.r
    )
    assert relationship_exists, message


def assert_affinity_level(relationship, expected_level: str):
    """
    Assert that relationship affinity is at the expected level.

    Args:
        relationship: The relationship object
        expected_level: Expected level ('high', 'medium', or 'low')

    Raises:
        AssertionError: If affinity doesn't match expected level
        ValueError: If expected_level is invalid
    """
    affinity = relationship.affinity

    if expected_level == "high":
        assert affinity >= 70, f"Affinity {affinity} is not high (expected >= 70)"
    elif expected_level == "medium":
        assert 40 <= affinity < 70, f"Affinity {affinity} is not medium (expected 40-69)"
    elif expected_level == "low":
        assert affinity < 40, f"Affinity {affinity} is not low (expected < 40)"
    else:
        raise ValueError(f"Invalid level '{expected_level}'. Use 'high', 'medium', or 'low'")


def assert_affinity_in_range(relationship, min_val: float, max_val: float):
    """
    Assert that relationship affinity is within range.

    Args:
        relationship: The relationship object
        min_val: Minimum affinity
        max_val: Maximum affinity

    Raises:
        AssertionError: If affinity is out of range
    """
    affinity = relationship.affinity
    assert min_val <= affinity <= max_val, \
        f"Affinity {affinity} not in range [{min_val}, {max_val}]"


def assert_has_location(player, location_id: str, message: str = None):
    """
    Assert that player has access to a specific location.

    Args:
        player: The player object
        location_id: ID of the location
        message: Optional custom error message

    Raises:
        AssertionError: If location not found
    """
    if not message:
        message = f"Location {location_id} not found for player"

    has_location = any(loc.id == location_id for loc in player.l)
    assert has_location, message


def assert_has_occupation(person, occupation: str):
    """
    Assert that person has the specified occupation.

    Args:
        person: The person/character object
        occupation: Expected occupation

    Raises:
        AssertionError: If occupation doesn't match
    """
    assert person.occupation == occupation, \
        f"Person has occupation '{person.occupation}', expected '{occupation}'"


def assert_age_in_range(person, min_age: int, max_age: int):
    """
    Assert that person's age is within range.

    Args:
        person: The person/character object
        min_age: Minimum age
        max_age: Maximum age

    Raises:
        AssertionError: If age is out of range
    """
    age = person.ageYears if hasattr(person, 'ageYears') else person.age
    assert min_age <= age <= max_age, \
        f"Age {age} not in range [{min_age}, {max_age}]"


def assert_money_changed(person, original_money: float, expected_change: float, tolerance: float = 0.01):
    """
    Assert that money changed by the expected amount.

    Args:
        person: The person/character object
        original_money: Original money amount
        expected_change: Expected change (positive or negative)
        tolerance: Acceptable difference (default 0.01)

    Raises:
        AssertionError: If money change doesn't match expected
    """
    actual_change = person.money - original_money
    diff = abs(actual_change - expected_change)
    assert diff <= tolerance, \
        f"Money changed by {actual_change}, expected {expected_change}"


def assert_list_contains(lst: List, item: Any, message: str = None):
    """
    Assert that a list contains a specific item.

    Args:
        lst: The list to check
        item: The item to find
        message: Optional custom error message

    Raises:
        AssertionError: If item not in list
    """
    if not message:
        message = f"Item {item} not found in list"
    assert item in lst, message


def assert_list_length(lst: List, expected_length: int, message: str = None):
    """
    Assert that a list has the expected length.

    Args:
        lst: The list to check
        expected_length: Expected length
        message: Optional custom error message

    Raises:
        AssertionError: If length doesn't match
    """
    if not message:
        message = f"List has length {len(lst)}, expected {expected_length}"
    assert len(lst) == expected_length, message


def assert_game_active(player):
    """
    Assert that the game is in active state.

    Args:
        player: The player object

    Raises:
        AssertionError: If game is not active
    """
    assert player.controller == 'active', \
        f"Game is '{player.controller}', expected 'active'"


def assert_game_paused(player):
    """
    Assert that the game is paused.

    Args:
        player: The player object

    Raises:
        AssertionError: If game is not paused
    """
    from config import config
    assert player.gameSpeed == config.SPEED_PAUSED or player.gameSpeed >= 10000, \
        f"Game speed is {player.gameSpeed}, expected paused state"


def assert_time_advanced(player, original_hour: int, original_minute: int):
    """
    Assert that game time has advanced.

    Args:
        player: The player object
        original_hour: Original hour
        original_minute: Original minute

    Raises:
        AssertionError: If time hasn't advanced
    """
    time_advanced = (
        player.hourOfDay > original_hour or
        (player.hourOfDay == original_hour and player.minuteOfHour > original_minute)
    )
    assert time_advanced, \
        f"Time has not advanced from {original_hour}:{original_minute:02d}"
