/*
 * 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/User.h>
#include <znc/Utils.h>
#include <znc/znc.h>

class UserTest : public ::testing::Test {
  protected:
    // A CZNC instance is required to instantiate CUsers
    UserTest() { CZNC::CreateInstance(); }
    ~UserTest() { CZNC::DestroyInstance(); }
};

TEST_F(UserTest, IsHostAllowed) {
    struct hostTest {
        CString sMask;
        CString sIP;
        bool bExpectedResult;
    };

    hostTest aHostTests[] = {
        {"127.0.0.1", "127.0.0.1", true},
        {"127.0.0.20", "127.0.0.1", false},
        {"127.0.0.20/24", "127.0.0.1", true},
        {"127.0.0.20/0", "127.0.0.1", true},
        {"127.0.0.20/32", "127.0.0.1", false},

        {"127.0.0.1", "127.0.0.0", false},
        {"127.0.0.20", "127.0.0.0", false},
        {"127.0.0.20/24", "127.0.0.0", true},
        {"127.0.0.20/0", "127.0.0.0", true},
        {"127.0.0.20/32", "127.0.0.0", false},

        {"127.0.0.1", "127.0.0.255", false},
        {"127.0.0.20", "127.0.0.255", false},
        {"127.0.0.20/24", "127.0.0.255", true},
        {"127.0.0.20/0", "127.0.0.255", true},
        {"127.0.0.20/32", "127.0.0.255", false},

        {"127.0.0.1", "127.0.1.1", false},
        {"127.0.0.20", "127.0.1.1", false},
        {"127.0.0.20/24", "127.0.1.1", false},
        {"127.0.0.20/16", "127.0.1.1", true},
        {"127.0.0.20/0", "127.0.1.1", true},
        {"127.0.0.20/32", "127.0.1.1", false},

        {"127.0.0.1", "0.0.0.0", false},
        {"127.0.0.20", "0.0.0.0", false},
        {"127.0.0.20/24", "0.0.0.0", false},
        {"127.0.0.20/16", "0.0.0.0", false},
        {"127.0.0.20/0", "0.0.0.0", true},
        {"127.0.0.20/32", "0.0.0.0", false},

        {"127.0.0.1", "255.255.255.255", false},
        {"127.0.0.20", "255.255.255.255", false},
        {"127.0.0.20/24", "255.255.255.255", false},
        {"127.0.0.20/16", "255.255.255.255", false},
        {"127.0.0.20/0", "255.255.255.255", true},
        {"127.0.0.20/32", "255.255.255.255", false},

        {"127.0.0.1", "::1", false},
        {"::1", "::1", true},
        {"::20/120", "::1", true},
        {"::20/120", "::ffff", false},
        {"::ff/0", "::1", true},
        {"::ff/0", "127.0.0.1", false},
        {"127.0.0.20/0", "::1", false},

        {"127.0.0.1/-1", "127.0.0.1", false},
        {"::0/-1", "::0", false},
        {"127.0.0.a/0", "127.0.0.1", false},
        {"::g/0", "::0", false},
        {"127.0.0.1/0", "127.0.0.a", false},
        {"::0/0", "::g", false},

        {"::0/0/0", "::0", false},
        {"2001:db8::/33", "2001:db9:0::", false},
        {"2001:db8::/32", "2001:db8:8000::", true},
        {"2001:db8::/33", "2001:db8:8000::", false},

        {"0.0.0.0/0", "::1", false},
        {"0.0.0.0/0", "127.0.0.1", true},
        {"::0/0", "127.0.0.1", false},
        {"::0/0", "::1", true},
        {"0.0.0.0", "127.0.0.1", false},
        {"0.0.0.0", "::1", false},
        {"::0", "::1", false},
        {"::0", "127.0.0.1", false},

        {"127.0.0.2/abc", "127.0.0.1", false},
        {"::2/abc", "::1", false},
        {"127.0.0.1/33", "127.0.0.1", false},
        {"::1/129", "::1", false},

        {"::2/00000000000", "::1", false},
        {"::2/0a", "::1", false},

        {"192.168.*", "192.168.0.1", true},
    };

    for (const hostTest& h : aHostTests) {
        CUser user("user");
        user.AddAllowedHost(h.sMask);
        EXPECT_EQ(user.IsHostAllowed(h.sIP), h.bExpectedResult)
            << "Allow-host is " << h.sMask;
    }
}

TEST_F(UserTest, TestAuthOnlyViaModule) {
    CUser user("user");
    user.SetPass("password", CUser::HASH_NONE);

    bool bAuthOnlyViaModuleDefault = CZNC::Get().GetAuthOnlyViaModule();

    CZNC::Get().SetAuthOnlyViaModule(false);
    user.SetAuthOnlyViaModule(false);

    EXPECT_TRUE(user.CheckPass("password"));

    // user-level only
    user.SetAuthOnlyViaModule(true);
    EXPECT_FALSE(user.CheckPass("password"));

    // re-enabling built-in authentication
    user.SetAuthOnlyViaModule(false);
    EXPECT_TRUE(user.CheckPass("password"));

    // on at global level, off at user level
    CZNC::Get().SetAuthOnlyViaModule(true);
    EXPECT_FALSE(user.CheckPass("password"));

    // on at both levels
    user.SetAuthOnlyViaModule(true);
    EXPECT_FALSE(user.CheckPass("password"));

    CZNC::Get().SetAuthOnlyViaModule(bAuthOnlyViaModuleDefault);
}

// Functional regression for the constant-time CheckPass paths (#2011).
// We can't measure timing in a unit test, but we verify the boolean
// contract for each hash mode: correct password matches, wrong password
// (including same-length and length-mismatch variants) does not.
TEST_F(UserTest, CheckPassPlain) {
    CUser user("user");
    user.SetPass("plaintext", CUser::HASH_NONE);

    EXPECT_FALSE(user.CheckPass(""));
    EXPECT_FALSE(user.CheckPass("plaintex"));    // shorter
    EXPECT_FALSE(user.CheckPass("plaintextX"));  // longer
    EXPECT_FALSE(user.CheckPass("plaintexT"));   // case-sensitive
    EXPECT_FALSE(user.CheckPass("Xlaintext"));   // first byte differs
    EXPECT_FALSE(user.CheckPass("plaintexX"));   // last byte differs
    EXPECT_TRUE(user.CheckPass("plaintext"));
}

TEST_F(UserTest, CheckPassMD5) {
    CUser user("user");
    CString sSalt = "the-salt";
    CString sCorrect = "correct-password";
    user.SetPass(CUtils::SaltedMD5Hash(sCorrect, sSalt), CUser::HASH_MD5,
                 sSalt);

    // Wrong inputs first; the success path may upgrade the stored hash
    // to argon2id, after which the hash type is no longer MD5.
    EXPECT_FALSE(user.CheckPass(""));
    EXPECT_FALSE(user.CheckPass("wrong-password"));
    EXPECT_FALSE(user.CheckPass("correct-passworX"));  // last byte differs
    EXPECT_FALSE(user.CheckPass("Xorrect-password"));  // first byte differs
    EXPECT_FALSE(user.CheckPass("Correct-password"));  // case-sensitive
    EXPECT_TRUE(user.CheckPass(sCorrect));
}

TEST_F(UserTest, CheckPassSHA256) {
    CUser user("user");
    CString sSalt = "another-salt";
    CString sCorrect = "another-password";
    user.SetPass(CUtils::SaltedSHA256Hash(sCorrect, sSalt),
                 CUser::HASH_SHA256, sSalt);

    EXPECT_FALSE(user.CheckPass(""));
    EXPECT_FALSE(user.CheckPass("wrong"));
    EXPECT_FALSE(user.CheckPass("another-passworX"));
    EXPECT_FALSE(user.CheckPass("Another-password"));
    EXPECT_TRUE(user.CheckPass(sCorrect));
}
