Fix bug roundtripping datetime.time objects after midnight in eastern hemisphere timezones (#2417) (#2438)

* Fix bug roundtripping datetime.time objects after midnight in eastern hemisphere timezones (#2417)

* tests: check more timezones

* Fix review remarks: remove useless comment and skip setting TZ environment variable on Windows
diff --git a/include/pybind11/chrono.h b/include/pybind11/chrono.h
index 6b9ab9b..6127c65 100644
--- a/include/pybind11/chrono.h
+++ b/include/pybind11/chrono.h
@@ -150,21 +150,28 @@
         // Lazy initialise the PyDateTime import
         if (!PyDateTimeAPI) { PyDateTime_IMPORT; }
 
-        std::time_t tt = system_clock::to_time_t(time_point_cast<system_clock::duration>(src));
+        // Get out microseconds, and make sure they are positive, to avoid bug in eastern hemisphere time zones
+        // (cfr. https://github.com/pybind/pybind11/issues/2417)
+        using us_t = duration<int, std::micro>;
+        auto us = duration_cast<us_t>(src.time_since_epoch() % seconds(1));
+        if (us.count() < 0)
+            us += seconds(1);
+
+        // Subtract microseconds BEFORE `system_clock::to_time_t`, because:
+        // > If std::time_t has lower precision, it is implementation-defined whether the value is rounded or truncated.
+        // (https://en.cppreference.com/w/cpp/chrono/system_clock/to_time_t)
+        std::time_t tt = system_clock::to_time_t(time_point_cast<system_clock::duration>(src - us));
         // this function uses static memory so it's best to copy it out asap just in case
         // otherwise other code that is using localtime may break this (not just python code)
         std::tm localtime = *std::localtime(&tt);
 
-        // Declare these special duration types so the conversions happen with the correct primitive types (int)
-        using us_t = duration<int, std::micro>;
-
         return PyDateTime_FromDateAndTime(localtime.tm_year + 1900,
                                           localtime.tm_mon + 1,
                                           localtime.tm_mday,
                                           localtime.tm_hour,
                                           localtime.tm_min,
                                           localtime.tm_sec,
-                                          (duration_cast<us_t>(src.time_since_epoch() % seconds(1))).count());
+                                          us.count());
     }
     PYBIND11_TYPE_CASTER(type, _("datetime.datetime"));
 };
diff --git a/tests/test_chrono.cpp b/tests/test_chrono.cpp
index 899d08d..1d79d4b 100644
--- a/tests/test_chrono.cpp
+++ b/tests/test_chrono.cpp
@@ -10,6 +10,7 @@
 
 #include "pybind11_tests.h"
 #include <pybind11/chrono.h>
+#include <chrono>
 
 TEST_SUBMODULE(chrono, m) {
     using system_time = std::chrono::system_clock::time_point;
diff --git a/tests/test_chrono.py b/tests/test_chrono.py
index 5392f8f..f94d5ba 100644
--- a/tests/test_chrono.py
+++ b/tests/test_chrono.py
@@ -1,6 +1,9 @@
 # -*- coding: utf-8 -*-
 from pybind11_tests import chrono as m
 import datetime
+import pytest
+
+import env  # noqa: F401
 
 
 def test_chrono_system_clock():
@@ -70,8 +73,30 @@
     assert time2.microsecond == 0
 
 
-def test_chrono_system_clock_roundtrip_time():
-    time1 = datetime.datetime.today().time()
+SKIP_TZ_ENV_ON_WIN = pytest.mark.skipif(
+    "env.WIN", reason="TZ environment variable only supported on POSIX"
+)
+
+
+@pytest.mark.parametrize("time1", [
+    datetime.datetime.today().time(),
+    datetime.time(0, 0, 0),
+    datetime.time(0, 0, 0, 1),
+    datetime.time(0, 28, 45, 109827),
+    datetime.time(0, 59, 59, 999999),
+    datetime.time(1, 0, 0),
+    datetime.time(5, 59, 59, 0),
+    datetime.time(5, 59, 59, 1),
+])
+@pytest.mark.parametrize("tz", [
+    None,
+    pytest.param("Europe/Brussels", marks=SKIP_TZ_ENV_ON_WIN),
+    pytest.param("Asia/Pyongyang", marks=SKIP_TZ_ENV_ON_WIN),
+    pytest.param("America/New_York", marks=SKIP_TZ_ENV_ON_WIN),
+])
+def test_chrono_system_clock_roundtrip_time(time1, tz, monkeypatch):
+    if tz is not None:
+        monkeypatch.setenv("TZ", "/usr/share/zoneinfo/{}".format(tz))
 
     # Roundtrip the time
     datetime2 = m.test_chrono2(time1)