"""
Tests for dating relationship events system.

Tests the relationship_events module functionality:
- Event triggering based on conditions
- Resolution options and consequences
- Affinity changes
- Diamond costs
- Event history tracking
"""

import pytest
from unittest.mock import Mock, MagicMock, patch
from datetime import datetime, timedelta
from types import SimpleNamespace

from dating.relationship_events import (
    check_relationship_triggers,
    trigger_event,
    process_resolution,
    get_event_history,
    get_relationship_stats,
    get_event_by_type,
    has_recent_event,
    has_met_family,
    get_affinity_change_24h,
    RELATIONSHIP_EVENTS
)


@pytest.fixture
def mock_player():
    """Create a mock player object for testing"""
    player = Mock()
    player.c = Mock()
    player.c.id = 'player_123'
    player.c.diamonds = 100
    player.c.married = False
    player.r = []  # Relationships list
    player.relData = []
    player.date = datetime.now()
    player.hourOfDay = 12
    player.gameSpeed = 1000
    player.previousGameSpeed = 1000
    player.messageQueue = []
    return player


@pytest.fixture
def mock_npc():
    """Create a mock NPC object for testing"""
    npc = Mock()
    npc.id = 'npc_456'
    npc.firstname = 'Alice'
    npc.lastname = 'Johnson'
    npc.affinity = 50
    npc.relationships = ['partner']
    return npc


@pytest.fixture
def mock_relationship():
    """Create a mock relationship object"""
    rel = Mock()
    rel.person1_id = 'player_123'
    rel.person2_id = 'npc_456'
    rel.started_date = datetime.now() - timedelta(days=365)  # 1 year ago
    return rel


class TestEventDefinitions:
    """Test event definitions and structure"""

    def test_all_events_have_required_fields(self):
        """Test that all event definitions have required fields"""
        required_fields = ['type', 'trigger', 'description', 'options']

        for event in RELATIONSHIP_EVENTS:
            for field in required_fields:
                assert field in event, f"Event missing required field: {field}"

    def test_all_events_have_valid_options(self):
        """Test that all event options have required fields"""
        required_option_fields = ['choice', 'label', 'affinity_change', 'diamond_cost']

        for event in RELATIONSHIP_EVENTS:
            assert len(event['options']) > 0, f"Event {event['type']} has no options"

            for option in event['options']:
                for field in required_option_fields:
                    assert field in option, f"Option missing required field: {field}"

    def test_event_types_are_unique(self):
        """Test that event types are unique"""
        event_types = [event['type'] for event in RELATIONSHIP_EVENTS]
        assert len(event_types) == len(set(event_types)), "Duplicate event types found"

    def test_diamond_costs_are_non_negative(self):
        """Test that diamond costs are non-negative"""
        for event in RELATIONSHIP_EVENTS:
            for option in event['options']:
                assert option['diamond_cost'] >= 0, f"Negative diamond cost in {event['type']}"


class TestEventTriggering:
    """Test event triggering logic"""

    @patch('dating.relationship_events.get_affinity_change_24h')
    @patch('dating.relationship_events.has_recent_event')
    def test_argument_trigger_on_affinity_drop(self, mock_has_recent, mock_affinity_change,
                                               mock_player, mock_npc):
        """Test that argument triggers when affinity drops significantly"""
        mock_affinity_change.return_value = -20
        mock_has_recent.return_value = False
        mock_npc.affinity = 40

        event = check_relationship_triggers(mock_player, mock_npc)

        assert event is not None
        assert event['type'] == 'argument'
        assert 'Alice Johnson' in event['description']

    @patch('dating.relationship_events.has_recent_event')
    def test_breakup_threat_trigger_on_low_affinity(self, mock_has_recent,
                                                     mock_player, mock_npc):
        """Test that breakup threat triggers when affinity is very low"""
        mock_has_recent.return_value = False
        mock_npc.affinity = 15

        event = check_relationship_triggers(mock_player, mock_npc)

        assert event is not None
        assert event['type'] == 'breakup_threat'

    @patch('dating.relationship_events.has_recent_event')
    def test_proposal_trigger_on_high_affinity(self, mock_has_recent,
                                               mock_player, mock_npc):
        """Test that proposal triggers when affinity is very high"""
        mock_has_recent.return_value = False
        mock_npc.affinity = 95

        event = check_relationship_triggers(mock_player, mock_npc)

        assert event is not None
        assert event['type'] == 'proposal'

    @patch('dating.relationship_events.has_recent_event')
    @patch('dating.relationship_events.get_affinity_change_24h')
    def test_anniversary_trigger(self, mock_affinity_change, mock_has_recent,
                                 mock_player, mock_npc, mock_relationship):
        """Test that anniversary triggers at correct time"""
        mock_has_recent.return_value = False
        mock_affinity_change.return_value = 0
        mock_player.relData = [mock_relationship]

        # Set relationship to exactly 1 year ago
        mock_relationship.started_date = datetime.now() - timedelta(days=365)

        event = check_relationship_triggers(mock_player, mock_npc)

        # Note: This might not trigger in every test run due to exact date matching
        # In production, you might want to add a more flexible date range
        if event:
            assert event['type'] == 'anniversary'

    def test_no_trigger_for_non_romantic_relationship(self, mock_player, mock_npc):
        """Test that events don't trigger for non-romantic relationships"""
        mock_npc.relationships = ['friend']  # Not romantic

        event = check_relationship_triggers(mock_player, mock_npc)

        assert event is None

    @patch('dating.relationship_events.has_recent_event')
    @patch('dating.relationship_events.get_affinity_change_24h')
    def test_no_trigger_with_recent_cooldown(self, mock_affinity_change, mock_has_recent,
                                             mock_player, mock_npc):
        """Test that events respect cooldown periods"""
        mock_affinity_change.return_value = -20
        mock_has_recent.return_value = True  # Event happened recently
        mock_npc.affinity = 40

        event = check_relationship_triggers(mock_player, mock_npc)

        # Should not trigger due to cooldown
        # Note: Other events might trigger, so we just check argument doesn't
        if event:
            assert event['type'] != 'argument'


class TestEventResolution:
    """Test event resolution processing"""

    @patch('monetization.diamond_economy.deduct_diamonds')
    @patch('dating.relationship_events.transaction')
    def test_resolution_with_diamond_cost(self, mock_transaction, mock_deduct,
                                         mock_player, mock_npc):
        """Test resolution that costs diamonds"""
        mock_player.r = [mock_npc]
        mock_npc.affinity = 50

        # Mock diamond deduction
        mock_deduct.return_value = {'success': True, 'new_balance': 80}

        # Mock transaction context manager
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = (1,)  # event_id
        mock_transaction.return_value.__enter__.return_value = (mock_conn, mock_cursor)

        result = process_resolution(
            mock_player,
            'npc_456',
            'argument',
            'buy_gift',
            affinity_change=25,
            diamond_cost=20
        )

        assert result['success'] is True
        mock_deduct.assert_called_once_with(mock_player.c.id, 'relationship_event_argument', 20)
        assert mock_npc.affinity == 75  # 50 + 25

    @patch('monetization.diamond_economy.deduct_diamonds')
    def test_resolution_insufficient_diamonds(self, mock_deduct, mock_player, mock_npc):
        """Test resolution fails with insufficient diamonds"""
        mock_player.c.diamonds = 10  # Not enough
        mock_player.r = [mock_npc]

        # Mock diamond deduction failure
        mock_deduct.return_value = {
            'success': False,
            'message': 'Insufficient diamonds. Need 20, have 10'
        }

        result = process_resolution(
            mock_player,
            'npc_456',
            'argument',
            'buy_gift',
            affinity_change=25,
            diamond_cost=20
        )

        assert result['success'] is False
        assert 'diamonds' in result['message'].lower()
        assert mock_npc.affinity == 50  # Unchanged

    @patch('dating.relationship_events.transaction')
    def test_resolution_free_option(self, mock_transaction, mock_player, mock_npc):
        """Test resolution with free option (no diamonds)"""
        mock_player.r = [mock_npc]
        mock_npc.affinity = 50

        # Mock transaction context manager
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = (1,)
        mock_transaction.return_value.__enter__.return_value = (mock_conn, mock_cursor)

        result = process_resolution(
            mock_player,
            'npc_456',
            'argument',
            'apologize',
            affinity_change=10,
            diamond_cost=0
        )

        assert result['success'] is True
        assert mock_player.c.diamonds == 100  # Unchanged
        assert mock_npc.affinity == 60  # 50 + 10

    @patch('dating.relationship_events.transaction')
    def test_resolution_negative_affinity_change(self, mock_transaction, mock_player, mock_npc):
        """Test resolution that decreases affinity"""
        mock_player.r = [mock_npc]
        mock_npc.affinity = 50

        # Mock transaction context manager
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = (1,)
        mock_transaction.return_value.__enter__.return_value = (mock_conn, mock_cursor)

        result = process_resolution(
            mock_player,
            'npc_456',
            'argument',
            'ignore',
            affinity_change=-10,
            diamond_cost=0
        )

        assert result['success'] is True
        assert mock_npc.affinity == 40  # 50 - 10

    @patch('dating.relationship_events.transaction')
    def test_affinity_capped_at_bounds(self, mock_transaction, mock_player, mock_npc):
        """Test that affinity is capped between -100 and 100"""
        mock_player.r = [mock_npc]

        # Mock transaction context manager
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = (1,)
        mock_transaction.return_value.__enter__.return_value = (mock_conn, mock_cursor)

        # Test upper bound
        mock_npc.affinity = 90
        process_resolution(mock_player, 'npc_456', 'proposal', 'accept',
                          affinity_change=30, diamond_cost=0)
        assert mock_npc.affinity == 100  # Capped at 100

        # Test lower bound
        mock_npc.affinity = -90
        process_resolution(mock_player, 'npc_456', 'breakup_threat', 'decline',
                          affinity_change=-40, diamond_cost=0)
        assert mock_npc.affinity == -100  # Capped at -100


class TestEventHistory:
    """Test event history tracking"""

    @patch('dating.relationship_events.get_database_connection')
    def test_get_event_history_for_player(self, mock_db_conn):
        """Test retrieving event history for a player"""
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchall.return_value = [
            {
                'id': 1,
                'npc_id': 'npc_456',
                'event_type': 'argument',
                'description_template': 'Test description',
                'resolution_chosen': 'apologize',
                'affinity_change': 10,
                'diamond_cost': 0,
                'occurred_date': datetime.now()
            }
        ]
        mock_conn.cursor.return_value = mock_cursor
        mock_db_conn.return_value = mock_conn

        history = get_event_history('player_123')

        assert len(history) == 1
        assert history[0]['event_type'] == 'argument'
        assert history[0]['resolution_chosen'] == 'apologize'

    @patch('dating.relationship_events.get_database_connection')
    def test_get_event_history_for_specific_npc(self, mock_db_conn):
        """Test retrieving event history for a specific NPC"""
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchall.return_value = []
        mock_conn.cursor.return_value = mock_cursor
        mock_db_conn.return_value = mock_conn

        history = get_event_history('player_123', npc_id='npc_456')

        # Should have called with npc_id parameter
        mock_cursor.execute.assert_called_once()
        call_args = mock_cursor.execute.call_args[0]
        assert 'npc_id' in call_args[0]  # SQL query should include npc_id filter

    @patch('dating.relationship_events.get_database_connection')
    def test_get_relationship_stats(self, mock_db_conn):
        """Test retrieving relationship statistics"""
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = {
            'total_events': 5,
            'total_affinity_change': 50,
            'total_diamonds_spent': 100,
            'first_event': datetime.now() - timedelta(days=30),
            'last_event': datetime.now()
        }
        mock_conn.cursor.return_value = mock_cursor
        mock_db_conn.return_value = mock_conn

        stats = get_relationship_stats('player_123', 'npc_456')

        assert stats['total_events'] == 5
        assert stats['total_affinity_change'] == 50
        assert stats['total_diamonds_spent'] == 100


class TestHelperFunctions:
    """Test helper functions"""

    def test_get_event_by_type(self, mock_npc):
        """Test getting event by type"""
        event = get_event_by_type('argument', mock_npc)

        assert event is not None
        assert event['type'] == 'argument'
        assert 'Alice Johnson' in event['description']
        assert event['npc_id'] == 'npc_456'

    def test_get_event_by_type_with_formatting(self, mock_npc):
        """Test getting event with custom formatting"""
        event = get_event_by_type('anniversary', mock_npc, years=2)

        assert event is not None
        assert event['type'] == 'anniversary'
        assert 'Alice Johnson' in event['description']

    def test_get_event_by_type_invalid(self, mock_npc):
        """Test getting non-existent event type"""
        event = get_event_by_type('nonexistent', mock_npc)

        assert event is None

    @patch('dating.relationship_events.get_database_connection')
    def test_has_recent_event_true(self, mock_db_conn):
        """Test has_recent_event returns True when event exists"""
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = {'count': 1}
        mock_conn.cursor.return_value = mock_cursor
        mock_db_conn.return_value = mock_conn

        result = has_recent_event('player_123', 'npc_456', 'argument', days=7)

        assert result is True

    @patch('dating.relationship_events.get_database_connection')
    def test_has_recent_event_false(self, mock_db_conn):
        """Test has_recent_event returns False when no event exists"""
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = {'count': 0}
        mock_conn.cursor.return_value = mock_cursor
        mock_db_conn.return_value = mock_conn

        result = has_recent_event('player_123', 'npc_456', 'argument', days=7)

        assert result is False

    @patch('dating.relationship_events.get_database_connection')
    def test_has_met_family_true(self, mock_db_conn):
        """Test has_met_family returns True when family meeting occurred"""
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = {'count': 1}
        mock_conn.cursor.return_value = mock_cursor
        mock_db_conn.return_value = mock_conn

        result = has_met_family('player_123', 'npc_456')

        assert result is True

    @patch('dating.relationship_events.get_database_connection')
    def test_has_met_family_false(self, mock_db_conn):
        """Test has_met_family returns False when no family meeting"""
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = {'count': 0}
        mock_conn.cursor.return_value = mock_cursor
        mock_db_conn.return_value = mock_conn

        result = has_met_family('player_123', 'npc_456')

        assert result is False

    @patch('dating.relationship_events.get_database_connection')
    def test_get_affinity_change_24h(self, mock_db_conn):
        """Test getting affinity change in last 24 hours"""
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = {'total_change': -20}
        mock_conn.cursor.return_value = mock_cursor
        mock_db_conn.return_value = mock_conn

        change = get_affinity_change_24h('player_123', 'npc_456')

        assert change == -20


class TestTriggerEvent:
    """Test event triggering function"""

    @patch('events.questionEvent')
    @patch('events.answerOption')
    def test_trigger_event_creates_question(self, mock_answer_class, mock_question_class,
                                           mock_player, mock_npc):
        """Test that trigger_event creates a proper question event"""
        mock_question = Mock()
        mock_question_class.return_value = mock_question

        mock_answer = Mock()
        mock_answer_class.return_value = mock_answer

        event = get_event_by_type('argument', mock_npc)
        result = trigger_event(mock_player, 'npc_456', event)

        assert result is not None
        assert mock_player.gameSpeed == 100000  # Game paused

    @patch('events.questionEvent')
    @patch('events.answerOption')
    def test_trigger_event_saves_previous_speed(self, mock_answer_class, mock_question_class,
                                                mock_player, mock_npc):
        """Test that trigger_event saves previous game speed"""
        mock_question = Mock()
        mock_question_class.return_value = mock_question

        mock_answer = Mock()
        mock_answer_class.return_value = mock_answer

        mock_player.gameSpeed = 5000

        event = get_event_by_type('argument', mock_npc)
        trigger_event(mock_player, 'npc_456', event)

        assert mock_player.previousGameSpeed == 5000


class TestIntegration:
    """Integration tests for full workflow"""

    @patch('dating.relationship_events.get_database_connection')
    @patch('dating.relationship_events.get_affinity_change_24h')
    @patch('dating.relationship_events.has_recent_event')
    def test_full_argument_workflow(self, mock_has_recent,
                                   mock_affinity_change, mock_db_conn,
                                   mock_player, mock_npc):
        """Test complete workflow: trigger -> resolve -> verify"""
        # Setup
        mock_affinity_change.return_value = -20
        mock_has_recent.return_value = False
        mock_npc.affinity = 40
        mock_player.r = [mock_npc]

        # Mock database
        mock_conn = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = (1,)
        mock_conn.cursor.return_value = mock_cursor
        mock_db_conn.return_value = mock_conn

        # 1. Check for trigger
        event = check_relationship_triggers(mock_player, mock_npc)
        assert event is not None
        assert event['type'] == 'argument'

        # 2. Trigger event (in real system this would show to player)
        # We skip this in test as it requires full event system

        # 3. Process resolution
        result = process_resolution(
            mock_player,
            'npc_456',
            'argument',
            'apologize',
            affinity_change=10,
            diamond_cost=0
        )

        # 4. Verify results
        assert result['success'] is True
        assert mock_npc.affinity == 50  # 40 + 10
        assert mock_player.c.diamonds == 100  # No cost


if __name__ == '__main__':
    pytest.main([__file__, '-v'])
