/*
 * Unit tests for SessionData and LocalSession:
 * - SessionData value semantics, group membership, GPO results
 * - LocalSession add/get/rem/list, thread safety, expiry
 * - Memory leak detection via scope-based destruction
 *
 * NOTE: authdb defines enum values (UserData=1, GroupData=3, etc.) that
 * shadow the class names. We must use the 'class' keyword to disambiguate.
 */
#include <gtest/gtest.h>
#include <thread>
#include <vector>
#include <algorithm>
#include <cstring>
#include <uuidp.h>

#include "session.h"
#include "backend.h"
#include "authdb.h"
#include "user.h"
#include "group.h"
#include "gpo.h"
#include "types.h"
#include "exception.h"
#include "client.h"
#include "cluster.h"

namespace authdb { std::mutex AuthClientLock; }

// ═══════════════════════════════════════════════
//  SessionData unit tests (no backend needed)
// ═══════════════════════════════════════════════

class SessionDataTest : public ::testing::Test {
protected:
    uuid::uuid sid, uid, did, g1, g2, g3;
    std::vector<uuid::uuid> members;

    void SetUp() override {
        sid.generate();
        uid.generate();
        did.generate();
        g1.generate();
        g2.generate();
        g3.generate();
        members = {g1, g2, g3};
    }
};

using authdb::SessionData;
using authdb::SessionInfo;
using authdb::LocalSession;
using authdb::AuthBackend;
using authdb::AuthHeader;
using authdb::User;

TEST_F(SessionDataTest, ConstructAndAccessUuids) {
    SessionData sess(sid, uid, did, members);

    uuid::uuid out_sid, out_uid, out_did;
    sess.getSid(out_sid);
    sess.getUid(out_uid);
    sess.getDid(out_did);

    EXPECT_EQ(out_sid, sid);
    EXPECT_EQ(out_uid, uid);
    EXPECT_EQ(out_did, did);
}

TEST_F(SessionDataTest, IsNotEmpty) {
    SessionData sess(sid, uid, did, members);
    EXPECT_FALSE(sess.empty());
}

TEST_F(SessionDataTest, DefaultIsEmpty) {
    SessionData sess;
    EXPECT_TRUE(sess.empty());
}

TEST_F(SessionDataTest, MembershipCheck) {
    SessionData sess(sid, uid, did, members);
    EXPECT_TRUE(sess.isMemberOf(g1));
    EXPECT_TRUE(sess.isMemberOf(g2));
    EXPECT_TRUE(sess.isMemberOf(g3));

    uuid::uuid alien;
    alien.generate();
    EXPECT_FALSE(sess.isMemberOf(alien));
}

TEST_F(SessionDataTest, GetMembers) {
    SessionData sess(sid, uid, did, members);

    std::vector<uuid::uuid> out;
    sess.getMembers(out);
    EXPECT_EQ(out.size(), 3u);
    EXPECT_TRUE(std::find(out.begin(), out.end(), g1) != out.end());
    EXPECT_TRUE(std::find(out.begin(), out.end(), g2) != out.end());
    EXPECT_TRUE(std::find(out.begin(), out.end(), g3) != out.end());
}

TEST_F(SessionDataTest, GPOResultsSetAndCheck) {
    SessionData sess(sid, uid, did, members);

    uuid::uuid gpo1, gpo2, gpo3;
    gpo1.generate();
    gpo2.generate();
    gpo3.generate();

    std::vector<std::pair<uuid::uuid, bool>> gpoResults = {
        {gpo1, true},
        {gpo2, false},
        {gpo3, true}
    };
    sess.setGPOResults(gpoResults);

    EXPECT_TRUE(sess.checkGPO(gpo1));
    EXPECT_FALSE(sess.checkGPO(gpo2));
    EXPECT_TRUE(sess.checkGPO(gpo3));

    uuid::uuid unknown;
    unknown.generate();
    EXPECT_FALSE(sess.checkGPO(unknown));
}

TEST_F(SessionDataTest, GPOResultsGetAll) {
    SessionData sess(sid, uid, did, members);

    uuid::uuid gpo1, gpo2;
    gpo1.generate();
    gpo2.generate();

    std::vector<std::pair<uuid::uuid, bool>> input = {
        {gpo1, true},
        {gpo2, false}
    };
    sess.setGPOResults(input);

    std::vector<std::pair<uuid::uuid, bool>> out;
    sess.getGPOResults(out);
    EXPECT_EQ(out.size(), 2u);
}

TEST_F(SessionDataTest, GPOResultsOverwrite) {
    SessionData sess(sid, uid, did, members);

    uuid::uuid gpo1;
    gpo1.generate();

    // Set first batch
    std::vector<std::pair<uuid::uuid, bool>> batch1 = {{gpo1, true}};
    sess.setGPOResults(batch1);
    EXPECT_TRUE(sess.checkGPO(gpo1));

    // Overwrite with different value
    std::vector<std::pair<uuid::uuid, bool>> batch2 = {{gpo1, false}};
    sess.setGPOResults(batch2);
    EXPECT_FALSE(sess.checkGPO(gpo1));
}

TEST_F(SessionDataTest, CopyConstructor) {
    SessionData orig(sid, uid, did, members);
    SessionData copy(orig);

    uuid::uuid copy_sid, copy_uid, copy_did;
    copy.getSid(copy_sid);
    copy.getUid(copy_uid);
    copy.getDid(copy_did);

    EXPECT_EQ(copy_sid, sid);
    EXPECT_EQ(copy_uid, uid);
    EXPECT_EQ(copy_did, did);
    EXPECT_TRUE(copy.isMemberOf(g1));
}

TEST_F(SessionDataTest, CopyFromPointer) {
    SessionData orig(sid, uid, did, members);
    SessionData copy(&orig);

    uuid::uuid copy_uid;
    copy.getUid(copy_uid);
    EXPECT_EQ(copy_uid, uid);
}

TEST_F(SessionDataTest, ClearResetsState) {
    SessionData sess(sid, uid, did, members);
    sess.clear();
    EXPECT_TRUE(sess.empty());
}

// ── GPOResult linked list memory safety ──

TEST_F(SessionDataTest, ManyGPOResultsNoLeak) {
    SessionData sess(sid, uid, did, members);

    std::vector<std::pair<uuid::uuid, bool>> results;
    for (int i = 0; i < 100; ++i) {
        uuid::uuid gpoid;
        gpoid.generate();
        results.emplace_back(gpoid, (i % 2 == 0));
    }
    sess.setGPOResults(results);

    // Overwrite with different set (tests cleanup of old linked list)
    std::vector<std::pair<uuid::uuid, bool>> results2;
    for (int i = 0; i < 50; ++i) {
        uuid::uuid gpoid;
        gpoid.generate();
        results2.emplace_back(gpoid, true);
    }
    sess.setGPOResults(results2);

    std::vector<std::pair<uuid::uuid, bool>> out;
    sess.getGPOResults(out);
    EXPECT_EQ(out.size(), 50u);
}

// ═══════════════════════════════════════════════
//  LocalSession tests (with file-backed backend)
// ═══════════════════════════════════════════════

class LocalSessionFileTest : public ::testing::Test {
protected:
    std::string dbPath;
    AuthBackend *backend = nullptr;
    LocalSession session;

    void SetUp() override {
        dbPath = "/tmp/authdb_test_session_" + std::to_string(getpid()) + ".db";
        backend = new AuthBackend(authdb::File, dbPath, "testdomain");
    }

    void TearDown() override {
        session.clear();
        delete backend;
        unlink(dbPath.c_str());
        unlink((dbPath + ".lock").c_str());
    }

    uuid::uuid createTestUser(const std::string &name) {
        uuid::uuid uid;
        uid.generate();
        class authdb::UserData udat(uid);
        udat.setUserName(name);
        udat.setPwHash("testhash");
        User user;
        user.create(*backend, &udat);
        return uid;
    }

    uuid::uuid createTestDomain() {
        uuid::uuid did;
        did.generate();
        return did;
    }
};

TEST_F(LocalSessionFileTest, AddAndGetSession) {
    uuid::uuid uid = createTestUser("testuser");
    uuid::uuid did = createTestDomain();

    const SessionData *sess = session.addSession(*backend, did, uid);
    ASSERT_NE(sess, nullptr);

    uuid::uuid sid;
    sess->getSid(sid);
    EXPECT_FALSE(sid.empty());

    const SessionData *got = session.getSession(sid);
    ASSERT_NE(got, nullptr);

    uuid::uuid got_uid;
    got->getUid(got_uid);
    EXPECT_EQ(got_uid, uid);
}

TEST_F(LocalSessionFileTest, DuplicateSessionReturnsExisting) {
    uuid::uuid uid = createTestUser("dupuser");
    uuid::uuid did = createTestDomain();

    const SessionData *s1 = session.addSession(*backend, did, uid);
    const SessionData *s2 = session.addSession(*backend, did, uid);

    uuid::uuid sid1, sid2;
    s1->getSid(sid1);
    s2->getSid(sid2);
    EXPECT_EQ(sid1, sid2);
}

TEST_F(LocalSessionFileTest, RemoveSession) {
    uuid::uuid uid = createTestUser("rmuser");
    uuid::uuid did = createTestDomain();

    const SessionData *sess = session.addSession(*backend, did, uid);
    uuid::uuid sid;
    sess->getSid(sid);

    session.remSession(sid);
    EXPECT_EQ(session.getSession(sid), nullptr);
}

TEST_F(LocalSessionFileTest, GetNonexistentReturnsNull) {
    uuid::uuid fake;
    fake.generate();
    EXPECT_EQ(session.getSession(fake), nullptr);
}

TEST_F(LocalSessionFileTest, ListSessions) {
    uuid::uuid u1 = createTestUser("listuser1");
    uuid::uuid u2 = createTestUser("listuser2");
    uuid::uuid did = createTestDomain();

    session.addSession(*backend, did, u1);
    session.addSession(*backend, did, u2);

    std::vector<SessionInfo> infos;
    session.listSessions(infos);
    EXPECT_EQ(infos.size(), 2u);
}

TEST_F(LocalSessionFileTest, ClearRemovesAll) {
    uuid::uuid uid = createTestUser("clearuser");
    uuid::uuid did = createTestDomain();

    session.addSession(*backend, did, uid);
    session.clear();

    std::vector<SessionInfo> infos;
    session.listSessions(infos);
    EXPECT_EQ(infos.size(), 0u);
}

// ── Thread safety ──

TEST_F(LocalSessionFileTest, ConcurrentAddAndGet) {
    const int NUM_USERS = 10;
    std::vector<uuid::uuid> uids;
    for (int i = 0; i < NUM_USERS; ++i) {
        uids.push_back(createTestUser("thread_user_" + std::to_string(i)));
    }
    uuid::uuid did = createTestDomain();

    std::vector<std::thread> threads;
    for (int i = 0; i < NUM_USERS; ++i) {
        threads.emplace_back([&, i]() {
            session.addSession(*backend, did, uids[i]);
        });
    }
    for (auto &t : threads) t.join();

    std::vector<SessionInfo> infos;
    session.listSessions(infos);
    EXPECT_EQ(infos.size(), static_cast<size_t>(NUM_USERS));
}

// ── Memory leak with many sessions ──

TEST_F(LocalSessionFileTest, ManySessionsNoLeak) {
    uuid::uuid did = createTestDomain();
    for (int i = 0; i < 50; ++i) {
        uuid::uuid uid = createTestUser("bulk_" + std::to_string(i));
        session.addSession(*backend, did, uid);
    }
    session.clear();

    std::vector<SessionInfo> infos;
    session.listSessions(infos);
    EXPECT_EQ(infos.size(), 0u);
}
