"""Discord bot — čisté helpery (custom_id, výběry → patch, deadline z voleb), bez sítě/discord lib.""" from __future__ import annotations import unittest from datetime import datetime, timezone from zoneinfo import ZoneInfo from services.discord_bot import ( action_to_patch, choice_label, departure_choice_to_deadline, parse_custom_id, select_to_patch, ) _PRAGUE = ZoneInfo("Europe/Prague") # 2026-06-12 je pátek; 10:00 UTC = 12:00 Europe/Prague (CEST) _NOW = datetime(2026, 6, 12, 10, 0, tzinfo=timezone.utc) def _prague(dt: datetime) -> str: return dt.astimezone(_PRAGUE).strftime("%Y-%m-%d %H:%M") class ParseCustomIdTests(unittest.TestCase): def test_valid_selects(self) -> None: self.assertEqual( parse_custom_id("ev:2:ev-charger-1:dep"), (2, "ev-charger-1", "dep") ) self.assertEqual( parse_custom_id("ev:2:ev-charger-1:tgt"), (2, "ev-charger-1", "tgt") ) def test_valid_legacy_buttons(self) -> None: self.assertEqual( parse_custom_id("ev:2:ev-charger-1:h2"), (2, "ev-charger-1", "h2") ) def test_invalid(self) -> None: for bad in ("", "ev:2:x:jump", "foo:1:c:h2", "ev:abc:c:h2", "ev:1:c:dep:x"): self.assertIsNone(parse_custom_id(bad)) class DepartureChoiceTests(unittest.TestCase): """Absolutní deadline z výběru „Kdy odjíždíš?" (Europe/Prague).""" def test_h2(self) -> None: dl = departure_choice_to_deadline("h2", now=_NOW) self.assertEqual(_prague(dl), "2026-06-12 14:00") def test_h4(self) -> None: dl = departure_choice_to_deadline("h4", now=_NOW) self.assertEqual(_prague(dl), "2026-06-12 16:00") def test_today18_before_18(self) -> None: dl = departure_choice_to_deadline("today18", now=_NOW) # 12:00 Prague self.assertEqual(_prague(dl), "2026-06-12 18:00") def test_today18_after_18_rolls_to_next_day(self) -> None: late = datetime(2026, 6, 12, 17, 30, tzinfo=timezone.utc) # 19:30 Prague dl = departure_choice_to_deadline("today18", now=late) self.assertEqual(_prague(dl), "2026-06-13 18:00") def test_tomorrow7_crosses_midnight(self) -> None: # 23:30 Prague v pátek → zítra (sobota) 07:00, tj. +7,5 h late = datetime(2026, 6, 12, 21, 30, tzinfo=timezone.utc) dl = departure_choice_to_deadline("tomorrow7", now=late) self.assertEqual(_prague(dl), "2026-06-13 07:00") def test_tomorrow12(self) -> None: dl = departure_choice_to_deadline("tomorrow12", now=_NOW) self.assertEqual(_prague(dl), "2026-06-13 12:00") def test_monday7_from_friday_allows_over_48h(self) -> None: # explicitní volba smí přes 48h limit fn_ev_session_defaults dl = departure_choice_to_deadline("monday7", now=_NOW) # pátek 12:00 self.assertEqual(_prague(dl), "2026-06-15 07:00") self.assertGreater((dl - _NOW).total_seconds(), 48 * 3600) def test_monday7_on_monday_before_7_is_today(self) -> None: mon_early = datetime(2026, 6, 15, 3, 0, tzinfo=timezone.utc) # po 05:00 dl = departure_choice_to_deadline("monday7", now=mon_early) self.assertEqual(_prague(dl), "2026-06-15 07:00") def test_monday7_on_monday_after_7_is_next_week(self) -> None: mon_late = datetime(2026, 6, 15, 8, 0, tzinfo=timezone.utc) # po 10:00 dl = departure_choice_to_deadline("monday7", now=mon_late) self.assertEqual(_prague(dl), "2026-06-22 07:00") def test_unknown_choice_raises(self) -> None: with self.assertRaises(ValueError): departure_choice_to_deadline("never", now=_NOW) class SelectPatchTests(unittest.TestCase): """Mapování výběrů na patch payload pro fn_ev_session_apply_patch.""" def test_dep_patches_only_deadline(self) -> None: p = select_to_patch("dep", "today18", now=_NOW, soc_at_connect=55.0) self.assertEqual(set(p), {"target_deadline"}) self.assertIn("2026-06-12T18:00", p["target_deadline"]) self.assertIn("+02:00", p["target_deadline"]) # Europe/Prague (CEST) def test_tgt_patches_only_target(self) -> None: for value, expected in (("30", 30.0), ("50", 50.0), ("70", 70.0), ("100", 100.0)): p = select_to_patch("tgt", value, now=_NOW, soc_at_connect=55.0) self.assertEqual(p, {"target_soc_pct": expected}) def test_tgt_stop_targets_connect_soc(self) -> None: p = select_to_patch("tgt", "stop", now=_NOW, soc_at_connect=42.5) self.assertEqual(p, {"target_soc_pct": 42.5}) def test_tgt_stop_without_soc(self) -> None: p = select_to_patch("tgt", "stop", now=_NOW, soc_at_connect=None) self.assertEqual(p, {"target_soc_pct": 0.0}) def test_unknown_kind_raises(self) -> None: with self.assertRaises(ValueError): select_to_patch("foo", "30", now=_NOW, soc_at_connect=None) def test_labels(self) -> None: self.assertEqual(choice_label("dep", "monday7"), "odjezd pondělí ráno 7:00") self.assertEqual(choice_label("tgt", "70"), "cíl 70 %") self.assertEqual(choice_label("tgt", "stop"), "nenabíjet") class LegacyActionPatchTests(unittest.TestCase): """Legacy tlačítka starších zpráv (h2/h4/morning/full/stop).""" def _patch(self, action: str, **kw): return action_to_patch( action, now=_NOW, soc_at_connect=kw.get("soc", 55.0), default_deadline_hour=kw.get("hour", 7), ) def test_h2_deadline(self) -> None: p = self._patch("h2") self.assertIn("2026-06-12T12:00", p["target_deadline"]) def test_morning_next_occurrence(self) -> None: p = self._patch("morning", hour=7) # 12:00 Prague > 7:00 → zítra 7:00 Prague self.assertIn("2026-06-13T07:00", p["target_deadline"]) def test_morning_today_if_before(self) -> None: early = datetime(2026, 6, 12, 2, 0, tzinfo=timezone.utc) # 4:00 Prague p = action_to_patch("morning", now=early, soc_at_connect=50, default_deadline_hour=7) self.assertIn("2026-06-12T07:00", p["target_deadline"]) def test_full(self) -> None: p = self._patch("full") self.assertEqual(p["target_soc_pct"], 100) def test_stop_targets_connect_soc(self) -> None: p = self._patch("stop", soc=42.5) self.assertEqual(p["target_soc_pct"], 42.5) if __name__ == "__main__": unittest.main()