"""
Unit tests for CharacterService - character CRUD operations and tier limits.

These tests verify character creation, retrieval, deletion, skill unlock,
and respec functionality with proper validation and error handling.
"""

import pytest
import json
from unittest.mock import Mock, MagicMock, patch
from datetime import datetime, timezone

from app.services.character_service import (
    CharacterService,
    CharacterLimitExceeded,
    CharacterNotFound,
    SkillUnlockError,
    InsufficientGold,
    CHARACTER_LIMITS
)
from app.models.character import Character
from app.models.stats import Stats
from app.models.skills import PlayerClass, SkillTree, SkillNode
from app.models.origins import Origin, StartingLocation, StartingBonus
from app.services.database_service import DatabaseDocument


class TestCharacterService:
    """Test suite for CharacterService."""

    @pytest.fixture
    def mock_db(self):
        """Mock database service."""
        return Mock()

    @pytest.fixture
    def mock_appwrite(self):
        """Mock Appwrite service."""
        return Mock()

    @pytest.fixture
    def mock_class_loader(self):
        """Mock class loader."""
        return Mock()

    @pytest.fixture
    def mock_origin_service(self):
        """Mock origin service."""
        return Mock()

    @pytest.fixture
    def character_service(self, mock_db, mock_appwrite, mock_class_loader, mock_origin_service):
        """Create CharacterService with mocked dependencies."""
        # Patch the singleton getters before instantiation
        with patch('app.services.character_service.get_database_service', return_value=mock_db), \
             patch('app.services.character_service.AppwriteService', return_value=mock_appwrite), \
             patch('app.services.character_service.get_class_loader', return_value=mock_class_loader), \
             patch('app.services.character_service.get_origin_service', return_value=mock_origin_service):

            service = CharacterService()

        # Ensure mocks are still assigned
        service.db = mock_db
        service.appwrite = mock_appwrite
        service.class_loader = mock_class_loader
        service.origin_service = mock_origin_service
        return service

    @pytest.fixture
    def sample_class(self):
        """Create a sample player class."""
        base_stats = Stats(strength=12, dexterity=10, constitution=14)
        skill_tree = SkillTree(
            tree_id="warrior_offense",
            name="Warrior Offense",
            description="Offensive skills",
            nodes=[
                SkillNode(
                    skill_id="power_strike",
                    name="Power Strike",
                    description="+5 Strength",
                    tier=1,
                    effects={"strength": 5},
                ),
                SkillNode(
                    skill_id="heavy_blow",
                    name="Heavy Blow",
                    description="+10 Strength",
                    tier=2,
                    prerequisites=["power_strike"],
                    effects={"strength": 10},
                ),
            ],
        )

        return PlayerClass(
            class_id="warrior",
            name="Warrior",
            description="Strong fighter",
            base_stats=base_stats,
            skill_trees=[skill_tree],
            starting_equipment=["basic_sword"],
            starting_abilities=["basic_attack"],
        )

    @pytest.fixture
    def sample_origin(self):
        """Create a sample origin."""
        starting_location = StartingLocation(
            id="test_crypt",
            name="Test Crypt",
            region="Test Region",
            description="A test location"
        )

        starting_bonus = StartingBonus(
            trait="Test Trait",
            description="Test bonus",
            effect="+1 to all stats"
        )

        return Origin(
            id="test_origin",
            name="Test Origin",
            description="A test origin",
            starting_location=starting_location,
            narrative_hooks=["Test hook"],
            starting_bonus=starting_bonus
        )

    def test_create_character_success(
        self, character_service, mock_appwrite, mock_class_loader,
        mock_origin_service, mock_db, sample_class, sample_origin
    ):
        """Test successful character creation."""
        # Setup mocks
        mock_appwrite.get_user_tier.return_value = 'free'
        mock_class_loader.load_class.return_value = sample_class
        mock_origin_service.load_origin.return_value = sample_origin

        # Mock count_user_characters to return 0
        character_service.count_user_characters = Mock(return_value=0)

        # Create character
        with patch('app.services.character_service.ID') as mock_id:
            mock_id.unique.return_value = 'char_123'

            character = character_service.create_character(
                user_id='user_001',
                name='Test Hero',
                class_id='warrior',
                origin_id='test_origin'
            )

        # Assertions
        assert character.character_id == 'char_123'
        assert character.user_id == 'user_001'
        assert character.name == 'Test Hero'
        assert character.level == 1
        assert character.experience == 0
        assert character.gold == 0
        assert character.current_location == 'test_crypt'

        # Verify database was called
        mock_db.create_document.assert_called_once()

    def test_create_character_exceeds_limit(
        self, character_service, mock_appwrite, mock_class_loader, mock_origin_service
    ):
        """Test character creation fails when tier limit exceeded."""
        # Setup: user on free tier (limit 1) with 1 existing character
        mock_appwrite.get_user_tier.return_value = 'free'
        character_service.count_user_characters = Mock(return_value=1)

        # Attempt to create second character
        with pytest.raises(CharacterLimitExceeded) as exc_info:
            character_service.create_character(
                user_id='user_001',
                name='Second Hero',
                class_id='warrior',
                origin_id='test_origin'
            )

        assert 'free tier' in str(exc_info.value)
        assert '1/1' in str(exc_info.value)

    def test_create_character_tier_limits(self):
        """Test that tier limits are correctly defined."""
        assert CHARACTER_LIMITS['free'] == 1
        assert CHARACTER_LIMITS['basic'] == 3
        assert CHARACTER_LIMITS['premium'] == 5
        assert CHARACTER_LIMITS['elite'] == 10

    def test_create_character_invalid_class(
        self, character_service, mock_appwrite, mock_class_loader, mock_origin_service
    ):
        """Test character creation fails with invalid class."""
        mock_appwrite.get_user_tier.return_value = 'free'
        character_service.count_user_characters = Mock(return_value=0)
        mock_class_loader.load_class.return_value = None

        with pytest.raises(ValueError) as exc_info:
            character_service.create_character(
                user_id='user_001',
                name='Test Hero',
                class_id='invalid_class',
                origin_id='test_origin'
            )

        assert 'Class not found' in str(exc_info.value)

    def test_create_character_invalid_origin(
        self, character_service, mock_appwrite, mock_class_loader,
        mock_origin_service, sample_class
    ):
        """Test character creation fails with invalid origin."""
        mock_appwrite.get_user_tier.return_value = 'free'
        character_service.count_user_characters = Mock(return_value=0)
        mock_class_loader.load_class.return_value = sample_class
        mock_origin_service.load_origin.return_value = None

        with pytest.raises(ValueError) as exc_info:
            character_service.create_character(
                user_id='user_001',
                name='Test Hero',
                class_id='warrior',
                origin_id='invalid_origin'
            )

        assert 'Origin not found' in str(exc_info.value)

    def test_get_character_success(self, character_service, mock_db, sample_class, sample_origin):
        """Test successfully retrieving a character."""
        # Create test character data
        character = Character(
            character_id='char_123',
            user_id='user_001',
            name='Test Hero',
            player_class=sample_class,
            origin=sample_origin,
            level=5,
            gold=500
        )

        # Mock database response
        mock_doc = Mock(spec=DatabaseDocument)
        mock_doc.id = 'char_123'
        mock_doc.data = {
            'userId': 'user_001',
            'characterData': json.dumps(character.to_dict()),
            'is_active': True
        }
        mock_db.get_document.return_value = mock_doc

        # Get character
        result = character_service.get_character('char_123', 'user_001')

        # Assertions
        assert result.character_id == 'char_123'
        assert result.name == 'Test Hero'
        assert result.level == 5
        assert result.gold == 500

    def test_get_character_not_found(self, character_service, mock_db):
        """Test getting non-existent character raises error."""
        mock_db.get_document.return_value = None

        with pytest.raises(CharacterNotFound):
            character_service.get_character('nonexistent', 'user_001')

    def test_get_character_wrong_owner(self, character_service, mock_db):
        """Test getting character owned by different user raises error."""
        mock_doc = Mock(spec=DatabaseDocument)
        mock_doc.data = {'userId': 'user_002'}  # Different user
        mock_db.get_document.return_value = mock_doc

        with pytest.raises(CharacterNotFound):
            character_service.get_character('char_123', 'user_001')

    def test_get_user_characters(self, character_service, mock_db, sample_class, sample_origin):
        """Test getting all characters for a user."""
        # Create test character data
        char1_data = Character(
            character_id='char_1',
            user_id='user_001',
            name='Hero 1',
            player_class=sample_class,
            origin=sample_origin
        ).to_dict()

        char2_data = Character(
            character_id='char_2',
            user_id='user_001',
            name='Hero 2',
            player_class=sample_class,
            origin=sample_origin
        ).to_dict()

        # Mock database response
        mock_doc1 = Mock(spec=DatabaseDocument)
        mock_doc1.id = 'char_1'
        mock_doc1.data = {'characterData': json.dumps(char1_data)}

        mock_doc2 = Mock(spec=DatabaseDocument)
        mock_doc2.id = 'char_2'
        mock_doc2.data = {'characterData': json.dumps(char2_data)}

        mock_db.list_rows.return_value = [mock_doc1, mock_doc2]

        # Get characters
        characters = character_service.get_user_characters('user_001')

        # Assertions
        assert len(characters) == 2
        assert characters[0].name == 'Hero 1'
        assert characters[1].name == 'Hero 2'

    def test_count_user_characters(self, character_service, mock_db):
        """Test counting user's characters."""
        mock_db.count_documents.return_value = 3

        count = character_service.count_user_characters('user_001')

        assert count == 3
        mock_db.count_documents.assert_called_once()

    def test_delete_character_success(self, character_service, mock_db):
        """Test successfully deleting a character."""
        # Mock get_character to return a valid character
        character_service.get_character = Mock(return_value=Mock(character_id='char_123'))

        # Delete character
        result = character_service.delete_character('char_123', 'user_001')

        # Assertions
        assert result is True
        mock_db.update_document.assert_called_once()
        # Verify it's a soft delete (is_active set to False)
        call_args = mock_db.update_document.call_args
        assert call_args[1]['data']['is_active'] is False

    def test_delete_character_not_found(self, character_service):
        """Test deleting non-existent character raises error."""
        character_service.get_character = Mock(side_effect=CharacterNotFound("Not found"))

        with pytest.raises(CharacterNotFound):
            character_service.delete_character('nonexistent', 'user_001')

    def test_unlock_skill_success(self, character_service, sample_class, sample_origin):
        """Test successfully unlocking a skill."""
        # Create character with level 2 (1 skill point available)
        character = Character(
            character_id='char_123',
            user_id='user_001',
            name='Test Hero',
            player_class=sample_class,
            origin=sample_origin,
            level=2,
            unlocked_skills=[]
        )

        # Mock get_character
        character_service.get_character = Mock(return_value=character)

        # Mock _save_character
        character_service._save_character = Mock()

        # Unlock skill
        result = character_service.unlock_skill('char_123', 'user_001', 'power_strike')

        # Assertions
        assert 'power_strike' in result.unlocked_skills
        character_service._save_character.assert_called_once()

    def test_unlock_skill_already_unlocked(self, character_service, sample_class, sample_origin):
        """Test unlocking already unlocked skill raises error."""
        character = Character(
            character_id='char_123',
            user_id='user_001',
            name='Test Hero',
            player_class=sample_class,
            origin=sample_origin,
            level=2,
            unlocked_skills=['power_strike']  # Already unlocked
        )

        character_service.get_character = Mock(return_value=character)

        with pytest.raises(SkillUnlockError) as exc_info:
            character_service.unlock_skill('char_123', 'user_001', 'power_strike')

        assert 'already unlocked' in str(exc_info.value)

    def test_unlock_skill_not_in_class(self, character_service, sample_class, sample_origin):
        """Test unlocking skill not in class raises error."""
        character = Character(
            character_id='char_123',
            user_id='user_001',
            name='Test Hero',
            player_class=sample_class,
            origin=sample_origin,
            level=2,
            unlocked_skills=[]
        )

        character_service.get_character = Mock(return_value=character)

        with pytest.raises(SkillUnlockError) as exc_info:
            character_service.unlock_skill('char_123', 'user_001', 'invalid_skill')

        assert 'not found in class' in str(exc_info.value)

    def test_unlock_skill_missing_prerequisite(self, character_service, sample_class, sample_origin):
        """Test unlocking skill without prerequisite raises error."""
        character = Character(
            character_id='char_123',
            user_id='user_001',
            name='Test Hero',
            player_class=sample_class,
            origin=sample_origin,
            level=2,
            unlocked_skills=[]  # Missing 'power_strike' prerequisite
        )

        character_service.get_character = Mock(return_value=character)

        with pytest.raises(SkillUnlockError) as exc_info:
            character_service.unlock_skill('char_123', 'user_001', 'heavy_blow')

        assert 'Prerequisite not met' in str(exc_info.value)
        assert 'power_strike' in str(exc_info.value)

    def test_unlock_skill_no_points_available(self, character_service, sample_class, sample_origin):
        """Test unlocking skill without available points raises error."""
        # Add a tier 3 skill to test with
        tier3_skill = SkillNode(
            skill_id="master_strike",
            name="Master Strike",
            description="+15 Strength",
            tier=3,
            effects={"strength": 15},
        )
        sample_class.skill_trees[0].nodes.append(tier3_skill)

        # Level 1 character with 1 skill already unlocked = 0 points remaining
        character = Character(
            character_id='char_123',
            user_id='user_001',
            name='Test Hero',
            player_class=sample_class,
            origin=sample_origin,
            level=1,
            unlocked_skills=['power_strike']  # Used the 1 point from level 1
        )

        character_service.get_character = Mock(return_value=character)

        # Try to unlock another skill (master_strike exists in class)
        with pytest.raises(SkillUnlockError) as exc_info:
            character_service.unlock_skill('char_123', 'user_001', 'master_strike')

        assert 'No skill points available' in str(exc_info.value)

    def test_respec_skills_success(self, character_service, sample_class, sample_origin):
        """Test successfully respecing character skills."""
        # Level 5 character with 3 skills and 500 gold
        # Respec cost = 5 * 100 = 500 gold
        character = Character(
            character_id='char_123',
            user_id='user_001',
            name='Test Hero',
            player_class=sample_class,
            origin=sample_origin,
            level=5,
            gold=500,
            unlocked_skills=['skill1', 'skill2', 'skill3']
        )

        character_service.get_character = Mock(return_value=character)
        character_service._save_character = Mock()

        # Respec skills
        result = character_service.respec_skills('char_123', 'user_001')

        # Assertions
        assert len(result.unlocked_skills) == 0  # Skills cleared
        assert result.gold == 0  # 500 - 500 = 0
        character_service._save_character.assert_called_once()

    def test_respec_skills_insufficient_gold(self, character_service, sample_class, sample_origin):
        """Test respec fails with insufficient gold."""
        # Level 5 character with only 100 gold (needs 500)
        character = Character(
            character_id='char_123',
            user_id='user_001',
            name='Test Hero',
            player_class=sample_class,
            origin=sample_origin,
            level=5,
            gold=100,  # Not enough
            unlocked_skills=['skill1']
        )

        character_service.get_character = Mock(return_value=character)

        with pytest.raises(InsufficientGold) as exc_info:
            character_service.respec_skills('char_123', 'user_001')

        assert '500' in str(exc_info.value)  # Cost
        assert '100' in str(exc_info.value)  # Available

    def test_update_character_success(self, character_service, sample_class, sample_origin):
        """Test successfully updating a character."""
        character = Character(
            character_id='char_123',
            user_id='user_001',
            name='Test Hero',
            player_class=sample_class,
            origin=sample_origin,
            gold=1000  # Updated gold
        )

        # Mock get_character to verify ownership
        character_service.get_character = Mock(return_value=character)
        character_service._save_character = Mock()

        # Update character
        result = character_service.update_character(character, 'user_001')

        # Assertions
        assert result.gold == 1000
        character_service._save_character.assert_called_once()

    def test_update_character_not_found(self, character_service, sample_class, sample_origin):
        """Test updating non-existent character raises error."""
        character = Character(
            character_id='nonexistent',
            user_id='user_001',
            name='Test Hero',
            player_class=sample_class,
            origin=sample_origin
        )

        character_service.get_character = Mock(side_effect=CharacterNotFound("Not found"))

        with pytest.raises(CharacterNotFound):
            character_service.update_character(character, 'user_001')
