"""
Unit tests for constraint builder.
Tests each constraint individually.
"""
import pytest
from ortools.sat.python import cp_model
import sys
from pathlib import Path

# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))

from constraint_builder import build_constraints, parse_time, is_overnight_shift


def test_parse_time():
    """Test time parsing."""
    assert parse_time("00:00") == 0
    assert parse_time("09:00") == 540
    assert parse_time("12:30") == 750
    assert parse_time("23:59") == 1439


def test_is_overnight_shift():
    """Test overnight shift detection."""
    assert is_overnight_shift("22:00", "06:00") is True
    assert is_overnight_shift("09:00", "17:00") is False
    assert is_overnight_shift("23:00", "01:00") is True


def test_c1_one_employee_per_shift():
    """Test C1: Each shift assigned to at most 1 employee."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {"userId": 1, "role": 4, "hoursMax": 40, "currentPeriodHours": 0, "shiftsThisWeek": 0, "availability": [], "timeOff": [], "allowedPositionIds": []},
            {"userId": 2, "role": 4, "hoursMax": 40, "currentPeriodHours": 0, "shiftsThisWeek": 0, "availability": [], "timeOff": [], "allowedPositionIds": []}
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 4, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": False}
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1"),
        (2, 1): model.NewBoolVar("x_2_1")
    }

    metadata = build_constraints(model, problem, x)

    # Constraint should be added
    assert 1 in metadata["c1_one_per_shift"]

    # Try to solve - both employees assigned should be infeasible
    model.Add(x[(1, 1)] == 1)
    model.Add(x[(2, 1)] == 1)

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status == cp_model.INFEASIBLE


def test_c2_locked_shifts_immutable():
    """Test C2: Locked shifts stay with assigned employee."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {
                "userId": 1,
                "role": 4,
                "hoursMax": 40,
                "currentPeriodHours": 0,
                "shiftsThisWeek": 0,
                "availability": [{"dayOfWeek": 1, "startTime": "08:00", "endTime": "22:00"}],
                "timeOff": [],
                "allowedPositionIds": []
            },
            {
                "userId": 2,
                "role": 4,
                "hoursMax": 40,
                "currentPeriodHours": 0,
                "shiftsThisWeek": 0,
                "availability": [{"dayOfWeek": 1, "startTime": "08:00", "endTime": "22:00"}],
                "timeOff": [],
                "allowedPositionIds": []
            }
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 4, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": True}
        ],
        "lockedShifts": [
            {"shiftId": 1, "employeeId": 1, "date": "2026-02-16", "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0}
        ]
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1"),
        (2, 1): model.NewBoolVar("x_2_1")
    }

    metadata = build_constraints(model, problem, x)

    assert (1, 1) in metadata["c2_locked_shifts"]

    # Add dummy objective to make model solvable
    model.Maximize(0)

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]
    assert solver.Value(x[(1, 1)]) == 1
    assert solver.Value(x[(2, 1)]) == 0


def test_c3_role_qualification():
    """Test C3: Employee role must be <= shift minRoleId."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {"userId": 1, "role": 5, "hoursMax": 40, "currentPeriodHours": 0, "shiftsThisWeek": 0, "availability": [], "timeOff": [], "allowedPositionIds": []},  # Role too low
            {"userId": 2, "role": 3, "hoursMax": 40, "currentPeriodHours": 0, "shiftsThisWeek": 0, "availability": [], "timeOff": [], "allowedPositionIds": []}   # Qualified
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 3, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": False}
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1"),
        (2, 1): model.NewBoolVar("x_2_1")
    }

    metadata = build_constraints(model, problem, x)

    # Employee 1 should be blocked
    assert any(item[0] == 1 and item[1] == 1 for item in metadata["c3_role_qualification"])

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]
    assert solver.Value(x[(1, 1)]) == 0  # Not qualified
    # Employee 2 can be assigned (or not, but not blocked by role)


def test_c4_opening_closing_requires_role_3():
    """Test C4: Opening/closing shifts (minRoleId <= 3) require Role <= 3."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {"userId": 1, "role": 4, "hoursMax": 40, "currentPeriodHours": 0, "shiftsThisWeek": 0, "availability": [], "timeOff": [], "allowedPositionIds": []},  # Not qualified for role 3 shifts
            {"userId": 2, "role": 3, "hoursMax": 40, "currentPeriodHours": 0, "shiftsThisWeek": 0, "availability": [], "timeOff": [], "allowedPositionIds": []}   # Qualified
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 3, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "07:00", "endTime": "15:00", "durationHours": 8.0, "isLocked": False}  # Requires role <= 3
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1"),
        (2, 1): model.NewBoolVar("x_2_1")
    }

    metadata = build_constraints(model, problem, x)

    # Employee 1 (role 4) should be blocked from shift requiring role <= 3
    assert any(item[0] == 1 and item[1] == 1 for item in metadata["c4_opening_closing"])

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]
    assert solver.Value(x[(1, 1)]) == 0  # Role 4 can't work shifts requiring role <= 3


def test_c5_availability_windows():
    """Test C5: Shifts must fall within availability windows."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {
                "userId": 1,
                "role": 4,
                "hoursMax": 40,
                "currentPeriodHours": 0,
                "shiftsThisWeek": 0,
                "availability": [
                    {"dayOfWeek": 1, "startTime": "12:00", "endTime": "20:00"}
                ],
                "timeOff": [],
                "allowedPositionIds": []
            }
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 4, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": False}  # Before availability
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1")
    }

    metadata = build_constraints(model, problem, x)

    # Should block assignment
    assert any(item[0] == 1 and item[1] == 1 for item in metadata["c5_availability"])

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]
    assert solver.Value(x[(1, 1)]) == 0


def test_c6_time_off():
    """Test C6: Time-off days are respected."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {
                "userId": 1,
                "role": 4,
                "hoursMax": 40,
                "currentPeriodHours": 0,
                "shiftsThisWeek": 0,
                "availability": [{"dayOfWeek": 1, "startTime": "08:00", "endTime": "22:00"}],
                "timeOff": [{"date": "2026-02-16", "reason": "Vacation"}],
                "allowedPositionIds": []
            }
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 4, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": False}
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1")
    }

    metadata = build_constraints(model, problem, x)

    assert any(item[0] == 1 and item[1] == 1 for item in metadata["c6_time_off"])

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]
    assert solver.Value(x[(1, 1)]) == 0


def test_c7_hours_max_hard_cap():
    """Test C7: hoursMax is never exceeded."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {
                "userId": 1,
                "role": 4,
                "hoursMax": 16.0,
                "currentPeriodHours": 8.0,
                "shiftsThisWeek": 1,
                "availability": [{"dayOfWeek": i, "startTime": "08:00", "endTime": "22:00"} for i in range(1, 8)],
                "timeOff": [],
                "allowedPositionIds": []
            }
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 4, "date": "2026-02-17", "dayOfWeek": 2, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": False},
            {"shiftId": 2, "minRoleId": 4, "date": "2026-02-18", "dayOfWeek": 3, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": False}
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1"),
        (1, 2): model.NewBoolVar("x_1_2")
    }

    metadata = build_constraints(model, problem, x)

    assert (1, 16.0) in metadata["c7_hours_max"]

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]

    # Employee can take at most 1 shift (8h current + 8h = 16h max)
    assigned = solver.Value(x[(1, 1)]) + solver.Value(x[(1, 2)])
    assert assigned <= 1


def test_c8_one_shift_per_day():
    """Test C8: One shift per employee per day."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {
                "userId": 1,
                "role": 4,
                "hoursMax": 40,
                "currentPeriodHours": 0,
                "shiftsThisWeek": 0,
                "availability": [{"dayOfWeek": 1, "startTime": "08:00", "endTime": "22:00"}],
                "timeOff": [],
                "allowedPositionIds": []
            }
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 4, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "09:00", "endTime": "13:00", "durationHours": 4.0, "isLocked": False},
            {"shiftId": 2, "minRoleId": 4, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "14:00", "endTime": "18:00", "durationHours": 4.0, "isLocked": False}
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1"),
        (1, 2): model.NewBoolVar("x_1_2")
    }

    metadata = build_constraints(model, problem, x)

    assert any(item[0] == 1 and item[1] == "2026-02-16" for item in metadata["c8_one_per_day"])

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]

    # Employee can take at most 1 shift per day
    assigned = solver.Value(x[(1, 1)]) + solver.Value(x[(1, 2)])
    assert assigned <= 1


def test_c10_max_five_shifts_per_week():
    """Test C10: Max 5 shifts per employee per week."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {
                "userId": 1,
                "role": 4,
                "hoursMax": 50,
                "currentPeriodHours": 0,
                "shiftsThisWeek": 2,  # Already has 2 shifts
                "availability": [{"dayOfWeek": i, "startTime": "08:00", "endTime": "22:00"} for i in range(1, 8)],
                "timeOff": [],
                "allowedPositionIds": []
            }
        ],
        "shifts": [
            {"shiftId": i, "minRoleId": 4, "date": f"2026-02-{16+i}", "dayOfWeek": 1+i, "startTime": "09:00", "endTime": "15:00", "durationHours": 6.0, "isLocked": False}
            for i in range(5)  # 5 more shifts available
        ],
        "lockedShifts": []
    }

    x = {
        (1, i): model.NewBoolVar(f"x_1_{i}")
        for i in range(5)
    }

    metadata = build_constraints(model, problem, x)

    assert (1, 2) in metadata["c10_max_shifts_per_week"]

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]

    # Employee can take at most 3 more shifts (2 + 3 = 5 total)
    assigned = sum(solver.Value(x[(1, i)]) for i in range(5))
    assert assigned <= 3


def test_c12_position_eligible_allowed():
    """Test C12: Employee with positions [1,3] assigned to shift with positionId=1 -> ALLOWED."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {
                "userId": 1,
                "role": 4,
                "hoursMax": 40,
                "currentPeriodHours": 0,
                "shiftsThisWeek": 0,
                "availability": [{"dayOfWeek": 1, "startTime": "08:00", "endTime": "22:00"}],
                "timeOff": [],
                "allowedPositionIds": [1, 3]
            }
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 4, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": False, "positionId": 1}
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1")
    }

    metadata = build_constraints(model, problem, x)

    # Employee 1 should NOT be blocked (they have position 1)
    assert not any(item[0] == 1 and item[1] == 1 for item in metadata["c12_position_eligibility"])

    # Add dummy objective to make model solvable
    model.Maximize(0)

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]
    # Employee can be assigned (not blocked by position)


def test_c12_position_ineligible_blocked():
    """Test C12: Employee with positions [1,3] assigned to shift with positionId=2 -> BLOCKED."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {
                "userId": 1,
                "role": 4,
                "hoursMax": 40,
                "currentPeriodHours": 0,
                "shiftsThisWeek": 0,
                "availability": [{"dayOfWeek": 1, "startTime": "08:00", "endTime": "22:00"}],
                "timeOff": [],
                "allowedPositionIds": [1, 3]
            }
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 4, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": False, "positionId": 2}
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1")
    }

    metadata = build_constraints(model, problem, x)

    # Employee 1 should be blocked (they don't have position 2)
    assert any(item[0] == 1 and item[1] == 1 for item in metadata["c12_position_eligibility"])

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]
    assert solver.Value(x[(1, 1)]) == 0  # Not qualified


def test_c12_no_positions_blocked():
    """Test C12: Employee with positions [] assigned to shift with positionId=1 -> BLOCKED."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {
                "userId": 1,
                "role": 4,
                "hoursMax": 40,
                "currentPeriodHours": 0,
                "shiftsThisWeek": 0,
                "availability": [{"dayOfWeek": 1, "startTime": "08:00", "endTime": "22:00"}],
                "timeOff": [],
                "allowedPositionIds": []
            }
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 4, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": False, "positionId": 1}
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1")
    }

    metadata = build_constraints(model, problem, x)

    # Employee 1 should be blocked (they have no positions)
    assert any(item[0] == 1 and item[1] == 1 for item in metadata["c12_position_eligibility"])

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]
    assert solver.Value(x[(1, 1)]) == 0  # Not qualified


def test_c12_null_position_any_employee():
    """Test C12: Employee with positions [1,3] assigned to shift with positionId=None -> ALLOWED."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {
                "userId": 1,
                "role": 4,
                "hoursMax": 40,
                "currentPeriodHours": 0,
                "shiftsThisWeek": 0,
                "availability": [{"dayOfWeek": 1, "startTime": "08:00", "endTime": "22:00"}],
                "timeOff": [],
                "allowedPositionIds": [1, 3]
            }
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 4, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": False, "positionId": None}
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1")
    }

    metadata = build_constraints(model, problem, x)

    # Employee 1 should NOT be blocked (shift has no position requirement)
    assert not any(item[0] == 1 and item[1] == 1 for item in metadata["c12_position_eligibility"])

    # Add dummy objective to make model solvable
    model.Maximize(0)

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]
    # Employee can be assigned (no position requirement)


def test_c12_null_position_no_positions_allowed():
    """Test C12: Employee with positions [] assigned to shift with positionId=None -> ALLOWED."""
    model = cp_model.CpModel()

    problem = {
        "employees": [
            {
                "userId": 1,
                "role": 4,
                "hoursMax": 40,
                "currentPeriodHours": 0,
                "shiftsThisWeek": 0,
                "availability": [{"dayOfWeek": 1, "startTime": "08:00", "endTime": "22:00"}],
                "timeOff": [],
                "allowedPositionIds": []
            }
        ],
        "shifts": [
            {"shiftId": 1, "minRoleId": 4, "date": "2026-02-16", "dayOfWeek": 1, "startTime": "09:00", "endTime": "17:00", "durationHours": 8.0, "isLocked": False, "positionId": None}
        ],
        "lockedShifts": []
    }

    x = {
        (1, 1): model.NewBoolVar("x_1_1")
    }

    metadata = build_constraints(model, problem, x)

    # Employee 1 should NOT be blocked (shift has no position requirement)
    assert not any(item[0] == 1 and item[1] == 1 for item in metadata["c12_position_eligibility"])

    # Add dummy objective to make model solvable
    model.Maximize(0)

    solver = cp_model.CpSolver()
    solver.parameters.log_search_progress = False
    status = solver.Solve(model)

    assert status in [cp_model.OPTIMAL, cp_model.FEASIBLE]
    # Employee with no positions can still work shifts with no position requirement
