from datetime import date, time

from app_new.core.services.day_planner import DayPlanner, PlanningCandidate, WorkWindow


def test_proposal_respects_pause_and_is_deterministic(monkeypatch):
    monkeypatch.setattr(
        DayPlanner,
        "work_windows",
        staticmethod(lambda day, user_id=None: [WorkWindow(time(8), time(12)), WorkWindow(time(13, 30), time(17))]),
    )
    candidates = [
        PlanningCandidate("scheduled_task", 2, "Contrôle B", duration_minutes=45),
        PlanningCandidate("scheduled_task", 1, "Contrôle A", duration_minutes=45),
    ]
    first = DayPlanner.propose(date(2026, 8, 24), candidates)
    assert [(item.proposed_start, item.proposed_end) for item in first] == [
        (time(8), time(8, 45)), (time(8, 45), time(9, 30))
    ]
    assert all(item.proposed_end <= time(12) for item in first)
    second = DayPlanner.propose(date(2026, 8, 24), [
        PlanningCandidate("scheduled_task", 2, "Contrôle B", duration_minutes=45),
        PlanningCandidate("scheduled_task", 1, "Contrôle A", duration_minutes=45),
    ])
    assert [(item.proposed_start, item.proposed_end) for item in first] == [
        (item.proposed_start, item.proposed_end) for item in second
    ]


def test_fixed_event_is_preserved(monkeypatch):
    monkeypatch.setattr(DayPlanner, "work_windows", staticmethod(lambda day, user_id=None: [WorkWindow(time(8), time(17))]))
    fixed = PlanningCandidate("company", 1, "Entreprise", constraint="fixed", fixed_start=time(9), fixed_end=time(10))
    flexible = PlanningCandidate("admin", 2, "Commande", duration_minutes=30)
    result = DayPlanner.propose(date(2026, 8, 24), [flexible, fixed])
    assert next(item for item in result if item.source_id == 1).proposed_start == time(9)
    assert next(item for item in result if item.source_id == 2).proposed_end <= time(9)


def test_task_too_long_is_marked_to_reschedule(monkeypatch):
    monkeypatch.setattr(DayPlanner, "work_windows", staticmethod(lambda day, user_id=None: [WorkWindow(time(8), time(9))]))
    result = DayPlanner.propose(date(2026, 8, 24), [PlanningCandidate("admin", 1, "Tâche", duration_minutes=90)])
    assert result[0].status == "à replanifier"
    assert result[0].proposed_start is None


def test_no_work_window_explains_missing_schedule(monkeypatch):
    monkeypatch.setattr(DayPlanner, "work_windows", staticmethod(lambda day, user_id=None: []))
    result = DayPlanner.propose(date(2026, 8, 24), [PlanningCandidate("admin", 1, "Tâche")])
    assert "horaire de travail" in result[0].explanation


def _full_day(monkeypatch):
    monkeypatch.setattr(DayPlanner, "work_windows", staticmethod(lambda day, user_id=None: [WorkWindow(time(8), time(12)), WorkWindow(time(13, 30), time(17))]))


def test_urgent_candidate_is_ordered_before_flexible(monkeypatch):
    _full_day(monkeypatch)
    rows = DayPlanner.propose(date(2026, 8, 24), [
        PlanningCandidate("admin", 1, "Flexible", priority=4, duration_minutes=20),
        PlanningCandidate("intervention", 2, "Fuite", task_type="emergency", constraint="urgent", priority=1, duration_minutes=20),
    ])
    assert rows[0].source_id == 2
    assert "créneau" in rows[0].explanation or rows[0].status == "proposé"


def test_deadline_candidate_does_not_finish_after_latest_end(monkeypatch):
    _full_day(monkeypatch)
    candidate = PlanningCandidate("admin", 1, "Commande", constraint="deadline", duration_minutes=60, latest_end=time(10))
    rows = DayPlanner.propose(date(2026, 8, 24), [candidate])
    assert rows[0].proposed_end <= time(10)


def test_deadline_candidate_is_marked_when_no_slot_before_deadline(monkeypatch):
    _full_day(monkeypatch)
    candidate = PlanningCandidate("admin", 1, "Commande", constraint="deadline", duration_minutes=30, earliest_start=time(11), latest_end=time(11, 15))
    rows = DayPlanner.propose(date(2026, 8, 24), [candidate])
    assert rows[0].status == "à replanifier"


def test_fixed_overlap_is_visible_as_conflict(monkeypatch):
    _full_day(monkeypatch)
    rows = DayPlanner.propose(date(2026, 8, 24), [
        PlanningCandidate("company", 1, "Entreprise", task_type="external_company", constraint="fixed", fixed_start=time(9), fixed_end=time(10)),
        PlanningCandidate("intervention", 2, "Urgence", task_type="emergency", constraint="fixed", fixed_start=time(9, 30), fixed_end=time(10, 15)),
    ])
    assert rows[1].status == "conflit"
    assert "chevauche" in rows[1].explanation


def test_geographic_labels_are_kept_for_explanation(monkeypatch):
    _full_day(monkeypatch)
    rows = DayPlanner.propose(date(2026, 8, 24), [
        PlanningCandidate("scheduled_task", 1, "Salle", building_name="A", zone_name="RDC", room_name="101"),
        PlanningCandidate("scheduled_task", 2, "Zone", building_name="A", zone_name="RDC", room_name="102"),
    ])
    assert rows[0].location_label == "A > RDC > 101"
    assert rows[1].location_label == "A > RDC > 102"


def test_unknown_location_is_explicit(monkeypatch):
    _full_day(monkeypatch)
    candidate = PlanningCandidate("admin", 1, "Dossier")
    assert DayPlanner.propose(date(2026, 8, 24), [candidate])[0].location_label == "Localisation non renseignée"


def test_room_occupation_rejects_slot(monkeypatch):
    _full_day(monkeypatch)
    monkeypatch.setattr("app_new.core.services.day_planner._resolved_room_occupancy_batch", lambda day, room_ids: {12: [(time(8), time(17), object())]})
    candidate = PlanningCandidate("scheduled_task", 1, "Salle occupée", room_id=12, duration_minutes=30)
    rows = DayPlanner.propose(date(2026, 8, 24), [candidate])
    assert rows[0].status == "à replanifier"


def test_room_without_id_can_use_work_window(monkeypatch):
    _full_day(monkeypatch)
    candidate = PlanningCandidate("scheduled_task", 1, "Sans salle", duration_minutes=30)
    rows = DayPlanner.propose(date(2026, 8, 24), [candidate])
    assert rows[0].status == "proposé"


def test_alternative_slots_exclude_fixed_event(monkeypatch):
    _full_day(monkeypatch)
    fixed = PlanningCandidate("company", 1, "Fixe", constraint="fixed", fixed_start=time(8), fixed_end=time(9))
    target = PlanningCandidate("admin", 2, "À replacer", duration_minutes=30)
    proposed = DayPlanner.propose(date(2026, 8, 24), [fixed, target])
    slots = DayPlanner.alternative_slots(date(2026, 8, 24), target, proposed)
    assert slots and slots[0][0] >= time(9)


def test_alternative_slots_are_limited_to_three(monkeypatch):
    _full_day(monkeypatch)
    target = PlanningCandidate("admin", 2, "À replacer", duration_minutes=15)
    slots = DayPlanner.alternative_slots(date(2026, 8, 24), target, [target], limit=3)
    assert len(slots) == 3


def test_explicit_user_parameter_is_forwarded(monkeypatch):
    seen = {}
    def windows(day, user_id=None):
        seen["user_id"] = user_id
        return [WorkWindow(time(8), time(9))]
    monkeypatch.setattr(DayPlanner, "work_windows", staticmethod(windows))
    DayPlanner.propose(date(2026, 8, 24), [PlanningCandidate("admin", 1, "Tâche")], user_id=42)
    assert seen["user_id"] == 42
