/*
 * 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;
}
