"""
Tests for Phase 3 review fixes.
Tests overnight shift availability, fairness objective, and coverage overcounting.
"""
import pytest
import json
import subprocess
import sys
from pathlib import Path

SOLVER_PATH = Path(__file__).parent.parent / "schedule_solver.py"


def run_solver(problem_data):
    """Run solver with problem data, return parsed result."""
    result = subprocess.run(
        [sys.executable, str(SOLVER_PATH)],
        input=json.dumps(problem_data),
        capture_output=True,
        text=True
    )

    if result.returncode != 0:
        print("STDERR:", result.stderr, file=sys.stderr)
        raise RuntimeError(f"Solver failed with exit code {result.returncode}")

    return json.loads(result.stdout)


def test_overnight_shift_requires_next_day_availability(base_config, base_employee, base_shift):
    """Test FIX H6: Overnight shifts check both start day and next day availability."""
    problem = {
        "config": base_config,
        "shifts": [
            {**base_shift, "shiftId": 1, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "22:00", "endTime": "06:00", "durationHours": 8.0}  # Overnight
        ],
        "employees": [
            {
                **base_employee,
                "userId": 1,
                "availability": [
                    {"dayOfWeek": 1, "startTime": "20:00", "endTime": "23:59"},  # Start day coverage
                    # Missing next day (dayOfWeek 2) availability
                ]
            }
        ],
        "lockedShifts": []
    }

    result = run_solver(problem)

    # Employee should NOT be assigned (missing next-day availability)
    if result["status"] in ["OPTIMAL", "FEASIBLE"]:
        emp_assignments = [a for a in result["assignments"] if a["employeeId"] == 1]
        assert len(emp_assignments) == 0, "Employee assigned overnight shift without next-day availability"


def test_overnight_shift_with_full_availability(base_config, base_employee, base_shift):
    """Test overnight shift CAN be assigned with proper availability on both days."""
    problem = {
        "config": base_config,
        "shifts": [
            {**base_shift, "shiftId": 1, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "22:00", "endTime": "06:00", "durationHours": 8.0}
        ],
        "employees": [
            {
                **base_employee,
                "userId": 1,
                "availability": [
                    {"dayOfWeek": 1, "startTime": "20:00", "endTime": "23:59"},  # Start day
                    {"dayOfWeek": 2, "startTime": "00:00", "endTime": "08:00"}   # Next day
                ]
            }
        ],
        "lockedShifts": []
    }

    result = run_solver(problem)

    # Employee SHOULD be assigned (has both-day availability)
    assert result["status"] in ["OPTIMAL", "FEASIBLE"]
    emp_assignments = [a for a in result["assignments"] if a["employeeId"] == 1]
    assert len(emp_assignments) == 1, "Employee should be assigned overnight shift with proper availability"


def test_fairness_penalizes_under_allocation(base_config, base_employee, base_shift):
    """Test FIX H3: Fairness objective penalizes under-allocation (not rewards it)."""
    # Two employees: one wants 32h, one wants 8h
    # One 8h shift available
    # Fairness should prefer giving it to the 32h requester (reduces their deviation from 32)
    problem = {
        "config": base_config,
        "shifts": [
            {**base_shift, "shiftId": 1, "durationHours": 8.0}
        ],
        "employees": [
            {**base_employee, "userId": 1, "hoursRequested": 32.0, "hourlyRate": 15.0},  # Wants more hours
            {**base_employee, "userId": 2, "hoursRequested": 8.0, "hourlyRate": 15.0}   # Already at target
        ],
        "lockedShifts": []
    }

    result = run_solver(problem)

    assert result["status"] in ["OPTIMAL", "FEASIBLE"]
    if result["assignments"]:
        assignment = result["assignments"][0]
        # Should prefer employee 1 (fairness prefers reducing larger deviation)
        # Employee 1: abs(8 - 32) = 24 → abs(16 - 32) = 16 (improvement of 8)
        # Employee 2: abs(0 - 8) = 8 → abs(8 - 8) = 0 (improvement of 8)
        # Both improve equally, but labor cost/other factors will decide
        # The key is: fairness does NOT reward employee 2 for going UNDER their target
        # This test mainly ensures solver doesn't crash with the new abs-value implementation
        assert assignment["employeeId"] in [1, 2]  # Either is valid


def test_coverage_counts_each_shift_once(base_config, base_employee, base_shift):
    """Test FIX H2: Coverage objective counts each shift once, not once per eligible employee."""
    # Create scenario where one shift has many eligible employees
    # Coverage weight should be same regardless of eligibility count
    problem = {
        "config": base_config,
        "shifts": [
            {**base_shift, "shiftId": 1, "minRoleId": 5},  # 10 employees eligible
            {**base_shift, "shiftId": 2, "date": "2026-02-17", "dayOfWeek": 2, "minRoleId": 1}  # 1 employee eligible
        ],
        "employees": [
            {**base_employee, "userId": 1, "role": 1},  # Qualified for both
            {**base_employee, "userId": 2, "role": 5},  # Qualified for shift 1 only
            {**base_employee, "userId": 3, "role": 5},
            {**base_employee, "userId": 4, "role": 5},
            {**base_employee, "userId": 5, "role": 5},
            {**base_employee, "userId": 6, "role": 5},
            {**base_employee, "userId": 7, "role": 5},
            {**base_employee, "userId": 8, "role": 5},
            {**base_employee, "userId": 9, "role": 5},
            {**base_employee, "userId": 10, "role": 5}
        ],
        "lockedShifts": []
    }

    result = run_solver(problem)

    assert result["status"] in ["OPTIMAL", "FEASIBLE"]
    # Both shifts should be filled (coverage has hard precedence)
    assert result["scorecard"]["coverageFilled"] == 2, "Both shifts should be filled with coverage hard-precedence"


def test_constraint_validation_detects_violations(base_config, base_employee, base_shift):
    """Test FIX M7: Post-solve validation counts constraint violations."""
    problem = {
        "config": base_config,
        "shifts": [
            {**base_shift, "shiftId": 1}
        ],
        "employees": [
            {**base_employee, "userId": 1, "role": 4}
        ],
        "lockedShifts": []
    }

    result = run_solver(problem)

    assert result["status"] in ["OPTIMAL", "FEASIBLE"]
    # Should have zero violations in a simple valid problem
    assert result["scorecard"]["constraintViolations"] == 0, "Valid solution should have no constraint violations"


def test_complex_store_fixture_solvable():
    """Test FIX M6: Complex store fixture is solvable."""
    fixtures_dir = Path(__file__).parent / "fixtures"
    with open(fixtures_dir / "complex_store.json") as f:
        problem = json.load(f)

    result = run_solver(problem)

    # Should return a feasible or optimal solution
    assert result["status"] in ["OPTIMAL", "FEASIBLE"], "Complex store fixture should be solvable"
    assert result["scorecard"]["coverageFilled"] > 0, "Should fill some shifts"
    assert result["scorecard"]["constraintViolations"] == 0, "Should have no constraint violations"


def test_objective_value_is_integer(base_config, base_employee, base_shift):
    """Test FIX M8: Objective value is reported as integer (dimensionless score)."""
    problem = {
        "config": base_config,
        "shifts": [{**base_shift, "shiftId": 1}],
        "employees": [{**base_employee, "userId": 1}],
        "lockedShifts": []
    }

    result = run_solver(problem)

    if result["status"] in ["OPTIMAL", "FEASIBLE"]:
        obj_val = result["objectiveValue"]
        assert isinstance(obj_val, int), f"Objective value should be integer, got {type(obj_val)}"
