/*
 * SPDX-License-Identifier: BSD-3-Clause
 *
 * Copyright © 2021 R. Diez
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the following
 *    disclaimer in the documentation and/or other materials provided
 *    with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#define _DEFAULT_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <string.h>

#define TIME_TM_YEAR_BASE 1900


static void
init_struct_tm ( struct tm * stm )
{
  memset(stm, 0, sizeof *stm);
}

static int
test_strftime(int line, const struct tm *tm, const char *fmt, const char *expect)
{
  char  strbuf[128];
  size_t len;
  size_t expect_len = strlen(expect);

  len = strftime(strbuf, sizeof(strbuf), fmt, tm);
  if (strcmp(strbuf, expect) != 0 || len != strlen(expect)) {
    printf("%s:%d: strftime(%s) = '%s'(%zd) expect '%s'(%zd)\n",
           __FILE__, line,
           fmt, strbuf, len, expect, expect_len);
    return 0;
  }
  return 1;
}

int
main(void)
{
  int ret = 0;
  // Time zone ART3 is America/Buenos_Aires.
  // There is no daylight saving time (aka sommer time).

  if ( 0 != setenv( "TZ", "ART3", 1 ) )
  {
    puts("Error calling setenv().");
    return 1;
  }

  tzset();


  struct tm dtForTimegm1;
  init_struct_tm( &dtForTimegm1 );
  // This is some arbitrary point in time.
  dtForTimegm1.tm_mday  = 3;  // 3rd
  dtForTimegm1.tm_mon   = 1;  // of February
  dtForTimegm1.tm_year  = 1970 - TIME_TM_YEAR_BASE;

  // Make a copy, because mktime() and friends modify the fields of the tm structure passed as an argument.
  struct tm dtForMktime2 = dtForTimegm1;

  // timegm() should be not affected by the time zone.

  time_t t1 = timegm( &dtForTimegm1 );

  if ( t1 != 2851200 ||
       dtForTimegm1.tm_wday != 2 )  // 2 means Tuesday.
  {
    puts("Test t1 failed.");
    ret = 1;
  }

  if (!test_strftime(__LINE__, &dtForTimegm1, "%a %b %d %Y %H:%M:%S", "Tue Feb 03 1970 00:00:00"))
    ret = 1;

  // mktime() is affected by the time zone. Check that the offset is the expected one.

  time_t t2 = mktime( &dtForMktime2 );

  if ( t2 != t1 + 3 * 60 * 60 ||  // The chosen time zone has a positive 3-hour difference.
       dtForMktime2.tm_wday != 2 )
  {
    puts("Test t2 failed.");
    ret = 1;
  }

  if (!test_strftime(__LINE__, &dtForMktime2, "%A %B %d %Y %H:%M:%S", "Tuesday February 03 1970 00:00:00"))
    ret = 1;

  // The European Central Time goes the other direction (negative)
  // and has a daylight saving time (aka sommer time).

  if ( 0 != setenv( "TZ", "CET-1CEST,M3.5.0,M10.5.0/3", 1 ) )
  {
    puts("Error calling setenv().");
    ret = 1;
  }

  tzset();


  struct tm dtForTimegm3;
  init_struct_tm( &dtForTimegm3 );

  // This is some arbitrary point in time with daylight saving time.
  dtForTimegm3.tm_mday  = 1;   // 1st
  dtForTimegm3.tm_mon   = 4;   // of May
  dtForTimegm3.tm_year  = 2021 - TIME_TM_YEAR_BASE;
  dtForTimegm3.tm_isdst = 1;  // Daylight saving time is in effect. That only affects local time.

  struct tm dtForMktime4 = dtForTimegm3;

  time_t t3 = timegm( &dtForTimegm3 );

  if ( t3 != 1619827200           ||
       dtForTimegm3.tm_wday  != 6 ||  // 6 means Saturday.
       dtForTimegm3.tm_isdst != 0  )  // timegm() should reset the daylight saving time flag.
  {
    puts("Test t3 failed.");
    ret = 1;
  }

  if (!test_strftime(__LINE__, &dtForTimegm3, "%U %u %Y %H:%M:%S", "17 6 2021 00:00:00"))
    ret = 1;

  time_t t4 = mktime( &dtForMktime4 );

  // The offset is -2 hours: 1 hour because of the time zone, and 1 hour because of the sommer time.
  if ( t4 != t3 - 2 * 60 * 60     ||
       dtForMktime4.tm_wday  != 6 ||  // 6 means Saturday.
       dtForMktime4.tm_isdst != 1  )  // mktime() should leave the daylight saving time flag set.
  {
    puts("Test t4 failed.");
    ret = 1;
  }

  if (!test_strftime(__LINE__, &dtForMktime4, "%V %u %a %b %d %Y %H:%M:%S", "17 6 Sat May 01 2021 00:00:00"))
    ret = 1;

  // Test the first non-negative time_t value of 0,
  // which is the "UNIX Epoch time" of 1st Januar 1970 00:00:00.

  struct tm dtForTimegm5;
  init_struct_tm( &dtForTimegm5 );

  dtForTimegm5.tm_mday  = 1;   // 1st day of the month.
  dtForTimegm5.tm_year  = 1970 - TIME_TM_YEAR_BASE;
  dtForTimegm5.tm_isdst = -123;  // A negative value makes mktime() calculate this flag,
                                 // but timegm() should unconditionally reset it.

  time_t t5 = timegm( &dtForTimegm5 );

  if ( t5 != 0                    ||
       dtForTimegm5.tm_wday  != 4 ||  // 4 means Thursday.
       dtForTimegm5.tm_isdst != 0  )
  {
    puts("Test t5 failed.");
    ret = 1;
  }

  if (!test_strftime(__LINE__, &dtForTimegm5, "%W %w %a %b %d %Y %H:%M:%S", "00 4 Thu Jan 01 1970 00:00:00"))
    ret = 1;

  // Test the last time_t value of 0x7FFFFFFF before the 32-bit signed integer overflow,
  // aka "year 2038 problem". That corresponds to 2038-01-19 03:14:07.

  struct tm dtForTimegm6;
  init_struct_tm( &dtForTimegm6 );

  dtForTimegm6.tm_sec  =  7;
  dtForTimegm6.tm_min  = 14;
  dtForTimegm6.tm_hour =  3,
  dtForTimegm6.tm_mon   = 0;  // January.
  dtForTimegm6.tm_mday  = 19;
  dtForTimegm6.tm_year  = 2038 - TIME_TM_YEAR_BASE;
  dtForTimegm6.tm_isdst = 123;  // Some value that should be reset.

  struct tm dtForTimegm7 = dtForTimegm6;  // Reuse the date for the next test.

  time_t t6 = timegm( &dtForTimegm6 );

  if ( t6 != 0x7FFFFFFF            ||
       dtForTimegm6.tm_wday  != 2  ||  // 2 means Tuesday.
       dtForTimegm6.tm_yday  != 18 ||
       dtForTimegm6.tm_isdst != 0   )
  {
    puts("Test t6 failed.");
    ret = 1;
  }

  if (!test_strftime(__LINE__, &dtForTimegm6, "%a %b %d %Y %R", "Tue Jan 19 2038 03:14"))
    ret = 1;

  if (sizeof (time_t) > 4)
  {
    // Test the next time_t value of 0x80000000 right after
    // the 32-bit signed integer overflow, aka "year 2038 problem".

    dtForTimegm7.tm_sec++;

    time_t t7 = timegm( &dtForTimegm7 );

    if ( (unsigned long) t7 != 0x80000000UL          ||
         dtForTimegm7.tm_wday  != 2  ||  // 2 means Tuesday.
         dtForTimegm7.tm_yday  != 18 ||
         dtForTimegm7.tm_isdst != 0   )
    {
      puts("Test t7 failed.");
      ret = 1;
    }

    if (!test_strftime(__LINE__, &dtForTimegm7, "%a %b %d %Y %I:%M:%S %p", "Tue Jan 19 2038 03:14:08 AM"))
      ret = 1;

    // Test the 29th of February in a leap year far in the future.

    struct tm dtForTimegm8;
    init_struct_tm( &dtForTimegm8 );

    dtForTimegm8.tm_mon   = 1;  // February.
    dtForTimegm8.tm_mday  = 29;
    dtForTimegm8.tm_year  = 2104 - TIME_TM_YEAR_BASE;

    time_t t8 = timegm( &dtForTimegm8 );

    if ( (unsigned long) t8 != 4233686400UL           ||  // That is much higher than INT32_MAX.
         dtForTimegm8.tm_wday != 5  ||  // 5 means Friday.
         dtForTimegm8.tm_yday != 59  )
    {
      puts("Test t8 failed.");
      ret = 1;
    }

    if (!test_strftime(__LINE__, &dtForTimegm8, "%a %b %d %Y %H:%M:%S", "Fri Feb 29 2104 00:00:00"))
      ret = 1;
  }

  return ret;
}
