/*
 * Copyright (C) 2004-2026 ZNC, see the NOTICE file for details.
 *
 * 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 <gtest/gtest.h>
#include <znc/FileUtils.h>
#include <znc/Utils.h>

namespace {
CString WriteTempFile(const CString& sContent, mode_t iMode) {
    char sName[] = "./copytest-XXXXXX";
    int fd = mkstemp(sName);
    EXPECT_NE(fd, -1);
    close(fd);

    CFile File(sName);
    EXPECT_TRUE(File.Open(O_WRONLY | O_TRUNC));
    File.Write(sContent);
    File.Close();
    EXPECT_TRUE(CFile::Chmod(sName, iMode));
    return sName;
}

unsigned ModeOf(const CString& sFile) {
    struct stat st;
    EXPECT_EQ(CFile::GetInfo(sFile, st), 0);
    return st.st_mode & 07777;
}
}  // namespace

// A 0600 source must never widen to the default 0644 during the copy, so the
// destination ends up restricted too.
TEST(FileUtilsTest, CopyKeepsRestrictiveMode) {
    CString sSrc = WriteTempFile("secret", 0600);
    CString sDst = sSrc + "-copy";

    EXPECT_TRUE(CFile::Copy(sSrc, sDst));
    EXPECT_EQ(ModeOf(sDst), 0600u);

    CFile::Delete(sSrc);
    CFile::Delete(sDst);
}

// A source the owner can't write to (r-xr-xr-x) must still copy and keep its
// mode; the copy forces owner write internally and chmods it back afterwards.
TEST(FileUtilsTest, CopyReadOnlySource) {
    CString sSrc = WriteTempFile("public", 0555);
    CString sDst = sSrc + "-copy";

    EXPECT_TRUE(CFile::Copy(sSrc, sDst));
    EXPECT_EQ(ModeOf(sDst), 0555u);

    CString sContent;
    CFile Dst(sDst);
    ASSERT_TRUE(Dst.Open());
    Dst.ReadFile(sContent);
    Dst.Close();
    EXPECT_EQ(sContent, "public");

    CFile::Delete(sSrc);
    CFile::Delete(sDst);
}

TEST(IRC32, GetMessageTags) {
    EXPECT_EQ(CUtils::GetMessageTags(""), MCString());
    EXPECT_EQ(CUtils::GetMessageTags(
                  ":nick!ident@host PRIVMSG #chan :hello world"), MCString());

    MCString exp = {{"a", "b"}};
    EXPECT_EQ(CUtils::GetMessageTags("@a=b"), exp);
    EXPECT_EQ(CUtils::GetMessageTags(
                  "@a=b :nick!ident@host PRIVMSG #chan :hello world"), exp);
    EXPECT_EQ(CUtils::GetMessageTags("@a=b :rest"), exp);
    exp.clear();

    exp = {{"ab", "cdef"}, {"znc.in/gh-ij", "klmn,op"}};
    EXPECT_EQ(CUtils::GetMessageTags("@ab=cdef;znc.in/gh-ij=klmn,op :rest"),
              exp);

    exp = {{"a", "==b=="}};
    EXPECT_EQ(CUtils::GetMessageTags("@a===b== :rest"), exp);
    exp.clear();

    exp = {{"a", ""}, {"b", "c"}, {"d", ""}};
    EXPECT_EQ(CUtils::GetMessageTags("@a;b=c;d :rest"), exp);

    exp = {{"semi-colon", ";"}, {"space", " "}, {"NUL", {'\0'}},
           {"backslash", "\\"}, {"CR", {'\r'}}, {"LF", {'\n'}}};
    EXPECT_EQ(
        CUtils::GetMessageTags(
            R"(@semi-colon=\:;space=\s;NUL=\0;backslash=\\;CR=\r;LF=\n :rest)"),
        exp);
    exp.clear();

    exp = {{"a", "; \\\r\n"}};
    EXPECT_EQ(CUtils::GetMessageTags(R"(@a=\:\s\\\r\n :rest)"), exp);
    exp.clear();
}

TEST(IRC32, SetMessageTags) {
    CString sLine;

    sLine = ":rest";
    CUtils::SetMessageTags(sLine, MCString());
    EXPECT_EQ(sLine, ":rest");

    MCString tags = {{"a", "b"}};
    CUtils::SetMessageTags(sLine, tags);
    EXPECT_EQ(sLine, "@a=b :rest");

    tags = {{"a", "b"}, {"c", "d"}};
    CUtils::SetMessageTags(sLine, tags);
    EXPECT_EQ(sLine, "@a=b;c=d :rest");

    tags = {{"a", "b"}, {"c", "d"}, {"e", ""}};
    CUtils::SetMessageTags(sLine, tags);
    EXPECT_EQ(sLine, "@a=b;c=d;e :rest");

    tags = {{"semi-colon", ";"}, {"space", " "}, {"NUL", {'\0'}},
            {"backslash", "\\"}, {"CR", {'\r'}}, {"LF", {'\n'}}};
    CUtils::SetMessageTags(sLine, tags);
    EXPECT_EQ(
        sLine,
        R"(@CR=\r;LF=\n;NUL=\0;backslash=\\;semi-colon=\:;space=\s :rest)");

    tags = {{"a", "; \\\r\n"}};
    CUtils::SetMessageTags(sLine, tags);
    EXPECT_EQ(sLine, R"(@a=\:\s\\\r\n :rest)");
}

TEST(UtilsTest, Timezone) {
    char* oldTZ = getenv("TZ");
    if (oldTZ) oldTZ = strdup(oldTZ);
    setenv("TZ", "Europe/Berlin", 1);
    tzset();

    EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", "UTC"), "2023-01-07 20:10:30");
    EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", "GMT"), "2023-01-07 20:10:30");
    EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", "GMT+7"), "2023-01-08 03:10:30");
    EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", "GMT-7"), "2023-01-07 13:10:30");
    EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", "Asia/Vladivostok"), "2023-01-08 06:10:30");
    // Local TZ, set to Berlin in this test
    EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", ""), "2023-01-07 21:10:30");

    if (oldTZ) {
        setenv("TZ", oldTZ, 1);
        free(oldTZ);
    } else {
        unsetenv("TZ");
    }
    tzset();
}

TEST(UtilsTest, ServerTime) {
    char* oldTZ = getenv("TZ");
    if (oldTZ) oldTZ = strdup(oldTZ);
    setenv("TZ", "UTC", 1);
    tzset();

    timeval tv1 = CUtils::ParseServerTime("2011-10-19T16:40:51.620Z");
    CString str1 = CUtils::FormatServerTime(tv1);
    EXPECT_EQ(str1, "2011-10-19T16:40:51.620Z");

    timeval now = CUtils::GetTime();

    // Strip microseconds, server time is ms only
    now.tv_usec = (now.tv_usec / 1000) * 1000;

    CString str2 = CUtils::FormatServerTime(now);
    timeval tv2 = CUtils::ParseServerTime(str2);
    EXPECT_EQ(tv2.tv_sec, now.tv_sec);
    EXPECT_EQ(tv2.tv_usec, now.tv_usec);

    timeval tv3 = CUtils::ParseServerTime("invalid");
    CString str3 = CUtils::FormatServerTime(tv3);
    EXPECT_EQ(str3, "1970-01-01T00:00:00.000Z");

    if (oldTZ) {
        setenv("TZ", oldTZ, 1);
        free(oldTZ);
    } else {
        unsetenv("TZ");
    }
    tzset();
}

TEST(UtilsTest, ConstantTimeEquals) {
    // Functional correctness for the helper introduced for #2011.
    // We can't measure timing in a unit test, so we verify the boolean
    // contract: equal inputs match, any difference (length or content)
    // does not.
    EXPECT_TRUE(CUtils::ConstantTimeEquals("", ""));
    EXPECT_TRUE(CUtils::ConstantTimeEquals("abc", "abc"));
    EXPECT_TRUE(CUtils::ConstantTimeEquals(CString("\x00\x01\x02", 3),
                                            CString("\x00\x01\x02", 3)));

    // Differs in last byte (the hardest case for short-circuit compare).
    EXPECT_FALSE(CUtils::ConstantTimeEquals("abc", "abd"));
    // Differs in first byte.
    EXPECT_FALSE(CUtils::ConstantTimeEquals("abc", "Xbc"));
    // Length mismatch on either side.
    EXPECT_FALSE(CUtils::ConstantTimeEquals("abc", "abcd"));
    EXPECT_FALSE(CUtils::ConstantTimeEquals("abcd", "abc"));
    EXPECT_FALSE(CUtils::ConstantTimeEquals("", "x"));
    EXPECT_FALSE(CUtils::ConstantTimeEquals("x", ""));
    // Case-sensitive (unlike CString::Equals default).
    EXPECT_FALSE(CUtils::ConstantTimeEquals("abc", "ABC"));
    // Embedded NUL is compared, not used as a terminator.
    EXPECT_FALSE(CUtils::ConstantTimeEquals(CString("a\x00""c", 3),
                                             CString("a\x00""d", 3)));
    EXPECT_TRUE(CUtils::ConstantTimeEquals(CString("a\x00""c", 3),
                                            CString("a\x00""c", 3)));
}

TEST(UtilsTest, ParseServerTime) {
    char* oldTZ = getenv("TZ");
    if (oldTZ) oldTZ = strdup(oldTZ);
    setenv("TZ", "America/Montreal", 1);
    tzset();

    timeval tv4 = CUtils::ParseServerTime("2011-10-19T16:40:51.620Z");
    CString str4 = CUtils::FormatServerTime(tv4);
    EXPECT_EQ(str4, "2011-10-19T16:40:51.620Z");


    if (oldTZ) {
        setenv("TZ", oldTZ, 1);
        free(oldTZ);
    } else {
        unsetenv("TZ");
    }
    tzset();
}

TEST(UtilsTest, ParseServerTimeOutOfRange) {
    // Years past 5 digits trigger int64 overflow inside cctz' microseconds
    // conversion (`seconds * 1_000_000`). Reject up front (#2008).
    timeval tv = CUtils::ParseServerTime("999999-01-01T00:00:00.000Z");
    EXPECT_EQ(tv.tv_sec, 0);
    EXPECT_EQ(tv.tv_usec, 0);

    tv = CUtils::ParseServerTime("12345678-01-01T00:00:00.000Z");
    EXPECT_EQ(tv.tv_sec, 0);
    EXPECT_EQ(tv.tv_usec, 0);

    // Junk and empty input still return a zeroed timeval.
    tv = CUtils::ParseServerTime("");
    EXPECT_EQ(tv.tv_sec, 0);
    EXPECT_EQ(tv.tv_usec, 0);

    tv = CUtils::ParseServerTime("not-a-date-at-all");
    EXPECT_EQ(tv.tv_sec, 0);
    EXPECT_EQ(tv.tv_usec, 0);

    // Canonical input still parses (regression).
    tv = CUtils::ParseServerTime("2011-10-19T16:40:51.620Z");
    EXPECT_EQ(CUtils::FormatServerTime(tv), "2011-10-19T16:40:51.620Z");
}

class TimeTest : public testing::TestWithParam<
                     std::tuple<timeval, CString, CString, CString>> {};

TEST_P(TimeTest, FormatTime) {
    timeval tv = std::get<0>(GetParam());
    EXPECT_EQ(std::get<1>(GetParam()), CUtils::FormatTime(tv, "%s.%f", "UTC"));
    EXPECT_EQ(std::get<2>(GetParam()), CUtils::FormatTime(tv, "%s.%6f", "UTC"));
    EXPECT_EQ(std::get<3>(GetParam()), CUtils::FormatTime(tv, "%s.%9f", "UTC"));
}

INSTANTIATE_TEST_CASE_P(
    TimeTest, TimeTest,
    testing::Values(
        // leading zeroes
        std::make_tuple(timeval{42, 12345}, "42.012", "42.012345", "42.012345000"),
        // (no) rounding
        std::make_tuple(timeval{42, 999999}, "42.999", "42.999999", "42.999999000"),
        // no tv_usec part
        std::make_tuple(timeval{42, 0}, "42.000", "42.000000", "42.000000000")));

TEST(UtilsTest, FormatTime) {
    // Test passthrough
    timeval tv1;
    tv1.tv_sec = 42;
    tv1.tv_usec = 123456;
    CString str1 = CUtils::FormatTime(tv1, "%s", "UTC");
    EXPECT_EQ(str1, "42");

    // Test escapes
    timeval tv2;
    tv2.tv_sec = 42;
    tv2.tv_usec = 123456;
    CString str2 = CUtils::FormatTime(tv2, "%%f", "UTC");
    EXPECT_EQ(str2, "%f");

    // Test suffix
    CString str3 = CUtils::FormatTime(tv2, "a%fb", "UTC");
    EXPECT_EQ(str3, "a123b");
}
