"""
Integration tests for Character API endpoints.

These tests verify the complete character management API flow including:
- List characters
- Get character details
- Create character (with tier limits)
- Delete character
- Unlock skills
- Respec skills
- Get classes and origins (reference data)

Tests use Flask test client with mocked authentication and database layers.
"""

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

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.character_service import (
    CharacterLimitExceeded,
    CharacterNotFound,
    SkillUnlockError,
    InsufficientGold
)
from app.services.database_service import DatabaseDocument


class TestCharacterAPIIntegration:
    """Integration tests for Character API endpoints."""

    @pytest.fixture
    def app(self):
        """Create Flask application for testing."""
        # Import here to avoid circular imports
        from app import create_app
        app = create_app()
        app.config['TESTING'] = True
        return app

    @pytest.fixture
    def client(self, app):
        """Create test client."""
        return app.test_client()

    @pytest.fixture(autouse=True)
    def mock_auth_decorator(self):
        """Mock the require_auth decorator to bypass authentication."""
        def pass_through_decorator(func):
            """Decorator that does nothing - passes through to the function."""
            return func

        with patch('app.api.characters.require_auth', side_effect=pass_through_decorator):
            yield

    @pytest.fixture
    def mock_user(self):
        """Create a mock authenticated user."""
        user = Mock()
        user.id = "test_user_123"
        user.email = "test@example.com"
        user.name = "Test User"
        user.tier = "free"
        user.email_verified = True
        return user

    @pytest.fixture
    def sample_class(self):
        """Create a sample player class."""
        base_stats = Stats(strength=14, dexterity=10, constitution=14,
                          intelligence=8, wisdom=10, charisma=9)

        skill_nodes = [
            SkillNode(
                skill_id="shield_wall",
                name="Shield Wall",
                description="Increase armor by 5",
                tier=1,
                prerequisites=[],
                effects={"armor": 5}
            ),
            SkillNode(
                skill_id="toughness",
                name="Toughness",
                description="Increase HP by 10",
                tier=2,
                prerequisites=["shield_wall"],
                effects={"hit_points": 10}
            )
        ]

        skill_tree = SkillTree(
            tree_id="shield_bearer",
            name="Shield Bearer",
            description="Defensive techniques",
            nodes=skill_nodes
        )

        return PlayerClass(
            class_id="vanguard",
            name="Vanguard",
            description="Armored warrior",
            base_stats=base_stats,
            skill_trees=[skill_tree],
            starting_equipment=["Rusty Sword", "Tattered Cloth Armor"],
            starting_abilities=[]
        )

    @pytest.fixture
    def sample_origin(self):
        """Create a sample origin."""
        starting_location = StartingLocation(
            id="forgotten_crypt",
            name="The Forgotten Crypt",
            region="The Deadlands",
            description="A crumbling stone tomb beneath a dead forest"
        )

        starting_bonus = StartingBonus(
            type="stat",
            value={"constitution": 1}
        )

        return Origin(
            id="soul_revenant",
            name="The Soul Revenant",
            description="You died. That much you remember...",
            starting_location=starting_location,
            narrative_hooks=["Who brought you back?"],
            starting_bonus=starting_bonus
        )

    @pytest.fixture
    def sample_character(self, sample_class, sample_origin):
        """Create a sample character."""
        return Character(
            character_id="char_123",
            user_id="test_user_123",
            name="Thorin Ironforge",
            player_class=sample_class,
            origin=sample_origin,
            level=5,
            experience=2400,
            base_stats=Stats(strength=16, dexterity=10, constitution=14,
                           intelligence=8, wisdom=12, charisma=10),
            unlocked_skills=["shield_wall"],
            inventory=[],
            equipped={},
            gold=500,
            active_quests=[],
            discovered_locations=["forgotten_crypt"],
            current_location="forgotten_crypt"
        )

    # ===== LIST CHARACTERS =====

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_list_characters_success(self, mock_get_service, mock_get_user,
                                     client, mock_user, sample_character):
        """Test listing user's characters."""
        # Setup
        mock_get_user.return_value = mock_user
        mock_service = Mock()
        mock_service.get_user_characters.return_value = [sample_character]
        mock_get_service.return_value = mock_service

        # Execute
        response = client.get('/api/v1/characters')

        # Verify
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data['status'] == 200
        assert len(data['result']['characters']) == 1
        assert data['result']['characters'][0]['name'] == "Thorin Ironforge"
        assert data['result']['characters'][0]['class'] == "vanguard"
        assert data['result']['count'] == 1
        assert data['result']['tier'] == "free"
        assert data['result']['limit'] == 1

    @patch('app.api.characters.get_current_user')
    def test_list_characters_unauthorized(self, mock_get_user, client):
        """Test listing characters without authentication."""
        # Setup - simulate no user logged in
        mock_get_user.return_value = None

        # Execute
        response = client.get('/api/v1/characters')

        # Verify
        assert response.status_code == 401

    # ===== GET CHARACTER =====

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_get_character_success(self, mock_get_service, mock_get_user,
                                   client, mock_user, sample_character):
        """Test getting a single character."""
        # Setup
        mock_get_user.return_value = mock_user
        mock_service = Mock()
        mock_service.get_character.return_value = sample_character
        mock_get_service.return_value = mock_service

        # Execute
        response = client.get('/api/v1/characters/char_123')

        # Verify
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data['status'] == 200
        assert data['result']['name'] == "Thorin Ironforge"
        assert data['result']['level'] == 5
        assert data['result']['gold'] == 500

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_get_character_not_found(self, mock_get_service, mock_get_user,
                                     client, mock_user):
        """Test getting a non-existent character."""
        # Setup
        mock_get_user.return_value = mock_user
        mock_service = Mock()
        mock_service.get_character.side_effect = CharacterNotFound("Character not found")
        mock_get_service.return_value = mock_service

        # Execute
        response = client.get('/api/v1/characters/char_999')

        # Verify
        assert response.status_code == 404
        data = json.loads(response.data)
        assert 'error' in data

    # ===== CREATE CHARACTER =====

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_create_character_success(self, mock_get_service, mock_get_user,
                                      client, mock_user, sample_character):
        """Test creating a new character."""
        # Setup
        mock_get_user.return_value = mock_user
        mock_service = Mock()
        mock_service.create_character.return_value = sample_character
        mock_get_service.return_value = mock_service

        # Execute
        response = client.post('/api/v1/characters',
                             json={
                                 'name': 'Thorin Ironforge',
                                 'class_id': 'vanguard',
                                 'origin_id': 'soul_revenant'
                             })

        # Verify
        assert response.status_code == 201
        data = json.loads(response.data)
        assert data['status'] == 201
        assert data['result']['name'] == "Thorin Ironforge"
        assert data['result']['class'] == "vanguard"
        assert data['result']['origin'] == "soul_revenant"

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_create_character_limit_exceeded(self, mock_get_service, mock_get_user,
                                             client, mock_user):
        """Test creating a character when tier limit is reached."""
        # Setup
        mock_get_user.return_value = mock_user
        mock_service = Mock()
        mock_service.create_character.side_effect = CharacterLimitExceeded(
            "Character limit reached for free tier (1/1)"
        )
        mock_get_service.return_value = mock_service

        # Execute
        response = client.post('/api/v1/characters',
                             json={
                                 'name': 'Thorin Ironforge',
                                 'class_id': 'vanguard',
                                 'origin_id': 'soul_revenant'
                             })

        # Verify
        assert response.status_code == 400
        data = json.loads(response.data)
        assert 'error' in data
        assert data['error']['code'] == 'CHARACTER_LIMIT_EXCEEDED'

    def test_create_character_validation_errors(self, client, mock_user):
        """Test character creation with invalid input."""
        with patch('app.api.characters.get_current_user', return_value=mock_user):
            # Test missing name
            response = client.post('/api/v1/characters',
                                 json={
                                     'class_id': 'vanguard',
                                     'origin_id': 'soul_revenant'
                                 })
            assert response.status_code == 400

            # Test invalid class_id
            response = client.post('/api/v1/characters',
                                 json={
                                     'name': 'Test',
                                     'class_id': 'invalid_class',
                                     'origin_id': 'soul_revenant'
                                 })
            assert response.status_code == 400

            # Test invalid origin_id
            response = client.post('/api/v1/characters',
                                 json={
                                     'name': 'Test',
                                     'class_id': 'vanguard',
                                     'origin_id': 'invalid_origin'
                                 })
            assert response.status_code == 400

            # Test name too short
            response = client.post('/api/v1/characters',
                                 json={
                                     'name': 'T',
                                     'class_id': 'vanguard',
                                     'origin_id': 'soul_revenant'
                                 })
            assert response.status_code == 400

    # ===== DELETE CHARACTER =====

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_delete_character_success(self, mock_get_service, mock_get_user,
                                      client, mock_user):
        """Test deleting a character."""
        # Setup
        mock_get_user.return_value = mock_user
        mock_service = Mock()
        mock_service.delete_character.return_value = True
        mock_get_service.return_value = mock_service

        # Execute
        response = client.delete('/api/v1/characters/char_123')

        # Verify
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data['status'] == 200
        assert 'deleted successfully' in data['result']['message']

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_delete_character_not_found(self, mock_get_service, mock_get_user,
                                       client, mock_user):
        """Test deleting a non-existent character."""
        # Setup
        mock_get_user.return_value = mock_user
        mock_service = Mock()
        mock_service.delete_character.side_effect = CharacterNotFound("Character not found")
        mock_get_service.return_value = mock_service

        # Execute
        response = client.delete('/api/v1/characters/char_999')

        # Verify
        assert response.status_code == 404

    # ===== UNLOCK SKILL =====

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_unlock_skill_success(self, mock_get_service, mock_get_user,
                                  client, mock_user, sample_character):
        """Test unlocking a skill."""
        # Setup
        mock_get_user.return_value = mock_user
        sample_character.unlocked_skills = ["shield_wall", "toughness"]
        mock_service = Mock()
        mock_service.unlock_skill.return_value = sample_character
        mock_get_service.return_value = mock_service

        # Execute
        response = client.post('/api/v1/characters/char_123/skills/unlock',
                             json={'skill_id': 'toughness'})

        # Verify
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data['status'] == 200
        assert data['result']['skill_id'] == 'toughness'
        assert 'toughness' in data['result']['unlocked_skills']

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_unlock_skill_prerequisites_not_met(self, mock_get_service, mock_get_user,
                                                client, mock_user):
        """Test unlocking a skill without meeting prerequisites."""
        # Setup
        mock_get_user.return_value = mock_user
        mock_service = Mock()
        mock_service.unlock_skill.side_effect = SkillUnlockError(
            "Prerequisite not met: shield_wall required for toughness"
        )
        mock_get_service.return_value = mock_service

        # Execute
        response = client.post('/api/v1/characters/char_123/skills/unlock',
                             json={'skill_id': 'toughness'})

        # Verify
        assert response.status_code == 400
        data = json.loads(response.data)
        assert 'error' in data
        assert data['error']['code'] == 'SKILL_UNLOCK_ERROR'

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_unlock_skill_no_points(self, mock_get_service, mock_get_user,
                                   client, mock_user):
        """Test unlocking a skill without available skill points."""
        # Setup
        mock_get_user.return_value = mock_user
        mock_service = Mock()
        mock_service.unlock_skill.side_effect = SkillUnlockError(
            "No skill points available (Level 1, 1 skills unlocked)"
        )
        mock_get_service.return_value = mock_service

        # Execute
        response = client.post('/api/v1/characters/char_123/skills/unlock',
                             json={'skill_id': 'shield_wall'})

        # Verify
        assert response.status_code == 400
        data = json.loads(response.data)
        assert 'error' in data

    # ===== RESPEC SKILLS =====

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_respec_skills_success(self, mock_get_service, mock_get_user,
                                   client, mock_user, sample_character):
        """Test respeccing character skills."""
        # Setup
        mock_get_user.return_value = mock_user

        # Get character returns current state
        char_before = sample_character
        char_before.unlocked_skills = ["shield_wall"]
        char_before.gold = 500

        # After respec, skills cleared and gold reduced
        char_after = sample_character
        char_after.unlocked_skills = []
        char_after.gold = 0  # 500 - (5 * 100)

        mock_service = Mock()
        mock_service.get_character.return_value = char_before
        mock_service.respec_skills.return_value = char_after
        mock_get_service.return_value = mock_service

        # Execute
        response = client.post('/api/v1/characters/char_123/skills/respec')

        # Verify
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data['status'] == 200
        assert data['result']['cost'] == 500  # level 5 * 100
        assert data['result']['remaining_gold'] == 0
        assert data['result']['available_points'] == 5

    @patch('app.api.characters.get_current_user')
    @patch('app.api.characters.get_character_service')
    def test_respec_skills_insufficient_gold(self, mock_get_service, mock_get_user,
                                            client, mock_user, sample_character):
        """Test respeccing without enough gold."""
        # Setup
        mock_get_user.return_value = mock_user
        sample_character.gold = 100  # Not enough for level 5 respec (needs 500)

        mock_service = Mock()
        mock_service.get_character.return_value = sample_character
        mock_service.respec_skills.side_effect = InsufficientGold(
            "Insufficient gold for respec. Cost: 500, Available: 100"
        )
        mock_get_service.return_value = mock_service

        # Execute
        response = client.post('/api/v1/characters/char_123/skills/respec')

        # Verify
        assert response.status_code == 400
        data = json.loads(response.data)
        assert 'error' in data
        assert data['error']['code'] == 'INSUFFICIENT_GOLD'

    # ===== CLASSES ENDPOINTS (REFERENCE DATA) =====

    @patch('app.api.characters.get_class_loader')
    def test_list_classes(self, mock_get_loader, client, sample_class):
        """Test listing all character classes."""
        # Setup
        mock_loader = Mock()
        mock_loader.get_all_class_ids.return_value = ['vanguard', 'arcanist']
        mock_loader.load_class.return_value = sample_class
        mock_get_loader.return_value = mock_loader

        # Execute
        response = client.get('/api/v1/classes')

        # Verify
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data['status'] == 200
        assert len(data['result']['classes']) == 2
        assert data['result']['count'] == 2

    @patch('app.api.characters.get_class_loader')
    def test_get_class_details(self, mock_get_loader, client, sample_class):
        """Test getting details of a specific class."""
        # Setup
        mock_loader = Mock()
        mock_loader.load_class.return_value = sample_class
        mock_get_loader.return_value = mock_loader

        # Execute
        response = client.get('/api/v1/classes/vanguard')

        # Verify
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data['status'] == 200
        assert data['result']['class_id'] == 'vanguard'
        assert data['result']['name'] == 'Vanguard'
        assert len(data['result']['skill_trees']) == 1

    @patch('app.api.characters.get_class_loader')
    def test_get_class_not_found(self, mock_get_loader, client):
        """Test getting a non-existent class."""
        # Setup
        mock_loader = Mock()
        mock_loader.load_class.return_value = None
        mock_get_loader.return_value = mock_loader

        # Execute
        response = client.get('/api/v1/classes/invalid_class')

        # Verify
        assert response.status_code == 404

    # ===== ORIGINS ENDPOINTS (REFERENCE DATA) =====

    @patch('app.api.characters.get_origin_service')
    def test_list_origins(self, mock_get_service, client, sample_origin):
        """Test listing all character origins."""
        # Setup
        mock_service = Mock()
        mock_service.get_all_origin_ids.return_value = ['soul_revenant', 'memory_thief']
        mock_service.load_origin.return_value = sample_origin
        mock_get_service.return_value = mock_service

        # Execute
        response = client.get('/api/v1/origins')

        # Verify
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data['status'] == 200
        assert len(data['result']['origins']) == 2
        assert data['result']['count'] == 2
