"""
Unit tests for Job Manager (ws/jobs/job_manager.py).

Tests job assignment, progression, application, quitting, salary calculations,
and occupation data management according to TESTING_PLAN.md Section 7.2.

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

import pytest
import sys
from pathlib import Path
from datetime import date
from unittest.mock import MagicMock, patch

# Add ws directory to Python path
ws_dir = Path(__file__).parent.parent.parent / 'ws'
sys.path.insert(0, str(ws_dir))

from jobs.job_manager import (
    JobLevel,
    OccupationClass,
    getOccupations,
    randomJob,
    setJob,
    handleJob,
    applyForJob,
    quitJob
)
from core.models import playerClass, personClass, ActivityRecord


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

@pytest.fixture
def teen_player():
    """
    Create a test player with a teenage character (age 16).

    The teen has completed high school and is eligible for entry-level jobs
    that require high school education.
    """
    # Create minimal player object to avoid database connections
    player = MagicMock()

    # Create mock person (character)
    person = MagicMock()
    person.id = "test_teen_id"
    person.firstname = "Teen"
    person.lastname = "Player"
    person.sex = "Male"
    person.ageYears = 16
    person.ageHours = 16 * 365 * 24
    person.ageDays = 16 * 365
    person.education = "high_school"
    person.money = 1000
    person.energy = 100
    person.job = None
    person.activities = []
    person.activityRecords = []

    player.c = person
    player.date = date.today().strftime('%m-%d')
    player.messageQueue = []
    player.occupations = getOccupations()
    return player


@pytest.fixture
def adult_player():
    """
    Create a test player with an adult character (age 30).

    The adult has a bachelor's degree and is eligible for professional
    jobs requiring higher education.
    """
    # Create minimal player object to avoid database connections
    player = MagicMock()

    # Create mock person (character)
    person = MagicMock()
    person.id = "test_adult_id"
    person.firstname = "Adult"
    person.lastname = "Player"
    person.sex = "Female"
    person.ageYears = 30
    person.ageHours = 30 * 365 * 24
    person.ageDays = 30 * 365
    person.education = "bachelors_degree"
    person.money = 5000
    person.energy = 100
    person.job = None
    person.activities = []
    person.activityRecords = []

    player.c = person
    player.date = date.today().strftime('%m-%d')
    player.messageQueue = []
    player.occupations = getOccupations()
    return player


@pytest.fixture
def sample_job():
    """Create a sample job for testing."""
    return OccupationClass(
        "Test Engineer",
        "Test job description",
        "Day shift",
        "bachelors_degree",
        [
            JobLevel('Junior Test Engineer', 2000, 30),
            JobLevel('Test Engineer', 3000, 40),
            JobLevel('Senior Test Engineer', 4000, 50),
        ],
        image="test_image.png"
    )


# ============================================================================
# JOB ASSIGNMENT TESTS (4 tests)
# ============================================================================

def test_set_job_assigns_occupation(adult_player, sample_job):
    """
    Test that setJob assigns a job to a character.

    Verifies:
    - Job is assigned to person.job
    - Job is added to person.activities
    - ActivityRecord is created
    - Starting level is first level
    """
    # Arrange
    person = adult_player.c
    test_date = adult_player.date

    # Act
    setJob(person, sample_job, test_date)

    # Assert
    assert person.job is not None
    assert person.job.title == "Test Engineer"
    assert sample_job in person.activities
    assert len(person.activityRecords) == 1

    # Check activity record
    record = person.activityRecords[0]
    assert record.id == sample_job.id
    assert record.type == "job"
    assert record.level == sample_job.levels[0]
    assert record.level.level == 'Junior Test Engineer'
    assert record.dateStarted == test_date


def test_set_job_validates_age(teen_player):
    """
    Test that job assignment respects minimum age requirement.

    randomJob should only assign jobs if person is over 22 years old.
    """
    # Arrange
    young_person = teen_player.c
    young_person.ageYears = 14

    # Act
    result = randomJob(teen_player, young_person)

    # Assert - no job assigned for age 14
    assert result.job is None or result.job == None

    # Arrange - older person
    older_person = teen_player.c
    older_person.ageYears = 23

    # Act
    result = randomJob(teen_player, older_person)

    # Assert - job should be assigned for age 23
    assert result.job is not None


def test_set_job_validates_education(adult_player):
    """
    Test that job assignment validates education requirements.

    Checks that jobs requiring degrees can only be assigned to
    characters with appropriate education levels.
    """
    # Arrange
    person = adult_player.c
    person.education = "high_school"  # Not enough for bachelors_degree jobs

    # Get a job requiring bachelor's degree
    occupations = getOccupations()
    bachelor_job = next((job for job in occupations if job.requirements == "bachelors_degree"), None)

    # Act - assign anyway (no validation in current implementation)
    setJob(person, bachelor_job, adult_player.date)

    # Assert - job is assigned (but in production should validate)
    assert person.job is not None
    # Note: Current implementation doesn't validate education requirements
    # This test documents expected behavior for future enhancement


def test_random_job_returns_valid_job(adult_player):
    """
    Test that randomJob returns an appropriate job for the character.

    Verifies that:
    - A job is assigned for characters over 22
    - The job comes from available occupations
    - The job has valid levels and salary
    """
    # Arrange
    person = adult_player.c
    person.ageYears = 25

    # Act
    result = randomJob(adult_player, person)

    # Assert
    assert result.job is not None
    assert result.job in adult_player.occupations
    assert len(result.job.levels) > 0
    assert result.job.levels[0].salary > 0


# ============================================================================
# JOB PROGRESSION TESTS (4 tests)
# ============================================================================

def test_handle_job_adds_salary(adult_player, sample_job):
    """
    Test that handleJob processes job performance and salary.

    Note: Current implementation doesn't add salary in handleJob,
    but tracks performance which affects promotions.
    """
    # Arrange
    person = adult_player.c
    setJob(person, sample_job, adult_player.date)

    # Set focus to "Work Hard" for positive performance
    person.activityRecords[0].focus = "Work Hard"
    initial_performance = person.activityRecords[0].performance

    # Act
    handleJob(adult_player, person)

    # Assert - performance should update
    final_performance = person.activityRecords[0].performance
    # Performance could go up or down based on random, but with "Work Hard" it should trend up
    assert final_performance >= 0
    assert final_performance <= 100


def test_handle_job_promotion(adult_player, sample_job):
    """
    Test that promotion occurs when performance exceeds 90.

    Verifies:
    - Character is promoted to next level
    - Performance resets to 0
    - Promotion message is added to queue
    """
    # Arrange
    person = adult_player.c
    setJob(person, sample_job, adult_player.date)

    # Set performance high enough to guarantee promotion even with negative random
    # Performance needs to be > 90 after random update (-1 to +1 with focus modifier)
    person.activityRecords[0].performance = 95
    person.activityRecords[0].focus = 'Work Hard'  # Ensures positive modifier
    initial_level = person.activityRecords[0].level.level

    # Act
    handleJob(adult_player, person)

    # Assert
    new_level = person.activityRecords[0].level.level
    assert new_level != initial_level
    assert new_level == 'Test Engineer'  # Promoted from Junior
    assert person.activityRecords[0].performance == 0  # Reset after promotion

    # Check for promotion message
    promotion_messages = [msg for msg in adult_player.messageQueue if "promoted" in str(msg)]
    assert len(promotion_messages) > 0


def test_handle_job_demotion(adult_player, sample_job):
    """
    Test behavior when performance is poor.

    Note: Current implementation fires for performance < 10,
    doesn't have explicit demotion logic.
    """
    # Arrange
    person = adult_player.c
    setJob(person, sample_job, adult_player.date)

    # Set low performance (but not low enough for firing)
    person.activityRecords[0].performance = 20
    person.activityRecords[0].focus = "Slack Off"

    # Act
    handleJob(adult_player, person)

    # Assert - should still have job
    assert person.job is not None
    # Note: Current implementation doesn't have demotion, only firing


def test_handle_job_firing(adult_player, sample_job):
    """
    Test that character is fired when performance drops below 10.

    Verifies:
    - Job is removed (person.job = False)
    - Firing message is added to queue
    """
    # Arrange
    person = adult_player.c
    setJob(person, sample_job, adult_player.date)

    # Set very low performance to trigger firing
    person.activityRecords[0].performance = 5

    # Act
    handleJob(adult_player, person)

    # Assert
    assert person.job is False  # Fired

    # Check for firing message
    firing_messages = [msg for msg in adult_player.messageQueue if "fired" in str(msg)]
    assert len(firing_messages) > 0


# ============================================================================
# JOB APPLICATION TESTS (3 tests)
# ============================================================================

def test_apply_for_job_requires_qualifications(adult_player):
    """
    Test that job application validates qualifications.

    Note: Current implementation doesn't validate qualifications,
    but this test documents expected behavior.
    """
    # Arrange
    person = adult_player.c
    person.education = "high_school"

    # Get a job requiring doctorate
    occupations = getOccupations()
    doctorate_job = next((job for job in occupations if job.requirements == "doctorate_degree"), None)

    assert doctorate_job is not None

    # Act - apply for job (no validation in current implementation)
    with patch('character.character_manager.create_coworkers', return_value=adult_player):
        applyForJob(adult_player, doctorate_job.id)

    # Assert - job is assigned (but should validate in production)
    # Note: Current implementation assigns job without validation
    # This test documents that proper validation should be added
    assert True  # Test documents expected behavior


def test_apply_for_job_success(adult_player):
    """
    Test successful job application for qualified candidate.

    Verifies:
    - Job is assigned to character
    - Success message is added
    - Coworkers are created
    """
    # Arrange
    person = adult_player.c
    person.education = "bachelors_degree"

    # Ensure message queue starts empty
    adult_player.messageQueue = []

    # Get target job from player's occupations list (not fresh from getOccupations())
    target_job = next((job for job in adult_player.occupations
                      if job.requirements == "bachelors_degree"), None)

    assert target_job is not None

    # Act - Mock the create_coworkers import at the right location
    with patch('character.character_manager.create_coworkers', return_value=adult_player):
        applyForJob(adult_player, target_job.id)

    # Assert - Check message was added
    application_messages = [msg for msg in adult_player.messageQueue
                           if "applied" in str(msg)]
    assert len(application_messages) > 0

    # Check job was assigned
    assert person.job is not None
    assert person.job.id == target_job.id


def test_apply_for_job_creates_event(adult_player):
    """
    Test that applying for a job creates appropriate events/messages.

    Verifies that application confirmation message is added to queue.
    """
    # Arrange
    # Use player's occupations list (not fresh getOccupations())
    target_job = adult_player.occupations[0]

    # Ensure message queue is a real list
    adult_player.messageQueue = []
    initial_queue_length = len(adult_player.messageQueue)

    # Act - Mock the create_coworkers import
    with patch('character.character_manager.create_coworkers', return_value=adult_player):
        applyForJob(adult_player, target_job.id)

    # Assert - message queue should have new message
    assert len(adult_player.messageQueue) > initial_queue_length
    assert any(target_job.title in str(msg) for msg in adult_player.messageQueue)


# ============================================================================
# QUIT JOB TESTS (3 tests)
# ============================================================================

def test_quit_job_removes_occupation(adult_player, sample_job):
    """
    Test that quitting removes the job from character.

    Verifies:
    - Job is removed from activities
    - Activity record is removed
    - Occupation is set to 'unemployed'
    """
    # Arrange
    person = adult_player.c
    setJob(person, sample_job, adult_player.date)

    assert person.job is not None
    assert len(person.activities) > 0
    assert len(person.activityRecords) > 0

    # Act
    adult_player.occupations = [sample_job]  # Add to player's occupations list
    quitJob(adult_player, sample_job.id)

    # Assert
    assert person.occupation == 'unemployed'
    assert sample_job not in person.activities
    assert len(person.activityRecords) == 0


def test_quit_job_updates_schedules(adult_player, sample_job):
    """
    Test that quitting removes work-related schedules.

    Note: Current implementation doesn't manage schedules in quitJob,
    but this test documents expected behavior.
    """
    # Arrange
    person = adult_player.c
    setJob(person, sample_job, adult_player.date)

    # Mock a work schedule (don't need to instantiate real scheduler)
    work_schedule = MagicMock()
    work_schedule.title = "Work"
    person.schedules = [work_schedule]

    initial_schedule_count = len(person.schedules)

    # Act
    adult_player.occupations = [sample_job]
    quitJob(adult_player, sample_job.id)

    # Assert
    # Note: Current implementation doesn't remove schedules
    # This documents that schedule management should be added
    assert person.occupation == 'unemployed'
    # Schedules should ideally be cleaned up (feature to be added)


def test_quit_job_triggers_event(adult_player, sample_job):
    """
    Test that quitting generates an exit event/message.

    Verifies that quit confirmation message is added to queue.
    """
    # Arrange
    person = adult_player.c
    setJob(person, sample_job, adult_player.date)

    initial_queue_length = len(adult_player.messageQueue)

    # Act
    adult_player.occupations = [sample_job]
    quitJob(adult_player, sample_job.id)

    # Assert
    assert len(adult_player.messageQueue) > initial_queue_length
    quit_messages = [msg for msg in adult_player.messageQueue if "quit" in str(msg)]
    assert len(quit_messages) > 0


# ============================================================================
# SALARY CALCULATION TESTS (5 tests)
# ============================================================================

def test_salary_based_on_job_level(sample_job):
    """
    Test that salary increases with job level.

    Verifies that each level in the career progression
    has a higher salary than the previous level.
    """
    # Arrange & Assert
    levels = sample_job.levels

    assert len(levels) == 3
    assert levels[0].salary == 2000  # Junior
    assert levels[1].salary == 3000  # Mid
    assert levels[2].salary == 4000  # Senior

    # Verify progression
    for i in range(len(levels) - 1):
        assert levels[i + 1].salary > levels[i].salary


def test_salary_based_on_performance(adult_player, sample_job):
    """
    Test that performance affects salary/promotions.

    Higher performance should lead to promotions which increase salary.
    """
    # Arrange
    person = adult_player.c
    setJob(person, sample_job, adult_player.date)

    initial_level = person.activityRecords[0].level
    initial_salary = initial_level.salary

    # Set high performance
    person.activityRecords[0].performance = 95

    # Act
    handleJob(adult_player, person)

    # Assert - promoted to higher level with higher salary
    new_level = person.activityRecords[0].level
    new_salary = new_level.salary

    assert new_salary > initial_salary


def test_salary_frequency_weekly():
    """
    Test salary calculation for weekly payment jobs.

    Note: Current implementation doesn't differentiate payment frequency.
    This test documents expected behavior for future implementation.
    """
    # Arrange
    weekly_job = OccupationClass(
        "Weekly Worker",
        "Paid weekly",
        "Day shift",
        "none",
        [JobLevel('Worker', 500, 30)],  # $500/week
    )

    # Assert
    assert weekly_job.levels[0].salary == 500
    # Note: Payment frequency logic should be added


def test_salary_frequency_monthly():
    """
    Test salary calculation for monthly payment jobs.

    Most jobs in the system are assumed to be monthly salaries.
    """
    # Arrange
    occupations = getOccupations()
    software_engineer = next((job for job in occupations
                             if job.title == "Software Engineer"), None)

    # Assert
    assert software_engineer is not None
    assert software_engineer.levels[0].salary == 2000  # Monthly salary
    assert software_engineer.levels[-1].salary == 10000  # CTO level


def test_salary_deductions():
    """
    Test salary deductions (taxes, insurance, etc.).

    Note: Current implementation doesn't handle deductions.
    This test documents expected behavior for future implementation.
    """
    # Arrange
    base_salary = 3000
    expected_deductions = base_salary * 0.20  # 20% for taxes/insurance
    expected_net = base_salary - expected_deductions

    # Assert - document expected behavior
    assert expected_net == 2400
    # Note: Deduction logic should be added to payment system


# ============================================================================
# OCCUPATION DATA TESTS (3 tests)
# ============================================================================

def test_get_occupations_returns_list():
    """
    Test that getOccupations returns a list of valid occupations.

    Verifies structure and basic properties of occupation list.
    """
    # Act
    occupations = getOccupations()

    # Assert
    assert isinstance(occupations, list)
    assert len(occupations) > 0

    # Check that all items are OccupationClass instances
    for occ in occupations:
        assert isinstance(occ, OccupationClass)
        assert hasattr(occ, 'title')
        assert hasattr(occ, 'levels')
        assert hasattr(occ, 'requirements')


def test_occupation_has_required_fields():
    """
    Test that each occupation has all required fields.

    Verifies:
    - title, description, shifts
    - requirements (education level)
    - levels list with salary information
    - images
    """
    # Arrange
    occupations = getOccupations()

    # Act & Assert
    for occ in occupations:
        # Required fields
        assert occ.title is not None and occ.title != ""
        assert occ.description is not None and occ.description != ""
        assert occ.shifts is not None
        assert occ.requirements in ["none", "high_school", "bachelors_degree", "doctorate_degree"]

        # Levels
        assert len(occ.levels) > 0
        for level in occ.levels:
            assert isinstance(level, JobLevel)
            assert level.salary > 0
            assert level.level is not None
            assert level.energy_modifier > 0


def test_occupation_levels():
    """
    Test that occupations have proper level progression.

    Verifies entry, mid, and senior levels with appropriate
    salary progression and titles.
    """
    # Arrange
    occupations = getOccupations()

    # Act & Assert - check a few specific occupations
    software_engineer = next((j for j in occupations if j.title == "Software Engineer"), None)
    assert software_engineer is not None
    assert len(software_engineer.levels) == 5  # Junior -> CTO
    assert "Junior" in software_engineer.levels[0].level
    assert "CTO" in software_engineer.levels[-1].level

    # Check nurse
    nurse = next((j for j in occupations if j.title == "Registered Nurse"), None)
    assert nurse is not None
    assert len(nurse.levels) == 5
    assert "Director" in nurse.levels[-1].level


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

def test_set_job_while_already_employed(adult_player, sample_job):
    """
    Test changing jobs while already employed.

    Verifies that:
    - Old job is replaced with new job
    - Activity records are updated correctly
    """
    # Arrange
    person = adult_player.c

    # Get two different jobs
    occupations = getOccupations()
    first_job = occupations[0]
    second_job = occupations[1]

    # Set first job
    setJob(person, first_job, adult_player.date)
    assert person.job.id == first_job.id

    # Act - set second job (job change)
    setJob(person, second_job, adult_player.date)

    # Assert
    assert person.job.id == second_job.id
    assert person.job.id != first_job.id
    # Note: Both jobs will be in activities list - cleanup should be added


def test_handle_job_without_employment(adult_player):
    """
    Test handleJob when person has no job.

    Should handle gracefully without errors.
    """
    # Arrange
    person = adult_player.c
    person.job = None
    person.activities = []

    # Act - should not raise exception
    try:
        handleJob(adult_player, person)
        success = True
    except Exception as e:
        success = False
        print(f"Error: {e}")

    # Assert
    assert success


def test_part_time_vs_full_time(sample_job):
    """
    Test different job types (part-time vs full-time).

    Verifies that:
    - Jobs have hourType attribute
    - Part-time and full-time have different salary calculations
    """
    # Arrange
    full_time_job = sample_job
    full_time_job.hourType = 'full-time'

    part_time_job = OccupationClass(
        "Part-Time Cashier",
        "Part-time retail work",
        "Variable shifts",
        "none",
        [JobLevel('Cashier', 500, 20)],
    )
    part_time_job.hourType = 'part-time'

    # Assert
    assert full_time_job.hourType == 'full-time'
    assert part_time_job.hourType == 'part-time'

    # Full-time should generally pay more (though not enforced by code)
    assert full_time_job.levels[0].salary > part_time_job.levels[0].salary


# ============================================================================
# ADDITIONAL VALIDATION TESTS
# ============================================================================

def test_job_level_initialization():
    """Test that JobLevel initializes with correct attributes."""
    # Arrange & Act
    level = JobLevel("Test Position", 5000, 35)

    # Assert
    assert level.id is not None
    assert level.level == "Test Position"
    assert level.salary == 5000
    assert level.energy_modifier == 35


def test_occupation_class_initialization(sample_job):
    """Test that OccupationClass initializes with correct attributes."""
    # Assert
    assert sample_job.id is not None
    assert sample_job.type == "job"
    assert sample_job.title == "Test Engineer"
    assert sample_job.description == "Test job description"
    assert sample_job.shifts == "Day shift"
    assert sample_job.hourType == 'full-time'
    assert sample_job.requirements == "bachelors_degree"
    assert len(sample_job.levels) == 3
    assert sample_job.people == []


# ============================================================================
# END OF TEST SUITE
# ============================================================================
