/*
 * Copyright (c) 2019 SK Telecom Co., Ltd. All rights reserved.
 *
 * 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 <iostream>

#include <algorithm>
#include <functional>
#include <glib.h>
#include <memory>

#include "playstack_manager.hh"

using namespace NuguCore;

static const unsigned int DEFAULT_NORMAL_HOLD_TIME_SEC = 7;
static const unsigned int DEFAULT_LONG_HOLD_TIME_SEC = 600;

class PlayStackManagerListener : public IPlayStackManagerListener {
public:
    void onStackAdded(const std::string& ps_id)
    {
        ps_ids.emplace_back(ps_id);
        stack_added_call_count++;
    }

    void onStackRemoved(const std::string& ps_id)
    {
        if (inter_hook_func)
            inter_hook_func();

        ps_ids.erase(
            std::remove_if(ps_ids.begin(), ps_ids.end(),
                [&](const std::string& element) {
                    return element == ps_id;
                }),
            ps_ids.end());
    }

    std::vector<std::string> getPlayServiceIds()
    {
        return ps_ids;
    }

    void setHookInOnStackRemoved(std::function<void()>&& hook_func)
    {
        inter_hook_func = hook_func;
    }

    int getStackAddedCallCount()
    {
        return stack_added_call_count;
    }

private:
    std::vector<std::string> ps_ids;
    std::function<void()> inter_hook_func = nullptr;
    int stack_added_call_count = 0;
};

static NuguDirective* createDirective(const std::string& name_space, const std::string& name, const std::string& groups)
{
    return nugu_directive_new(name_space.c_str(), name.c_str(), "1.0", "msg_1", "dlg_1",
        "ref_1", "{}", groups.c_str());
}

typedef struct {
    std::shared_ptr<PlayStackManager> playstack_manager;
    std::shared_ptr<PlayStackManagerListener> playstack_manager_listener;
    std::shared_ptr<PlayStackManagerListener> playstack_manager_listener_snd;

    NuguDirective* ndir_info;
    NuguDirective* ndir_media;
    NuguDirective* ndir_disp;
    NuguDirective* ndir_expect_speech;
    NuguDirective* ndir_alert;
} TestFixture;

static void setup(TestFixture* fixture, gconstpointer user_data)
{
    fixture->playstack_manager = std::make_shared<PlayStackManager>();
    fixture->playstack_manager_listener = std::make_shared<PlayStackManagerListener>();
    fixture->playstack_manager_listener_snd = std::make_shared<PlayStackManagerListener>();

    fixture->ndir_info = createDirective("TTS", "test", "{ \"directives\": [\"TTS.Speak\"] }");
    fixture->ndir_media = createDirective("AudioPlayer", "test", "{ \"directives\": [\"AudioPlayer.Play\"] }");
    fixture->ndir_disp = createDirective("Display", "test", "{ \"directives\": [\"Display.FullText1\", \"TTS.Speak\"] }");
    fixture->ndir_expect_speech = createDirective("ASR", "test", "{ \"directives\": [\"TTS.Speak\", \"ASR.ExpectSpeech\", \"Session.Set\"] }");
    fixture->ndir_alert = createDirective("Alerts", "test", "{ \"directives\": [\"Alerts.SetAlert\"] }");
}

static void teardown(TestFixture* fixture, gconstpointer user_data)
{
    nugu_directive_unref(fixture->ndir_info);
    nugu_directive_unref(fixture->ndir_media);
    nugu_directive_unref(fixture->ndir_disp);
    nugu_directive_unref(fixture->ndir_expect_speech);
    nugu_directive_unref(fixture->ndir_alert);

    fixture->playstack_manager_listener.reset();
    fixture->playstack_manager_listener_snd.reset();
    fixture->playstack_manager.reset();
}

#define G_TEST_ADD_FUNC(name, func) \
    g_test_add(name, TestFixture, nullptr, setup, func, teardown);

static void test_playstack_manager_basic(TestFixture* fixture, gconstpointer ignored)
{
    const auto& playstack_container = fixture->playstack_manager->getPlayStackContainer();

    fixture->playstack_manager->add("", fixture->ndir_info);
    g_assert(playstack_container.first.empty());

    fixture->playstack_manager->add("ps_id_1", fixture->ndir_info);
    g_assert(!playstack_container.first.empty());

    // check adding duplicately
    fixture->playstack_manager->add("ps_id_1", fixture->ndir_info);
    g_assert(!playstack_container.first.empty());

    fixture->playstack_manager->remove("", PlayStackRemoveMode::Immediately);
    g_assert(!playstack_container.first.empty());

    fixture->playstack_manager->remove("ps_id_2", PlayStackRemoveMode::Immediately);
    g_assert(!playstack_container.first.empty());

    fixture->playstack_manager->remove("ps_id_1", PlayStackRemoveMode::Immediately);
    g_assert(playstack_container.first.empty());
}

static void test_playstack_manager_get_stack(TestFixture* fixture, gconstpointer ignored)
{
    g_assert(fixture->playstack_manager->getAllPlayStackItems().empty());

    fixture->playstack_manager->add("ps_id_1", fixture->ndir_info);
    g_assert(fixture->playstack_manager->getAllPlayStackItems().at(0) == "ps_id_1");
    g_assert(fixture->playstack_manager->getPlayStackActivity("") == PlayStackActivity::None);
    g_assert(fixture->playstack_manager->getPlayStackActivity("ps_id_1") == PlayStackActivity::TTS);

    fixture->playstack_manager->remove("ps_id_1", PlayStackRemoveMode::Immediately);
    fixture->playstack_manager->add("ps_id_2", fixture->ndir_media);
    fixture->playstack_manager->add("ps_id_3", fixture->ndir_info);
    auto play_stack_items = fixture->playstack_manager->getAllPlayStackItems();
    g_assert(play_stack_items.at(0) == "ps_id_3" && play_stack_items.at(1) == "ps_id_2");
}

static void test_playstack_manager_listener(TestFixture* fixture, gconstpointer ignored)
{
    fixture->playstack_manager->addListener(nullptr);
    g_assert(fixture->playstack_manager->getListenerCount() == 0);

    fixture->playstack_manager->addListener(fixture->playstack_manager_listener.get());
    g_assert(fixture->playstack_manager->getListenerCount() == 1);

    // check adding duplicately
    fixture->playstack_manager->addListener(fixture->playstack_manager_listener.get());
    g_assert(fixture->playstack_manager->getListenerCount() == 1);

    fixture->playstack_manager->addListener(fixture->playstack_manager_listener_snd.get());
    g_assert(fixture->playstack_manager->getListenerCount() == 2);

    fixture->playstack_manager->add("ps_id_1", fixture->ndir_info);
    g_assert(fixture->playstack_manager_listener->getPlayServiceIds().at(0) == "ps_id_1"
        && fixture->playstack_manager_listener_snd->getPlayServiceIds().at(0) == "ps_id_1");

    fixture->playstack_manager->remove("ps_id_1", PlayStackRemoveMode::Immediately);
    g_assert(fixture->playstack_manager_listener->getPlayServiceIds().empty()
        && fixture->playstack_manager_listener_snd->getPlayServiceIds().empty());

    fixture->playstack_manager->removeListener(nullptr);
    g_assert(fixture->playstack_manager->getListenerCount() == 2);

    fixture->playstack_manager->removeListener(fixture->playstack_manager_listener.get());
    g_assert(fixture->playstack_manager->getListenerCount() == 1);

    fixture->playstack_manager->removeListener(fixture->playstack_manager_listener_snd.get());
    g_assert(fixture->playstack_manager->getListenerCount() == 0);
}

static void test_playstack_manager_hold_stack(TestFixture* fixture, gconstpointer ignored)
{
    const auto& playstack_container = fixture->playstack_manager->getPlayStackContainer();

    // hold stack during 7s'
    fixture->playstack_manager->add("ps_id_1", fixture->ndir_info);
    fixture->playstack_manager->remove("ps_id_1");
    g_assert(!playstack_container.first.empty());

    // remove stack immediately
    fixture->playstack_manager->add("ps_id_2", fixture->ndir_info);
    fixture->playstack_manager->remove("ps_id_2", PlayStackRemoveMode::Immediately);
    g_assert(playstack_container.first.empty());

    // hold stack during 10m'
    fixture->playstack_manager->add("ps_id_3", fixture->ndir_info);
    fixture->playstack_manager->remove("ps_id_3", PlayStackRemoveMode::Later);
    g_assert(!playstack_container.first.empty());
}

static void test_playstack_manager_layer_policy(TestFixture* fixture, gconstpointer ignored)
{
    const auto& playstack_container = fixture->playstack_manager->getPlayStackContainer();

    // Info to Info -> replace
    fixture->playstack_manager->add("ps_id_1", fixture->ndir_info);
    fixture->playstack_manager->add("ps_id_2", fixture->ndir_info);
    g_assert(playstack_container.first.size() == 1
        && playstack_container.first.cbegin()->first == "ps_id_2"
        && playstack_container.first.cbegin()->second == PlayStackActivity::TTS);

    // Info to Media -> replace
    fixture->playstack_manager->add("ps_id_3", fixture->ndir_media);
    g_assert(playstack_container.first.size() == 1
        && playstack_container.first.cbegin()->first == "ps_id_3"
        && playstack_container.first.cbegin()->second == PlayStackActivity::Media);

    // Media to Info -> stacked
    fixture->playstack_manager->add("ps_id_4", fixture->ndir_info);
    g_assert(playstack_container.first.size() == 2);

    // Media to Media -> replace
    fixture->playstack_manager->remove("ps_id_4", PlayStackRemoveMode::Immediately);
    fixture->playstack_manager->add("ps_id_5", fixture->ndir_media);
    g_assert(playstack_container.first.size() == 1
        && playstack_container.first.cbegin()->first == "ps_id_5"
        && playstack_container.first.cbegin()->second == PlayStackActivity::Media);
}

static void test_playstack_manager_update_activity(TestFixture* fixture, gconstpointer ignored)
{
    const auto& playstack_container = fixture->playstack_manager->getPlayStackContainer();
    fixture->playstack_manager->addListener(fixture->playstack_manager_listener.get());

    fixture->playstack_manager->add("ps_id_1", fixture->ndir_info);
    g_assert(playstack_container.first.cbegin()->second == PlayStackActivity::TTS);
    g_assert(fixture->playstack_manager_listener->getStackAddedCallCount() == 1);

    // Not Media : update activity as holding playstack (not call listener)
    fixture->playstack_manager->add("ps_id_1", fixture->ndir_alert);
    g_assert(playstack_container.first.cbegin()->second == PlayStackActivity::Alert);
    g_assert(fixture->playstack_manager_listener->getStackAddedCallCount() == 1);

    // Media : update activity as removing and adding playstack (call listener)
    fixture->playstack_manager->add("ps_id_1", fixture->ndir_media);
    g_assert(playstack_container.first.cbegin()->second == PlayStackActivity::Media);
    g_assert(fixture->playstack_manager_listener->getStackAddedCallCount() == 2);
}

static void test_playstack_manager_control_holding(TestFixture* fixture, gconstpointer ignored)
{
    const auto& playstack_container = fixture->playstack_manager->getPlayStackContainer();

    // stop holding
    fixture->playstack_manager->add("ps_id_1", fixture->ndir_info);
    fixture->playstack_manager->remove("ps_id_1");
    bool has_stack_before = !playstack_container.first.empty();
    fixture->playstack_manager->stopHolding();
    g_assert(has_stack_before && !playstack_container.first.empty());

    // reset holding
    fixture->playstack_manager->resetHolding();
    g_assert(fixture->playstack_manager->isActiveHolding());

    // clear holding
    fixture->playstack_manager->clearHolding();
    g_assert(!fixture->playstack_manager->isActiveHolding());
}

static void test_playstack_manager_check_stack(TestFixture* fixture, gconstpointer ignored)
{
    fixture->playstack_manager->add("ps_id_1", fixture->ndir_info);
    g_assert(!fixture->playstack_manager->isStackedCondition(fixture->ndir_info));

    fixture->playstack_manager->remove("ps_id_1", PlayStackRemoveMode::Immediately);
    fixture->playstack_manager->add("ps_id_2", fixture->ndir_media);
    g_assert(fixture->playstack_manager->isStackedCondition(fixture->ndir_info));
    g_assert(!fixture->playstack_manager->isStackedCondition(fixture->ndir_media));
}

static void test_playstack_manager_check_expect_speech(TestFixture* fixture, gconstpointer ignored)
{
    g_assert(!fixture->playstack_manager->hasExpectSpeech(nullptr));
    g_assert(!fixture->playstack_manager->hasExpectSpeech(fixture->ndir_info));
    g_assert(fixture->playstack_manager->hasExpectSpeech(fixture->ndir_expect_speech));
}

static void test_playstack_manager_check_adding_playstack(TestFixture* fixture, gconstpointer ignored)
{
    fixture->playstack_manager->addListener(fixture->playstack_manager_listener.get());

    fixture->playstack_manager->add("ps_id_1", fixture->ndir_info);
    g_assert(fixture->playstack_manager_listener->getPlayServiceIds().at(0) == "ps_id_1");

    // has not display rendering info (TTS only)
    fixture->playstack_manager_listener->setHookInOnStackRemoved([&]() {
        g_assert(!fixture->playstack_manager->hasAddingPlayStack());
    });

    fixture->playstack_manager->add("ps_id_2", fixture->ndir_info);
    g_assert(fixture->playstack_manager_listener->getPlayServiceIds().at(0) == "ps_id_2");
    g_assert(!fixture->playstack_manager->hasAddingPlayStack());

    // has display rendering info (Display, AudioPlayer)
    fixture->playstack_manager_listener->setHookInOnStackRemoved([&]() {
        g_assert(fixture->playstack_manager->hasAddingPlayStack());
    });

    fixture->playstack_manager->add("ps_id_3", fixture->ndir_disp);
    g_assert(fixture->playstack_manager_listener->getPlayServiceIds().at(0) == "ps_id_3");
    g_assert(!fixture->playstack_manager->hasAddingPlayStack());

    fixture->playstack_manager->add("ps_id_4", fixture->ndir_media);
    g_assert(fixture->playstack_manager_listener->getPlayServiceIds().at(0) == "ps_id_4");
    g_assert(!fixture->playstack_manager->hasAddingPlayStack());
}

static void test_playstack_manager_adjust_playstack_hold_time(TestFixture* fixture, gconstpointer ignored)
{
    // check default hold times
    const auto& hold_times(fixture->playstack_manager->getPlayStackHoldTime());
    g_assert(hold_times.normal_time == DEFAULT_NORMAL_HOLD_TIME_SEC);
    g_assert(hold_times.long_time == DEFAULT_LONG_HOLD_TIME_SEC);

    // check custom hold times
    fixture->playstack_manager->setPlayStackHoldTime({ 5, 10 });
    g_assert(hold_times.normal_time == 5);
    g_assert(hold_times.long_time == 10);

    // check whether setting long time by default value if not pass 2nd argument
    fixture->playstack_manager->setPlayStackHoldTime({ 15 });
    g_assert(hold_times.normal_time == 15);
    g_assert(hold_times.long_time == DEFAULT_LONG_HOLD_TIME_SEC);
}

static void test_playstack_manager_reset_playstack_hold_time(TestFixture* fixture, gconstpointer ignored)
{
    const auto& hold_times(fixture->playstack_manager->getPlayStackHoldTime());
    fixture->playstack_manager->setPlayStackHoldTime({ 5, 10 });
    g_assert(hold_times.normal_time == 5);
    g_assert(hold_times.long_time == 10);

    // reset hold time
    fixture->playstack_manager->resetPlayStackHoldTime();
    g_assert(hold_times.normal_time == DEFAULT_NORMAL_HOLD_TIME_SEC);
    g_assert(hold_times.long_time == DEFAULT_LONG_HOLD_TIME_SEC);
}

static void test_playstack_manager_set_default_playstack_hold_time(TestFixture* fixture, gconstpointer ignored)
{
    using HoldTimes = PlayStackManager::PlayStakcHoldTimes;
    const HoldTimes NEW_DEFAULT_HOLD_TIME { 3, 300 };
    const auto& hold_times(fixture->playstack_manager->getPlayStackHoldTime());

    g_assert(hold_times.normal_time == DEFAULT_NORMAL_HOLD_TIME_SEC);
    g_assert(hold_times.long_time == DEFAULT_LONG_HOLD_TIME_SEC);

    // check validation
    fixture->playstack_manager->setDefaultPlayStackHoldTime({});
    g_assert(hold_times.normal_time == DEFAULT_NORMAL_HOLD_TIME_SEC);
    g_assert(hold_times.long_time == DEFAULT_LONG_HOLD_TIME_SEC);

    fixture->playstack_manager->setDefaultPlayStackHoldTime({ 0, 100 });
    g_assert(hold_times.normal_time == DEFAULT_NORMAL_HOLD_TIME_SEC);
    g_assert(hold_times.long_time == 100);

    fixture->playstack_manager->setDefaultPlayStackHoldTime({ 4, 0 });
    g_assert(hold_times.normal_time == 4);
    g_assert(hold_times.long_time == 100);

    // change default hold time
    fixture->playstack_manager->setDefaultPlayStackHoldTime(HoldTimes { NEW_DEFAULT_HOLD_TIME });
    g_assert(hold_times.normal_time == NEW_DEFAULT_HOLD_TIME.normal_time);
    g_assert(hold_times.long_time == NEW_DEFAULT_HOLD_TIME.long_time);

    // adjust hold time
    fixture->playstack_manager->setPlayStackHoldTime({ 5, 100 });
    g_assert(hold_times.normal_time == 5);
    g_assert(hold_times.long_time == 100);

    // reset hold time
    fixture->playstack_manager->resetPlayStackHoldTime();
    g_assert(hold_times.normal_time == NEW_DEFAULT_HOLD_TIME.normal_time);
    g_assert(hold_times.long_time == NEW_DEFAULT_HOLD_TIME.long_time);
}

static void test_playstack_manager_handle_relistening(TestFixture* fixture, gconstpointer ignored)
{
    const auto& playstack_container = fixture->playstack_manager->getPlayStackContainer();

    auto checkMediaActivity = [&] {
        g_assert(playstack_container.first.size() == 1);
        g_assert(playstack_container.first.cbegin()->first == "ps_id_1");
        g_assert(playstack_container.first.cbegin()->second == PlayStackActivity::Media);
    };

    // [1] play media
    fixture->playstack_manager->add("ps_id_1", fixture->ndir_media);
    checkMediaActivity();

    // [2] handle ASR re-listening (Th playServiceId is same.)
    fixture->playstack_manager->add("ps_id_1", fixture->ndir_expect_speech);
    checkMediaActivity();

    // [3] pause media and start TTS
    fixture->playstack_manager->remove("ps_id_1", PlayStackRemoveMode::Later);
    g_assert(playstack_container.first.size() == 1);

    // [4] TTS finished (If the playstack has a media activity, it skip to release sync.)
    g_assert(fixture->playstack_manager->getPlayStackActivity("ps_id_1") == PlayStackActivity::Media);

    // [5] mic on by ASR DM
    fixture->playstack_manager->stopHolding();

    // [6] mic off after ASR DM
    fixture->playstack_manager->resetHolding();
    checkMediaActivity();
}

static void test_playstack_manager_reset(TestFixture* fixture, gconstpointer ignored)
{
    const auto& playstack_container = fixture->playstack_manager->getPlayStackContainer();

    fixture->playstack_manager->add("ps_id_1", fixture->ndir_expect_speech);
    auto flag_set_before = fixture->playstack_manager->getFlagSet();
    g_assert(flag_set_before.find(true) != flag_set_before.cend());
    g_assert(!playstack_container.first.empty());

    fixture->playstack_manager->add("ps_id_2", fixture->ndir_info);
    fixture->playstack_manager->remove("ps_id_2");
    g_assert(fixture->playstack_manager->isActiveHolding());

    fixture->playstack_manager->reset();
    auto flag_set_after = fixture->playstack_manager->getFlagSet();
    g_assert(flag_set_after.find(true) == flag_set_after.cend());
    g_assert(playstack_container.first.empty());
    g_assert(!fixture->playstack_manager->isActiveHolding());
    g_assert(!fixture->playstack_manager->hasAddingPlayStack());
}

int main(int argc, char* argv[])
{
#if !GLIB_CHECK_VERSION(2, 36, 0)
    g_type_init();
#endif

    g_test_init(&argc, &argv, (void*)NULL);
    g_log_set_always_fatal((GLogLevelFlags)G_LOG_FATAL_MASK);

    G_TEST_ADD_FUNC("/core/PlayStackManager/basic", test_playstack_manager_basic);
    G_TEST_ADD_FUNC("/core/PlayStackManager/getStack", test_playstack_manager_get_stack);
    G_TEST_ADD_FUNC("/core/PlayStackManager/listener", test_playstack_manager_listener);
    G_TEST_ADD_FUNC("/core/PlayStackManager/holdStack", test_playstack_manager_hold_stack);
    G_TEST_ADD_FUNC("/core/PlayStackManager/layerPolicy", test_playstack_manager_layer_policy);
    G_TEST_ADD_FUNC("/core/PlayStackManager/updateActivity", test_playstack_manager_update_activity);
    G_TEST_ADD_FUNC("/core/PlayStackManager/controlHolding", test_playstack_manager_control_holding);
    G_TEST_ADD_FUNC("/core/PlayStackManager/checkStack", test_playstack_manager_check_stack);
    G_TEST_ADD_FUNC("/core/PlayStackManager/checkExpectSpeech", test_playstack_manager_check_expect_speech);
    G_TEST_ADD_FUNC("/core/PlayStackManager/checkAddingPlayStack", test_playstack_manager_check_adding_playstack);
    G_TEST_ADD_FUNC("/core/PlayStackManager/adjustPlayStackHoldTime", test_playstack_manager_adjust_playstack_hold_time);
    G_TEST_ADD_FUNC("/core/PlayStackManager/resetPlayStackHoldTime", test_playstack_manager_reset_playstack_hold_time);
    G_TEST_ADD_FUNC("/core/PlayStackManager/setDefaultPlayStackHoldTime", test_playstack_manager_set_default_playstack_hold_time);
    G_TEST_ADD_FUNC("/core/PlayStackManager/handleRelistening", test_playstack_manager_handle_relistening);
    G_TEST_ADD_FUNC("/core/PlayStackManager/reset", test_playstack_manager_reset);

    return g_test_run();
}
