/*
 * Runtime performance tests for authdb:
 * - Memory usage (RSS) tracking during bulk operations
 * - Storage (database file size) behaviour during CRUD + vacuum
 * - Access-time benchmarks for create / read / list / remove
 *
 * NOTE: authdb defines enum values (UserData=1, GroupData=3, etc.) that
 * shadow the class names.  We use the 'class' keyword to disambiguate.
 */
#include <gtest/gtest.h>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <numeric>
#include <string>
#include <vector>

#ifdef _WIN32
#include <process.h>
#include <io.h>
#include <windows.h>
#include <psapi.h>
#define test_getpid  _getpid
#define test_unlink  _unlink
#else
#include <unistd.h>
#define test_getpid  getpid
#define test_unlink  unlink
#endif

#include <uuidp.h>

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

namespace authdb { std::mutex AuthClientLock; }

using authdb::AuthBackend;
using authdb::AuthHeader;
using authdb::User;
using authdb::Group;
using authdb::Gpo;
using authdb::RecordIndex;
using authdb::SessionData;
using authdb::LocalSession;

// ═════════════════════════════════════════════════════════════
//  Helpers
// ═════════════════════════════════════════════════════════════

static size_t getRSSKb() {
#ifdef _WIN32
    PROCESS_MEMORY_COUNTERS pmc;
    if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc)))
        return pmc.WorkingSetSize / 1024;
    return 0;
#else
    std::ifstream ifs("/proc/self/status");
    std::string line;
    while (std::getline(ifs, line)) {
        if (line.rfind("VmRSS:", 0) == 0) {
            size_t kb = 0;
            sscanf(line.c_str(), "VmRSS: %zu kB", &kb);
            return kb;
        }
    }
    return 0;
#endif
}

static size_t getFileSize(const std::string &path) {
    std::ifstream ifs(path, std::ios::ate | std::ios::binary);
    if (!ifs) return 0;
    return static_cast<size_t>(ifs.tellg());
}

using Clock = std::chrono::high_resolution_clock;

struct TimingStats {
    double min_us;
    double max_us;
    double avg_us;
    double total_ms;
};

static TimingStats computeStats(const std::vector<double> &samples_us) {
    TimingStats s{};
    if (samples_us.empty()) return s;
    s.min_us = *std::min_element(samples_us.begin(), samples_us.end());
    s.max_us = *std::max_element(samples_us.begin(), samples_us.end());
    s.total_ms = std::accumulate(samples_us.begin(), samples_us.end(), 0.0) / 1000.0;
    s.avg_us = (s.total_ms * 1000.0) / static_cast<double>(samples_us.size());
    return s;
}

static void printStats(const char *label, const TimingStats &s, size_t n) {
    std::cout << "  " << label << " (" << n << " ops): "
              << "min=" << s.min_us << " µs  "
              << "avg=" << s.avg_us << " µs  "
              << "max=" << s.max_us << " µs  "
              << "total=" << s.total_ms << " ms\n";
}

// ═════════════════════════════════════════════════════════════
//  Fixture
// ═════════════════════════════════════════════════════════════

static std::string tempDir() {
#ifdef _WIN32
    const char *tmp = std::getenv("TEMP");
    if (!tmp) tmp = std::getenv("TMP");
    return tmp ? std::string(tmp) + "\\" : "C:\\Temp\\";
#else
    return "/tmp/";
#endif
}

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

    void SetUp() override {
        dbPath = tempDir() + "authdb_perf_" + std::to_string(test_getpid()) + ".db";
        backend = new AuthBackend(authdb::File, dbPath, "perfdomain");
    }

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

// ═════════════════════════════════════════════════════════════
//  1.  User CRUD – timing + memory + storage
// ═════════════════════════════════════════════════════════════

TEST_F(PerformanceTest, UserCreateReadRemoveBenchmark) {
    constexpr size_t N = 500;

    std::vector<uuid::uuid> uids(N);
    std::vector<double> createTimes, readTimes, listTimes, removeTimes;
    createTimes.reserve(N);
    readTimes.reserve(N);
    removeTimes.reserve(N);

    size_t rssBefore = getRSSKb();
    size_t dbSizeBefore = getFileSize(dbPath);

    // ── CREATE ──
    User user;
    for (size_t i = 0; i < N; ++i) {
        uids[i].generate();
        class authdb::UserData udat(uids[i]);
        udat.setUserName("user_" + std::to_string(i));
        udat.setFirstName("First" + std::to_string(i));
        udat.setLastName("Last" + std::to_string(i));
        udat.setMail("user" + std::to_string(i) + "@perf.test");
        udat.setPwHash("hash_" + std::to_string(i));

        auto t0 = Clock::now();
        user.create(*backend, &udat);
        auto t1 = Clock::now();
        createTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    size_t rssAfterCreate = getRSSKb();
    size_t dbSizeAfterCreate = getFileSize(dbPath);

    // ── LIST ──
    {
        std::vector<uuid::uuid> listed;
        auto t0 = Clock::now();
        user.list(*backend, listed);
        auto t1 = Clock::now();
        listTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
        EXPECT_EQ(listed.size(), N);
    }

    // ── READ (info) ──
    for (size_t i = 0; i < N; ++i) {
        class authdb::UserData udat(uids[i]);
        size_t pos = sizeof(AuthHeader);

        auto t0 = Clock::now();
        user.info(*backend, udat, pos);
        auto t1 = Clock::now();
        readTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());

        EXPECT_EQ(udat.getUsername(), "user_" + std::to_string(i));
    }

    size_t rssAfterRead = getRSSKb();

    // ── REMOVE ──
    for (size_t i = 0; i < N; ++i) {
        auto t0 = Clock::now();
        user.remove(*backend, uids[i]);
        auto t1 = Clock::now();
        removeTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    size_t dbSizeAfterRemove = getFileSize(dbPath);

    // ── VACUUM ──
    size_t sizeBefore = 0, sizeAfter = 0;
    backend->vacuum(sizeBefore, sizeAfter);
    size_t dbSizeAfterVacuum = getFileSize(dbPath);
    size_t rssAfterAll = getRSSKb();

    // ── Report ──
    std::cout << "\n=== User CRUD Benchmark (" << N << " records) ===\n";
    printStats("CREATE", computeStats(createTimes), N);
    printStats("READ  ", computeStats(readTimes), N);
    printStats("LIST  ", computeStats(listTimes), 1);
    printStats("REMOVE", computeStats(removeTimes), N);

    std::cout << "\n  Memory (RSS kB):\n"
              << "    before       = " << rssBefore << "\n"
              << "    after create = " << rssAfterCreate << "\n"
              << "    after read   = " << rssAfterRead << "\n"
              << "    after all    = " << rssAfterAll << "\n"
              << "    delta create = " << (long long)rssAfterCreate - (long long)rssBefore << "\n";

    std::cout << "\n  Storage (bytes):\n"
              << "    before       = " << dbSizeBefore << "\n"
              << "    after create = " << dbSizeAfterCreate << "\n"
              << "    after remove = " << dbSizeAfterRemove << "\n"
              << "    after vacuum = " << dbSizeAfterVacuum << "\n"
              << "    vacuum saved = " << (long long)dbSizeAfterRemove - (long long)dbSizeAfterVacuum << " bytes\n";

    // Sanity check: vacuum should shrink the file
    EXPECT_LE(dbSizeAfterVacuum, dbSizeAfterRemove);
}

// ═════════════════════════════════════════════════════════════
//  2.  Group operations benchmark
// ═════════════════════════════════════════════════════════════

TEST_F(PerformanceTest, GroupCreateWithMembersBenchmark) {
    constexpr size_t N = 200;
    constexpr size_t MEMBERS_PER_GROUP = 10;

    std::vector<double> createTimes, readTimes;
    std::vector<uuid::uuid> gids(N);

    size_t rssBefore = getRSSKb();

    Group grp;
    for (size_t i = 0; i < N; ++i) {
        gids[i].generate();
        class authdb::GroupData g(gids[i]);
        g.setName("group_" + std::to_string(i));
        for (size_t m = 0; m < MEMBERS_PER_GROUP; ++m) {
            uuid::uuid mid;
            mid.generate();
            g.addMember(mid);
        }

        auto t0 = Clock::now();
        grp.create(*backend, &g);
        auto t1 = Clock::now();
        createTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    size_t rssAfterCreate = getRSSKb();
    size_t dbSize = getFileSize(dbPath);

    for (size_t i = 0; i < N; ++i) {
        class authdb::GroupData g(gids[i]);
        size_t pos = sizeof(AuthHeader);

        auto t0 = Clock::now();
        grp.info(*backend, g, pos);
        auto t1 = Clock::now();
        readTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());

        EXPECT_EQ(g.getName(), "group_" + std::to_string(i));
    }

    std::cout << "\n=== Group Benchmark (" << N << " groups, "
              << MEMBERS_PER_GROUP << " members each) ===\n";
    printStats("CREATE", computeStats(createTimes), N);
    printStats("READ  ", computeStats(readTimes), N);

    std::cout << "  Memory delta = "
              << (long long)rssAfterCreate - (long long)rssBefore << " kB\n"
              << "  DB size      = " << dbSize << " bytes\n";
}

// ═════════════════════════════════════════════════════════════
//  3.  Session memory benchmark (in-memory store)
// ═════════════════════════════════════════════════════════════

TEST_F(PerformanceTest, SessionAddGetRemoveBenchmark) {
    constexpr size_t N = 200;

    // Create some users in the backend so addSession can resolve them
    User user;
    uuid::uuid did;
    did.generate();

    std::vector<uuid::uuid> userIds(N);
    for (size_t i = 0; i < N; ++i) {
        userIds[i].generate();
        class authdb::UserData udat(userIds[i]);
        udat.setUserName("sessuser_" + std::to_string(i));
        udat.setPwHash("pw");
        user.create(*backend, &udat);
    }

    LocalSession sessions;
    std::vector<uuid::uuid> sids(N);
    std::vector<double> addTimes, getTimes, remTimes;

    size_t rssBefore = getRSSKb();

    // ── ADD sessions ──
    for (size_t i = 0; i < N; ++i) {
        auto t0 = Clock::now();
        const SessionData *sd = sessions.addSession(*backend, did, userIds[i]);
        auto t1 = Clock::now();
        addTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());

        ASSERT_NE(sd, nullptr);
        sd->getSid(sids[i]);
    }

    size_t rssAfterAdd = getRSSKb();

    // ── GET sessions ──
    for (size_t i = 0; i < N; ++i) {
        auto t0 = Clock::now();
        const SessionData *out = sessions.getSession(sids[i]);
        auto t1 = Clock::now();
        getTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());

        EXPECT_NE(out, nullptr);
    }

    // ── REMOVE sessions ──
    for (size_t i = 0; i < N; ++i) {
        auto t0 = Clock::now();
        sessions.remSession(sids[i]);
        auto t1 = Clock::now();
        remTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
    }

    size_t rssAfterRemove = getRSSKb();

    std::cout << "\n=== Session Benchmark (" << N << " sessions) ===\n";
    printStats("ADD   ", computeStats(addTimes), N);
    printStats("GET   ", computeStats(getTimes), N);
    printStats("REMOVE", computeStats(remTimes), N);

    std::cout << "  Memory (RSS kB):\n"
              << "    before     = " << rssBefore << "\n"
              << "    after add  = " << rssAfterAdd << "\n"
              << "    after rem  = " << rssAfterRemove << "\n"
              << "    delta add  = " << (long long)rssAfterAdd - (long long)rssBefore << "\n"
              << "    delta rem  = " << (long long)rssAfterRemove - (long long)rssAfterAdd << "\n";
}

// ═════════════════════════════════════════════════════════════
//  4.  RecordIndex in-memory lookup performance
// ═════════════════════════════════════════════════════════════

TEST_F(PerformanceTest, IndexLookupBenchmark) {
    constexpr size_t N = 5000;

    RecordIndex idx;
    std::vector<uuid::uuid> uids(N);
    std::vector<std::string> names(N);

    for (size_t i = 0; i < N; ++i) {
        uids[i].generate();
        names[i] = "entry_" + std::to_string(i);
        idx.addEntry(uids[i], authdb::UserData, names[i].c_str(), i * 128);
    }

    size_t rssAfterBuild = getRSSKb();

    // ── UUID lookup ──
    std::vector<double> uuidTimes;
    for (size_t i = 0; i < N; ++i) {
        auto t0 = Clock::now();
        bool found = idx.findByUuid(authdb::UserData, uids[i]);
        auto t1 = Clock::now();
        uuidTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
        EXPECT_TRUE(found);
    }

    // ── Name lookup ──
    std::vector<double> nameTimes;
    for (size_t i = 0; i < N; ++i) {
        uuid::uuid out;
        auto t0 = Clock::now();
        bool found = idx.findByName(authdb::UserData, "username", names[i], out);
        auto t1 = Clock::now();
        nameTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
        EXPECT_TRUE(found);
    }

    // ── Offset lookup ──
    std::vector<double> offTimes;
    for (size_t i = 0; i < N; ++i) {
        auto t0 = Clock::now();
        size_t off = idx.findOffset(uids[i], authdb::UserData);
        auto t1 = Clock::now();
        offTimes.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
        EXPECT_EQ(off, i * 128);
    }

    // ── listByType ──
    std::vector<uuid::uuid> allUids;
    auto tList0 = Clock::now();
    idx.listByType(authdb::UserData, allUids);
    auto tList1 = Clock::now();
    double listUs = std::chrono::duration<double, std::micro>(tList1 - tList0).count();

    std::cout << "\n=== RecordIndex Benchmark (" << N << " entries) ===\n";
    printStats("UUID lookup  ", computeStats(uuidTimes), N);
    printStats("Name lookup  ", computeStats(nameTimes), N);
    printStats("Offset lookup", computeStats(offTimes), N);
    std::cout << "  listByType   : " << listUs << " µs  (" << allUids.size() << " results)\n";
    std::cout << "  Index RSS    = " << rssAfterBuild << " kB\n";

    EXPECT_EQ(allUids.size(), N);
}

// ═════════════════════════════════════════════════════════════
//  5.  Storage growth – linear growth check
// ═════════════════════════════════════════════════════════════

TEST_F(PerformanceTest, StorageGrowthIsLinear) {
    constexpr size_t STEP = 100;
    constexpr size_t STEPS = 5;

    User user;
    std::vector<size_t> sizes;

    for (size_t s = 0; s < STEPS; ++s) {
        for (size_t i = 0; i < STEP; ++i) {
            uuid::uuid uid;
            uid.generate();
            class authdb::UserData udat(uid);
            udat.setUserName("u_" + std::to_string(s * STEP + i));
            udat.setPwHash("pw");
            user.create(*backend, &udat);
        }
        sizes.push_back(getFileSize(dbPath));
    }

    std::cout << "\n=== Storage Growth (+" << STEP << " users per step) ===\n";
    for (size_t s = 0; s < STEPS; ++s) {
        std::cout << "  " << (s + 1) * STEP << " users -> " << sizes[s] << " bytes";
        if (s > 0) {
            long long delta = (long long)sizes[s] - (long long)sizes[s - 1];
            std::cout << "  (+" << delta << ")";
        }
        std::cout << "\n";
    }

    // Verify roughly linear: each delta should be within 2x of the first delta
    if (sizes.size() >= 3) {
        long long firstDelta = (long long)sizes[1] - (long long)sizes[0];
        for (size_t s = 2; s < sizes.size(); ++s) {
            long long delta = (long long)sizes[s] - (long long)sizes[s - 1];
            EXPECT_GT(delta, firstDelta / 3) << "Growth at step " << s << " unexpectedly small";
            EXPECT_LT(delta, firstDelta * 3) << "Growth at step " << s << " unexpectedly large";
        }
    }
}

// ═════════════════════════════════════════════════════════════
//  6.  Memory – AuthData linked-list allocation/deallocation
// ═════════════════════════════════════════════════════════════

TEST_F(PerformanceTest, AuthDataBulkAllocDealloc) {
    constexpr size_t N = 2000;

    size_t rssBefore = getRSSKb();

    {
        std::vector<std::unique_ptr<authdb::AuthData>> records;
        records.reserve(N);
        for (size_t i = 0; i < N; ++i) {
            uuid::uuid id;
            id.generate();
            auto dat = std::make_unique<authdb::AuthData>(id);

            authdb::AuthData::Record *rec = nullptr;
            std::string val = "value_" + std::to_string(i);
            setAuthData<char>(authdb::UserData, authdb::TextStorage, dat.get(), &rec,
                              "field", val.c_str(), val.size() + 1);
            records.push_back(std::move(dat));
        }

        size_t rssWithAll = getRSSKb();
        std::cout << "\n=== AuthData Alloc/Dealloc (" << N << " records) ===\n"
                  << "  RSS before alloc = " << rssBefore << " kB\n"
                  << "  RSS with " << N << " recs  = " << rssWithAll << " kB\n"
                  << "  delta            = " << (long long)rssWithAll - (long long)rssBefore << " kB\n";

        // records go out of scope here → destructors run
    }

    size_t rssAfterFree = getRSSKb();
    std::cout << "  RSS after free   = " << rssAfterFree << " kB\n"
              << "  returned         = " << (long long)rssAfterFree - (long long)rssBefore << " kB overhead\n";
}

// ═════════════════════════════════════════════════════════════
//  7.  Vacuum effectiveness
// ═════════════════════════════════════════════════════════════

TEST_F(PerformanceTest, VacuumEffectiveness) {
    constexpr size_t N = 300;

    User user;
    std::vector<uuid::uuid> uids(N);

    for (size_t i = 0; i < N; ++i) {
        uids[i].generate();
        class authdb::UserData udat(uids[i]);
        udat.setUserName("vac_user_" + std::to_string(i));
        udat.setPwHash("pw");
        user.create(*backend, &udat);
    }

    size_t sizeAfterCreate = getFileSize(dbPath);

    // Remove half the records
    for (size_t i = 0; i < N / 2; ++i) {
        user.remove(*backend, uids[i]);
    }
    size_t sizeAfterHalfRemove = getFileSize(dbPath);

    // Vacuum
    auto t0 = Clock::now();
    size_t beforeVac = 0, afterVac = 0;
    backend->vacuum(beforeVac, afterVac);
    auto t1 = Clock::now();
    double vacuumUs = std::chrono::duration<double, std::micro>(t1 - t0).count();

    size_t sizeAfterVacuum = getFileSize(dbPath);

    // Verify remaining users are still readable
    std::vector<uuid::uuid> remaining;
    user.list(*backend, remaining);

    std::cout << "\n=== Vacuum Effectiveness (" << N << " users, removed " << N / 2 << ") ===\n"
              << "  after create = " << sizeAfterCreate << " bytes\n"
              << "  after remove = " << sizeAfterHalfRemove << " bytes\n"
              << "  after vacuum = " << sizeAfterVacuum << " bytes\n"
              << "  vacuum time  = " << vacuumUs << " µs\n"
              << "  reclaimed    = " << (long long)sizeAfterHalfRemove - (long long)sizeAfterVacuum << " bytes\n"
              << "  remaining    = " << remaining.size() << " users\n";

    EXPECT_EQ(remaining.size(), N - N / 2);
    EXPECT_LT(sizeAfterVacuum, sizeAfterCreate);
}
