"""
Unit tests for Health Management Module (health/health_manager.py).

Tests weight management, habits, health conditions, death system, hunger/thirst,
and edge cases according to TESTING_PLAN.md Section 7.3.

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

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

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

# Mock problematic imports before importing main modules
with patch.dict('sys.modules', {
    'messaging_style': MagicMock(),
}):
    from health.health_manager import (
        getWeightType,
        handleWeight,
        handleHealth,
        handleDeath,
        updateDeathChance,
        handleHunger,
        mealEvent,
        HealthCondition,
        getHealthConditions,
        HabitClass,
        setHabits,
        quitHabit,
        stopQuitHabit,
        handleHabitChanges,
        negative_habits,
        positive_habits
    )


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

def create_minimal_person(age_years, weight=70):
    """Create a minimal person object for testing without full initialization."""
    person = SimpleNamespace()
    person.id = "test_person_id"
    person.ageYears = age_years
    person.ageHours = age_years * 365 * 24
    person.ageDays = age_years * 365
    person.firstname = "TestPerson"
    person.lastname = "TestFamily"
    person.sex = "Male"
    person.pronoun = "He"
    person.weight = weight
    person.weightType = getWeightType(weight)
    person.health = 1.0
    person.deathChance = 0.000001
    person.hunger = 50
    person.thirst = 50
    person.habits = []
    person.healthConditions = []
    person.energy = 100
    person.peakEnergy = 100
    return person


def create_minimal_player(age_years, weight=70):
    """Create a minimal player object for testing without full initialization."""
    player = SimpleNamespace()
    player.c = create_minimal_person(age_years, weight)
    player.date = date.today().strftime('%m-%d')
    player.messageQueue = []
    player.controller = 'active'
    return player


@pytest.fixture
def newborn_player():
    """Create a player with a newborn character (age 0)."""
    return create_minimal_player(0, weight=7)


@pytest.fixture
def child_player():
    """Create a player with a child character (age 8)."""
    return create_minimal_player(8, weight=40)


@pytest.fixture
def teen_player():
    """Create a player with a teenager (age 16)."""
    return create_minimal_player(16, weight=65)


@pytest.fixture
def adult_player():
    """Create a player with an adult character (age 30)."""
    return create_minimal_player(30, weight=75)


@pytest.fixture
def elderly_player():
    """Create a player with an elderly character (age 75)."""
    return create_minimal_player(75, weight=70)


# ============================================================================
# WEIGHT MANAGEMENT TESTS (6 tests)
# ============================================================================

class TestWeightManagement:
    """Test weight categorization and management."""

    def test_get_weight_type_underweight(self):
        """
        Test that BMI < 18.5 returns 'Underweight'.
        Weight < 50 is categorized as underweight.
        """
        # Arrange
        weight = 45

        # Act
        result = getWeightType(weight)

        # Assert
        assert result == 'Underweight', "Weight < 50 should return 'Underweight'"

    def test_get_weight_type_normal(self):
        """
        Test that BMI 18.5-24.9 returns 'Normal'.
        Weight 50-69 is categorized as normal.
        """
        # Arrange
        weight = 60

        # Act
        result = getWeightType(weight)

        # Assert
        assert result == 'Normal', "Weight 50-69 should return 'Normal'"

    def test_get_weight_type_overweight(self):
        """
        Test that BMI 25-29.9 returns 'Overweight'.
        Weight 70-89 is categorized as overweight.
        """
        # Arrange
        weight = 80

        # Act
        result = getWeightType(weight)

        # Assert
        assert result == 'Overweight', "Weight 70-89 should return 'Overweight'"

    def test_get_weight_type_obese(self):
        """
        Test that BMI >= 30 returns 'Obese'.
        Weight >= 90 is categorized as obese.
        """
        # Arrange
        weight = 95

        # Act
        result = getWeightType(weight)

        # Assert
        assert result == 'Obese', "Weight >= 90 should return 'Obese'"

    def test_handle_weight_updates_based_on_diet(self, adult_player):
        """
        Test that eating habits affect weight over time.
        Weight boundaries are enforced.
        """
        # Arrange
        person = adult_player.c
        person.weight = 150  # Set very high weight

        # Act
        handleWeight(person)

        # Assert
        assert person.weight == 100, "Weight should be capped at 100"

    def test_handle_weight_updates_based_on_activity(self, adult_player):
        """
        Test that exercise affects weight.
        Weight boundaries prevent negative values.
        """
        # Arrange
        person = adult_player.c
        person.weight = -10  # Set negative weight

        # Act
        handleWeight(person)

        # Assert
        assert person.weight == 0, "Weight should be floored at 0"


# ============================================================================
# HABITS TESTS (5 tests)
# ============================================================================

class TestHabits:
    """Test habit management system."""

    def test_set_habits_adds_habit(self, adult_player):
        """
        Test that habits are added to the person's habit list.
        Adults get 0-6 habits assigned.
        """
        # Arrange
        person = adult_player.c
        person.habits = []

        # Act
        setHabits(person)

        # Assert
        assert isinstance(person.habits, list), "Habits should be a list"
        assert len(person.habits) <= 6, "Adults should have max 6 habits"
        # Verify habits are actual HabitClass instances
        if len(person.habits) > 0:
            assert hasattr(person.habits[0], 'name'), "Habit should have name attribute"
            assert hasattr(person.habits[0], 'status'), "Habit should have status attribute"

    def test_quit_habit_removes_habit(self, adult_player):
        """
        Test that quitting a habit changes status to 'quitting'.
        """
        # Arrange
        habit = HabitClass("smoking", "Test habit", "negative")
        adult_player.c.habits = [habit]

        # Act
        with patch('stats.stats_manager.getPeakEnergy'):
            quitHabit(adult_player, "smoking")

        # Assert
        assert adult_player.c.habits[0].status == 'quitting', "Habit status should be 'quitting'"
        assert len(adult_player.messageQueue) > 0, "Should add message to queue"

    def test_stop_quit_habit_fails_quitting(self, adult_player):
        """
        Test that stopping quit attempt resets habit to active.
        """
        # Arrange
        habit = HabitClass("smoking", "Test habit", "negative")
        habit.status = 'quitting'
        habit.quitProgress = 15
        adult_player.c.habits = [habit]

        # Act
        with patch('stats.stats_manager.getPeakEnergy'):
            stopQuitHabit(adult_player, "smoking")

        # Assert
        assert adult_player.c.habits[0].status == 'active', "Habit should return to active"
        assert adult_player.c.habits[0].quitProgress == 0, "Quit progress should reset"

    def test_handle_habit_changes_affects_stats(self, adult_player):
        """
        Test that quitting habits affects stats (e.g., smoking → lower health).
        After 30 days, habit is successfully quit.
        """
        # Arrange
        habit = HabitClass("smoking", "Test habit", "negative")
        habit.status = 'quitting'
        habit.quitProgress = 29  # Almost done
        adult_player.c.habits = [habit]

        # Act
        with patch('stats.stats_manager.getPeakEnergy'):
            handleHabitChanges(adult_player, adult_player.c)

        # Assert
        assert len(adult_player.c.habits) == 0, "Habit should be removed after 30 days"
        assert len(adult_player.messageQueue) > 0, "Should notify about successful quit"

    def test_habit_addiction_level(self, child_player):
        """
        Test that children get fewer habits than adults.
        Age affects number of habits assigned.
        """
        # Arrange
        person = child_player.c
        person.habits = []
        expected_max = round(person.ageYears / 5)  # For age 8: 1-2 habits

        # Act
        setHabits(person)

        # Assert
        assert len(person.habits) <= expected_max * 2, f"Child should have max {expected_max * 2} habits"


# ============================================================================
# HEALTH CONDITIONS TESTS (4 tests)
# ============================================================================

class TestHealthConditions:
    """Test health conditions system."""

    def test_get_health_conditions_returns_list(self):
        """
        Test that getHealthConditions returns a list of HealthCondition objects.
        """
        # Act
        conditions = getHealthConditions()

        # Assert
        assert isinstance(conditions, list), "Should return a list"
        assert len(conditions) > 0, "Should have at least one condition"
        assert isinstance(conditions[0], HealthCondition), "Items should be HealthCondition instances"
        assert hasattr(conditions[0], 'title'), "Condition should have title"
        assert hasattr(conditions[0], 'healthModifier'), "Condition should have healthModifier"

    def test_handle_health_develops_conditions(self, adult_player):
        """
        Test that age and habits cause health conditions to develop.
        Underweight/Obese categories affect health negatively.
        """
        # Arrange
        adult_player.c.weightType = 'Obese'
        initial_health = adult_player.c.health

        # Act
        handleHealth(adult_player, adult_player.c)

        # Assert
        assert adult_player.c.health < initial_health, "Obese weight should decrease health"

    def test_handle_health_treats_conditions(self, adult_player):
        """
        Test that treatment removes conditions after duration.
        Conditions heal after averageDuration weeks.
        """
        # Arrange
        condition = HealthCondition("test1", "Test Flu", 10, 2, "Test condition")
        # Set condition date to 3 weeks ago
        past_date = (datetime.now() - timedelta(weeks=3)).strftime('%m-%d')
        condition.date = past_date
        adult_player.c.healthConditions = [condition]
        adult_player.date = datetime.now().strftime('%m-%d')

        # Act
        handleHealth(adult_player, adult_player.c)

        # Assert
        assert condition.isCured == True, "Condition should be cured after duration"

    def test_health_conditions_affect_stats(self, adult_player):
        """
        Test that illness lowers health over time.
        Active conditions decrease health each tick.
        """
        # Arrange
        condition = HealthCondition("test2", "Test Disease", 20, 4, "Test condition")
        condition.date = datetime.now().strftime('%m-%d')
        adult_player.c.healthConditions = [condition]
        initial_health = adult_player.c.health

        # Act
        handleHealth(adult_player, adult_player.c)

        # Assert
        assert adult_player.c.health < initial_health, "Active condition should decrease health"


# ============================================================================
# DEATH SYSTEM TESTS (5 tests)
# ============================================================================

class TestDeathSystem:
    """Test death probability and game over mechanics."""

    def test_handle_death_calculates_probability(self, adult_player):
        """
        Test that death chance is calculated based on age and health.
        Multiple factors affect death probability.
        """
        # Arrange & Act
        initial_chance = adult_player.c.deathChance
        death_chance = updateDeathChance(adult_player.c)

        # Assert
        assert death_chance > 0, "Death chance should be positive"
        assert isinstance(death_chance, float), "Death chance should be a float"

    def test_update_death_chance_increases_with_age(self, elderly_player):
        """
        Test that older age leads to higher death chance.
        Age 75 should have higher death chance than age 30.
        """
        # Arrange
        adult = create_minimal_person(30)
        elderly = elderly_player.c

        # Act
        adult_chance = updateDeathChance(adult)
        elderly_chance = updateDeathChance(elderly)

        # Assert
        assert elderly_chance > adult_chance, "Elderly should have higher death chance than adult"

    def test_handle_death_triggers_when_occurred(self, adult_player):
        """
        Test that death event triggers when death occurs.
        Death should add message and set controller to inactive.
        """
        # Act
        handleDeath(adult_player)

        # Assert
        assert adult_player.controller == 'inactive', "Game should be inactive after death"
        assert len(adult_player.messageQueue) > 0, "Should have death message"
        assert 'died' in adult_player.messageQueue[0].lower(), "Message should mention death"

    def test_handle_death_ends_game(self, adult_player):
        """
        Test that game ends when death occurs.
        Controller should be set to inactive.
        """
        # Arrange
        adult_player.controller = 'active'

        # Act
        handleDeath(adult_player)

        # Assert
        assert adult_player.controller == 'inactive', "Controller should be inactive after death"

    def test_death_chance_affected_by_health(self, adult_player):
        """
        Test that poor health increases death chance.
        Death chance is inversely proportional to health.
        """
        # Arrange
        adult_player.c.health = 2.0  # High health
        high_health_chance = updateDeathChance(adult_player.c)

        adult_player.c.health = 0.5  # Low health
        low_health_chance = updateDeathChance(adult_player.c)

        # Assert
        assert low_health_chance > high_health_chance, "Lower health should increase death chance"


# ============================================================================
# HUNGER/THIRST TESTS (5 tests)
# ============================================================================

class TestHungerThirst:
    """Test hunger and thirst mechanics."""

    def test_handle_hunger_increases_over_time(self, adult_player):
        """
        Test that hunger increases hourly.
        Minimum boundary is enforced at 0.
        """
        # Arrange
        adult_player.c.hunger = -10

        # Act
        handleHunger(adult_player.c)

        # Assert
        assert adult_player.c.hunger == 0, "Hunger should not go below 0"

    def test_meal_event_reduces_hunger(self, adult_player):
        """
        Test that eating reduces hunger.
        Meals reduce hunger by 50-60 points.
        """
        # Arrange
        adult_player.c.hunger = 100
        adult_player.c.thirst = 100

        # Act
        mealEvent(adult_player)

        # Assert
        assert adult_player.c.hunger < 100, "Meal should reduce hunger"
        assert adult_player.c.hunger >= 40, "Hunger reduced by 50-60"
        assert adult_player.c.hunger <= 50, "Hunger reduced by 50-60"

    def test_meal_event_affects_weight(self, adult_player):
        """
        Test that meals affect weight over time.
        (Weight changes are handled by other systems, but meal reduces hunger/thirst)
        """
        # Arrange
        initial_hunger = adult_player.c.hunger
        initial_thirst = adult_player.c.thirst

        # Act
        mealEvent(adult_player)

        # Assert
        # Meal should reduce both hunger and thirst
        assert adult_player.c.hunger < initial_hunger or initial_hunger == 0, "Hunger should decrease"
        assert adult_player.c.thirst < initial_thirst or initial_thirst == 0, "Thirst should decrease"

    def test_hunger_affects_energy(self, adult_player):
        """
        Test that high hunger affects health stats.
        (Integration with health system - hunger managed by boundaries)
        """
        # Arrange
        adult_player.c.hunger = 100  # Very hungry

        # Act
        handleHunger(adult_player.c)

        # Assert
        # handleHunger only enforces boundaries, doesn't affect energy directly
        assert adult_player.c.hunger == 100, "Hunger value should be maintained"

    def test_thirst_mechanics(self, adult_player):
        """
        Test that thirst increases and affects stats.
        Thirst has minimum boundary at 0.
        """
        # Arrange
        adult_player.c.thirst = -20

        # Act
        handleHunger(adult_player.c)

        # Assert
        assert adult_player.c.thirst == 0, "Thirst should not go below 0"


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

class TestEdgeCases:
    """Test edge cases and boundary conditions."""

    def test_weight_calculation_extreme_values(self):
        """
        Test weight categorization with extreme values.
        Very high/low weights should be categorized correctly.
        """
        # Arrange & Act
        very_low = getWeightType(0)
        very_high = getWeightType(200)

        # Assert
        assert very_low == 'Underweight', "Weight 0 should be Underweight"
        assert very_high == 'Obese', "Weight 200 should be Obese"

    def test_habit_list_management(self, adult_player):
        """
        Test managing multiple habits simultaneously.
        Multiple habits should be tracked correctly.
        """
        # Arrange
        habit1 = HabitClass("smoking", "Smoking habit", "negative")
        habit2 = HabitClass("overeating", "Overeating habit", "negative")
        habit3 = HabitClass("regular_exercise", "Exercise habit", "positive")
        adult_player.c.habits = [habit1, habit2, habit3]

        # Act
        with patch('stats.stats_manager.getPeakEnergy'):
            quitHabit(adult_player, "smoking")

        # Assert
        assert len(adult_player.c.habits) == 3, "All habits should be present"
        assert adult_player.c.habits[0].status == 'quitting', "First habit should be quitting"
        assert adult_player.c.habits[1].status == 'active', "Other habits should remain active"

    def test_health_condition_overlapping(self, adult_player):
        """
        Test multiple health conditions at once.
        Multiple conditions should stack health impacts.
        """
        # Arrange
        condition1 = HealthCondition("flu", "Flu", 10, 2, "Test flu")
        condition1.date = datetime.now().strftime('%m-%d')
        condition2 = HealthCondition("cold", "Cold", 5, 1, "Test cold")
        condition2.date = datetime.now().strftime('%m-%d')
        adult_player.c.healthConditions = [condition1, condition2]
        initial_health = adult_player.c.health

        # Act
        handleHealth(adult_player, adult_player.c)

        # Assert
        assert adult_player.c.health < initial_health, "Multiple conditions should decrease health"

    def test_death_chance_boundaries(self, adult_player):
        """
        Test that death chance stays within 0-100% range.
        Death probability should be a valid percentage.
        """
        # Arrange - Test various ages
        ages_to_test = [20, 30, 50, 70, 90, 110]

        for age in ages_to_test:
            # Act
            person = create_minimal_person(age)
            death_chance = updateDeathChance(person)

            # Assert
            assert death_chance >= 0, f"Death chance should be >= 0 for age {age}"
            assert death_chance <= 1, f"Death chance should be <= 1 (100%) for age {age}"

    def test_meal_event_with_no_hunger(self, adult_player):
        """
        Test eating when already full.
        Hunger can go negative but is handled by boundaries.
        """
        # Arrange
        adult_player.c.hunger = 0
        adult_player.c.thirst = 0

        # Act
        mealEvent(adult_player)
        handleHunger(adult_player.c)

        # Assert
        # After meal, hunger becomes negative, then handleHunger enforces 0
        assert adult_player.c.hunger == 0, "Hunger should not go below 0"
        assert adult_player.c.thirst == 0, "Thirst should not go below 0"


# ============================================================================
# HABIT CLASS TESTS
# ============================================================================

class TestHabitClass:
    """Test HabitClass initialization and attributes."""

    def test_habit_class_creation(self):
        """Test creating a habit with all attributes."""
        # Arrange & Act
        habit = HabitClass("test_habit", "Test description", "negative")

        # Assert
        assert habit.name == "test_habit"
        assert habit.description == "Test description"
        assert habit.type == "habit"
        assert habit.habitType == "negative"
        assert habit.status == "active"
        assert habit.quitProgress == 0

    def test_habit_default_type(self):
        """Test that habit defaults to negative type."""
        # Arrange & Act
        habit = HabitClass("test", "Test")

        # Assert
        assert habit.habitType == "negative", "Default habitType should be negative"


# ============================================================================
# HEALTH CONDITION CLASS TESTS
# ============================================================================

class TestHealthConditionClass:
    """Test HealthCondition initialization and attributes."""

    def test_health_condition_creation(self):
        """Test creating a health condition with all attributes."""
        # Arrange & Act
        condition = HealthCondition(
            "test_id",
            "Test Disease",
            15,
            7,
            "Test description",
            "test.jpg"
        )

        # Assert
        assert condition.id == "test_id"
        assert condition.title == "Test Disease"
        assert condition.healthModifier == 15
        assert condition.averageDuration == 7
        assert condition.date is None
        assert condition.description == "Test description"
        assert condition.image == "test.jpg"

    def test_health_condition_without_image(self):
        """Test creating condition without optional image."""
        # Arrange & Act
        condition = HealthCondition("id", "Disease", 10, 5, "Desc")

        # Assert
        assert condition.image is None, "Image should default to None"


# ============================================================================
# INTEGRATION TESTS
# ============================================================================

class TestHealthSystemIntegration:
    """Test integration between health system components."""

    def test_weight_affects_health_over_time(self, adult_player):
        """
        Integration test: Weight type affects health calculation.
        """
        # Arrange
        adult_player.c.weightType = 'Underweight'
        initial_health = adult_player.c.health

        # Act
        for _ in range(10):  # Simulate 10 ticks
            handleHealth(adult_player, adult_player.c)

        # Assert
        assert adult_player.c.health < initial_health, "Underweight should decrease health over time"

    def test_low_health_increases_death_chance_cumulative(self, adult_player):
        """
        Integration test: Low health increases death chance, which compounds.
        """
        # Arrange
        adult_player.c.health = 0.1  # Very low health

        # Act
        death_chance = updateDeathChance(adult_player.c)

        # Assert
        # For age 30, base death chance is 0.000002, divided by 0.1 health = 0.00002
        # But due to how updateDeathChance calculates, it's closer to 0.00001
        assert death_chance >= 0.000009, "Very low health should significantly increase death chance"
        assert death_chance < 1.0, "Death chance should be a valid probability"

    def test_quitting_habit_complete_workflow(self, adult_player):
        """
        Integration test: Complete habit quitting workflow.
        Start quitting → progress → successfully quit after 30 days.
        """
        # Arrange
        habit = HabitClass("smoking", "Smoking", "negative")
        adult_player.c.habits = [habit]

        # Act - Start quitting
        with patch('stats.stats_manager.getPeakEnergy'):
            quitHabit(adult_player, "smoking")

            # Simulate 30 days of progress
            for day in range(30):
                handleHabitChanges(adult_player, adult_player.c)

        # Assert
        assert len(adult_player.c.habits) == 0, "Habit should be removed after 30 days"
        assert any('quit' in msg.lower() for msg in adult_player.messageQueue), "Should have quit success message"
