"""
Unit tests for event base classes (ws/events/base.py).

Tests the core event system classes and helper functions:
- messageEvent: One-way notifications with costs/rewards
- questionEvent: Binary/multiple choice questions that pause game
- answerOption: Answer choices with effects
- dilemmaClass: Multi-step decision trees
- Helper functions: messageFunction, questionFunction

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

import pytest
from datetime import datetime
from unittest.mock import Mock, MagicMock, patch
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 events.base import (
    messageEvent,
    questionEvent,
    answerOption,
    dilemmaClass,
    messageFunction,
    questionFunction,
)
from config import config


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

@pytest.fixture
def basic_player():
    """Create a basic test player with minimal setup (mocked to avoid DB calls)"""
    # Create a mock player object without full initialization
    player = Mock()
    player.userID = "test_user_123"
    player.date = "11-14"
    player.hourOfDay = 12
    player.gameSpeed = config.SPEED_DEFAULT
    player.previousGameSpeed = config.SPEED_DEFAULT
    player.events = set()
    player.askedQuestions = set()

    # Create mock character
    player.c = Mock()
    player.c.money = 1000
    player.c.energy = 100
    player.c.happiness = 75

    return player


@pytest.fixture
def test_character():
    """Create a test character/person (mocked to avoid DB calls)"""
    char = Mock()
    char.firstname = "John"
    char.lastname = "Doe"
    char.id = "char_123"
    char.image = "test_image.png"
    return char


# ============================================================================
# messageEvent TESTS (6 tests)
# ============================================================================

def test_message_event_creation():
    """Test that messageEvent creates with title, description, datetime"""
    # Arrange & Act
    event = messageEvent()
    event.id = "test_event"
    event.title = "Test Title"
    event.message = "Test message description"
    event.date = "11-14"
    event.hour = 12

    # Assert
    assert event.id == "test_event"
    assert event.title == "Test Title"
    assert event.message == "Test message description"
    assert event.date == "11-14"
    assert event.hour == 12
    assert event.type == 'messageEvent'


def test_message_event_with_cost():
    """Test that cost is properly set and can be deducted from money"""
    # Arrange
    event = messageEvent()
    event.moneyCost = 100
    initial_money = 1000

    # Act
    remaining_money = initial_money - event.moneyCost

    # Assert
    assert event.moneyCost == 100
    assert remaining_money == 900


def test_message_event_with_reward():
    """Test that reward is properly set and can be added to money"""
    # Arrange
    event = messageEvent()
    event.moneyCost = -50  # Negative cost = reward
    initial_money = 1000

    # Act
    new_money = initial_money - event.moneyCost  # Subtracting negative = adding

    # Assert
    assert event.moneyCost == -50
    assert new_money == 1050


def test_message_event_with_stat_changes():
    """Test that energyCost, diamondCost, and affinityChange are applied"""
    # Arrange
    event = messageEvent()
    event.energyCost = 10
    event.diamondCost = 5
    event.affinityChange = 15

    # Assert
    assert event.energyCost == 10
    assert event.diamondCost == 5
    assert event.affinityChange == 15


def test_message_event_serialization():
    """Test that messageEvent converts to dict for WebSocket (has all required fields)"""
    # Arrange
    event = messageEvent()
    event.id = "event_123"
    event.title = "Test"
    event.message = "Message"
    event.date = "11-14"
    event.hour = 12
    event.energyCost = 5
    event.moneyCost = 10
    event.diamondCost = 2
    event.affinityChange = 3
    event.image = "test.png"
    event.characters = [{"id": "1", "firstname": "John", "lastname": "Doe", "image": "john.png"}]

    # Act - convert to dict via __dict__
    event_dict = event.__dict__

    # Assert - all fields present
    assert event_dict['id'] == "event_123"
    assert event_dict['title'] == "Test"
    assert event_dict['message'] == "Message"
    assert event_dict['type'] == 'messageEvent'
    assert event_dict['energyCost'] == 5
    assert event_dict['moneyCost'] == 10
    assert event_dict['diamondCost'] == 2
    assert event_dict['affinityChange'] == 3
    assert event_dict['image'] == "test.png"
    assert len(event_dict['characters']) == 1


def test_message_event_does_not_pause_game():
    """Test that messageEvent does not pause game (gameSpeed unchanged)"""
    # Arrange
    player = Mock()
    player.gameSpeed = config.SPEED_DEFAULT
    initial_speed = player.gameSpeed

    # Act - message events don't modify gameSpeed
    event = messageEvent()

    # Assert
    assert player.gameSpeed == initial_speed


# ============================================================================
# questionEvent TESTS (7 tests)
# ============================================================================

def test_question_event_creation():
    """Test that questionEvent creates with title, question, answers"""
    # Arrange & Act
    event = questionEvent()
    event.id = "question_123"
    event.message = "Do you want to continue?"
    event.answers = [answerOption("Yes"), answerOption("No")]

    # Assert
    assert event.type == 'questionEvent'
    assert event.message == "Do you want to continue?"
    assert len(event.answers) == 2
    assert event.answers[0].option == "Yes"
    assert event.answers[1].option == "No"


def test_question_event_pauses_game(basic_player):
    """Test that questionEvent sets gameSpeed to SPEED_QUESTION_PAUSE"""
    # Arrange
    player = basic_player
    initial_speed = player.gameSpeed

    # Act
    result = questionFunction(
        "test_question",
        "Do you want to proceed?",
        player=player,
        check=True
    )

    # Assert
    assert player.previousGameSpeed == initial_speed
    assert player.gameSpeed == config.SPEED_QUESTION_PAUSE
    assert result is not None


def test_question_event_answer_validation():
    """Test that only valid answer IDs are accepted"""
    # Arrange
    event = questionEvent()
    event.answers = [
        answerOption("Option A", "0"),
        answerOption("Option B", "1"),
        answerOption("Option C", "2")
    ]

    # Act & Assert
    valid_ids = [ans.id for ans in event.answers]
    assert "0Option A" in valid_ids
    assert "1Option B" in valid_ids
    assert "2Option C" in valid_ids
    assert "3Option D" not in valid_ids


def test_question_event_executes_answer_function():
    """Test that answer's function is called when selected (via data field)"""
    # Arrange
    def test_callback():
        return "callback_executed"

    answer = answerOption("Test Answer", data="callback")

    # Assert - answer has data field that can store function name
    assert answer.data == "callback"
    assert answer.id == "callbackTest Answer"


def test_question_event_applies_answer_costs():
    """Test that answer cost/reward is properly set and can be applied"""
    # Arrange
    answer_with_cost = answerOption("Expensive", "exp", energyCost=20, moneyCost=100, diamondCost=5)
    answer_with_reward = answerOption("Profitable", "prof", energyCost=-10, moneyCost=-50)

    # Assert
    assert answer_with_cost.energyCost == 20
    assert answer_with_cost.moneyCost == 100
    assert answer_with_cost.diamondCost == 5

    assert answer_with_reward.energyCost == -10
    assert answer_with_reward.moneyCost == -50


def test_question_event_removes_from_queue_after_answer():
    """Test that event can be removed from player.askedQuestions"""
    # Arrange
    player = Mock()
    player.askedQuestions = set()
    event_id = "test_question_123"
    player.askedQuestions.add(event_id)

    # Act
    player.askedQuestions.discard(event_id)

    # Assert
    assert event_id not in player.askedQuestions


def test_question_event_resumes_game_after_answer():
    """Test that gameSpeed can be restored after answering"""
    # Arrange
    player = Mock()
    player.previousGameSpeed = config.SPEED_DEFAULT
    player.gameSpeed = config.SPEED_QUESTION_PAUSE

    # Act - simulate resuming game
    player.gameSpeed = player.previousGameSpeed

    # Assert
    assert player.gameSpeed == config.SPEED_DEFAULT


# ============================================================================
# answerOption TESTS (3 tests)
# ============================================================================

def test_answer_option_creation():
    """Test that answerOption creates with text, function, cost, reward"""
    # Arrange & Act
    answer = answerOption(
        option="Choose this",
        data="callback_function",
        energyCost=15,
        diamondCost=3,
        moneyCost=50
    )

    # Assert
    assert answer.option == "Choose this"
    assert answer.data == "callback_function"
    assert answer.id == "callback_functionChoose this"
    assert answer.energyCost == 15
    assert answer.diamondCost == 3
    assert answer.moneyCost == 50


def test_answer_option_with_stat_effects():
    """Test that stat changes can be defined on answer options"""
    # Arrange & Act
    answer = answerOption(
        "Work harder",
        data="work",
        energyCost=30,  # High energy cost
        moneyCost=-100  # Earns money (negative cost)
    )

    # Assert
    assert answer.energyCost == 30
    assert answer.moneyCost == -100


def test_answer_option_function_execution():
    """Test that function can be stored and identified via data field"""
    # Arrange
    answer = answerOption("Execute", data="my_function_name")

    # Act & Assert - data field stores function name for later execution
    assert answer.data == "my_function_name"
    assert answer.id == "my_function_nameExecute"


# ============================================================================
# dilemmaClass TESTS (4 tests)
# ============================================================================

def test_dilemma_creation():
    """Test that dilemma creates with multi-step decision tree"""
    # Arrange
    answer_options = [
        answerOption("Path A", "a"),
        answerOption("Path B", "b")
    ]

    # Act
    dilemma = dilemmaClass("test_dilemma", answer_options)

    # Assert
    assert dilemma.type == 'dilemma'
    assert dilemma.function == "test_dilemma"
    assert dilemma.answer is None
    assert len(dilemma.answerOptions) == 2
    assert dilemma.step == 2


def test_dilemma_state_progression():
    """Test that dilemma moves through steps correctly"""
    # Arrange
    dilemma = dilemmaClass("multi_step", [answerOption("Next", "1")])
    initial_step = dilemma.step

    # Act - simulate progression
    dilemma.answer = "1"
    dilemma.step = dilemma.step + 1

    # Assert
    assert dilemma.answer == "1"
    assert dilemma.step == initial_step + 1


def test_dilemma_completion():
    """Test that dilemma can reach completion state"""
    # Arrange
    dilemma = dilemmaClass("final_choice", [answerOption("Finish", "done")])

    # Act - simulate completion
    dilemma.answer = "done"
    dilemma.step = 10  # High step number indicates completion

    # Assert
    assert dilemma.answer == "done"
    assert dilemma.step >= 2


def test_dilemma_serialization():
    """Test that complex dilemma state serializes correctly"""
    # Arrange
    dilemma = dilemmaClass(
        "complex_dilemma",
        [
            answerOption("Option 1", "1", energyCost=10),
            answerOption("Option 2", "2", moneyCost=50)
        ]
    )
    dilemma.answer = "1"

    # Act - convert to dict
    dilemma_dict = dilemma.__dict__

    # Assert
    assert dilemma_dict['type'] == 'dilemma'
    assert dilemma_dict['function'] == "complex_dilemma"
    assert dilemma_dict['answer'] == "1"
    assert len(dilemma_dict['answerOptions']) == 2
    assert dilemma_dict['step'] == 2


# ============================================================================
# HELPER FUNCTIONS TESTS (6 tests)
# ============================================================================

def test_message_function_creates_message_event(basic_player):
    """Test that messageFunction() returns messageEvent"""
    # Arrange
    player = basic_player

    # Act
    event = messageFunction(
        "test_message",
        "This is a test message",
        player=player,
        check=True,
        title="Test Title"
    )

    # Assert
    assert event is not None
    assert event.type == 'messageEvent'
    assert event.id == "test_message"
    assert event.message == "This is a test message"
    assert event.title == "Test Title"


def test_question_function_creates_question_event(basic_player):
    """Test that questionFunction() returns questionEvent"""
    # Arrange
    player = basic_player

    # Act
    event = questionFunction(
        "test_question",
        "Do you agree?",
        player=player,
        check=True
    )

    # Assert
    assert event is not None
    assert event.id == "test_question"
    assert event.message == "Do you agree?"


def test_message_function_adds_to_player_events():
    """Test that event can be added to player.events set"""
    # Arrange
    player = Mock()
    player.events = set()
    event_id = "new_message_event"

    # Act
    player.events.add(event_id)

    # Assert
    assert event_id in player.events
    assert len(player.events) == 1


def test_question_function_adds_to_asked_questions():
    """Test that event can be added to player.askedQuestions set"""
    # Arrange
    player = Mock()
    player.askedQuestions = set()
    event_id = "new_question_event"

    # Act
    player.askedQuestions.add(event_id)

    # Assert
    assert event_id in player.askedQuestions
    assert len(player.askedQuestions) == 1


def test_message_function_prevents_duplicates():
    """Test that checking fname not in events prevents duplicates"""
    # Arrange
    player = Mock()
    player.events = set()
    event_id = "duplicate_test"
    player.events.add(event_id)

    # Act - check if already exists
    should_create = event_id not in player.events

    # Assert
    assert not should_create  # Should NOT create duplicate


def test_question_function_prevents_duplicates():
    """Test that checking fname not in askedQuestions prevents duplicates"""
    # Arrange
    player = Mock()
    player.askedQuestions = set()
    question_id = "duplicate_question"
    player.askedQuestions.add(question_id)

    # Act - check if already exists
    should_create = question_id not in player.askedQuestions

    # Assert
    assert not should_create  # Should NOT create duplicate


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

def test_event_with_no_datetime(basic_player):
    """Test that event uses current time if datetime not specified"""
    # Arrange
    player = basic_player

    # Act
    event = messageFunction(
        "no_time_event",
        "Message without explicit time",
        player=player,
        check=True
    )

    # Assert - should use player's current date/hour
    assert event.date == player.date
    assert event.hour == player.hourOfDay


def test_answer_option_with_no_function():
    """Test that answerOption works without function (no function is ok)"""
    # Arrange & Act
    answer = answerOption("Simple Answer")

    # Assert
    assert answer.option == "Simple Answer"
    assert answer.data == ""
    assert answer.id == "Simple Answer"


def test_message_event_negative_cost():
    """Test that negative values are handled (rewards)"""
    # Arrange
    event = messageEvent()
    event.energyCost = -20  # Energy reward
    event.moneyCost = -100  # Money reward
    event.diamondCost = -5  # Diamond reward

    # Act
    initial_money = 100
    new_money = initial_money - event.moneyCost

    # Assert
    assert event.energyCost == -20
    assert event.moneyCost == -100
    assert event.diamondCost == -5
    assert new_money == 200  # Money increased


def test_question_event_multiple_answers():
    """Test that questionEvent works with many answer options"""
    # Arrange
    event = questionEvent()
    answers = [
        answerOption(f"Option {i}", str(i))
        for i in range(10)
    ]
    event.answers = answers

    # Assert
    assert len(event.answers) == 10
    assert event.answers[0].option == "Option 0"
    assert event.answers[9].option == "Option 9"


# ============================================================================
# CHARACTER SERIALIZATION TESTS (2 additional tests)
# ============================================================================

def test_message_function_serializes_characters(basic_player, test_character):
    """Test that messageFunction properly serializes character data"""
    # Arrange
    player = basic_player
    characters = [test_character]

    # Act
    event = messageFunction(
        "char_message",
        "Event with character",
        player=player,
        check=True,
        characters=characters
    )

    # Assert
    assert len(event.characters) == 1
    assert event.characters[0]['id'] == test_character.id
    assert event.characters[0]['firstname'] == test_character.firstname
    assert event.characters[0]['lastname'] == test_character.lastname
    assert event.characters[0]['image'] == test_character.image


def test_question_function_serializes_characters(basic_player, test_character):
    """Test that questionFunction properly serializes character data"""
    # Arrange
    player = basic_player
    characters = [test_character]

    # Act
    event = questionFunction(
        "char_question",
        "Question with character",
        player=player,
        check=True,
        characters=characters
    )

    # Assert
    assert len(event.characters) == 1
    assert event.characters[0]['id'] == test_character.id
    assert event.characters[0]['firstname'] == test_character.firstname


# ============================================================================
# STRING ANSWER OPTIONS TEST (1 additional test)
# ============================================================================

def test_question_function_converts_string_answers_to_answer_options(basic_player):
    """Test that questionFunction converts string list to answerOption objects"""
    # Arrange
    player = basic_player
    string_answers = ["Yes", "No", "Maybe"]

    # Act
    event = questionFunction(
        "string_test",
        "Choose an option",
        player=player,
        check=True,
        answerOptions=string_answers
    )

    # Assert
    assert len(event.answers) == 3
    assert all(isinstance(ans, answerOption) for ans in event.answers)
    assert event.answers[0].option == "Yes"
    assert event.answers[1].option == "No"
    assert event.answers[2].option == "Maybe"
    # IDs should be numeric strings
    assert event.answers[0].data == "0"
    assert event.answers[1].data == "1"
    assert event.answers[2].data == "2"
