"""
Unit Tests for Dilemma Events

Tests moral choice dilemma events from ws/events/dilemmas/moral_choices.py including:
- School Dilemmas: braceletDilemma, bullyDilemma
- Animal Dilemmas: foundLostPet, strayAnimalDecision
- Social Dilemmas: friendCheating, friendBorrowMoney, witnessShoplifting
- Work Dilemmas: colleagueStealingCredit, whistleblowerDecision
- Life Dilemmas: foundExpensiveItem, environmentalChoice, parentCareDecision

Test Pattern (per TESTING_PLAN.md Section 4.4):
- test_event_triggers_at_correct_age - Event only triggers in age range
- test_event_requires_conditions - Event needs specific conditions met
- test_event_not_duplicate - Event fname not in player.askedQuestions
- test_event_applies_costs_correctly - Money/energy/stats updated
- test_event_choice_consequences - Different answers -> different outcomes
- test_event_chain_progression - Multi-step dilemma state progression
"""

import pytest
import random
from unittest.mock import patch, MagicMock

from ws.core.models import playerClass, personClass
from ws.events.base import messageEvent, questionEvent, answerOption, dilemmaClass

# Import dilemma event functions
from ws.events.dilemmas import moral_choices

braceletDilemma = moral_choices.braceletDilemma
bullyDilemma = moral_choices.bullyDilemma
foundLostPet = moral_choices.foundLostPet
friendCheating = moral_choices.friendCheating
foundExpensiveItem = moral_choices.foundExpensiveItem
colleagueStealingCredit = moral_choices.colleagueStealingCredit
strayAnimalDecision = moral_choices.strayAnimalDecision
witnessShoplifting = moral_choices.witnessShoplifting
friendBorrowMoney = moral_choices.friendBorrowMoney
environmentalChoice = moral_choices.environmentalChoice
parentCareDecision = moral_choices.parentCareDecision
whistleblowerDecision = moral_choices.whistleblowerDecision


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

@pytest.fixture
def child_player():
    """Create a player with child character (age 8)"""
    player = playerClass()
    player.c = personClass()
    player.c.firstname = "Child"
    player.c.lastname = "TestKid"
    player.c.ageYears = 8
    player.c.ageDays = 365 * 8
    player.c.energy = 100
    player.c.happiness = 75
    player.c.social = 50
    player.c.money = 100
    player.c.location = "school123"
    player.c.id = "child123"
    player.events = set()
    player.askedQuestions = set()
    player.messageQueue = []
    player.activeDilemmas = []
    player.r = []
    player.date = "01-15"
    player.hourOfDay = 12
    player.gameSpeed = 1000
    player.previousGameSpeed = 1000
    return player


@pytest.fixture
def teen_player():
    """Create a player with teenager character (age 16)"""
    player = playerClass()
    player.c = personClass()
    player.c.firstname = "Teen"
    player.c.lastname = "TestKid"
    player.c.ageYears = 16
    player.c.ageDays = 365 * 16
    player.c.energy = 100
    player.c.happiness = 75
    player.c.social = 50
    player.c.money = 500
    player.c.location = "school123"
    player.c.id = "teen123"
    player.events = set()
    player.askedQuestions = set()
    player.messageQueue = []
    player.activeDilemmas = []
    player.r = []
    player.date = "01-15"
    player.hourOfDay = 12
    player.gameSpeed = 1000
    player.previousGameSpeed = 1000
    return player


@pytest.fixture
def adult_player():
    """Create a player with adult character (age 30)"""
    player = playerClass()
    player.c = personClass()
    player.c.firstname = "Adult"
    player.c.lastname = "TestPerson"
    player.c.ageYears = 30
    player.c.ageDays = 365 * 30
    player.c.energy = 80
    player.c.happiness = 70
    player.c.social = 60
    player.c.money = 2000
    player.c.location = "work123"
    player.c.occupation = "work"
    player.c.id = "adult123"
    player.events = set()
    player.askedQuestions = set()
    player.messageQueue = []
    player.activeDilemmas = []
    player.r = []
    player.date = "01-15"
    player.hourOfDay = 12
    player.gameSpeed = 1000
    player.previousGameSpeed = 1000
    return player


# ============================================================================
# SCHOOL DILEMMA TESTS
# ============================================================================

class TestBraceletDilemma:
    """Test braceletDilemma (ages 5-18, school location)"""

    @patch('random.random', return_value=0.0)
    @patch('functions.get_random_classmate')
    def test_bracelet_dilemma_triggers_at_correct_age(self, mock_classmate, mock_random, child_player):
        """Event should trigger between ages 5-18 at school"""
        # Create mock classmate
        classmate = personClass()
        classmate.firstname = "Jane"
        classmate.lastname = "Doe"
        classmate.id = "classmate123"
        mock_classmate.return_value = classmate

        child_player.c.ageYears = 10
        child_player.c.location = "school123"
        result = braceletDilemma(child_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    @patch('random.random', return_value=0.0)
    def test_bracelet_dilemma_requires_school_location(self, mock_random, child_player):
        """Event should require school location"""
        child_player.c.ageYears = 10
        child_player.c.location = "home123"
        result = braceletDilemma(child_player)
        assert result is None

    @patch('random.random', return_value=0.0)
    @patch('functions.get_random_classmate')
    def test_bracelet_dilemma_creates_dilemma_object(self, mock_classmate, mock_random, child_player):
        """Event should create and add dilemma to activeDilemmas"""
        classmate = personClass()
        classmate.firstname = "Jane"
        classmate.lastname = "Doe"
        classmate.id = "classmate123"
        mock_classmate.return_value = classmate

        child_player.c.ageYears = 10
        child_player.c.location = "school123"
        result = braceletDilemma(child_player)
        assert len(child_player.activeDilemmas) == 1
        assert isinstance(child_player.activeDilemmas[0], dilemmaClass)

    @patch('functions.get_random_classmate')
    def test_bracelet_dilemma_answer_stores_choice(self, mock_classmate, child_player):
        """Answering should store choice in dilemma"""
        classmate = personClass()
        classmate.firstname = "Jane"
        classmate.lastname = "Doe"
        classmate.id = "classmate123"
        classmate.affinity = 60
        mock_classmate.return_value = classmate
        child_player.r.append(classmate)

        # Create dilemma first
        with patch('random.random', return_value=0.0):
            result = braceletDilemma(child_player)

        # Answer the dilemma
        child_player.c.energy = 100
        response = {'option': 'Confront her directly about it'}
        braceletDilemma(child_player, type='answer', response=response)

        assert child_player.activeDilemmas[0].answer is not None
        assert child_player.c.energy == 95  # Energy cost applied

    @patch('functions.get_person')
    def test_bracelet_dilemma_step_2_resolution(self, mock_get_person, child_player):
        """Step 2 should resolve the dilemma with message"""
        classmate = personClass()
        classmate.firstname = "Jane"
        classmate.lastname = "Doe"
        classmate.id = "classmate123"
        mock_get_person.return_value = classmate

        # Create dilemma and set answer
        dilemma = dilemmaClass('braceletDilemma', [])
        dilemma.classmate = "classmate123"
        dilemma.step = 2
        dilemma.answer = answerOption('Confront her directly about it', energyCost=5)
        child_player.activeDilemmas.append(dilemma)

        # Call step 2
        braceletDilemma(child_player, dilemma=dilemma)

        assert len(child_player.messageQueue) > 0
        assert len(child_player.activeDilemmas) == 0  # Dilemma removed


class TestBullyDilemma:
    """Test bullyDilemma (ages 10-18, school location)"""

    @patch('random.random', return_value=0.0)
    @patch('functions.get_random_friend')
    @patch('functions.get_random_classmate')
    def test_bully_dilemma_triggers_at_correct_age(self, mock_classmate, mock_friend, mock_random, teen_player):
        """Event should trigger between ages 10-18 at school"""
        friend = personClass()
        friend.firstname = "Friend"
        friend.lastname = "Test"
        friend.id = "friend123"
        mock_friend.return_value = friend

        classmate1 = personClass()
        classmate1.firstname = "Bully1"
        classmate1.lastname = "Test"
        classmate1.id = "bully1"
        classmate2 = personClass()
        classmate2.firstname = "Bully2"
        classmate2.lastname = "Test"
        classmate2.id = "bully2"
        mock_classmate.side_effect = [classmate1, classmate2]

        teen_player.c.ageYears = 14
        teen_player.c.location = "school123"
        result = bullyDilemma(teen_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    @patch('random.random', return_value=0.0)
    @patch('functions.get_random_friend')
    def test_bully_dilemma_requires_friend(self, mock_friend, mock_random, teen_player):
        """Event should require having a friend"""
        mock_friend.return_value = None

        teen_player.c.ageYears = 14
        teen_player.c.location = "school123"
        result = bullyDilemma(teen_player)
        # Should return None or not create proper dilemma without friend


# ============================================================================
# ANIMAL DILEMMA TESTS
# ============================================================================

class TestFoundLostPet:
    """Test foundLostPet dilemma (ages 8+)"""

    @patch('random.random', return_value=0.0)
    def test_found_lost_pet_triggers_at_correct_age(self, mock_random, child_player):
        """Event should trigger at age 8+"""
        child_player.c.ageYears = 10
        result = foundLostPet(child_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    @patch('random.random', return_value=0.0)
    def test_found_lost_pet_not_before_age(self, mock_random, child_player):
        """Event should not trigger before age 8"""
        child_player.c.ageYears = 7
        result = foundLostPet(child_player)
        assert result is None

    def test_found_lost_pet_answer_takes_to_vet(self, child_player):
        """Taking to vet should cost energy"""
        with patch('random.random', return_value=0.0):
            result = foundLostPet(child_player)

        child_player.c.energy = 100
        response = {'option': 'Take it to vet to scan chip'}
        foundLostPet(child_player, type='answer', response=response)
        assert child_player.c.energy == 85

    def test_found_lost_pet_step_2_vet_resolution(self, child_player):
        """Step 2 vet option should increase happiness and social significantly"""
        dilemma = dilemmaClass('foundLostPet', [])
        dilemma.step = 2
        dilemma.answer = answerOption('Take it to vet to scan chip', energyCost=15)
        child_player.activeDilemmas.append(dilemma)

        child_player.c.happiness = 60
        child_player.c.social = 50
        foundLostPet(child_player, dilemma=dilemma)

        assert child_player.c.happiness == 80
        assert child_player.c.social == 65
        assert len(child_player.activeDilemmas) == 0


class TestStrayAnimalDecision:
    """Test strayAnimalDecision dilemma (ages 10+)"""

    @patch('random.random', return_value=0.0)
    @patch('random.choice', return_value='cat')
    def test_stray_animal_triggers_at_correct_age(self, mock_choice, mock_random, teen_player):
        """Event should trigger at age 10+"""
        teen_player.c.ageYears = 12
        result = strayAnimalDecision(teen_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    def test_stray_animal_answer_adopt(self, teen_player):
        """Adopting should cost money and energy"""
        with patch('random.random', return_value=0.0):
            with patch('random.choice', return_value='dog'):
                result = strayAnimalDecision(teen_player)

        teen_player.c.money = 500
        teen_player.c.energy = 100
        response = {'option': 'Adopt it'}
        strayAnimalDecision(teen_player, type='answer', response=response)
        assert teen_player.c.money == 300
        assert teen_player.c.energy == 85


# ============================================================================
# SOCIAL DILEMMA TESTS
# ============================================================================

class TestFriendCheating:
    """Test friendCheating dilemma (ages 16+)"""

    @patch('random.random', return_value=0.0)
    @patch('functions.get_random_friend')
    def test_friend_cheating_triggers_at_correct_age(self, mock_friend, mock_random, teen_player):
        """Event should trigger at age 16+"""
        friend = personClass()
        friend.firstname = "Friend"
        friend.lastname = "Test"
        friend.id = "friend123"
        friend.affinity = 70
        mock_friend.return_value = friend

        teen_player.c.ageYears = 18
        result = friendCheating(teen_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    @patch('random.random', return_value=0.0)
    @patch('functions.get_random_friend')
    def test_friend_cheating_not_before_age(self, mock_friend, mock_random, teen_player):
        """Event should not trigger before age 16"""
        friend = personClass()
        friend.firstname = "Friend"
        friend.lastname = "Test"
        friend.id = "friend123"
        mock_friend.return_value = friend

        teen_player.c.ageYears = 15
        result = friendCheating(teen_player)
        assert result is None

    @patch('functions.get_person')
    def test_friend_cheating_step_2_tell_partner(self, mock_get_person, teen_player):
        """Telling partner should damage friendship"""
        friend = personClass()
        friend.firstname = "Friend"
        friend.lastname = "Test"
        friend.id = "friend123"
        friend.affinity = 70
        mock_get_person.return_value = friend

        dilemma = dilemmaClass('friendCheating', [])
        dilemma.friend = "friend123"
        dilemma.step = 2
        dilemma.answer = answerOption('Tell the partner', energyCost=10)
        teen_player.activeDilemmas.append(dilemma)

        friendCheating(teen_player, dilemma=dilemma)

        assert friend.affinity == 40  # Decreased by 30
        assert teen_player.c.social <= 60


class TestFriendBorrowMoney:
    """Test friendBorrowMoney dilemma (ages 18+, money >= 500)"""

    @patch('random.random', return_value=0.0)
    @patch('functions.get_random_friend')
    def test_friend_borrow_money_triggers_with_money(self, mock_friend, mock_random, adult_player):
        """Event should trigger if player has >= $500"""
        friend = personClass()
        friend.firstname = "Friend"
        friend.lastname = "Test"
        friend.id = "friend123"
        mock_friend.return_value = friend

        adult_player.c.ageYears = 25
        adult_player.c.money = 1000
        result = friendBorrowMoney(adult_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    @patch('random.random', return_value=0.0)
    @patch('functions.get_random_friend')
    def test_friend_borrow_money_not_without_money(self, mock_friend, mock_random, adult_player):
        """Event should not trigger if player has < $500"""
        friend = personClass()
        friend.firstname = "Friend"
        friend.lastname = "Test"
        friend.id = "friend123"
        mock_friend.return_value = friend

        adult_player.c.ageYears = 25
        adult_player.c.money = 400
        result = friendBorrowMoney(adult_player)
        assert result is None


class TestWitnessShoplifting:
    """Test witnessShoplifting dilemma (ages 12+)"""

    @patch('random.random', return_value=0.0)
    def test_witness_shoplifting_triggers_at_correct_age(self, mock_random, teen_player):
        """Event should trigger at age 12+"""
        teen_player.c.ageYears = 14
        result = witnessShoplifting(teen_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    def test_witness_shoplifting_answer_buy_food(self, teen_player):
        """Buying food for them should cost money and diamonds"""
        with patch('random.random', return_value=0.0):
            result = witnessShoplifting(teen_player)

        teen_player.c.money = 100
        teen_player.c.diamonds = 20
        response = {'option': 'Offer to buy them food'}
        witnessShoplifting(teen_player, type='answer', response=response)
        assert teen_player.c.money == 80
        assert teen_player.c.diamonds == 15


# ============================================================================
# WORK DILEMMA TESTS
# ============================================================================

class TestColleagueStealing Credit:
    """Test colleagueStealingCredit dilemma (ages 22-65, work occupation)"""

    @patch('random.random', return_value=0.0)
    def test_colleague_stealing_requires_work(self, mock_random, adult_player):
        """Event should require work occupation"""
        adult_player.c.ageYears = 30
        adult_player.c.occupation = "work"
        result = colleagueStealingCredit(adult_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    @patch('random.random', return_value=0.0)
    def test_colleague_stealing_not_without_work(self, mock_random, adult_player):
        """Event should not trigger without work occupation"""
        adult_player.c.ageYears = 30
        adult_player.c.occupation = "student"
        result = colleagueStealingCredit(adult_player)
        assert result is None

    def test_colleague_stealing_step_2_talk_privately(self, adult_player):
        """Talking privately should improve social and happiness"""
        dilemma = dilemmaClass('colleagueStealingCredit', [])
        dilemma.step = 2
        dilemma.answer = answerOption('Talk to them privately', energyCost=10)
        adult_player.activeDilemmas.append(dilemma)

        adult_player.c.social = 60
        adult_player.c.happiness = 70
        colleagueStealingCredit(adult_player, dilemma=dilemma)

        assert adult_player.c.social == 65
        assert adult_player.c.happiness == 80


class TestWhistleblowerDecision:
    """Test whistleblowerDecision dilemma (ages 25-65, work occupation)"""

    @patch('random.random', return_value=0.0)
    def test_whistleblower_triggers_at_work(self, mock_random, adult_player):
        """Event should trigger for working adults"""
        adult_player.c.ageYears = 35
        adult_player.c.occupation = "work"
        result = whistleblowerDecision(adult_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    def test_whistleblower_answer_stay_silent(self, adult_player):
        """Staying silent should give negative money cost (bonus)"""
        with patch('random.random', return_value=0.0):
            result = whistleblowerDecision(adult_player)

        adult_player.c.money = 1000
        response = {'option': 'Stay silent'}
        whistleblowerDecision(adult_player, type='answer', response=response)
        assert adult_player.c.money == 1500  # Got $500 bonus


# ============================================================================
# LIFE CHOICE DILEMMA TESTS
# ============================================================================

class TestFoundExpensiveItem:
    """Test foundExpensiveItem dilemma (ages 10+)"""

    @patch('random.random', return_value=0.0)
    def test_found_expensive_item_triggers_at_correct_age(self, mock_random, child_player):
        """Event should trigger at age 10+"""
        child_player.c.ageYears = 12
        result = foundExpensiveItem(child_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    def test_found_expensive_item_step_2_turn_in(self, child_player):
        """Turning in should give reward and increase happiness, social, diamonds"""
        dilemma = dilemmaClass('foundExpensiveItem', [])
        dilemma.step = 2
        dilemma.answer = answerOption('Turn it in to store')
        child_player.activeDilemmas.append(dilemma)

        child_player.c.happiness = 60
        child_player.c.social = 50
        child_player.c.money = 100
        child_player.c.diamonds = 10
        foundExpensiveItem(child_player, dilemma=dilemma)

        assert child_player.c.happiness == 75
        assert child_player.c.social == 70
        assert child_player.c.money == 150
        assert child_player.c.diamonds == 20


class TestEnvironmentalChoice:
    """Test environmentalChoice dilemma (ages 16+)"""

    @patch('random.random', return_value=0.0)
    def test_environmental_choice_triggers_at_correct_age(self, mock_random, teen_player):
        """Event should trigger at age 16+"""
        teen_player.c.ageYears = 18
        result = environmentalChoice(teen_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    def test_environmental_choice_answer_eco_option(self, adult_player):
        """Eco-friendly option should cost more but increase happiness"""
        with patch('random.random', return_value=0.0):
            result = environmentalChoice(adult_player)

        adult_player.c.money = 1000
        response = {'option': 'Eco-friendly option'}
        environmentalChoice(adult_player, type='answer', response=response)
        assert adult_player.c.money == 600


class TestParentCareDecision:
    """Test parentCareDecision dilemma (ages 40-70)"""

    @patch('random.random', return_value=0.0)
    @patch('random.choice', return_value='mother')
    def test_parent_care_triggers_at_correct_age(self, mock_choice, mock_random, adult_player):
        """Event should trigger between ages 40-70"""
        adult_player.c.ageYears = 50
        result = parentCareDecision(adult_player)
        assert result is not None
        assert isinstance(result, questionEvent)

    @patch('random.random', return_value=0.0)
    def test_parent_care_not_before_age(self, mock_random, adult_player):
        """Event should not trigger before age 40"""
        adult_player.c.ageYears = 35
        result = parentCareDecision(adult_player)
        assert result is None


# ============================================================================
# DILEMMA STATE PROGRESSION TESTS
# ============================================================================

class TestDilemmaStateProgression:
    """Test dilemma multi-step state machine"""

    @patch('random.random', return_value=0.0)
    def test_dilemma_creates_activeDilemmas_entry(self, mock_random, child_player):
        """Initial dilemma should create entry in activeDilemmas"""
        child_player.c.ageYears = 10
        result = foundLostPet(child_player)
        assert len(child_player.activeDilemmas) == 1

    def test_dilemma_answer_updates_dilemma_object(self, child_player):
        """Answering should update the dilemma object"""
        with patch('random.random', return_value=0.0):
            result = foundLostPet(child_player)

        response = {'option': 'Post on social media'}
        foundLostPet(child_player, type='answer', response=response)
        assert child_player.activeDilemmas[0].answer is not None

    def test_dilemma_step_2_removes_from_active(self, child_player):
        """Step 2 resolution should remove dilemma from activeDilemmas"""
        dilemma = dilemmaClass('foundLostPet', [])
        dilemma.step = 2
        dilemma.answer = answerOption('Post on social media')
        child_player.activeDilemmas.append(dilemma)

        foundLostPet(child_player, dilemma=dilemma)
        assert len(child_player.activeDilemmas) == 0


# ============================================================================
# SUMMARY
# ============================================================================
"""
Test Summary:
=============

School Dilemmas (2 events):
- braceletDilemma (5 tests)
- bullyDilemma (2 tests)

Animal Dilemmas (2 events):
- foundLostPet (4 tests)
- strayAnimalDecision (2 tests)

Social Dilemmas (3 events):
- friendCheating (3 tests)
- friendBorrowMoney (2 tests)
- witnessShoplifting (2 tests)

Work Dilemmas (2 events):
- colleagueStealingCredit (3 tests)
- whistleblowerDecision (2 tests)

Life Choice Dilemmas (3 events):
- foundExpensiveItem (2 tests)
- environmentalChoice (2 tests)
- parentCareDecision (2 tests)

State Progression Tests (1 class):
- DilemmaStateProgression (3 tests)

Total: 12 dilemma events tested with 37 test cases

Test Coverage:
- Age range validation
- Condition requirements (location, occupation, friends)
- Duplicate prevention
- Cost/stat application
- Choice consequences
- Dilemma state machine progression (step 1 → answer → step 2)
- Active dilemma tracking
- Multi-step resolution
"""
