/*
 * Copyright 2014 Canonical Ltd.
 *
 * Authors:
 *   Charles Kerr <charles.kerr@canonical.com>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3, as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranties of
 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <datetime/appointment.h>
#include <datetime/dbus-shared.h>
#include <datetime/settings.h>
#include <datetime/snap.h>

#include <notifications/dbus-shared.h>
#include <notifications/notifications.h>

#include <libdbustest/dbus-test.h>

#include <glib.h>

using namespace unity::indicator::datetime;

#include "glib-fixture.h"

/***
****
***/

namespace
{
  static constexpr char const * APP_NAME {"indicator-datetime-service"};
}

using namespace unity::indicator::datetime;

class SnapFixture: public GlibFixture
{
private:

  typedef GlibFixture super;

  static constexpr char const * NOTIFY_BUSNAME   {"org.freedesktop.Notifications"};
  static constexpr char const * NOTIFY_INTERFACE {"org.freedesktop.Notifications"};
  static constexpr char const * NOTIFY_PATH      {"/org/freedesktop/Notifications"};

protected:

  static constexpr char const * HAPTIC_METHOD_VIBRATE_PATTERN {"VibratePattern"};

  static constexpr int SCREEN_COOKIE {8675309};
  static constexpr char const * SCREEN_METHOD_KEEP_DISPLAY_ON {"keepDisplayOn"}; 
  static constexpr char const * SCREEN_METHOD_REMOVE_DISPLAY_ON_REQUEST {"removeDisplayOnRequest"};

  static constexpr int POWERD_SYS_STATE_ACTIVE = 1;
  static constexpr char const * POWERD_COOKIE {"567-48-8307"};
  static constexpr char const * POWERD_METHOD_REQUEST_SYS_STATE {"requestSysState"};
  static constexpr char const * POWERD_METHOD_CLEAR_SYS_STATE {"clearSysState"};

  static constexpr int FIRST_NOTIFY_ID {1000};

  static constexpr int NOTIFICATION_CLOSED_EXPIRED   {1};
  static constexpr int NOTIFICATION_CLOSED_DISMISSED {2};
  static constexpr int NOTIFICATION_CLOSED_API       {3};
  static constexpr int NOTIFICATION_CLOSED_UNDEFINED {4};

  static constexpr char const * METHOD_CLOSE {"CloseNotification"};
  static constexpr char const * METHOD_GET_CAPS {"GetCapabilities"};
  static constexpr char const * METHOD_GET_INFO {"GetServerInformation"};
  static constexpr char const * METHOD_NOTIFY {"Notify"};

  static constexpr char const * SIGNAL_CLOSED {"NotificationClosed"};

  static constexpr char const * HINT_TIMEOUT {"x-canonical-snap-decisions-timeout"};

  Appointment appt;
  GDBusConnection * system_bus = nullptr;
  GDBusConnection * session_bus = nullptr;
  DbusTestService * service = nullptr;
  DbusTestDbusMock * notify_mock = nullptr;
  DbusTestDbusMock * powerd_mock = nullptr;
  DbusTestDbusMock * screen_mock = nullptr;
  DbusTestDbusMock * haptic_mock = nullptr;
  DbusTestDbusMockObject * notify_obj = nullptr;
  DbusTestDbusMockObject * powerd_obj = nullptr;
  DbusTestDbusMockObject * screen_obj = nullptr;
  DbusTestDbusMockObject * haptic_obj = nullptr;

  void SetUp() override
  {
    GError * error = nullptr;
    char * str = nullptr;

    super::SetUp();

    // init the Appointment
    appt.color = "green";
    appt.summary = "Alarm";
    appt.uid = "D4B57D50247291478ED31DED17FF0A9838DED402";
    appt.type = Appointment::EVENT;
    const auto christmas = DateTime::Local(2015,12,25,0,0,0);
    appt.begin = christmas.start_of_day();
    appt.end = christmas.end_of_day();
    appt.alarms.push_back(Alarm{"Alarm Text", "", appt.begin, std::chrono::seconds::zero()});

    service = dbus_test_service_new(nullptr);

    ///
    ///  Add the Notifications mock
    ///

    notify_mock = dbus_test_dbus_mock_new(NOTIFY_BUSNAME);
    notify_obj = dbus_test_dbus_mock_get_object(notify_mock,
                                                NOTIFY_PATH,
                                                NOTIFY_INTERFACE,
                                                &error);
    g_assert_no_error(error);
   
    // METHOD_GET_INFO 
    str = g_strdup("ret = ('mock-notify', 'test vendor', '1.0', '1.1')");
    dbus_test_dbus_mock_object_add_method(notify_mock,
                                          notify_obj,
                                          METHOD_GET_INFO,
                                          nullptr,
                                          G_VARIANT_TYPE("(ssss)"),
                                          str,
                                          &error);
    g_assert_no_error (error);
    g_free (str);

    // METHOD_NOTIFY
    str = g_strdup_printf("try:\n"
                          "  self.NextNotifyId\n"
                          "except AttributeError:\n"
                          "  self.NextNotifyId = %d\n"
                          "ret = self.NextNotifyId\n"
                          "self.NextNotifyId += 1\n",
                          FIRST_NOTIFY_ID);
    dbus_test_dbus_mock_object_add_method(notify_mock,
                                          notify_obj,
                                          METHOD_NOTIFY,
                                          G_VARIANT_TYPE("(susssasa{sv}i)"),
                                          G_VARIANT_TYPE_UINT32,
                                          str,
                                          &error);
    g_assert_no_error (error);
    g_free (str);

    // METHOD_CLOSE 
    str = g_strdup_printf("self.EmitSignal('%s', '%s', 'uu', [ args[0], %d ])",
                          NOTIFY_INTERFACE,
                          SIGNAL_CLOSED,
                          NOTIFICATION_CLOSED_API);
    dbus_test_dbus_mock_object_add_method(notify_mock,
                                          notify_obj,
                                          METHOD_CLOSE,
                                          G_VARIANT_TYPE("(u)"),
                                          nullptr,
                                          str,
                                          &error);
    g_assert_no_error (error);
    g_free (str);

    dbus_test_service_add_task(service, DBUS_TEST_TASK(notify_mock));

    ///
    ///  Add the powerd mock
    ///

    powerd_mock = dbus_test_dbus_mock_new(BUS_POWERD_NAME);
    powerd_obj = dbus_test_dbus_mock_get_object(powerd_mock,
                                                BUS_POWERD_PATH,
                                                BUS_POWERD_INTERFACE,
                                                &error);
    g_assert_no_error(error);
   
    str = g_strdup_printf ("ret = '%s'", POWERD_COOKIE); 
    dbus_test_dbus_mock_object_add_method(powerd_mock,
                                          powerd_obj,
                                          POWERD_METHOD_REQUEST_SYS_STATE,
                                          G_VARIANT_TYPE("(si)"),
                                          G_VARIANT_TYPE("(s)"),
                                          str,
                                          &error);
    g_assert_no_error (error);
    g_free (str);

    dbus_test_dbus_mock_object_add_method(powerd_mock,
                                          powerd_obj,
                                          POWERD_METHOD_CLEAR_SYS_STATE,
                                          G_VARIANT_TYPE("(s)"),
                                          nullptr,
                                          "",
                                          &error);
    g_assert_no_error (error);

    dbus_test_service_add_task(service, DBUS_TEST_TASK(powerd_mock));

    ///
    ///  Add the Screen mock
    ///

    screen_mock = dbus_test_dbus_mock_new(BUS_SCREEN_NAME);
    screen_obj = dbus_test_dbus_mock_get_object(screen_mock,
                                                BUS_SCREEN_PATH,
                                                BUS_SCREEN_INTERFACE,
                                                &error);
    g_assert_no_error(error);
   
    str = g_strdup_printf ("ret = %d", SCREEN_COOKIE); 
    dbus_test_dbus_mock_object_add_method(screen_mock,
                                          screen_obj,
                                          SCREEN_METHOD_KEEP_DISPLAY_ON,
                                          nullptr,
                                          G_VARIANT_TYPE("(i)"),
                                          str,
                                          &error);
    g_assert_no_error (error);
    g_free (str);

    dbus_test_dbus_mock_object_add_method(screen_mock,
                                          screen_obj,
                                          SCREEN_METHOD_REMOVE_DISPLAY_ON_REQUEST,
                                          G_VARIANT_TYPE("(i)"),
                                          nullptr,
                                          "",
                                          &error);
    g_assert_no_error (error);
    dbus_test_service_add_task(service, DBUS_TEST_TASK(screen_mock));

    ///
    ///  Add the haptic mock
    ///

    haptic_mock = dbus_test_dbus_mock_new(BUS_HAPTIC_NAME);
    haptic_obj = dbus_test_dbus_mock_get_object(haptic_mock,
                                                BUS_HAPTIC_PATH,
                                                BUS_HAPTIC_INTERFACE,
                                                &error);
   
    dbus_test_dbus_mock_object_add_method(haptic_mock,
                                          haptic_obj,
                                          HAPTIC_METHOD_VIBRATE_PATTERN,
                                          G_VARIANT_TYPE("(auu)"),
                                          nullptr,
                                          "",
                                          &error);
    g_assert_no_error (error);
    dbus_test_service_add_task(service, DBUS_TEST_TASK(haptic_mock));


    // start 'em up.
    // make the system bus work off the mock bus too, since that's
    // where the upower and screen are on the system bus...

    dbus_test_service_start_tasks(service);
    g_setenv("DBUS_SYSTEM_BUS_ADDRESS", g_getenv("DBUS_SESSION_BUS_ADDRESS"), TRUE);

    session_bus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
    ASSERT_NE(nullptr, session_bus);
    g_dbus_connection_set_exit_on_close(session_bus, false);
    g_object_add_weak_pointer(G_OBJECT(session_bus), (gpointer *)&session_bus);

    system_bus = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, nullptr);
    ASSERT_NE(nullptr, system_bus);
    g_dbus_connection_set_exit_on_close(system_bus, FALSE);
    g_object_add_weak_pointer(G_OBJECT(system_bus), (gpointer *)&system_bus);
  }

  void TearDown() override
  {
    g_clear_object(&haptic_mock);
    g_clear_object(&screen_mock);
    g_clear_object(&powerd_mock);
    g_clear_object(&notify_mock);
    g_clear_object(&service);
    g_object_unref(session_bus);
    g_object_unref(system_bus);

    // wait a little while for the scaffolding to shut down,
    // but don't block on it forever...
    unsigned int cleartry = 0;
    while (((system_bus != nullptr) || (session_bus != nullptr)) && (cleartry < 50))
      {
        g_usleep(100000);
        while (g_main_pending())
          g_main_iteration(true);
        cleartry++;
      }

    super::TearDown();
  }

  void make_interactive()
  {
    // GetCapabilities returns an array containing 'actions',
    // so our snap decision will be interactive.
    // For this test, it means we should get a timeout Notify Hint
    // that matches duration_minutes
    GError * error = nullptr;
    dbus_test_dbus_mock_object_add_method(notify_mock,
                                          notify_obj,
                                          METHOD_GET_CAPS,
                                          nullptr,
                                          G_VARIANT_TYPE_STRING_ARRAY,
                                          "ret = ['actions', 'body']",
                                          &error);
    g_assert_no_error (error);
  }
};

/***
****
***/

namespace
{
  gboolean quit_idle (gpointer gloop)
  {
    g_main_loop_quit(static_cast<GMainLoop*>(gloop));
    return G_SOURCE_REMOVE;
  };
}

TEST_F(SnapFixture, InteractiveDuration)
{
  static constexpr int duration_minutes = 120;
  auto settings = std::make_shared<Settings>();
  settings->alarm_duration.set(duration_minutes);
  auto ne = std::make_shared<unity::indicator::notifications::Engine>(APP_NAME);
  Snap snap (ne, settings);

  make_interactive();

  // call the Snap Decision
  auto func = [this](const Appointment&, const Alarm&){g_idle_add(quit_idle, loop);};
  snap(appt, appt.alarms.front(), func, func);

  // confirm that Notify got called once
  guint len = 0;
  GError * error = nullptr;
  const auto calls = dbus_test_dbus_mock_object_get_method_calls (notify_mock,
                                                                  notify_obj,
                                                                  METHOD_NOTIFY,
                                                                  &len,
                                                                  &error);
  g_assert_no_error(error);
  ASSERT_EQ(1, len);

  // confirm that the app_name passed to Notify was APP_NAME
  const auto& params = calls[0].params;
  ASSERT_EQ(8, g_variant_n_children(params));
  const char * str = nullptr;
  g_variant_get_child (params, 0, "&s", &str);
  ASSERT_STREQ(APP_NAME, str);

  // confirm that the icon passed to Notify was "alarm-clock"
  g_variant_get_child (params, 2, "&s", &str);
  ASSERT_STREQ("alarm-clock", str);

  // confirm that the hints passed to Notify included a timeout matching duration_minutes
  int32_t i32;
  bool b;
  auto hints = g_variant_get_child_value (params, 6);
  b = g_variant_lookup (hints, HINT_TIMEOUT, "i", &i32);
  EXPECT_TRUE(b);
  const auto duration = std::chrono::minutes(duration_minutes);
  EXPECT_EQ(std::chrono::duration_cast<std::chrono::milliseconds>(duration).count(), i32);
  g_variant_unref(hints);
  ne.reset();
}

/***
****
***/

TEST_F(SnapFixture, InhibitSleep)
{
  auto settings = std::make_shared<Settings>();
  auto ne = std::make_shared<unity::indicator::notifications::Engine>(APP_NAME);
  auto snap = new Snap (ne, settings);

  make_interactive();

  // invoke the notification
  auto func = [this](const Appointment&, const Alarm&){g_idle_add(quit_idle, loop);};
  (*snap)(appt, appt.alarms.front(), func, func);

  wait_msec(1000);

  // confirm that sleep got inhibited
  GError * error = nullptr;
  EXPECT_TRUE (dbus_test_dbus_mock_object_check_method_call (powerd_mock,
                                                             powerd_obj,
                                                             POWERD_METHOD_REQUEST_SYS_STATE,
                                                             g_variant_new("(si)", APP_NAME, POWERD_SYS_STATE_ACTIVE),
                                                             &error));

  // confirm that the screen got forced on
  EXPECT_TRUE (dbus_test_dbus_mock_object_check_method_call (screen_mock,
                                                             screen_obj,
                                                             SCREEN_METHOD_KEEP_DISPLAY_ON,
                                                             nullptr,
                                                             &error));

  // force-close the snap
  wait_msec(100);
  delete snap;
  wait_msec(100);

  // confirm that sleep got uninhibted
  EXPECT_TRUE (dbus_test_dbus_mock_object_check_method_call (powerd_mock,
                                                             powerd_obj,
                                                             POWERD_METHOD_CLEAR_SYS_STATE,
                                                             g_variant_new("(s)", POWERD_COOKIE),
                                                             &error));

  // confirm that the screen's no longer forced on
  EXPECT_TRUE (dbus_test_dbus_mock_object_check_method_call (screen_mock,
                                                             screen_obj,
                                                             SCREEN_METHOD_REMOVE_DISPLAY_ON_REQUEST,
                                                             g_variant_new("(i)", SCREEN_COOKIE),
                                                             &error));

  g_assert_no_error (error);
}

/***
****
***/

TEST_F(SnapFixture, ForceScreen)
{
  auto settings = std::make_shared<Settings>();
  auto ne = std::make_shared<unity::indicator::notifications::Engine>(APP_NAME);
  auto snap = new Snap (ne, settings);

  make_interactive();

  // invoke the notification
  auto func = [this](const Appointment&, const Alarm&){g_idle_add(quit_idle, loop);};
  (*snap)(appt, appt.alarms.front(), func, func);

  wait_msec(1000);

  // confirm that sleep got inhibited
  GError * error = nullptr;
  EXPECT_TRUE (dbus_test_dbus_mock_object_check_method_call (powerd_mock,
                                                             powerd_obj,
                                                             POWERD_METHOD_REQUEST_SYS_STATE,
                                                             g_variant_new("(si)", APP_NAME, POWERD_SYS_STATE_ACTIVE),
                                                             &error));
  g_assert_no_error(error);

  // force-close the snap
  wait_msec(100);
  delete snap;
  wait_msec(100);

  // confirm that sleep got uninhibted
  EXPECT_TRUE (dbus_test_dbus_mock_object_check_method_call (powerd_mock,
                                                             powerd_obj,
                                                             POWERD_METHOD_CLEAR_SYS_STATE,
                                                             g_variant_new("(s)", POWERD_COOKIE),
                                                             &error));
  g_assert_no_error(error);
}

/***
****
***/

TEST_F(SnapFixture, HapticModes)
{
  auto settings = std::make_shared<Settings>();
  auto ne = std::make_shared<unity::indicator::notifications::Engine>(APP_NAME);
  auto func = [this](const Appointment&, const Alarm&){g_idle_add(quit_idle, loop);};
  GError * error = nullptr;

  // invoke a snap decision while haptic feedback is set to "pulse",
  // confirm that VibratePattern got called
  settings->alarm_haptic.set("pulse");
  auto snap = new Snap (ne, settings);
  (*snap)(appt, appt.alarms.front(), func, func);
  wait_msec(100);
  EXPECT_TRUE (dbus_test_dbus_mock_object_check_method_call (haptic_mock,
                                                             haptic_obj,
                                                             HAPTIC_METHOD_VIBRATE_PATTERN,
                                                             nullptr,
                                                             &error));
  delete snap;

  // invoke a snap decision while haptic feedback is set to "none",
  // confirm that VibratePattern =didn't= get called
  wait_msec(100);
  dbus_test_dbus_mock_object_clear_method_calls (haptic_mock, haptic_obj, &error);
  settings->alarm_haptic.set("none");
  snap = new Snap (ne, settings);
  (*snap)(appt, appt.alarms.front(), func, func);
  wait_msec(100);
  EXPECT_FALSE (dbus_test_dbus_mock_object_check_method_call (haptic_mock,
                                                              haptic_obj,
                                                              HAPTIC_METHOD_VIBRATE_PATTERN,
                                                              nullptr,
                                                              &error));
  delete snap;

  g_assert_no_error (error);
}