"""
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"


@pytest.mark.parametrize(
    "availability",
    [
        [
            {"dayOfWeek": 1, "startTime": "20:00", "endTime": "23:00"},
            {"dayOfWeek": 2, "startTime": "00:00", "endTime": "08:00"},
        ],
        [
            {"dayOfWeek": 1, "startTime": "20:00", "endTime": "23:59"},
            {"dayOfWeek": 2, "startTime": "05:00", "endTime": "08:00"},
        ],
    ],
)
def test_overnight_shift_requires_continuous_midnight_coverage(
    base_config,
    base_employee,
    base_shift,
    availability,
):
    """Endpoint-only windows must not leave an unmodelled gap around midnight."""
    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": availability}],
        "lockedShifts": [],
    }

    result = run_solver(problem)

    assert result["status"] in ["OPTIMAL", "FEASIBLE"]
    assert not result["assignments"]


def test_twenty_four_hour_shift_conflicts_with_next_day_shift(
    base_config,
    base_employee,
    base_shift,
):
    """Equal wall times plus 24h duration must remain a real dated interval."""
    full_day = [
        {"dayOfWeek": day, "startTime": "00:00", "endTime": "24:00"}
        for day in (1, 2)
    ]
    problem = {
        "config": base_config,
        "shifts": [
            {
                **base_shift,
                "shiftId": 1,
                "date": "2026-02-16",
                "dayOfWeek": 1,
                "startTime": "09:00",
                "endTime": "09:00",
                "durationHours": 24.0,
            },
            {
                **base_shift,
                "shiftId": 2,
                "date": "2026-02-17",
                "dayOfWeek": 2,
                "startTime": "08:00",
                "endTime": "12:00",
                "durationHours": 4.0,
            },
        ],
        "employees": [{**base_employee, "userId": 1, "availability": full_day}],
        "lockedShifts": [],
    }

    result = run_solver(problem)

    assert result["status"] in ["OPTIMAL", "FEASIBLE"]
    assert len(result["assignments"]) == 1


def test_twenty_four_hour_shift_requires_continuous_two_day_availability(
    base_config,
    base_employee,
    base_shift,
):
    """A 24h shift cannot pass based only on its equal start/end clock value."""
    problem = {
        "config": base_config,
        "shifts": [{
            **base_shift,
            "shiftId": 1,
            "date": "2026-02-16",
            "dayOfWeek": 1,
            "startTime": "09:00",
            "endTime": "09:00",
            "durationHours": 24.0,
        }],
        "employees": [{
            **base_employee,
            "userId": 1,
            "availability": [
                {"dayOfWeek": 1, "startTime": "08:00", "endTime": "24:00"},
                {"dayOfWeek": 2, "startTime": "10:00", "endTime": "18:00"},
            ],
        }],
        "lockedShifts": [],
    }

    result = run_solver(problem)

    assert result["status"] in ["OPTIMAL", "FEASIBLE"]
    assert not result["assignments"]


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)

    # The fixture predates explicit position/keyholder qualification. Model
    # the production enrichment contract instead of silently treating missing
    # qualifications as permission to work every position.
    for employee in problem["employees"]:
        is_keyholder = employee["role"] <= 3
        employee["isKeyholder"] = is_keyholder
        employee["allowedPositionIds"] = [1, 2, 3] if is_keyholder else [2]

    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)}"
