"""
Unit tests for Character Manager Module

Tests character creation, family relationships, NPC generation, and appearance
management according to TESTING_PLAN.md.

Test Coverage:
- Character Creation (5 tests)
- Family & Relationships (8 tests)
- NPC Generation (8 tests)
- Appearance (7 tests)
- Name Generation (3 tests)
- Character Retrieval (6 tests)
"""
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))

# Enable test mode to use mock data for database calls
os.environ['BAOLIFE_TEST_MODE'] = 'true'

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

# Import the character manager functions
from character.character_manager import (
    create_character,
    add_parents,
    add_child,
    add_older_siblings,
    add_grandparents,
    add_friend,
    create_classmates,
    create_coworkers,
    get_random_classmate,
    get_random_friend,
    get_random_character,
    get_random_family,
    get_allFamily,
    get_person,
    get_relationship,
    set_avatar,
    get_hair_type,
    get_hair_color,
    get_facial_hair,
    get_accessory,
    get_firstname,
    get_lastname,
    setBirthday,
    characterSetup,
    setValues
)

from core.models import playerClass, personClass


# ============================================================
# Test Fixtures
# ============================================================

@pytest.fixture
def empty_player():
    """Create empty player for testing."""
    from jobs.job_manager import OccupationClass, JobLevel

    player = playerClass()
    player.userID = 'test_user'
    player.date = '06-15'
    player.c.firstname = 'Test'
    player.c.lastname = 'Player'
    player.c.sex = 'Male'
    player.c.ageYears = 25
    player.c.ageDays = 25 * 365
    player.c.avatar_settings.skin_color = 'PALE'

    # Create proper mock occupations with levels
    mock_occupation = OccupationClass(
        title='Test Job',
        description='A test job for testing',
        shifts='Day shift',
        requirements='none',
        levels=[
            JobLevel('Junior Test Worker', 2000, 30),
            JobLevel('Senior Test Worker', 3000, 40)
        ]
    )

    mock_teacher_occupation = OccupationClass(
        title='Elementary School Teacher',
        description='Teaches elementary school',
        shifts='Day shift',
        requirements='bachelors_degree',
        levels=[
            JobLevel('Teacher', 3000, 40),
            JobLevel('Senior Teacher', 4000, 50)
        ]
    )

    player.occupations = [mock_occupation, mock_teacher_occupation]
    return player


@pytest.fixture
def player_with_family():
    """Create player with basic family."""
    from jobs.job_manager import OccupationClass, JobLevel

    player = playerClass()
    player.userID = 'test_user_family'
    player.date = '06-15'
    player.c.firstname = 'Test'
    player.c.lastname = 'Player'
    player.c.sex = 'Male'
    player.c.ageYears = 10
    player.c.ageDays = 10 * 365
    player.c.avatar_settings.skin_color = 'PALE'

    # Create proper mock occupations with levels
    mock_occupation = OccupationClass(
        title='Test Job',
        description='A test job for testing',
        shifts='Day shift',
        requirements='none',
        levels=[
            JobLevel('Junior Test Worker', 2000, 30),
            JobLevel('Senior Test Worker', 3000, 40)
        ]
    )

    mock_teacher_occupation = OccupationClass(
        title='Elementary School Teacher',
        description='Teaches elementary school',
        shifts='Day shift',
        requirements='bachelors_degree',
        levels=[
            JobLevel('Teacher', 3000, 40),
            JobLevel('Senior Teacher', 4000, 50)
        ]
    )

    player.occupations = [mock_occupation, mock_teacher_occupation]

    # Add mother
    mother = personClass()
    mother.sex = 'Female'
    mother.relationships = ['mother', 'family']
    mother.title = 'Mother'
    mother.familyLevel = 1
    mother.ageYears = 35
    mother.ageDays = 35 * 365
    player.r.append(mother)

    # Add father
    father = personClass()
    father.sex = 'Male'
    father.relationships = ['father', 'family']
    father.title = 'Father'
    father.familyLevel = 1
    father.ageYears = 37
    father.ageDays = 37 * 365
    player.r.append(father)

    return player


# ============================================================
# Character Creation Tests
# ============================================================

class TestCharacterCreation:
    """Tests for character creation functions."""

    def test_create_character_with_valid_data(self, empty_player):
        """Test creating a character with valid parameters."""
        initial_count = len(empty_player.r)

        new_char = create_character(empty_player, sex='Female', age='similar',
                                    relationship='friend')

        assert len(empty_player.r) == initial_count + 1
        assert new_char.sex == 'Female'
        assert 'friend' in new_char.relationships
        assert new_char.firstname is not None
        assert new_char.lastname is not None

    def test_create_character_sets_birthday(self, empty_player):
        """Test that character birthday is calculated from age."""
        new_char = create_character(empty_player, relationship='friend')

        # Character should have ageDays set
        assert hasattr(new_char, 'ageDays')
        assert new_char.ageDays > 0

        # Birthday should be set
        assert hasattr(new_char, 'birthday')

    def test_create_character_initializes_stats(self, empty_player):
        """Test that character starts with default stat values."""
        new_char = create_character(empty_player, relationship='friend')

        # Check default stats (setValues may modify happiness slightly based on likes/dislikes)
        assert new_char.energy == 100
        assert new_char.hunger == 0
        assert new_char.thirst == 0
        assert 80 <= new_char.happiness <= 100  # Happiness can vary based on randomly generated likes/dislikes
        assert new_char.health == 1
        assert new_char.money == 0

    def test_create_character_sets_avatar(self, empty_player):
        """Test that avatar is generated for new character."""
        new_char = create_character(empty_player, relationship='friend')

        # Avatar settings should exist
        assert hasattr(new_char, 'avatar_settings')
        assert hasattr(new_char.avatar_settings, 'skin_color')

        # Image URL should be set (even if it's None initially)
        assert hasattr(new_char, 'image')

    def test_character_setup_adds_to_player(self):
        """Test that characterSetup creates main character and family."""
        from jobs.job_manager import OccupationClass, JobLevel

        player = playerClass()
        player.userID = 'test_setup'
        player.date = '06-15'

        # Add test occupations for characterSetup to use
        test_occupation = OccupationClass(
            title='Test Job',
            description='A test job',
            shifts='Day shift',
            requirements='none',
            levels=[
                JobLevel('Junior Worker', 2000, 30),
                JobLevel('Senior Worker', 3000, 40)
            ]
        )
        player.occupations = [test_occupation]

        data = {
            'name': 'John Doe',
            'age': '25',
            'sex': 'Male'
        }

        player = characterSetup(player, data)

        # Check main character
        assert player.c.firstname == 'John'
        assert player.c.lastname == 'Doe'
        assert player.c.ageYears == 25
        assert player.c.sex == 'Male'

        # Check family was added
        assert len(player.r) > 0

        # Check parents exist
        has_mother = any('mother' in r.relationships for r in player.r)
        has_father = any('father' in r.relationships for r in player.r)
        assert has_mother
        assert has_father

        # Check status updated
        assert player.status == "setupComplete"
        assert player.controller == "active"


# ============================================================
# Family & Relationships Tests
# ============================================================

class TestFamilyRelationships:
    """Tests for family creation and relationship management."""

    def test_add_parents_creates_two_parents(self, empty_player):
        """Test that add_parents creates mother and father."""
        # Don't clear occupations - setValues needs them to assign jobs
        initial_count = len(empty_player.r)

        player = add_parents(empty_player)

        # Should have added 2 parents
        assert len(player.r) == initial_count + 2

        # Check for mother
        mother = next((r for r in player.r if 'mother' in r.relationships), None)
        assert mother is not None
        assert mother.sex == 'Female'
        assert mother.title == 'Mother'
        assert mother.familyLevel == 1

        # Check for father
        father = next((r for r in player.r if 'father' in r.relationships), None)
        assert father is not None
        assert father.sex == 'Male'
        assert father.title == 'Father'
        assert father.familyLevel == 1

    def test_add_child_updates_parent_relationship(self, empty_player):
        """Test that add_child creates child with proper relationships."""
        # add_child returns the child person object and modifies player.r
        child = add_child(empty_player)

        # Verify child was added to player.r
        assert len(empty_player.r) == 1
        assert empty_player.r[0] == child

        assert 'child' in child.relationships
        assert 'family' in child.relationships
        assert child.familyLevel == 1
        assert child.lastname == empty_player.c.lastname
        assert child.ageYears == 0
        assert child.ageDays == 0

    def test_add_siblings_age_appropriate(self, player_with_family):
        """Test that siblings have appropriate ages."""
        # Add siblings multiple times to test randomness
        for _ in range(5):
            from jobs.job_manager import OccupationClass, JobLevel

            test_player = playerClass()
            test_player.userID = 'test_sibling'
            test_player.date = '06-15'
            test_player.c = personClass()
            test_player.c.ageYears = 10
            test_player.c.lastname = 'Test'
            test_player.c.avatar_settings.skin_color = 'PALE'

            # Add test occupations
            test_occupation = OccupationClass(
                title='Test Job',
                description='A test job',
                shifts='Day shift',
                requirements='none',
                levels=[JobLevel('Worker', 2000, 30)]
            )
            test_player.occupations = [test_occupation]

            test_player = add_older_siblings(test_player)

            # If siblings were added, check their ages
            siblings = [r for r in test_player.r if 'sibling' in r.relationships]
            for sibling in siblings:
                # Older sibling should be at least 9 months older
                assert sibling.ageDays >= test_player.c.ageDays + 270

    def test_add_grandparents_only_if_parent_age_allows(self, player_with_family):
        """Test that grandparents are created with appropriate ages."""
        # Don't clear occupations - setValues needs them
        player = add_grandparents(player_with_family)

        # Check for all four grandparents
        grandparents = [r for r in player.r if 'grandmother' in r.relationships[0]
                       or 'grandfather' in r.relationships[0]]

        assert len(grandparents) == 4

        # Get parents
        father = get_relationship(player, 'father')
        mother = get_relationship(player, 'mother')

        # Grandparents should be older than parents
        for gp in grandparents:
            if 'paternal' in gp.relationships[0]:
                assert gp.ageYears > father.ageYears
            else:
                assert gp.ageYears > mother.ageYears

    def test_add_friend_creates_bidirectional_relationship(self, empty_player):
        """Test that add_friend creates friend with proper attributes."""
        player = add_friend(empty_player, sex='Female', age='similar')

        # Friend should be added
        friend = player.r[0]

        assert 'friend' in friend.relationships
        assert friend.title == 'Friend'
        assert friend.sex == 'Female'
        # Similar age means within a few years
        assert abs(friend.ageYears - empty_player.c.ageYears) <= 6

    def test_relationship_affinity_initialization(self, empty_player):
        """Test that different relationships start with appropriate affinity."""
        # Friend should have high affinity
        player = add_friend(empty_player, sex='Female', age='similar')
        friend = player.r[0]
        assert friend.affinity >= 70
        assert friend.affinity <= 100

    def test_add_parents_same_last_name(self, empty_player):
        """Test that father shares last name with player."""
        # Don't clear occupations
        player = add_parents(empty_player)

        father = get_relationship(player, 'father')
        assert father.lastname == empty_player.c.lastname

    def test_family_skin_color_inheritance(self, empty_player):
        """Test that family members inherit skin color."""
        # Don't clear occupations
        empty_player.c.avatar_settings.skin_color = 'BROWN'

        player = add_parents(empty_player)

        father = get_relationship(player, 'father')
        mother = get_relationship(player, 'mother')

        assert father.avatar_settings.skin_color == 'BROWN'
        assert mother.avatar_settings.skin_color == 'BROWN'


# ============================================================
# NPC Generation Tests
# ============================================================

class TestNPCGeneration:
    """Tests for NPC generation functions."""

    def test_create_classmates_age_appropriate(self, empty_player):
        """Test that classmates are created with similar ages."""
        # empty_player fixture already has proper occupations including Elementary School Teacher
        create_classmates(empty_player)

        classmates = [r for r in empty_player.r if 'classmate' in r.relationships]

        # Should have 10 classmates
        assert len(classmates) == 10

        # All should have similar ages (within 1 year)
        for classmate in classmates:
            assert abs(classmate.ageYears - empty_player.c.ageYears) <= 1

    def test_create_classmates_includes_teacher(self, empty_player):
        """Test that create_classmates also creates a teacher."""
        # empty_player fixture already has proper occupations including Elementary School Teacher
        create_classmates(empty_player)

        teachers = [r for r in empty_player.r if 'teacher' in r.relationships]

        # Should have 1 teacher
        assert len(teachers) == 1

        # Teacher should be adult
        teacher = teachers[0]
        assert teacher.ageYears >= 18

    def test_create_coworkers_at_same_location(self, empty_player):
        """Test that coworkers are created with same job."""
        occupation = SimpleNamespace(title='Software Engineer')

        create_coworkers(empty_player, occupation)

        coworkers = [r for r in empty_player.r if 'coworker' in r.relationships]
        bosses = [r for r in empty_player.r if 'boss' in r.relationships]

        # Should have 5-8 coworkers
        assert 5 <= len(coworkers) <= 8

        # Should have 1 boss
        assert len(bosses) == 1

        # All should have the same job
        for coworker in coworkers:
            assert coworker.job == occupation

        assert bosses[0].job == occupation

    def test_get_random_classmate_returns_classmate(self, empty_player):
        """Test that get_random_classmate only returns actual classmates."""
        # empty_player fixture already has proper occupations
        create_classmates(empty_player)

        # Get random classmate
        classmate = get_random_classmate(empty_player)

        assert 'classmate' in classmate.relationships

    def test_get_random_classmate_creates_if_none_exist(self, empty_player):
        """Test that get_random_classmate creates classmates if none exist."""
        # empty_player fixture already has proper occupations
        # No classmates initially
        assert len([r for r in empty_player.r if 'classmate' in r.relationships]) == 0

        # Should create classmates and return one
        classmate = get_random_classmate(empty_player)

        assert 'classmate' in classmate.relationships
        assert len([r for r in empty_player.r if 'classmate' in r.relationships]) > 0

    def test_get_random_friend_returns_friend(self, empty_player):
        """Test that get_random_friend only returns friends."""
        # Add some friends
        add_friend(empty_player, sex='Male', age='similar')
        add_friend(empty_player, sex='Female', age='similar')

        friend = get_random_friend(empty_player)

        assert friend is not False
        assert 'friend' in friend.relationships

    def test_get_random_friend_returns_false_if_no_friends(self, empty_player):
        """Test that get_random_friend returns False when no friends."""
        result = get_random_friend(empty_player)

        assert result is False

    def test_create_character_with_random_sex(self, empty_player):
        """Test creating character with random sex selection."""
        # Create 10 characters and check that both sexes can be generated
        sexes = set()
        for _ in range(10):
            char = create_character(empty_player, sex='random',
                                   age='similar', relationship='friend')
            sexes.add(char.sex)

        # With 10 characters, we should have at least one of each sex
        # (statistically very likely, though not guaranteed)
        assert char.sex in ['Male', 'Female']


# ============================================================
# Appearance Tests
# ============================================================

class TestAppearance:
    """Tests for avatar and appearance generation."""

    def test_set_avatar_assigns_all_features(self, empty_player):
        """Test that set_avatar assigns all required features."""
        person = personClass()
        person.avatar_settings.hair = 'shortFlat'
        person.avatar_settings.hair_color = '2c1b18'
        person.avatar_settings.facial_hair = 'NONE'
        person.avatar_settings.accessory = 'NONE'
        person.avatar_settings.skin_color = 'PALE'
        person.avatar_settings.clothing = 'hoodie'
        person.avatar_settings.mouth = 'smile'

        url = set_avatar(person)

        # Should return a URL
        assert url is not None
        assert isinstance(url, str)
        assert 'dicebear.com' in url

        # Image URL should be set
        assert person.imageURL == url

    def test_avatar_features_age_appropriate(self):
        """Test that avatar features are age-appropriate."""
        # Baby should have no hair
        baby = personClass()
        baby.ageYears = 0
        baby.sex = 'Male'
        baby.avatar_settings.skin_color = 'PALE'

        hair_type = get_hair_type(baby)
        assert hair_type == 'NONE'

        # Child should not have facial hair
        child = personClass()
        child.ageYears = 10
        child.sex = 'Male'

        facial_hair = get_facial_hair(child)
        assert facial_hair == 'NONE'

        # Female should not have facial hair
        female = personClass()
        female.ageYears = 30
        female.sex = 'Female'

        facial_hair = get_facial_hair(female)
        assert facial_hair == 'NONE'

    def test_get_lastname_returns_string(self):
        """Test that get_lastname returns a valid string."""
        # Test mode is enabled via environment variable, so this will return a test name
        lastname = get_lastname()

        assert isinstance(lastname, str)
        assert len(lastname) > 0
        # Should be one of the test lastnames
        test_lastnames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez']
        assert lastname in test_lastnames

    def test_get_firstname_matches_sex(self):
        """Test that get_firstname returns names for correct gender."""
        # Test mode is enabled via environment variable
        male_name = get_firstname('Male')
        assert isinstance(male_name, str)
        assert len(male_name) > 0
        # Should be one of the test male names
        test_male_names = ['Liam', 'Noah', 'Oliver', 'Elijah', 'James', 'William', 'Benjamin', 'Lucas', 'Henry', 'Alexander']
        assert male_name in test_male_names

        female_name = get_firstname('Female')
        assert isinstance(female_name, str)
        assert len(female_name) > 0
        # Should be one of the test female names
        test_female_names = ['Emma', 'Olivia', 'Ava', 'Isabella', 'Sophia', 'Mia', 'Charlotte', 'Amelia', 'Harper', 'Evelyn']
        assert female_name in test_female_names

    def test_get_hair_color_age_appropriate(self):
        """Test that older people can have grey hair."""
        elderly = personClass()
        elderly.ageYears = 70
        elderly.avatar_settings.skin_color = 'PALE'

        # Test multiple times since it's random
        hair_colors = set()
        for _ in range(20):
            color = get_hair_color(elderly)
            hair_colors.add(color)

        # Should return valid hex color
        for color in hair_colors:
            assert isinstance(color, str)
            assert len(color) == 6  # Hex color without #

    def test_get_accessory_probability_by_age(self):
        """Test that accessory probability varies by age."""
        # Test child (lower probability)
        child = personClass()
        child.ageYears = 10

        child_accessories = []
        for _ in range(100):
            child_accessories.append(get_accessory(child))

        # Most should be NONE for children
        none_count = child_accessories.count('NONE')
        assert none_count >= 50  # At least 50% NONE for children

        # Test elderly (higher probability)
        elderly = personClass()
        elderly.ageYears = 70

        elderly_accessories = []
        for _ in range(100):
            elderly_accessories.append(get_accessory(elderly))

        # Fewer should be NONE for elderly
        none_count_elderly = elderly_accessories.count('NONE')
        assert none_count_elderly < none_count  # Less NONE for elderly

    def test_facial_hair_only_adult_males(self):
        """Test that only adult males can have facial hair."""
        # Adult male - can have facial hair
        adult_male = personClass()
        adult_male.ageYears = 25
        adult_male.sex = 'Male'

        facial_hair = get_facial_hair(adult_male)
        assert facial_hair in ['beardLight', 'beardMedium', 'moustacheMagnum', 'NONE']

        # Teen male - no facial hair
        teen = personClass()
        teen.ageYears = 16
        teen.sex = 'Male'

        facial_hair = get_facial_hair(teen)
        assert facial_hair == 'NONE'

        # Old male - no facial hair (over 50)
        old_male = personClass()
        old_male.ageYears = 55
        old_male.sex = 'Male'

        facial_hair = get_facial_hair(old_male)
        assert facial_hair == 'NONE'


# ============================================================
# Character Retrieval Tests
# ============================================================

class TestCharacterRetrieval:
    """Tests for character lookup and retrieval functions."""

    def test_get_person_by_id(self, player_with_family):
        """Test that get_person returns correct person by ID."""
        mother = player_with_family.r[0]
        mother_id = mother.id

        found = get_person(player_with_family, mother_id)

        assert found is not None
        assert found.id == mother_id
        assert 'mother' in found.relationships

    def test_get_person_returns_none_if_not_found(self, player_with_family):
        """Test that get_person returns None for invalid ID."""
        found = get_person(player_with_family, 'nonexistent_id')

        assert found is None

    def test_get_relationship_by_type(self, player_with_family):
        """Test that get_relationship finds person by relationship type."""
        mother = get_relationship(player_with_family, 'mother')

        assert mother is not None
        assert 'mother' in mother.relationships
        assert mother.sex == 'Female'

    def test_get_random_character(self, player_with_family):
        """Test that get_random_character returns any character."""
        char = get_random_character(player_with_family)

        assert char is not None
        assert char in player_with_family.r

    def test_get_random_family(self, player_with_family):
        """Test that get_random_family returns immediate family."""
        family_member = get_random_family(player_with_family)

        assert family_member is not None
        assert family_member.familyLevel == 1
        assert 'family' in family_member.relationships

    def test_get_allFamily(self, player_with_family):
        """Test that get_allFamily returns all immediate family."""
        family = get_allFamily(player_with_family)

        assert len(family) == 2  # Mother and father

        for member in family:
            assert member.familyLevel == 1
            assert 'family' in member.relationships


# ============================================================
# Helper Function Tests
# ============================================================

class TestHelperFunctions:
    """Tests for helper and utility functions."""

    def test_setBirthday_calculates_from_age(self, empty_player):
        """Test that setBirthday correctly calculates birthday from ageDays."""
        person = personClass()
        person.ageDays = 365 * 10 + 150  # 10 years and 150 days

        setBirthday(person, empty_player)

        assert hasattr(person, 'birthday')
        assert person.birthday is not None

    def test_create_character_no_relationship(self, empty_player):
        """Test creating character without adding to relationships."""
        initial_count = len(empty_player.r)

        new_char = create_character(empty_player, sex='Male', age='similar',
                                    relationship='none')

        # Should not be added to player.r
        assert len(empty_player.r) == initial_count

        # But should still be returned
        assert new_char is not None
        assert new_char.sex == 'Male'

    def test_create_character_age_random_adults(self, empty_player):
        """Test creating character with random adult age."""
        new_char = create_character(empty_player, age='random_adults',
                                    relationship='none')

        # Age should be between 18 and 58 (18 + 40)
        assert 18 <= new_char.ageYears <= 58

    def test_create_character_age_random_teens_adults(self, empty_player):
        """Test creating character with random teen/adult age."""
        new_char = create_character(empty_player, age='random_teens_adults',
                                    relationship='none')

        # Age should be between 13 and 58 (13 + 45)
        assert 13 <= new_char.ageYears <= 58


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