/*
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "clar.h"

#include "services/common/cron.h"
#include "util/size.h"

#include <pebbleos/cron.h>

#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_passert.h"
#include "stubs_regular_timer.h"
#include "fake_rtc.h"

// Tests
///////////////////////////////////////////////////////////
// Thursday 2015 Nov 12, 00:00:00 GMT
static const time_t s_2015_nov12_000000_gmt = 1447286400;
// Thursday 2015 Nov 12, 12:34:56 GMT
static const time_t s_2015_nov12_123456_gmt = 1447331696;
// Saturday 2015 Dec 19, 12:34:56 GMT
static const time_t s_2015_dec19_123456_gmt = 1450528496;

// DST points
static const time_t s_2015_nov20_020000_gmt = 1447984800;
static const time_t s_2015_dec20_020000_gmt = 1450576800;
static const TimezoneInfo s_timezone_gmt = {
  .tm_zone = "GMT",
  .dst_id = 0,
  .timezone_id = 0,
  .tm_gmtoff = 0,
  .dst_start = 0,
  .dst_end = 0,
};

void test_cron__initialize(void) {
  cron_service_init();
}

void test_cron__cleanup(void) {
  cron_service_deinit();
}

static TimezoneInfo g_timezone;
static void prv_set_rtc(time_t t, const TimezoneInfo *tz_info) {
  fake_rtc_init(0, t);
  g_timezone = *tz_info;
  time_util_update_timezone(&g_timezone);
}

static void prv_cron_callback(CronJob *job, void* data) {
  job->cb_data = (void*)((uintptr_t)data + 1);
}

static void prv_clock_change(int32_t time_diff, int32_t gmt_diff, bool dst_trans) {
  PebbleSetTimeEvent set_time_info = {
    .utc_time_delta = time_diff,
    .gmt_offset_delta = gmt_diff,
    .dst_changed = dst_trans,
  };
  rtc_set_time(rtc_get_time() + time_diff);
  g_timezone.tm_gmtoff += gmt_diff;
  time_util_update_timezone(&g_timezone);
  cron_service_handle_clock_change(&set_time_info);
}

void test_cron__time_change_basic(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 45,
    .hour = CRON_HOUR_ANY,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,

    .may_be_instant = true,

    .clock_change_tolerance = 0,
  };
  CronJob *job = &test_cron;
  time_t base = s_2015_nov12_123456_gmt;
  prv_set_rtc(base, &s_timezone_gmt);
  // 2015 Nov 12, 12:45:00
  int32_t target = 1447332300;

  cron_clear_all_jobs();
  cl_assert_equal_i(cron_service_get_job_count(), 0);

  cron_job_schedule(job);
  cl_assert_equal_i((uintptr_t)job->cb_data, 0);
  cl_assert_equal_i(job->cached_execute_time, target);
  cl_assert_equal_i(cron_service_get_job_count(), 1);

  // Mutate the execute time to see if we actually effect change.
  job->cached_execute_time = UINT32_MAX;
  job->clock_change_tolerance = 10;
  prv_clock_change(0, 0, false);
  cl_assert_equal_i(job->cached_execute_time, UINT32_MAX);
  job->cached_execute_time = UINT32_MAX;
  prv_clock_change(0, 0, true);
  cl_assert_equal_i(job->cached_execute_time, target);
  job->cached_execute_time = UINT32_MAX;
  prv_clock_change(0, 1, false);
  target--; // adjust for GMT offset change
  cl_assert_equal_i(job->cached_execute_time, target);
  job->cached_execute_time = UINT32_MAX;
  prv_clock_change(0, 1, true);
  target--; // adjust for GMT offset change
  cl_assert_equal_i(job->cached_execute_time, target);

  job->cached_execute_time = UINT32_MAX;
  job->clock_change_tolerance = 0;
  prv_clock_change(0, 0, false);
  cl_assert_equal_i(job->cached_execute_time, target);

  job->cached_execute_time = UINT32_MAX;
  job->clock_change_tolerance = 0;
  prv_clock_change(1, 0, false);
  cl_assert_equal_i(job->cached_execute_time, target);

  job->cached_execute_time = UINT32_MAX;
  job->clock_change_tolerance = 1;
  prv_clock_change(0, 0, false);
  cl_assert_equal_i(job->cached_execute_time, UINT32_MAX);

  job->cached_execute_time = UINT32_MAX;
  job->clock_change_tolerance = 1;
  prv_clock_change(1, 0, false);
  cl_assert_equal_i(job->cached_execute_time, target);

  job->cached_execute_time = UINT32_MAX;
  job->clock_change_tolerance = UINT32_MAX;
  prv_clock_change(INT32_MAX, 0, false);
  cl_assert_equal_i(job->cached_execute_time, UINT32_MAX);

  cron_clear_all_jobs();
}

void test_cron__time_change_instant(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 35,
    .hour = CRON_HOUR_ANY,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,

    .may_be_instant = true,

    .clock_change_tolerance = 0,
  };
  CronJob *job = &test_cron;
  time_t base = s_2015_nov12_123456_gmt;
  prv_set_rtc(base, &s_timezone_gmt);
  // 2015 Nov 12, 12:35:00
  int32_t target = 1447331700;

  cron_clear_all_jobs();
  cl_assert_equal_i(cron_service_get_job_count(), 0);

  cron_job_schedule(job);
  cl_assert_equal_i((uintptr_t)job->cb_data, 0);
  cl_assert_equal_i(job->cached_execute_time, target);
  cl_assert_equal_i(cron_service_get_job_count(), 1);

  // Mutate the execute time to see if we actually effect change.
  job->clock_change_tolerance = 100;
  prv_clock_change(10, 0, false);
  cl_assert_equal_i((uintptr_t)job->cb_data, 1);
  cl_assert_equal_i(job->cached_execute_time, target);
  cl_assert_equal_i(cron_service_get_job_count(), 0);

  cron_clear_all_jobs();
}

static void prv_basic_test(const TimezoneInfo *tz_info, CronJob *job, time_t base,
                           time_t offset, time_t increment, int dst_type) {
  TimezoneInfo new_tz_info = *tz_info;
  switch (dst_type) {
    case 0:
      break;
    case 1:
      new_tz_info.dst_start = s_2015_nov20_020000_gmt;
      new_tz_info.dst_end = s_2015_dec20_020000_gmt;
      break;
    case 2:
      new_tz_info.dst_start = 1;
      new_tz_info.dst_end = INT32_MAX;
      break;
  }
  prv_set_rtc(base, &new_tz_info);

  cron_clear_all_jobs();
  cl_assert_equal_i(cron_service_get_job_count(), 0);

  job->cb_data = (void*)0;

  cron_job_schedule(job);
  cl_assert_equal_i((uintptr_t)job->cb_data, 0);
  cl_assert_equal_i(job->cached_execute_time, base + offset);
  cl_assert_equal_i(cron_service_get_job_count(), 1);

  // Check that the timer doesn't fire early
  if (offset > 0) {
    fake_rtc_increment_time(increment - 1);
    cron_service_wakeup();
    cl_assert_equal_i((uintptr_t)job->cb_data, 0);
    cl_assert_equal_i(job->cached_execute_time, base + offset);
    cl_assert_equal_i(cron_service_get_job_count(), 1);
    fake_rtc_increment_time(1);
  } else {
    fake_rtc_increment_time(increment);
  }

  cron_service_wakeup();
  cl_assert_equal_i((uintptr_t)job->cb_data, 1);
  cl_assert_equal_i(job->cached_execute_time, base + offset);
  cl_assert_equal_i(cron_service_get_job_count(), 0);
}

void test_cron__1_basic(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = CRON_MINUTE_ANY,
    .hour = CRON_HOUR_ANY,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,

    .may_be_instant = true,
  };

  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_123456_gmt, 0, 0, 0);
}

void test_cron__4_basic(void) {
  CronJob test_cron[4] = {
    { .cb = prv_cron_callback,
      .cb_data = (void*)0,

      .minute = 45,
      .hour = CRON_HOUR_ANY,
      .mday = CRON_MDAY_ANY,
      .month = CRON_MONTH_ANY,

      .may_be_instant = true,
    },
    { .cb = prv_cron_callback,
      .cb_data = (void*)0,

      .minute = CRON_MINUTE_ANY,
      .hour = 13,
      .mday = CRON_MDAY_ANY,
      .month = CRON_MONTH_ANY,

      .may_be_instant = true,
    },
    { .cb = prv_cron_callback,
      .cb_data = (void*)0,

      .minute = CRON_MINUTE_ANY,
      .hour = CRON_HOUR_ANY,
      .mday = 12,
      .month = CRON_MONTH_ANY,

      .may_be_instant = true,
    },
    { .cb = prv_cron_callback,
      .cb_data = (void*)0,

      .minute = CRON_MINUTE_ANY,
      .hour = CRON_HOUR_ANY,
      .mday = CRON_MDAY_ANY,
      .month = 11,

      .may_be_instant = true,
    },
  };
  int timestamps[4] = {
    1447332300, // 2015 Nov 12, 12:45:00 GMT
    1447333200, // 2015 Nov 12, 13:00:00 GMT
    1447372800, // 2015 Nov 13, 00:00:00 GMT
    1448928000, // 2015 Dec  1, 00:00:00 GMT
  };

  prv_set_rtc(s_2015_nov12_123456_gmt, &s_timezone_gmt);

  cron_clear_all_jobs();
  cl_assert_equal_i(cron_service_get_job_count(), 0);

  // Add the jobs in reverse order to make sure they add properly.
  for (int i = 0; i < 4; i++) {
    CronJob *job = &test_cron[4 - i - 1];
    cron_job_schedule(job);
    cl_assert_equal_i((uintptr_t)job->cb_data, 0);
    cl_assert_equal_i(job->cached_execute_time, timestamps[4 - i - 1]);
    cl_assert_equal_i(cron_service_get_job_count(), i+1);
  }

  time_t left = s_2015_nov12_123456_gmt;
  for (int i = 0; i < 4; i++) {
    fake_rtc_increment_time(timestamps[i] - left);
    left = timestamps[i];
    cron_service_wakeup();
    cl_assert_equal_i(cron_service_get_job_count(), 4 - i - 1);
    for (int l = 0; l < 4; l++) {
      cl_assert_equal_i((uintptr_t)test_cron[l].cb_data, i >= l ? 1 : 0);
    }
  }
}

void test_cron__already_elapsed(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = CRON_MINUTE_ANY,
    .hour = CRON_HOUR_ANY,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,

    .may_be_instant = true,
  };

  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_123456_gmt, 0, SECONDS_PER_MINUTE, 0);
}

struct {
  int month, mday, hour, minute;
  int8_t wday;
  time_t dest_time;
} s_cron_test_info[] = {
  //////// 'future' time finding
  // minute
  // 2015 Nov 12, 12:45:00
  { -1,-1,-1,45, WDAY_ANY, 1447332300},
  // hour
  // 2015 Nov 12, 13:00:00
  { -1,-1,13,-1, WDAY_ANY, 1447333200},
  // hour+minute
  // 2015 Nov 12, 13:45:00
  { -1,-1,13,45, WDAY_ANY, 1447335900},
  // mday
  // 2015 Nov 13, 00:00:00
  { -1,12,-1,-1, WDAY_ANY, 1447372800},
  // mday+minute
  // 2015 Nov 13, 00:45:00
  { -1,12,-1,45, WDAY_ANY, 1447375500},
  // mday+hour
  // 2015 Nov 13, 13:00:00
  { -1,12,13,-1, WDAY_ANY, 1447419600},
  // mday+hour+minute
  // 2015 Nov 13, 13:45:00
  { -1,12,13,45, WDAY_ANY, 1447422300},
  // month
  // 2015 Dec  1, 00:00:00
  { 11,-1,-1,-1, WDAY_ANY, 1448928000},
  // month+minute
  // 2015 Dec  1, 00:45:00
  { 11,-1,-1,45, WDAY_ANY, 1448930700},
  // month+hour
  // 2015 Dec  1, 13:00:00
  { 11,-1,13,-1, WDAY_ANY, 1448974800},
  // month+hour+minute
  // 2015 Dec  1, 13:45:00
  { 11,-1,13,45, WDAY_ANY, 1448977500},
  // month+mday
  // 2015 Dec 13, 00:00:00
  { 11,12,-1,-1, WDAY_ANY, 1449964800},
  // month+mday+minute
  // 2015 Dec 13, 00:45:00
  { 11,12,-1,45, WDAY_ANY, 1449967500},
  // month+mday+hour
  // 2015 Dec 13, 13:00:00
  { 11,12,13,-1, WDAY_ANY, 1450011600},
  // month+mday+hour+minute
  // 2015 Dec 13, 13:45:00
  { 11,12,13,45, WDAY_ANY, 1450014300},

  //////// 'past' time finding
  // minute
  // 2015 Nov 12, 13:23:00
  { -1,-1,-1,23, WDAY_ANY, 1447334580},
  // hour
  // 2015 Nov 13, 11:00:00
  { -1,-1,11,-1, WDAY_ANY, 1447412400},
  // day
  // 2015 Dec 11, 00:00:00
  { -1,10,-1,-1, WDAY_ANY, 1449792000},
  // month
  // 2016 Oct  1, 00:00:00
  {  9,-1,-1,-1, WDAY_ANY, 1475280000},
  // month+hour
  // 2016 Oct  1, 12:00:00
  {  9,-1,12,-1, WDAY_ANY, 1475323200},

  //////// wday time finding
  // now, -Th
  // 2015 Nov 13, 00:00:00
  { -1,-1,-1,-1, WDAY_ANY & ~WDAY_THURSDAY, 1447372800},
  // now, -Th-Fr
  // 2015 Nov 14, 00:00:00
  { -1,-1,-1,-1, WDAY_ANY & ~(WDAY_THURSDAY|WDAY_FRIDAY), 1447459200},
  // now, -Th-Fr-Sa
  // 2015 Nov 15, 00:00:00
  { -1,-1,-1,-1, WDAY_ANY & ~(WDAY_THURSDAY|WDAY_FRIDAY|WDAY_SATURDAY), 1447545600},
  // now, -Th-Fr-Sa-Su
  // 2015 Nov 16, 00:00:00
  { -1,-1,-1,-1, WDAY_MONDAY|WDAY_TUESDAY|WDAY_WEDNESDAY, 1447632000},
  // now, -Th-Fr-Sa-Su-Mo
  // 2015 Nov 17, 00:00:00
  { -1,-1,-1,-1, WDAY_TUESDAY|WDAY_WEDNESDAY, 1447718400},
  // now, -Th-Fr-Sa-Su-Mo-Tu
  // 2015 Nov 18, 00:00:00
  { -1,-1,-1,-1, WDAY_WEDNESDAY, 1447804800},
  // now, -We
  // now
  { -1,-1,-1,-1, WDAY_ANY & ~WDAY_WEDNESDAY, s_2015_nov12_123456_gmt},
  // now, wday=0
  // now
  { -1,-1,-1,-1, 0, s_2015_nov12_123456_gmt},

  //////// wday+ time finding
  // 19th, -Th
  // 2015 Nov 20, 00:00:00
  { -1,18,-1,-1, WDAY_ANY & ~WDAY_THURSDAY, 1447977600},
  // Dec, -Tu
  // 2015 Dec  2, 00:00:00
  { 11,-1,-1,-1, WDAY_ANY & ~WDAY_TUESDAY, 1449014400},

  //////// 'bogus' time finding
  // minute
  // 2015 Nov 12, 12:60:00 = 2015 Nov 12, 13:00:00
  { -1,-1,-1,60, WDAY_ANY, 1447333200},
  // hour
  // 2015 Nov 12, 24:00:00 = 2015 Nov 13, 00:00:00
  { -1,-1,24,-1, WDAY_ANY, 1447372800},
  // mday
  // 2015 Nov 33, 00:00:00 = 2015 Dec  3, 00:00:00
  { -1,32,-1,-1, WDAY_ANY, 1449100800},
  // month
  // 2015 Month13 1, 00:00:00 = 2016 Jan  1, 00:00:00
  { 12,-1,-1,-1, WDAY_ANY, 1451606400},

  // Sentinel
  { 0,0,0,0, 0, 0},
};

void test_cron__simples(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = CRON_MINUTE_ANY,
    .hour = CRON_HOUR_ANY,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,

    .may_be_instant = true,
  };
  for (int i = 0; ; i++) {
    if (s_cron_test_info[i].dest_time == 0) {
      break;
    }
    test_cron.minute = s_cron_test_info[i].minute;
    test_cron.hour = s_cron_test_info[i].hour;
    test_cron.mday = s_cron_test_info[i].mday;
    test_cron.month = s_cron_test_info[i].month;
    test_cron.wday = s_cron_test_info[i].wday;
    time_t base = s_2015_nov12_123456_gmt;
    time_t advance = s_cron_test_info[i].dest_time - base;
    // DST off
    prv_basic_test(&s_timezone_gmt, &test_cron, base, advance, advance, 0);
    // DST on
    base -= SECONDS_PER_HOUR;
    prv_basic_test(&s_timezone_gmt, &test_cron, base, advance, advance, 2);
  }
}

void test_cron__dst_simple_to(void) {
  // Nov 21st, 01:00:00 local
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 0,
    .hour = 1,
    .mday = 20,
    .month = 10,

    .may_be_instant = true,
  };
  // 2015 Nov 21, 00:00:00 GMT
  const time_t advance = 1448064000 - s_2015_nov12_123456_gmt;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_123456_gmt, advance, advance, 1);
}

void test_cron__dst_simple_from(void) {
  // Dec 21st, 01:00:00 local
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 0,
    .hour = 1,
    .mday = 20,
    .month = 11,

    .may_be_instant = true,
  };
  // 2015 Dec 21, 01:00:00 GMT
  const time_t advance = 1450659600 - s_2015_dec19_123456_gmt;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_dec19_123456_gmt, advance, advance, 1);
}

void test_cron__dst_rollover_to(void) {
  // Nov 20th, 03:00:00 local
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 0,
    .hour = 3,
    .mday = 19,
    .month = 10,

    .may_be_instant = true,
  };
  // 2015 Nov 20, 02:00:00 GMT
  const time_t advance = 1447984800 - s_2015_nov12_123456_gmt;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_123456_gmt, advance, advance, 1);
}

void test_cron__dst_rollover_from(void) {
  // Dec 20th, 02:00:00 local
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 0,
    .hour = 2,
    .mday = 19,
    .month = 11,

    .may_be_instant = true,
  };
  // 2015 Dec 20, 02:00:00 GMT
  time_t advance = 1450576800 - s_2015_dec19_123456_gmt;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_dec19_123456_gmt, advance, advance, 1);
}

void test_cron__dst_hole_to(void) {
  // NOTE: This behavior is SUPER weird, and it could change in the future.
  // A failure in this test is not necessarily a problem.

  // Nov 20th, 02:30:00 local
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 30,
    .hour = 2,
    .mday = 19,
    .month = 10,

    .may_be_instant = true,
  };
  // 2015 Nov 20, 02:00:00 GMT (DST start)
  const time_t advance = 1447984800 - s_2015_nov12_123456_gmt;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_123456_gmt, advance, advance, 1);
}

void test_cron__dst_hole_from(void) {
  // NOTE: This behavior is SUPER weird, and it could change in the future.
  // A failure in this test is not necessarily a problem.

  // Dec 20th, 01:30:00 local
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 30,
    .hour = 1,
    .mday = 19,
    .month = 11,

    .may_be_instant = true,
  };
  // 2015 Dec 20, 00:30:00 GMT (the 'first' 1:30)
  const time_t advance = 1450571400 - s_2015_nov12_123456_gmt;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_123456_gmt, advance, advance, 1);
}

static void prv_counting_cb(CronJob *job, void *cb_data) {
  static int s_counter = 0;
  job->cb_data = (void*)((uintptr_t)++s_counter);
}

#define CRON_JOB(min, hr, day, mo, callback) \
  { \
    .cb = callback, \
    .cb_data = (void*)0, \
    .minute = min, \
    .hour = hr, \
    .mday = day, \
    .month = mo, \
    .may_be_instant = true, \
  },

void test_cron__scheduled_after(void) {
  CronJob jobs[] = {
    CRON_JOB(CRON_MINUTE_ANY, CRON_HOUR_ANY, CRON_MDAY_ANY, CRON_MONTH_ANY, prv_counting_cb)
    CRON_JOB(CRON_MINUTE_ANY, CRON_HOUR_ANY, CRON_MDAY_ANY, CRON_MONTH_ANY, prv_cron_callback)
    CRON_JOB(1, CRON_HOUR_ANY, CRON_MDAY_ANY, CRON_MONTH_ANY, prv_cron_callback)
    CRON_JOB(3, CRON_HOUR_ANY, CRON_MDAY_ANY, CRON_MONTH_ANY, prv_cron_callback)
    CRON_JOB(10, CRON_HOUR_ANY, 1, CRON_MONTH_ANY, prv_cron_callback)
    CRON_JOB(25, CRON_HOUR_ANY, CRON_MDAY_ANY, CRON_MONTH_ANY, prv_cron_callback)
    CRON_JOB(55, 1, CRON_MDAY_ANY, CRON_MONTH_ANY, prv_cron_callback)
    CRON_JOB(CRON_MINUTE_ANY, CRON_HOUR_ANY, 1, CRON_MONTH_ANY, prv_cron_callback)
  };

  CronJob new_job = {
    .cb = prv_counting_cb,
    .cb_data = (void*)0,
  };

  prv_set_rtc(s_2015_nov12_123456_gmt, &s_timezone_gmt);

  cron_clear_all_jobs();
  cl_assert_equal_i(cron_service_get_job_count(), 0);

  for (int i = 0; i < ARRAY_LENGTH(jobs); ++i) {
    cron_job_schedule(&jobs[i]);
  }
  cron_job_schedule_after(&jobs[0], &new_job);

  cl_assert_equal_i((uintptr_t)jobs[0].cb_data, 0);
  cl_assert_equal_i((uintptr_t)new_job.cb_data, 0);
  cl_assert_equal_i(cron_service_get_job_count(), ARRAY_LENGTH(jobs) + 1);

  fake_rtc_increment_time(0);

  cron_service_wakeup();
  cl_assert_equal_i((uintptr_t)jobs[0].cb_data, 1);
  cl_assert_equal_i((uintptr_t)new_job.cb_data, 2);
  cl_assert_equal_i(cron_service_get_job_count(), 6);

  fake_rtc_increment_time(SECONDS_PER_DAY * 60);
  cron_service_wakeup();
  cl_assert_equal_i(cron_service_get_job_count(), 0);
}

void test_cron__offset_negative_seconds_one_wday(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 30,
    .hour = 0,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,
    .offset_seconds = -SECONDS_PER_DAY,

    .wday = WDAY_FRIDAY,
    .may_be_instant = false,
  };

  const time_t advance = 30 * SECONDS_PER_MINUTE;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_000000_gmt, advance, advance, 1);
}

void test_cron__offset_negative_seconds_any_day(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 30,
    .hour = 0,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,
    .offset_seconds = -SECONDS_PER_DAY,

    .may_be_instant = false,
  };

  const time_t advance = 30 * SECONDS_PER_MINUTE;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_000000_gmt, advance, advance, 1);
}

void test_cron__offset_positive_seconds_one_wday(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 30,
    .hour = 0,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,
    .offset_seconds = SECONDS_PER_DAY,

    .wday = WDAY_THURSDAY,
    .may_be_instant = false,
  };

  const time_t advance = 30 * SECONDS_PER_MINUTE + SECONDS_PER_DAY;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_000000_gmt, advance, advance, 1);
}

void test_cron__offset_positive_seconds_any_day(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 30,
    .hour = 0,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,
    .offset_seconds = SECONDS_PER_DAY,

    .may_be_instant = false,
  };

  const time_t advance = 30 * SECONDS_PER_MINUTE;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_000000_gmt, advance, advance, 1);
}

void test_cron__offset_negative_seconds_every_second(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = CRON_MINUTE_ANY,
    .hour = CRON_HOUR_ANY,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,
    .offset_seconds = -SECONDS_PER_MINUTE,

    .may_be_instant = true,
  };

  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_123456_gmt, 0, 0, 0);
}

void test_cron__offset_positive_seconds_every_second(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = CRON_MINUTE_ANY,
    .hour = CRON_HOUR_ANY,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,
    .offset_seconds = SECONDS_PER_MINUTE,

    .may_be_instant = true,
  };

  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_123456_gmt, 0, 0, 0);
}

void test_cron__offset_negative_seconds_any_day_dst(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 30,
    .hour = 1,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,
    .offset_seconds = -30 * SECONDS_PER_MINUTE,

    .may_be_instant = false,
  };

  const time_t advance = SECONDS_PER_DAY;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_000000_gmt, advance, advance, 2);
}

void test_cron__offset_positive_seconds_any_day_dst(void) {
  CronJob test_cron = {
    .cb = prv_cron_callback,
    .cb_data = (void*)0,

    .minute = 30,
    .hour = 0,
    .mday = CRON_MDAY_ANY,
    .month = CRON_MONTH_ANY,
    .offset_seconds = 30 * SECONDS_PER_MINUTE,

    .may_be_instant = false,
  };

  const time_t advance = SECONDS_PER_DAY;
  prv_basic_test(&s_timezone_gmt, &test_cron, s_2015_nov12_000000_gmt, advance, advance, 2);
}