/*
 * Tests that we can successfully change the group state and that all peers in the group
 * receive the correct state changes.
 */

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>

#include "auto_test_support.h"
#include "check_compat.h"

#define NUM_GROUP_TOXES 5

#define PEER_LIMIT_1 NUM_GROUP_TOXES
#define PEER_LIMIT_2 50

#define PASSWORD "dadada"
#define PASS_LEN (sizeof(PASSWORD) - 1)

#define GROUP_NAME "The Crystal Palace"
#define GROUP_NAME_LEN (sizeof(GROUP_NAME) - 1)

#define PEER0_NICK "David"
#define PEER0_NICK_LEN (sizeof(PEER0_NICK) - 1)

typedef struct State {
    size_t  num_peers;
} State;

static bool all_group_peers_connected(const AutoTox *autotoxes, uint32_t tox_count, uint32_t groupnumber,
                                      size_t name_length, uint32_t peer_limit)
{
    for (uint32_t i = 0; i < tox_count; ++i) {
        // make sure we got an invite response
        if (tox_group_get_name_size(autotoxes[i].tox, groupnumber, nullptr) != name_length) {
            return false;
        }

        // make sure we got a sync response
        if (tox_group_get_peer_limit(autotoxes[i].tox, groupnumber, nullptr) != peer_limit) {
            return false;
        }

        // make sure we're actually connected
        if (!tox_group_is_connected(autotoxes[i].tox, groupnumber, nullptr)) {
            return false;
        }

        const State *state = (const State *)autotoxes[i].state;

        // make sure all peers are connected to one another
        if (state->num_peers < NUM_GROUP_TOXES - 1) {
            return false;
        }
    }

    return true;
}

static void group_topic_lock_handler(const Tox_Event_Group_Topic_Lock *event,
                                     void *user_data)
{
    const AutoTox *autotox = (const AutoTox *)user_data;
    ck_assert(autotox != nullptr);

    const uint32_t groupnumber = tox_event_group_topic_lock_get_group_number(event);
    const Tox_Group_Topic_Lock topic_lock = tox_event_group_topic_lock_get_topic_lock(event);

    Tox_Err_Group_State_Query err;
    Tox_Group_Topic_Lock current_topic_lock = tox_group_get_topic_lock(autotox->tox, groupnumber, &err);

    ck_assert(err == TOX_ERR_GROUP_STATE_QUERY_OK);
    ck_assert_msg(current_topic_lock == topic_lock, "topic locks don't match in callback: %d %d",
                  topic_lock, current_topic_lock);
}

static void group_voice_state_handler(const Tox_Event_Group_Voice_State *event,
                                      void *user_data)
{
    const AutoTox *autotox = (const AutoTox *)user_data;
    ck_assert(autotox != nullptr);

    const uint32_t groupnumber = tox_event_group_voice_state_get_group_number(event);
    const Tox_Group_Voice_State voice_state = tox_event_group_voice_state_get_voice_state(event);

    Tox_Err_Group_State_Query err;
    Tox_Group_Voice_State current_voice_state = tox_group_get_voice_state(autotox->tox, groupnumber, &err);

    ck_assert(err == TOX_ERR_GROUP_STATE_QUERY_OK);
    ck_assert_msg(current_voice_state == voice_state, "voice states don't match in callback: %d %d",
                  voice_state, current_voice_state);
}

static void group_privacy_state_handler(const Tox_Event_Group_Privacy_State *event,
                                        void *user_data)
{
    const AutoTox *autotox = (const AutoTox *)user_data;
    ck_assert(autotox != nullptr);

    const uint32_t groupnumber = tox_event_group_privacy_state_get_group_number(event);
    const Tox_Group_Privacy_State privacy_state = tox_event_group_privacy_state_get_privacy_state(event);

    Tox_Err_Group_State_Query err;
    Tox_Group_Privacy_State current_pstate = tox_group_get_privacy_state(autotox->tox, groupnumber, &err);

    ck_assert(err == TOX_ERR_GROUP_STATE_QUERY_OK);
    ck_assert_msg(current_pstate == privacy_state, "privacy states don't match in callback");
}

static void group_peer_limit_handler(const Tox_Event_Group_Peer_Limit *event, void *user_data)
{
    const AutoTox *autotox = (const AutoTox *)user_data;
    ck_assert(autotox != nullptr);

    const uint32_t groupnumber = tox_event_group_peer_limit_get_group_number(event);
    const uint32_t peer_limit = tox_event_group_peer_limit_get_peer_limit(event);

    Tox_Err_Group_State_Query err;
    uint32_t current_plimit = tox_group_get_peer_limit(autotox->tox, groupnumber, &err);

    ck_assert(err == TOX_ERR_GROUP_STATE_QUERY_OK);
    ck_assert_msg(peer_limit == current_plimit,
                  "Peer limits don't match in callback: %u, %u\n", peer_limit, current_plimit);
}

static void group_password_handler(const Tox_Event_Group_Password *event,
                                   void *user_data)
{
    AutoTox *autotox = (AutoTox *)user_data;
    ck_assert(autotox != nullptr);

    const uint32_t groupnumber = tox_event_group_password_get_group_number(event);
    const uint8_t *password = tox_event_group_password_get_password(event);
    const size_t length = tox_event_group_password_get_password_length(event);

    Tox_Err_Group_State_Query err;
    size_t curr_pwlength = tox_group_get_password_size(autotox->tox, groupnumber, &err);

    ck_assert(err == TOX_ERR_GROUP_STATE_QUERY_OK);
    ck_assert(length == curr_pwlength);

    uint8_t current_password[TOX_GROUP_MAX_PASSWORD_SIZE];
    tox_group_get_password(autotox->tox, groupnumber, current_password, &err);

    ck_assert(err == TOX_ERR_GROUP_STATE_QUERY_OK);
    ck_assert_msg(memcmp(current_password, password, length) == 0,
                  "Passwords don't match: %s, %s", password, current_password);
}

static void group_peer_join_handler(const Tox_Event_Group_Peer_Join *event, void *user_data)
{
    AutoTox *autotox = (AutoTox *)user_data;
    ck_assert(autotox != nullptr);

    State *state = (State *)autotox->state;

    ++state->num_peers;
    ck_assert(state->num_peers < NUM_GROUP_TOXES);
}

/* Returns 0 if group state is equal to the state passed to this function.
 * Returns negative integer if state is invalid.
 */
static int check_group_state(const Tox *tox, uint32_t groupnumber, uint32_t peer_limit,
                             Tox_Group_Privacy_State priv_state, Tox_Group_Voice_State voice_state,
                             const uint8_t *password, size_t pass_len, Tox_Group_Topic_Lock topic_lock)
{
    Tox_Err_Group_State_Query query_err;

    Tox_Group_Privacy_State my_priv_state = tox_group_get_privacy_state(tox, groupnumber, &query_err);
    ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERY_OK, "Failed to get privacy state: %d", query_err);

    if (my_priv_state != priv_state) {
        return -1;
    }

    uint32_t my_peer_limit = tox_group_get_peer_limit(tox, groupnumber, &query_err);
    ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERY_OK, "Failed to get peer limit: %d", query_err);

    if (my_peer_limit != peer_limit) {
        return -2;
    }

    size_t my_pass_len = tox_group_get_password_size(tox, groupnumber, &query_err);
    ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERY_OK, "Failed to get password size: %d", query_err);

    if (my_pass_len != pass_len) {
        return -5;
    }

    if (password != nullptr && my_pass_len > 0) {
        ck_assert(my_pass_len <= TOX_GROUP_MAX_PASSWORD_SIZE);

        uint8_t my_pass[TOX_GROUP_MAX_PASSWORD_SIZE + 1];
        tox_group_get_password(tox, groupnumber, my_pass, &query_err);
        my_pass[my_pass_len] = 0;
        ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERY_OK, "Failed to get password: %d", query_err);

        if (memcmp(my_pass, password, my_pass_len) != 0) {
            return -6;
        }
    }

    /* Group name should never change */
    size_t my_gname_len = tox_group_get_name_size(tox, groupnumber, &query_err);
    ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERY_OK, "Failed to get group name size: %d", query_err);

    if (my_gname_len != GROUP_NAME_LEN) {
        return -7;
    }

    ck_assert(my_gname_len <= TOX_GROUP_MAX_GROUP_NAME_LENGTH);

    uint8_t my_gname[TOX_GROUP_MAX_GROUP_NAME_LENGTH + 1];
    tox_group_get_name(tox, groupnumber, my_gname, &query_err);
    my_gname[my_gname_len] = 0;

    if (memcmp(my_gname, (const uint8_t *)GROUP_NAME, my_gname_len) != 0) {
        return -8;
    }

    Tox_Group_Topic_Lock current_topic_lock = tox_group_get_topic_lock(tox, groupnumber, &query_err);
    ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERY_OK, "Failed to get topic lock: %d", query_err);

    if (current_topic_lock != topic_lock) {
        return -9;
    }

    Tox_Group_Voice_State current_voice_state = tox_group_get_voice_state(tox, groupnumber, &query_err);
    ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERY_OK, "Failed to get voice state: %d", query_err);

    if (current_voice_state != voice_state) {
        return -10;
    }

    return 0;
}

static void set_group_state(Tox *tox, uint32_t groupnumber, uint32_t peer_limit, Tox_Group_Privacy_State priv_state,
                            Tox_Group_Voice_State voice_state, const uint8_t *password, size_t pass_len,
                            Tox_Group_Topic_Lock topic_lock)
{

    Tox_Err_Group_Set_Peer_Limit limit_set_err;
    tox_group_set_peer_limit(tox, groupnumber, peer_limit, &limit_set_err);
    ck_assert_msg(limit_set_err == TOX_ERR_GROUP_SET_PEER_LIMIT_OK, "failed to set peer limit: %d", limit_set_err);

    Tox_Err_Group_Set_Privacy_State priv_err;
    tox_group_set_privacy_state(tox, groupnumber, priv_state, &priv_err);
    ck_assert_msg(priv_err == TOX_ERR_GROUP_SET_PRIVACY_STATE_OK, "failed to set privacy state: %d", priv_err);

    Tox_Err_Group_Set_Password pass_set_err;
    tox_group_set_password(tox, groupnumber, password, pass_len, &pass_set_err);
    ck_assert_msg(pass_set_err == TOX_ERR_GROUP_SET_PASSWORD_OK, "failed to set password: %d", pass_set_err);

    Tox_Err_Group_Set_Topic_Lock lock_set_err;
    tox_group_set_topic_lock(tox, groupnumber, topic_lock, &lock_set_err);
    ck_assert_msg(lock_set_err == TOX_ERR_GROUP_SET_TOPIC_LOCK_OK, "failed to set topic lock: %d",
                  lock_set_err);

    Tox_Err_Group_Set_Voice_State voice_set_err;
    tox_group_set_voice_state(tox, groupnumber, voice_state, &voice_set_err);
    ck_assert_msg(voice_set_err == TOX_ERR_GROUP_SET_VOICE_STATE_OK, "failed to set voice state: %d",
                  voice_set_err);
}

static void group_state_test(AutoTox *autotoxes)
{
    ck_assert_msg(NUM_GROUP_TOXES >= 3, "NUM_GROUP_TOXES is too small: %d", NUM_GROUP_TOXES);

    for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) {
        tox_events_callback_group_privacy_state(autotoxes[i].dispatch, group_privacy_state_handler);
        tox_events_callback_group_peer_limit(autotoxes[i].dispatch, group_peer_limit_handler);
        tox_events_callback_group_password(autotoxes[i].dispatch, group_password_handler);
        tox_events_callback_group_peer_join(autotoxes[i].dispatch, group_peer_join_handler);
        tox_events_callback_group_voice_state(autotoxes[i].dispatch, group_voice_state_handler);
        tox_events_callback_group_topic_lock(autotoxes[i].dispatch, group_topic_lock_handler);
    }

    Tox *tox0 = autotoxes[0].tox;

    /* Tox 0 creates a group and is the founder of a newly created group */
    Tox_Err_Group_New new_err;
    uint32_t groupnum = tox_group_new(tox0, TOX_GROUP_PRIVACY_STATE_PUBLIC, (const uint8_t *)GROUP_NAME, GROUP_NAME_LEN,
                                      (const uint8_t *)PEER0_NICK, PEER0_NICK_LEN, &new_err);

    ck_assert_msg(new_err == TOX_ERR_GROUP_NEW_OK, "tox_group_new failed: %d", new_err);

    /* Founder sets default group state before anyone else joins */
    set_group_state(tox0, groupnum, PEER_LIMIT_1, TOX_GROUP_PRIVACY_STATE_PUBLIC, TOX_GROUP_VOICE_STATE_ALL,
                    (const uint8_t *)PASSWORD, PASS_LEN, TOX_GROUP_TOPIC_LOCK_ENABLED);

    /* Founder gets the Chat ID and implicitly shares it publicly */
    Tox_Err_Group_State_Query id_err;
    uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE];
    tox_group_get_chat_id(tox0, groupnum, chat_id, &id_err);

    ck_assert_msg(id_err == TOX_ERR_GROUP_STATE_QUERY_OK, "tox_group_get_chat_id failed %d", id_err);

    /* All other peers join the group using the Chat ID and password */
    for (size_t i = 1; i < NUM_GROUP_TOXES; ++i) {
        iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL);

        Tox_Err_Group_Join join_err;
        tox_group_join(autotoxes[i].tox, chat_id, (const uint8_t *)"Test", 4, (const uint8_t *)PASSWORD, PASS_LEN,
                       &join_err);
        ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "tox_group_join failed: %d", join_err);
    }

    fprintf(stderr, "Peers attempting to join group\n");

    /* Keep checking if all instances have connected to the group until test times out */
    while (!all_group_peers_connected(autotoxes, NUM_GROUP_TOXES, groupnum, GROUP_NAME_LEN, PEER_LIMIT_1)) {
        iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL);
    }

    /* Change group state and check that all peers received the changes */
    set_group_state(tox0, groupnum, PEER_LIMIT_2, TOX_GROUP_PRIVACY_STATE_PRIVATE, TOX_GROUP_VOICE_STATE_MODERATOR,
                    nullptr, 0, TOX_GROUP_TOPIC_LOCK_DISABLED);

    fprintf(stderr, "Changing state\n");

    while (1) {
        iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL);

        uint32_t count = 0;

        for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) {
            if (check_group_state(autotoxes[i].tox, groupnum, PEER_LIMIT_2, TOX_GROUP_PRIVACY_STATE_PRIVATE,
                                  TOX_GROUP_VOICE_STATE_MODERATOR, nullptr, 0, TOX_GROUP_TOPIC_LOCK_DISABLED) == 0) {
                ++count;
            }
        }

        if (count == NUM_GROUP_TOXES) {
            fprintf(stderr, "%u peers successfully received state changes\n", count);
            break;
        }
    }

    for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) {
        Tox_Err_Group_Leave err_exit;
        tox_group_leave(autotoxes[i].tox, groupnum, nullptr, 0, &err_exit);
        ck_assert_msg(err_exit == TOX_ERR_GROUP_LEAVE_OK, "%d", err_exit);
    }

    fprintf(stderr, "All tests passed!\n");
}

int main(void)
{
    setvbuf(stdout, nullptr, _IONBF, 0);

    Run_Auto_Options autotest_opts = default_run_auto_options();
    autotest_opts.graph = GRAPH_COMPLETE;

    run_auto_test(nullptr, NUM_GROUP_TOXES, group_state_test, sizeof(State), &autotest_opts);

    return 0;
}

#undef PEER0_NICK
#undef PEER0_NICK_LEN
#undef GROUP_NAME_LEN
#undef GROUP_NAME
#undef PASS_LEN
#undef PASSWORD
#undef PEER_LIMIT_2
#undef PEER_LIMIT_1
#undef NUM_GROUP_TOXES