/*
 * test_tofu_flow — M4 TOFU server-identity gate (voicecat.h's VC_EVENT_SERVER_IDENTITY /
 * vc_confirm_server_identity / vc_get_server_identity_display).
 *
 * Needs a real server (in-process, like the other ABI tests) so a real TLS handshake
 * happens — pinning a fingerprint against a mock would prove nothing.
 *
 *   1. First connect to a fresh server blocks (no AUTH_RESULT) until
 *      vc_confirm_server_identity() is called; then it proceeds normally.
 *   2. Rejecting (accept=0) disconnects with VC_ERR_CRYPTO and does NOT persist a pin — a
 *      second attempt to the same server still reports FIRST_CONNECT.
 *   3. Reconnecting to a server with the SAME identity (same data_dir, restarted on the
 *      same port) reports MATCHED.
 *   4. Reconnecting to a server with a DIFFERENT identity on the same host:port (key
 *      rotation / MITM) reports MISMATCH.
 *   5. vc_confirm_server_identity with nothing pending returns VC_ERR_INVALID_ARG.
 *   6. vc_get_server_identity_display is empty pre-connect and populated (64 hex chars —
 *      the raw, colon-free encoding of the Ed25519 fingerprint) after ServerHello.
 */
#include <cstdio>

#ifdef VOICECAT_HAS_NET

#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <filesystem>
#include <mutex>
#include <string>
#include <thread>

#include "voicecat.h"
#include "server.h"

// ── Event tracking — deliberately does NOT auto-confirm, so the test drives the gate ──────

struct GatedEventStore {
    std::mutex              mu;
    std::condition_variable cv;

    bool           auth_ok{false};
    bool           got_identity{false};
    vc_tofu_status identity_status{};
    bool           disconnected{false};
    vc_result      disconnect_result{VC_OK};

    vc_client* client{nullptr};
};

static void on_event_gated(void* user, const vc_event* ev) {
    auto* s = static_cast<GatedEventStore*>(user);
    std::lock_guard lk(s->mu);
    switch (ev->type) {
        case VC_EVENT_SERVER_IDENTITY:
            s->got_identity    = true;
            s->identity_status = static_cast<vc_tofu_status>(ev->u32a);
            break;
        case VC_EVENT_AUTH_RESULT:
            s->auth_ok = (ev->result == VC_OK);
            break;
        case VC_EVENT_DISCONNECTED:
            s->disconnected      = true;
            s->disconnect_result = static_cast<vc_result>(ev->result);
            break;
        default:
            break;
    }
    s->cv.notify_all();
}

template <typename Pred>
static bool wait_for(GatedEventStore& s, Pred pred, int timeout_ms) {
    auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
    std::unique_lock lk(s.mu);
    return s.cv.wait_until(lk, deadline, [&] { return pred(s); });
}

// ── Test harness ──────────────────────────────────────────────────────────────

static int g_failures = 0;
#define CHECK(cond)                                                        \
    do {                                                                   \
        if (!(cond)) {                                                     \
            std::printf("FAIL: %s (%s:%d)\n", #cond, __FILE__, __LINE__); \
            ++g_failures;                                                  \
        }                                                                  \
    } while (0)

// ── A small helper to start/stop an in-process server on a chosen (or OS-assigned) port ───

struct RunningServer {
    voicecat::server::Config cfg;
    std::unique_ptr<voicecat::server::Server> server;
    std::thread server_thread;
    uint16_t    port{0};

    bool start(const std::string& data_dir, uint16_t want_port, const char* name) {
        std::atomic<uint16_t> bound_port{0};
        std::mutex              ready_mu;
        std::condition_variable ready_cv;
        bool                    ready{false};

        cfg.data_dir     = data_dir;
        cfg.bind_port    = want_port;
        cfg.media_port   = 0;
        cfg.server_name  = name;
        cfg.allow_guests = true;
        cfg.on_ready = [&](uint16_t p) {
            bound_port.store(p);
            { std::lock_guard lk(ready_mu); ready = true; }
            ready_cv.notify_all();
        };

        server = std::make_unique<voicecat::server::Server>(cfg);
        server_thread = std::thread([this] { server->run(); });

        std::unique_lock lk(ready_mu);
        bool ok = ready_cv.wait_for(lk, std::chrono::seconds(10), [&] { return ready; });
        if (!ok) return false;
        port = bound_port.load();
        return true;
    }

    void stop_and_join() {
        if (server) server->stop();
        if (server_thread.joinable()) server_thread.join();
    }
};

// ── 1. First connect blocks until confirmed ─────────────────────────────────────
static void test_first_connect_blocks(uint16_t port, const std::string& tofu_path) {
    GatedEventStore ev;
    vc_callbacks cb{on_event_gated, nullptr, &ev};
    vc_config cfg{"test-gate", "0.1", VC_LOG_OFF};
    cfg.tofu_store_path = tofu_path.c_str();
    vc_client* c = vc_client_create(&cfg, cb);
    CHECK(c != nullptr);
    ev.client = c;

    CHECK(vc_connect(c, "127.0.0.1", port) == VC_OK);
    CHECK(vc_authenticate_guest(c, "Gated") == VC_OK);

    CHECK(wait_for(ev, [](GatedEventStore& s) { return s.got_identity; }, 5000));
    { std::lock_guard lk(ev.mu); CHECK(ev.identity_status == VC_TOFU_FIRST_CONNECT); }

    // No confirmation yet — auth must NOT complete within a short window.
    CHECK(!wait_for(ev, [](GatedEventStore& s) { return s.auth_ok; }, 1000));

    CHECK(vc_confirm_server_identity(c, 1) == VC_OK);
    CHECK(wait_for(ev, [](GatedEventStore& s) { return s.auth_ok; }, 5000));

    vc_disconnect(c);
    vc_client_destroy(c);
    std::printf("test_first_connect_blocks: ok\n");
}

// ── 2. Reject doesn't persist a pin ──────────────────────────────────────────────
static void test_reject_does_not_persist(uint16_t port, const std::string& tofu_path) {
    {
        GatedEventStore ev;
        vc_callbacks cb{on_event_gated, nullptr, &ev};
        vc_config cfg{"test-reject", "0.1", VC_LOG_OFF};
        cfg.tofu_store_path = tofu_path.c_str();
        vc_client* c = vc_client_create(&cfg, cb);
        CHECK(c != nullptr);
        ev.client = c;

        CHECK(vc_connect(c, "127.0.0.1", port) == VC_OK);
        CHECK(wait_for(ev, [](GatedEventStore& s) { return s.got_identity; }, 5000));
        { std::lock_guard lk(ev.mu); CHECK(ev.identity_status == VC_TOFU_FIRST_CONNECT); }

        CHECK(vc_confirm_server_identity(c, 0) == VC_OK);
        CHECK(wait_for(ev, [](GatedEventStore& s) { return s.disconnected; }, 5000));
        { std::lock_guard lk(ev.mu); CHECK(ev.disconnect_result == VC_ERR_CRYPTO); }

        vc_client_destroy(c);
    }
    // Second attempt to the SAME server, SAME pin file: still FIRST_CONNECT — the rejected
    // pin from above must not have been written to disk.
    {
        GatedEventStore ev;
        vc_callbacks cb{on_event_gated, nullptr, &ev};
        vc_config cfg{"test-reject2", "0.1", VC_LOG_OFF};
        cfg.tofu_store_path = tofu_path.c_str();
        vc_client* c = vc_client_create(&cfg, cb);
        CHECK(c != nullptr);
        ev.client = c;

        CHECK(vc_connect(c, "127.0.0.1", port) == VC_OK);
        CHECK(wait_for(ev, [](GatedEventStore& s) { return s.got_identity; }, 5000));
        { std::lock_guard lk(ev.mu); CHECK(ev.identity_status == VC_TOFU_FIRST_CONNECT); }

        vc_disconnect(c);
        vc_client_destroy(c);
    }
    std::printf("test_reject_does_not_persist: ok\n");
}

// ── 3/4. MATCHED on identity reuse, MISMATCH on identity rotation ──────────────
static void test_matched_and_mismatch(const std::string& tofu_path) {
    auto tmp = std::filesystem::temp_directory_path() /
               ("vctest_tofu_" + std::to_string(
                   std::chrono::steady_clock::now().time_since_epoch().count()));
    std::filesystem::create_directories(tmp);
    auto data_dir_1 = (tmp / "server1").string();  // identity A
    auto data_dir_2 = (tmp / "server2").string();  // identity B (different)

    // ── Server 1 (identity A), first connect: accept + pin ──────────────────────
    RunningServer server1;
    CHECK(server1.start(data_dir_1, 0, "VoiceCat-TofuA"));
    uint16_t port = server1.port;
    std::printf("test_matched_and_mismatch: server1 ready on :%u\n", port);

    {
        GatedEventStore ev;
        vc_callbacks cb{on_event_gated, nullptr, &ev};
        vc_config cfg{"test-pin", "0.1", VC_LOG_OFF};
        cfg.tofu_store_path = tofu_path.c_str();
        vc_client* c = vc_client_create(&cfg, cb);
        CHECK(c != nullptr);
        ev.client = c;
        CHECK(vc_connect(c, "127.0.0.1", port) == VC_OK);
        CHECK(vc_authenticate_guest(c, "Pin") == VC_OK);
        CHECK(wait_for(ev, [](GatedEventStore& s) { return s.got_identity; }, 5000));
        { std::lock_guard lk(ev.mu); CHECK(ev.identity_status == VC_TOFU_FIRST_CONNECT); }
        CHECK(vc_confirm_server_identity(c, 1) == VC_OK);
        CHECK(wait_for(ev, [](GatedEventStore& s) { return s.auth_ok; }, 5000));
        vc_disconnect(c);
        vc_client_destroy(c);
    }
    server1.stop_and_join();

    // ── Server 1 restarted on the SAME port, SAME data_dir (identity A reloaded from disk
    // — ServerIdentityManager::init's load-existing-files path) — expect MATCHED. ──────────
    RunningServer server1_restarted;
    CHECK(server1_restarted.start(data_dir_1, port, "VoiceCat-TofuA"));
    {
        GatedEventStore ev;
        vc_callbacks cb{on_event_gated, nullptr, &ev};
        vc_config cfg{"test-matched", "0.1", VC_LOG_OFF};
        cfg.tofu_store_path = tofu_path.c_str();
        vc_client* c = vc_client_create(&cfg, cb);
        CHECK(c != nullptr);
        ev.client = c;
        CHECK(vc_connect(c, "127.0.0.1", port) == VC_OK);
        CHECK(vc_authenticate_guest(c, "Matched") == VC_OK);
        CHECK(wait_for(ev, [](GatedEventStore& s) { return s.got_identity; }, 5000));
        { std::lock_guard lk(ev.mu); CHECK(ev.identity_status == VC_TOFU_MATCHED); }
        CHECK(vc_confirm_server_identity(c, 1) == VC_OK);
        CHECK(wait_for(ev, [](GatedEventStore& s) { return s.auth_ok; }, 5000));
        vc_disconnect(c);
        vc_client_destroy(c);
    }
    server1_restarted.stop_and_join();

    // ── A DIFFERENT server (identity B, fresh data_dir) on the SAME port — expect
    // MISMATCH. Reject it, and confirm the pin file still reflects identity A afterwards. ───
    RunningServer server2;
    CHECK(server2.start(data_dir_2, port, "VoiceCat-TofuB"));
    {
        GatedEventStore ev;
        vc_callbacks cb{on_event_gated, nullptr, &ev};
        vc_config cfg{"test-mismatch", "0.1", VC_LOG_OFF};
        cfg.tofu_store_path = tofu_path.c_str();
        vc_client* c = vc_client_create(&cfg, cb);
        CHECK(c != nullptr);
        ev.client = c;
        CHECK(vc_connect(c, "127.0.0.1", port) == VC_OK);
        CHECK(wait_for(ev, [](GatedEventStore& s) { return s.got_identity; }, 5000));
        { std::lock_guard lk(ev.mu); CHECK(ev.identity_status == VC_TOFU_MISMATCH); }
        CHECK(vc_confirm_server_identity(c, 0) == VC_OK);  // reject the rotated identity
        CHECK(wait_for(ev, [](GatedEventStore& s) { return s.disconnected; }, 5000));
        vc_client_destroy(c);
    }
    server2.stop_and_join();

    // ── Server 1 (identity A) once more — rejecting the mismatch above must not have
    // clobbered the original pin. ───────────────────────────────────────────────────────────
    RunningServer server1_again;
    CHECK(server1_again.start(data_dir_1, port, "VoiceCat-TofuA"));
    {
        GatedEventStore ev;
        vc_callbacks cb{on_event_gated, nullptr, &ev};
        vc_config cfg{"test-still-matched", "0.1", VC_LOG_OFF};
        cfg.tofu_store_path = tofu_path.c_str();
        vc_client* c = vc_client_create(&cfg, cb);
        CHECK(c != nullptr);
        ev.client = c;
        CHECK(vc_connect(c, "127.0.0.1", port) == VC_OK);
        CHECK(wait_for(ev, [](GatedEventStore& s) { return s.got_identity; }, 5000));
        { std::lock_guard lk(ev.mu); CHECK(ev.identity_status == VC_TOFU_MATCHED); }
        vc_disconnect(c);
        vc_client_destroy(c);
    }
    server1_again.stop_and_join();

    std::filesystem::remove_all(tmp);
    std::printf("test_matched_and_mismatch: ok\n");
}

// ── 5. confirm_server_identity with nothing pending ─────────────────────────────
static void test_confirm_with_nothing_pending() {
    vc_config cfg{"test-nopending", "0.1", VC_LOG_OFF};
    vc_callbacks cb{};
    vc_client* c = vc_client_create(&cfg, cb);
    CHECK(c != nullptr);
    CHECK(vc_confirm_server_identity(c, 1) == VC_ERR_INVALID_ARG);
    vc_client_destroy(c);
    std::printf("test_confirm_with_nothing_pending: ok\n");
}

// ── 6. vc_get_server_identity_display ───────────────────────────────────────────
static void test_get_server_identity_display(uint16_t port, const std::string& tofu_path) {
    vc_config cfg{"test-display", "0.1", VC_LOG_OFF};
    cfg.tofu_store_path = tofu_path.c_str();

    GatedEventStore ev;
    vc_callbacks cb{on_event_gated, nullptr, &ev};
    vc_client* c = vc_client_create(&cfg, cb);
    CHECK(c != nullptr);
    ev.client = c;

    // Pre-connect: empty.
    size_t len = 12345;
    CHECK(vc_get_server_identity_display(c, nullptr, 0, &len) == VC_OK);
    CHECK(len == 0);

    CHECK(vc_connect(c, "127.0.0.1", port) == VC_OK);
    CHECK(vc_authenticate_guest(c, "Display") == VC_OK);
    CHECK(wait_for(ev, [](GatedEventStore& s) { return s.got_identity; }, 5000));
    CHECK(vc_confirm_server_identity(c, 1) == VC_OK);
    CHECK(wait_for(ev, [](GatedEventStore& s) { return s.auth_ok; }, 5000));

    char buf[256] = {};
    CHECK(vc_get_server_identity_display(c, buf, sizeof(buf), &len) == VC_OK);
    CHECK(len == 64);  // 32-byte Ed25519 fingerprint, raw hex, no colons
    CHECK(std::strlen(buf) == 64);

    vc_disconnect(c);
    vc_client_destroy(c);
    std::printf("test_get_server_identity_display: ok\n");
}

int main() {
    test_confirm_with_nothing_pending();

    auto tmp = std::filesystem::temp_directory_path() /
               ("vctest_tofu_main_" + std::to_string(
                   std::chrono::steady_clock::now().time_since_epoch().count()));
    std::filesystem::create_directories(tmp);

    {
        RunningServer server;
        CHECK(server.start((tmp / "srv").string(), 0, "VoiceCat-TofuFlow"));
        uint16_t port = server.port;
        std::printf("test_tofu_flow: server ready on :%u\n", port);

        test_first_connect_blocks(port, (tmp / "pins_blocks.txt").string());
        test_reject_does_not_persist(port, (tmp / "pins_reject.txt").string());
        test_get_server_identity_display(port, (tmp / "pins_display.txt").string());

        server.stop_and_join();
    }

    test_matched_and_mismatch((tmp / "pins_matched.txt").string());

    std::filesystem::remove_all(tmp);

    if (g_failures == 0) {
        std::printf("tofu_flow: all checks passed\n");
        return 0;
    }
    std::printf("tofu_flow: %d failure(s)\n", g_failures);
    return 1;
}

#else  // !VOICECAT_HAS_NET

int main() {
    std::printf("tofu_flow: SKIP (VOICECAT_HAS_NET not defined)\n");
    return 0;
}

#endif  // VOICECAT_HAS_NET
