/*
 * Copyright 2024 Google LLC
 *
 * 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 "clar.h"

#include "services/common/debounced_connection_service.h"
#include "services/common/regular_timer.h"
#include "syscall/syscall.h"

#include "fake_new_timer.h"
#include "fake_rtc.h"
#include "fake_system_task.h"
#include "fake_pbl_malloc.h"
#include "fake_session.h"
#include "stubs_bt_lock.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_hexdump.h"
#include "stubs_passert.h"

// Fakes
///////////////////////////////////////////////////////////

static TimerID s_debounce_timer;
static bool s_default_connection_state;
static unsigned int s_event_count = 0;
static PebbleEvent s_cached_event;

static Transport *s_transport;
static CommSession *s_session;

bool remote_is_connected(void) {
  return s_default_connection_state;
}

void event_put(PebbleEvent* event) {
  s_event_count++;

  s_cached_event = (PebbleEvent) {
    .type = event->type,
    .bluetooth.comm_session_event = event->bluetooth.comm_session_event
  };
}

// Helper functions
///////////////////////////////////////////////////////////
static void init(bool connected) {
  if (connected) {
    s_transport = fake_transport_create(TransportDestinationSystem, NULL, NULL);
    s_session = fake_transport_set_connected(s_transport, true);
  }
  
  s_default_connection_state = connected;

  regular_timer_init();
  debounced_connection_service_init();
}

static void check_connected(void) {
  // Connected = true
  cl_assert(sys_mobile_app_is_connected_debounced());
}

static void check_waiting_to_send_disconnect(void) {
  // Connected = true
  cl_assert(sys_mobile_app_is_connected_debounced());
}

static void check_waiting_to_send_second_disconnect(void) {
  // Connected = false
  cl_assert(!sys_mobile_app_is_connected_debounced());
}

static void check_disconnected(void) {
  // Connected = false
  cl_assert(!sys_mobile_app_is_connected_debounced());
}

static void prv_send_connection_event(bool is_connected) {
  //! Get connected event
  PebbleCommSessionEvent event = {
    .is_open = is_connected,
    .is_system = true,
  };
  debounced_connection_service_handle_event(&event);
}

static void prv_assert_event_received(bool is_connected) {
  cl_assert_equal_i(s_event_count, 1);
  cl_assert_equal_b(s_cached_event.bluetooth.comm_session_event.is_open, is_connected);
}

// Tests
///////////////////////////////////////////////////////////

void test_debounced_connection_service__cleanup(void) {
  s_event_count = 0;
  regular_timer_deinit();
  fake_comm_session_cleanup();
}

void test_debounced_connection_service__connected_to_disconnected(void) {
  init(true /* connected */);

  check_connected();

  //! Get disconnected event
  prv_send_connection_event(false);

  check_waiting_to_send_disconnect();

  // No event put
  cl_assert_equal_i(s_event_count, 0);

  //! Timer fires
  regular_timer_fire_seconds(1);

  // Event put
  prv_assert_event_received(false);

  check_disconnected();
}

void test_debounced_connection_service__disconnected_to_connected(void) {
  init(false /* disconnected */);

  check_disconnected();

  //! Get connected event
  prv_send_connection_event(true);

  // Event put
  prv_assert_event_received(true);

  check_connected();
}

void test_debounced_connection_service__connected_to_connected(void) {
  init(true /* connected */);

  check_connected();

  //! Get connected event
  prv_send_connection_event(true);

  check_connected();

  // Event put
  prv_assert_event_received(true);
}

void test_debounced_connection_service__disconnected_to_disconnected(void) {
  //! Currently disconnected
  init(false /* disconnected */);

  check_disconnected();

  //! Get disconnected event
  prv_send_connection_event(false);

  check_waiting_to_send_second_disconnect();

  // No event put
  cl_assert_equal_i(s_event_count, 0);
}

void test_debounced_connection_service__disconnected_wait_disconnected(void) {
  //! Currently disconnected
  init(false /* disconnected */);

  check_disconnected();

  //! Get disconnected event
  prv_send_connection_event(false);

  check_waiting_to_send_second_disconnect();

  //! Timer fires
  regular_timer_fire_seconds(1);

  check_disconnected();

  // Event put
  prv_assert_event_received(false);
}

void test_debounced_connection_service__reconnected_quickly(void) {
  //! Currently connected
  init(true /* connected */);

  check_connected();

  //! Get disconnected event
  prv_send_connection_event(false);
  check_waiting_to_send_disconnect();

  // No event put
  cl_assert_equal_i(s_event_count, 0);

  //! Get connected event before timer fires
  prv_send_connection_event(true);

  check_connected();

  // No event put
  cl_assert_equal_i(s_event_count, 0);
}