/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */

#include "clar.h"

#include "services/normal/music.h"
#include "services/normal/music_endpoint.h"
#include "services/normal/music_internal.h"

#include "services/common/comm_session/session.h"
#include "services/common/comm_session/session_remote_os.h"

#include "kernel/events.h"

#include "util/size.h"

// Stubs & Fakes
///////////////////////////////////////////////////////////

#include "fake_events.h"
#include "fake_rtc.h"
#include "fake_session.h"
#include "fake_system_task.h"

#include "stubs_app_manager.h"
#include "stubs_app_install_manager.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_bt_lock.h"
#include "stubs_hexdump.h"
#include "stubs_logging.h"
#include "stubs_mutex.h"
#include "stubs_serial.h"
#include "stubs_tick.h"

extern void music_protocol_msg_callback(CommSession *session, const uint8_t* msg, size_t length);

// Helpers
///////////////////////////////////////////////////////////

static void prv_receive_app_info_event(bool is_android) {
  const PebbleRemoteAppInfoEvent app_info_event = (const PebbleRemoteAppInfoEvent) {
    .os = is_android ? RemoteOSAndroid : RemoteOSiOS,
  };
  music_endpoint_handle_mobile_app_info_event(&app_info_event);
}

static void prv_receive_app_event(bool is_open) {
  const PebbleCommSessionEvent app_event = (const PebbleCommSessionEvent) {
    .is_open = is_open,
    .is_system = true
  };
  music_endpoint_handle_mobile_app_event(&app_event);
}

static void prv_receive_pp_data(const uint8_t *data, uint16_t length) {
  fake_event_clear_last();
  music_protocol_msg_callback(NULL, data, length);
}

static void prv_receive_and_assert_now_playing(bool expect_is_handled) {
  uint8_t msg[] = { 0x10, 3, 'o', 'n', 'e', 3, 't', 'w', 'o', 5, 't', 'h', 'r', 'e', 'e', 0xAA,
                    0x00, 0x00, 0x00, 0xAA, 0x00, 0xAA, 0x00 };
  prv_receive_pp_data(msg, sizeof(msg));

  PebbleEvent e = fake_event_get_last();
  if (expect_is_handled) {
    cl_assert_equal_i(e.type, PEBBLE_MEDIA_EVENT);
    cl_assert_equal_i(e.media.type, PebbleMediaEventTypeTrackPosChanged);

    char artist[MUSIC_BUFFER_LENGTH];
    char album[MUSIC_BUFFER_LENGTH];
    char title[MUSIC_BUFFER_LENGTH];

    music_get_now_playing(title, artist, album);

    cl_assert_equal_s(artist, "one");
    cl_assert_equal_s(album, "two");
    cl_assert_equal_s(title, "three");

    uint32_t track_position, track_duration;
    music_get_pos(&track_position, &track_duration);
    cl_assert_equal_i(track_duration, 0xAA);
  } else {
    cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
  }
}

static void prv_receive_and_assert_play_state(bool expect_is_handled) {
  uint8_t msg[] = { 0x11, 0x01, 0xAA, 0x00, 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x01, 0x01 };
  prv_receive_pp_data(msg, sizeof(msg));

  PebbleEvent e = fake_event_get_last();
  if (expect_is_handled) {
    cl_assert_equal_i(e.type, PEBBLE_MEDIA_EVENT);
    cl_assert_equal_i(e.media.type, PebbleMediaEventTypeTrackPosChanged);

    cl_assert_equal_i(music_get_playback_state(), 0x01);
    uint32_t track_position, track_duration;
    music_get_pos(&track_position, &track_duration);
    cl_assert_equal_i(track_position, 0xAA);
    cl_assert_equal_i(music_get_playback_rate_percent(), 0xAA);

  } else {
    cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
  }
}

static void prv_receive_and_assert_volume_info(bool expect_is_handled) {
  uint8_t msg[] = { 0x12, 0x33 };
  prv_receive_pp_data(msg, sizeof(msg));

  PebbleEvent e = fake_event_get_last();
  if (expect_is_handled) {
    cl_assert_equal_i(e.type, PEBBLE_MEDIA_EVENT);
    cl_assert_equal_i(e.media.type, PebbleMediaEventTypeVolumeChanged);

    cl_assert_equal_i(music_get_volume_percent(), 0x33);

  } else {
    cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
  }
}

static void prv_receive_and_assert_player_info(bool expect_is_handled) {
  uint8_t msg[] = { 0x13, 17, 'c', 'o', 'm', '.', 's', 'p', 'o', 't', 'i', 'f', 'y', '.', 'm',
                    'u', 's', 'i', 'c', 7, 'S', 'p', 'o', 't', 'i', 'f', 'y' };
  prv_receive_pp_data(msg, sizeof(msg));

  PebbleEvent e = fake_event_get_last();
  if (expect_is_handled) {
    cl_assert_equal_i(e.type, PEBBLE_MEDIA_EVENT);
    cl_assert_equal_i(e.media.type, PebbleMediaEventTypeNowPlayingChanged);

    char player_name[MUSIC_BUFFER_LENGTH];
    music_get_player_name(player_name);
    cl_assert_equal_s(player_name, "Spotify");

  } else {
    cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
  }
}

static void prv_receive_and_assert_all(bool expect_is_handled) {
  prv_receive_and_assert_now_playing(expect_is_handled);
  prv_receive_and_assert_play_state(expect_is_handled);
  prv_receive_and_assert_volume_info(expect_is_handled);
  prv_receive_and_assert_player_info(expect_is_handled);
}


static const MusicServerImplementation s_dummy_server_implementation = {};
static void prv_set_dummy_server_connected(bool connected) {
  music_set_connected_server(&s_dummy_server_implementation,
                             connected /* connected */);
}

static void prv_assert_no_data_sent_cb(uint16_t endpoint_id,
                                       const uint8_t* data, unsigned int data_length) {
  cl_assert(false);
}

static bool s_now_playing_requested;
static void prv_assert_now_playing_requested_cb(uint16_t endpoint_id,
                                                const uint8_t* data, unsigned int data_length) {
  cl_assert_equal_i(data_length, 1);
  cl_assert_equal_i(data[0], 0x08);  // MusicEndpointCmdIDGetAllInfo
  s_now_playing_requested = true;
}

static bool s_next_track_command_sent;
static void prv_assert_next_track_command_sent_cb(uint16_t endpoint_id,
                                                  const uint8_t* data, unsigned int data_length) {
  cl_assert_equal_i(data_length, 1);
  cl_assert_equal_i(data[0], 0x04);  // MusicEndpointCmdIDNextTrack
  s_next_track_command_sent = true;
}

static bool s_is_playback_cmd_sent;
static uint8_t s_playback_cmd_sent;
static void prv_assert_playback_command_sent_cb(uint16_t endpoint_id,
                                                const uint8_t* data, unsigned int data_length) {
  cl_assert_equal_i(data_length, 1);
  if (!s_is_playback_cmd_sent) {
    s_playback_cmd_sent = data[0];
    s_is_playback_cmd_sent = true;
  } else {
    // Playback command is always followed by:
    cl_assert_equal_i(data[0], 0x08);  // MusicEndpointCmdIDGetAllInfo
  }
}

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

Transport *s_transport;

void test_music_endpoint__initialize(void) {
  s_now_playing_requested = false;
  s_next_track_command_sent = false;
  s_is_playback_cmd_sent = false;
  s_playback_cmd_sent = ~0;
  fake_event_init();
  fake_rtc_init(0, 0);
  fake_comm_session_init();
  music_init();

  s_transport = fake_transport_create(TransportDestinationSystem, NULL, NULL);
  fake_transport_set_connected(s_transport, true /* connected */);

  // Simulate connecting Pebble mobile app:
  prv_receive_app_event(true /* is_open */);
}

void test_music_endpoint__cleanup(void) {
  fake_comm_session_process_send_next();

  // Simulate disconnecting Pebble mobile app:
  prv_receive_app_event(false /* is_open */);

  fake_comm_session_cleanup();
}

void test_music_endpoint__ignore_now_playing_while_not_connected(void) {
  // Don't connect app, but receive Now Playing info. Should be ignored:
  prv_receive_and_assert_all(false /* expect_is_handled */);
}

void test_music_endpoint__ignore_now_playing_while_other_server_connected(void) {
  // Another music server connects:
  prv_set_dummy_server_connected(true /* connected */);

  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);

  // Receive Now Playing info. Should be ignored, because other server is connected:
 prv_receive_and_assert_all(false /* expect_is_handled*/);

  // Disconnect dummy server, to clean up after ourselves:
  prv_set_dummy_server_connected(false /* connected */);
}

void test_music_endpoint__ignore_now_playing_from_ios_app(void) {
  // iOS app connects:
  prv_receive_app_info_event(false /* is_android */);
  // iOS app is not supposed to use this endpoint:
  prv_receive_and_assert_all(false /* expect_is_handled*/);
}

void test_music_endpoint__request_now_playing_upon_connect(void) {
  fake_transport_set_sent_cb(s_transport, &prv_assert_now_playing_requested_cb);

  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);

  fake_comm_session_process_send_next();
  cl_assert_equal_b(s_now_playing_requested, true);
}

void test_music_endpoint__receive_now_playing_while_connected(void) {
  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);
  prv_receive_and_assert_all(true /* expect_is_handled*/);
}

void test_music_endpoint__ignore_unknown_message(void) {
  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);
  uint8_t unknown_msg[] = { 0xff };
  prv_receive_pp_data(unknown_msg, sizeof(unknown_msg));
  PebbleEvent e = fake_event_get_last();
  cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
}

void test_music_endpoint__receive_zero_length_now_playing(void) {
  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);
  prv_receive_and_assert_all(true /* expect_is_handled*/);
  cl_assert_equal_b(music_has_now_playing(), true);

  uint8_t zero_length_now_playing[] = { 0x10, 0, 0, 0 };
  prv_receive_pp_data(zero_length_now_playing, sizeof(zero_length_now_playing));
  cl_assert_equal_b(music_has_now_playing(), false);
}

void test_music_endpoint__ignore_malformatted_messages(void) {
  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);
  const uint8_t malformatted_artist[] = {
    0x10, 14, 'o', 'n', 'e', 3, 't', 'w', 'o', 5, 't', 'h', 'r', 'e', 'e'
  };
  const uint8_t malformatted_album[] = {
    0x10, 3, 'o', 'n', 'e', 10, 't', 'w', 'o', 5, 't', 'h', 'r', 'e', 'e'
  };
  const uint8_t malformatted_title[] = {
    0x10, 3, 'o', 'n', 'e', 3, 't', 'w', 'o', 6, 't', 'h', 'r', 'e', 'e'
  };
  const uint8_t malformatted_player[] = {
    0x13, 17, 'c', 'o', 'm', '.', 's', 'p', 'o', 't', 'i', 'f', 'y', '.', 'm', 'u', 's', 'i', 'c',
    9, 'S', 'p', 'o', 't', 'i', 'f', 'y'
  };
  struct {
    const uint8_t *data;
    uint16_t length;
  } test_vectors[] = {
    { malformatted_artist, sizeof(malformatted_artist) },
    { malformatted_album, sizeof(malformatted_album) },
    { malformatted_title, sizeof(malformatted_title) },
    { malformatted_player, sizeof(malformatted_player) }
  };
  for (int i = 0; i < ARRAY_LENGTH(test_vectors); ++i) {
    prv_receive_pp_data(test_vectors[i].data, test_vectors[i].length);
    PebbleEvent e = fake_event_get_last();
    cl_assert_equal_i(e.type, PEBBLE_NULL_EVENT);
  }
}

void test_music_endpoint__supported_capabilities(void) {
  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);
  // music_is_progress_reporting_supported() relies on a valid track duration
  prv_receive_and_assert_all(true /* expect_is_handled*/);

  cl_assert_equal_b(music_is_playback_state_reporting_supported(), true);
  cl_assert_equal_b(music_is_progress_reporting_supported(), true);
  cl_assert_equal_b(music_is_volume_reporting_supported(), true);
  cl_assert_equal_b(music_needs_user_to_start_playback_on_phone(), false);
  for (MusicCommand cmd = 0; cmd < NumMusicCommand; ++cmd) {
    bool expect_supported = true;
    if (cmd == MusicCommandAdvanceRepeatMode ||
        cmd == MusicCommandAdvanceShuffleMode ||
        cmd == MusicCommandSkipForward ||
        cmd == MusicCommandSkipBackward ||
        cmd == MusicCommandLike ||
        cmd == MusicCommandDislike ||
        cmd == MusicCommandBookmark) {
      expect_supported = false;
    }
    cl_assert_equal_b(music_is_command_supported(cmd), expect_supported);
  }
}

void test_music_endpoint__reduced_latency(void) {
  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);

  cl_assert_equal_b(fake_comm_session_is_latency_reduced(), false);
  music_request_reduced_latency(true);
  cl_assert_equal_b(fake_comm_session_is_latency_reduced(), true);
  music_request_reduced_latency(false);
  cl_assert_equal_b(fake_comm_session_is_latency_reduced(), false);
}

void test_music_endpoint__low_latency_for_period(void) {
  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);

  cl_assert_equal_i(fake_comm_session_get_responsiveness_max_period(), 0);
  music_request_low_latency_for_period(2000);
  cl_assert_equal_i(fake_comm_session_get_responsiveness_max_period(), 2);
}

void test_music_endpoint__send_unsupported_command(void) {
  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);
  fake_comm_session_process_send_next();  // send out any pending data

  // Attempting to send an unsupported command should not result in any data getting sent out:
  fake_transport_set_sent_cb(s_transport, prv_assert_no_data_sent_cb);
  music_command_send(MusicCommandAdvanceRepeatMode);
  fake_comm_session_process_send_next();
}

void test_music_endpoint__send_next_track_command(void) {
  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);
  fake_comm_session_process_send_next();  // send out any pending data

  fake_transport_set_sent_cb(s_transport, prv_assert_next_track_command_sent_cb);
  music_command_send(MusicCommandNextTrack);
  fake_comm_session_process_send_next();

  cl_assert_equal_b(s_next_track_command_sent, true);
}

void test_music_endpoint__send_playback_command(void) {
  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);
  fake_comm_session_process_send_next();  // send out any pending data

  MusicCommand cmds[] = {
    MusicCommandTogglePlayPause,
    MusicCommandPause,
    MusicCommandPlay,
  };

  uint8_t opcodes[] = {
    0x1,
    0x2,
    0x3,
  };

  fake_transport_set_sent_cb(s_transport, prv_assert_playback_command_sent_cb);
  for (int i = 0; i < ARRAY_LENGTH(cmds); ++i) {
    music_command_send(cmds[i]);
    fake_comm_session_process_send_next();
    cl_assert_equal_i(s_is_playback_cmd_sent, true);
    cl_assert_equal_i(s_playback_cmd_sent, opcodes[i]);
    s_is_playback_cmd_sent = false;
  }
}

void test_music_endpoint__player_name_not_available(void) {
  // Android app connects:
  prv_receive_app_info_event(true /* is_android */);

  cl_assert_equal_b(music_get_player_name(NULL), false);
}
