"""Test Days module."""
-from unittest import TestCase
-from datetime import datetime
+from datetime import datetime, timedelta
 from typing import Callable
-from tests.utils import TestCaseWithDB, TestCaseWithServer
-from plomtask.dating import date_in_n_days
+from tests.utils import TestCaseSansDB, TestCaseWithDB, TestCaseWithServer
+from plomtask.dating import date_in_n_days, DATE_FORMAT
 from plomtask.days import Day
 
 
-class TestsSansDB(TestCase):
+class TestsSansDB(TestCaseSansDB):
     """Days module tests not requiring DB setup."""
-    legal_ids = ['2024-01-01']
-    illegal_ids = ['foo', '2024-02-30', '2024-02-01 23:00:00']
+    checked_class = Day
+    legal_ids = ['2024-01-01', '2024-02-29']
+    illegal_ids = ['foo', '2023-02-29', '2024-02-30', '2024-02-01 23:00:00']
+
+    def test_date_in_n_days(self) -> None:
+        """Test dating.date_in_n_days, as we rely on it in later tests."""
+        for n in [-100, -2, -1, 0, 1, 2, 1000]:
+            date = datetime.now() + timedelta(days=n)
+            self.assertEqual(date_in_n_days(n), date.strftime(DATE_FORMAT))
 
     def test_Day_datetime_weekday_neighbor_dates(self) -> None:
-        """Test Day's date parsing."""
+        """Test Day's date parsing and neighbourhood resolution."""
         self.assertEqual(datetime(2024, 5, 1), Day('2024-05-01').datetime)
         self.assertEqual('Sunday', Day('2024-03-17').weekday)
         self.assertEqual('March', Day('2024-03-17').month_name)
         self.assertEqual('2023-12-31', Day('2024-01-01').prev_date)
         self.assertEqual('2023-03-01', Day('2023-02-28').next_date)
 
-    def test_Day_sorting(self) -> None:
-        """Test sorting by .__lt__ and Day.__eq__."""
-        day1 = Day('2024-01-01')
-        day2 = Day('2024-01-02')
-        day3 = Day('2024-01-03')
-        days = [day3, day1, day2]
-        self.assertEqual(sorted(days), [day1, day2, day3])
-
 
 class TestsWithDB(TestCaseWithDB):
     """Tests requiring DB, but not server setup."""
     checked_class = Day
     default_ids = ('2024-01-01', '2024-01-02', '2024-01-03')
 
-    def test_Day_by_date_range_filled(self) -> None:
-        """Test Day.by_date_range_filled."""
-        date1, date2, date3 = self.default_ids
-        day1 = Day(date1)
-        day2 = Day(date2)
-        day3 = Day(date3)
-        for day in [day1, day2, day3]:
-            day.save(self.db_conn)
-        # check date range includes limiter days
-        self.assertEqual(Day.by_date_range_filled(self.db_conn, date1, date3),
-                         [day1, day2, day3])
-        # check first date range value excludes what's earlier
-        self.assertEqual(Day.by_date_range_filled(self.db_conn, date2, date3),
-                         [day2, day3])
-        # check second date range value excludes what's later
-        self.assertEqual(Day.by_date_range_filled(self.db_conn, date1, date2),
-                         [day1, day2])
-        # check swapped (impossible) date range returns emptiness
-        self.assertEqual(Day.by_date_range_filled(self.db_conn, date3, date1),
-                         [])
-        # check fill_gaps= instantiates unsaved dates within date range
-        # (but does not store them)
-        day5 = Day('2024-01-05')
-        day6 = Day('2024-01-06')
-        day6.save(self.db_conn)
-        day7 = Day('2024-01-07')
-        self.assertEqual(Day.by_date_range_filled(self.db_conn,
-                                                  day5.date, day7.date),
-                         [day5, day6, day7])
-        self.check_identity_with_cache_and_db([day1, day2, day3, day6])
-        # check 'today' is interpreted as today's date
-        today = Day(date_in_n_days(0))
-        self.assertEqual(Day.by_date_range_filled(self.db_conn,
-                                                  'today', 'today'),
-                         [today])
-        prev_day = Day(date_in_n_days(-1))
-        next_day = Day(date_in_n_days(1))
-        self.assertEqual(Day.by_date_range_filled(self.db_conn,
-                                                  'yesterday', 'tomorrow'),
-                         [prev_day, today, next_day])
+    def test_Day_by_date_range_with_limits(self) -> None:
+        """Test .by_date_range_with_limits."""
+        self.check_by_date_range_with_limits('id', set_id_field=False)
+
+    def test_Day_with_filled_gaps(self) -> None:
+        """Test .with_filled_gaps."""
+
+        def test(range_indexes: tuple[int, int], indexes_to_provide: list[int]
+                 ) -> None:
+            start_i, end_i = range_indexes
+            days_provided = []
+            days_expected = days_sans_comment[:]
+            for i in indexes_to_provide:
+                day_with_comment = days_with_comment[i]
+                days_provided += [day_with_comment]
+                days_expected[i] = day_with_comment
+            days_expected = days_expected[start_i:end_i+1]
+            start, end = dates[start_i], dates[end_i]
+            days_result = self.checked_class.with_filled_gaps(days_provided,
+                                                              start, end)
+            self.assertEqual(days_result, days_expected)
+
+        # for provided Days we use those from days_with_comment, to identify
+        # them against mere filler days by their lack of comment (identity
+        # with Day at the respective position in days_sans_comment)
+        dates = [f'2024-02-0{n+1}' for n in range(9)]
+        days_with_comment = [Day(date, comment=date[-1:]) for date in dates]
+        days_sans_comment = [Day(date, comment='') for date in dates]
+        # check provided Days recognizable in (full-range) interval
+        test((0, 8), [0, 4, 8])
+        # check limited range, but limiting Days provided
+        test((2, 6), [2, 5, 6])
+        # check Days within range but beyond provided Days also filled in
+        test((1, 7), [2, 5])
+        # check provided Days beyond range ignored
+        test((3, 5), [1, 2, 4, 6, 7])
+        # check inversion of start_date and end_date returns empty list
+        test((5, 3), [2, 4, 6])
+        # check empty provision still creates filler elements in interval
+        test((3, 5), [])
+        # check single-element selection creating only filler beyond provided
+        test((1, 1), [2, 4, 6])
+        # check (un-saved) filler Days don't show up in cache or DB
+        # dates = [f'2024-02-0{n}' for n in range(1, 6)]
+        day = Day(dates[3])
+        day.save(self.db_conn)
+        self.checked_class.with_filled_gaps([day], dates[0], dates[-1])
+        self.check_identity_with_cache_and_db([day])
+        # check 'today', 'yesterday', 'tomorrow' are interpreted
+        yesterday = Day('yesterday')
+        tomorrow = Day('tomorrow')
+        today = Day('today')
+        result = self.checked_class.with_filled_gaps([today], 'yesterday',
+                                                     'tomorrow')
+        self.assertEqual(result, [yesterday, today, tomorrow])
 
 
 class TestsWithServer(TestCaseWithServer):
 
     @classmethod
     def GET_calendar_dict(cls, start: int, end: int) -> dict[str, object]:
-        """Return JSON of GET /calendar to expect."""
+        """Return JSON of GET /calendar to expect.
+
+        NB: the date string list to key 'days' implies/expects a continuous (=
+        gaps filled) alphabetical order of dates by virtue of range(start,
+        end+1) and date_in_n_days tested in TestsSansDB.test_date_in_n_days.
+        """
         today_date = date_in_n_days(0)
         start_date = date_in_n_days(start)
         end_date = date_in_n_days(end)
         # check illegal date range delimiters
         self.check_get('/calendar?start=foo', 400)
         self.check_get('/calendar?end=foo', 400)
-        # check default range without saved days
+        # check default range for expected selection/order without saved days
         expected = self.GET_calendar_dict(-1, 366)
         self.check_json_get('/calendar', expected)
         self.check_json_get('/calendar?start=&end=', expected)
-        # check named days as delimiters
+        # check with named days as delimiters
         expected = self.GET_calendar_dict(-1, +1)
         self.check_json_get('/calendar?start=yesterday&end=tomorrow', expected)
         # check zero-element range
         expected = self.GET_calendar_dict(+1, 0)
         self.check_json_get('/calendar?start=tomorrow&end=today', expected)
-        # check saved day shows up in results with proven by its comment
+        # check saved day shows up in results, proven by its comment
         post_day: dict[str, object] = {'day_comment': 'foo', 'make_type': ''}
         date1 = date_in_n_days(-2)
         self._post_day(f'date={date1}', post_day)
 
 from typing import Mapping, Any, Callable
 from threading import Thread
 from http.client import HTTPConnection
-from datetime import datetime
+from datetime import datetime, timedelta
 from time import sleep
 from json import loads as json_loads
 from urllib.parse import urlencode
 from plomtask.processes import Process, ProcessStep
 from plomtask.conditions import Condition
 from plomtask.days import Day
+from plomtask.dating import DATE_FORMAT
 from plomtask.todos import Todo
 from plomtask.versioned_attributes import VersionedAttribute, TIMESTAMP_FMT
 from plomtask.exceptions import NotFoundException, HandledException
 
 class TestCaseSansDB(TestCaseAugmented):
     """Tests requiring no DB setup."""
-    legal_ids = [1, 5]
-    illegal_ids = [0]
+    legal_ids: list[str] | list[int] = [1, 5]
+    illegal_ids: list[str] | list[int] = [0]
 
     @TestCaseAugmented._run_if_checked_class
     def test_id_validation(self) -> None:
         hashes_db_found = [hash(x) for x in db_found]
         self.assertEqual(sorted(hashes_content), sorted(hashes_db_found))
 
+    def check_by_date_range_with_limits(self,
+                                        date_col: str,
+                                        set_id_field: bool = True
+                                        ) -> None:
+        """Test .by_date_range_with_limits."""
+        # pylint: disable=too-many-locals
+        f = self.checked_class.by_date_range_with_limits
+        # check illegal ranges
+        legal_range = ('yesterday', 'tomorrow')
+        for i in [0, 1]:
+            for bad_date in ['foo', '2024-02-30', '2024-01-01 12:00:00']:
+                date_range = list(legal_range[:])
+                date_range[i] = bad_date
+                with self.assertRaises(HandledException):
+                    f(self.db_conn, date_range, date_col)
+        # check empty, translation of 'yesterday' and 'tomorrow'
+        items, start, end = f(self.db_conn, legal_range, date_col)
+        self.assertEqual(items, [])
+        yesterday = datetime.now() + timedelta(days=-1)
+        tomorrow = datetime.now() + timedelta(days=+1)
+        self.assertEqual(start, yesterday.strftime(DATE_FORMAT))
+        self.assertEqual(end, tomorrow.strftime(DATE_FORMAT))
+        # make dated items for non-empty results
+        kwargs_with_date = self.default_init_kwargs.copy()
+        if set_id_field:
+            kwargs_with_date['id_'] = None
+        objs = []
+        dates = ['2024-01-01', '2024-01-02', '2024-01-04']
+        for date in ['2024-01-01', '2024-01-02', '2024-01-04']:
+            kwargs_with_date['date'] = date
+            obj = self.checked_class(**kwargs_with_date)
+            objs += [obj]
+        # check ranges still empty before saving
+        date_range = [dates[0], dates[-1]]
+        self.assertEqual(f(self.db_conn, date_range, date_col)[0], [])
+        # check all objs displayed within closed interval
+        for obj in objs:
+            obj.save(self.db_conn)
+        self.assertEqual(f(self.db_conn, date_range, date_col)[0], objs)
+        # check that only displayed what exists within interval
+        date_range = ['2023-12-20', '2024-01-03']
+        expected = [objs[0], objs[1]]
+        self.assertEqual(f(self.db_conn, date_range, date_col)[0], expected)
+        date_range = ['2024-01-03', '2024-01-30']
+        expected = [objs[2]]
+        self.assertEqual(f(self.db_conn, date_range, date_col)[0], expected)
+        # check that inverted interval displays nothing
+        date_range = [dates[-1], dates[0]]
+        self.assertEqual(f(self.db_conn, date_range, date_col)[0], [])
+        # check that "today" is interpreted, and single-element interval
+        today_date = datetime.now().strftime(DATE_FORMAT)
+        kwargs_with_date['date'] = today_date
+        obj_today = self.checked_class(**kwargs_with_date)
+        obj_today.save(self.db_conn)
+        date_range = ['today', 'today']
+        items, start, end = f(self.db_conn, date_range, date_col)
+        self.assertEqual(start, today_date)
+        self.assertEqual(start, end)
+        self.assertEqual(items, [obj_today])
+
     @TestCaseAugmented._run_on_versioned_attributes
     def test_saving_versioned_attributes(self,
                                          owner: Any,