/*
 * 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/comm_session/session_internal.h"
#include "services/common/comm_session/session_send_queue.h"
#include "util/math.h"

extern void comm_session_send_queue_cleanup(CommSession *session);

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

#include "fake_kernel_malloc.h"

#include "stubs_bt_lock.h"
#include "stubs_logging.h"
#include "stubs_passert.h"

static CommSession s_session;
static CommSession *s_valid_session;

void comm_session_analytics_inc_bytes_sent(CommSession *session, uint16_t length) {
}

bool comm_session_is_valid(const CommSession *session) {
  if (!session) {
    return false;
  }
  return (s_valid_session == session);
}

void comm_session_send_next(CommSession *session) {
}


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

typedef struct {
  SessionSendQueueJob job;
  size_t consumed_length;
  size_t length;
  uint8_t data[];
} TestSendJob;

static size_t prv_get_length(const TestSendJob *sb) {
  return (sb->length - sb->consumed_length);
}

static const uint8_t *prv_get_read_pointer(const TestSendJob *sb) {
  return (sb->data + sb->consumed_length);
}

static size_t prv_send_job_impl_get_length(const SessionSendQueueJob *send_job) {
  return prv_get_length((TestSendJob *)send_job);
}

size_t prv_send_job_impl_copy(const SessionSendQueueJob *send_job, int start_offset,
                              size_t length, uint8_t *data_out) {
  TestSendJob *sb = (TestSendJob *)send_job;
  const size_t length_remaining = prv_get_length(sb);
  const size_t length_after_offset = (length_remaining - start_offset);
  const size_t length_to_copy = MIN(length_after_offset, length);
  memcpy(data_out, prv_get_read_pointer(sb) + start_offset, length_to_copy);
  return length_to_copy;
}

size_t prv_send_job_impl_get_read_pointer(const SessionSendQueueJob *send_job,
                                          const uint8_t **data_out) {
  TestSendJob *sb = (TestSendJob *)send_job;
  *data_out = prv_get_read_pointer(sb);
  return prv_get_length(sb);
}

void prv_send_job_impl_consume(const SessionSendQueueJob *send_job, size_t length) {
  TestSendJob *sb = (TestSendJob *)send_job;
  sb->consumed_length += length;
}

static int s_free_count;

void prv_send_job_impl_free(SessionSendQueueJob *send_job) {
  kernel_free(send_job);
  ++s_free_count;
}

static const SessionSendJobImpl s_test_job_impl = {
  .get_length = prv_send_job_impl_get_length,
  .copy = prv_send_job_impl_copy,
  .get_read_pointer = prv_send_job_impl_get_read_pointer,
  .consume = prv_send_job_impl_consume,
  .free = prv_send_job_impl_free,
};

SessionSendQueueJob *prv_create_test_job(const uint8_t *data, size_t length) {
  TestSendJob *job = kernel_malloc(sizeof(TestSendJob) + length);
  cl_assert(job);
  *job = (const TestSendJob) {
    .job = {
      .impl = &s_test_job_impl,
    },
    .length = length,
  };
  if (length && data) {
    memcpy(job->data, data, length);
  }
  return (SessionSendQueueJob *)job;
}

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

static const uint8_t TEST_DATA[] = {0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff};

void test_session_send_queue__initialize(void) {
  s_valid_session = &s_session;
  s_free_count = 0;
  fake_kernel_malloc_init();
  fake_kernel_malloc_enable_stats(true);
  fake_kernel_malloc_mark();
  s_session = (const CommSession) {};
}

void test_session_send_queue__cleanup(void) {
  if (s_valid_session) {
    comm_session_send_queue_cleanup(s_valid_session);
  }

  // Check for leaks:
  fake_kernel_malloc_mark_assert_equal();
  fake_kernel_malloc_deinit();
}

void test_session_send_queue__get_length_returns_summed_length_of_all_jobs(void) {
  cl_assert_equal_i(0, comm_session_send_queue_get_length(s_valid_session));

  int num_jobs = 3;
  for (int i = 0; i < num_jobs; ++i) {
    SessionSendQueueJob *job = prv_create_test_job(TEST_DATA, sizeof(TEST_DATA));
    comm_session_send_queue_add_job(s_valid_session, &job);
    cl_assert(job);
    cl_assert_equal_i((i + 1) * sizeof(TEST_DATA),
                      comm_session_send_queue_get_length(s_valid_session));
  }
}

static void prv_add_jobs(int num_jobs) {
  for (int i = 0; i < num_jobs; ++i) {
    SessionSendQueueJob *job = prv_create_test_job(TEST_DATA, sizeof(TEST_DATA));
    comm_session_send_queue_add_job(s_valid_session, &job);
    cl_assert(job);
  }
}

void test_session_send_queue__copy_empty_queue(void) {
  uint8_t data_out[2];
  cl_assert_equal_i(0,
                    comm_session_send_queue_copy(s_valid_session, 0, sizeof(data_out), data_out));
}

void test_session_send_queue__copy_less_than_head_job_zero_offset(void) {
  int num_jobs = 3;
  prv_add_jobs(num_jobs);

  uint8_t data_out[2];
  memset(data_out, 0, sizeof(data_out));
  cl_assert_equal_i(sizeof(data_out),
                    comm_session_send_queue_copy(s_valid_session, 0, sizeof(data_out), data_out));
  cl_assert_equal_m(data_out, TEST_DATA, sizeof(data_out));
}

void test_session_send_queue__copy_less_than_head_job_with_offset_shorter_than_job(void) {
  int num_jobs = 3;
  prv_add_jobs(num_jobs);

  uint8_t data_out[1];
  memset(data_out, 0, sizeof(data_out));
  int offset = 1;
  cl_assert_equal_i(sizeof(data_out),
                    comm_session_send_queue_copy(s_valid_session, offset,
                                                 sizeof(data_out), data_out));
  cl_assert_equal_m(data_out, TEST_DATA + offset, sizeof(data_out));
}

void test_session_send_queue__copy_less_than_head_job_with_offset_longer_than_job(void) {
  int num_jobs = 3;
  prv_add_jobs(num_jobs);

  uint8_t data_out[sizeof(TEST_DATA) - 1];
  memset(data_out, 0, sizeof(data_out));
  int offset = sizeof(TEST_DATA) + 1;
  cl_assert_equal_i(sizeof(data_out),
                    comm_session_send_queue_copy(s_valid_session, offset,
                                                 sizeof(data_out), data_out));
  cl_assert_equal_m(data_out, TEST_DATA + (offset % sizeof(TEST_DATA)), sizeof(data_out));
}

void test_session_send_queue__copy_overlapping_multiple_jobs_with_offset(void) {
  int num_jobs = 3;
  prv_add_jobs(num_jobs);

  uint8_t data_out[2 * sizeof(TEST_DATA)];
  memset(data_out, 0, sizeof(data_out));
  int offset = 1;
  cl_assert_equal_i(sizeof(data_out),
                    comm_session_send_queue_copy(s_valid_session, offset,
                                                 sizeof(data_out), data_out));
  for (int i = 0; i < 2; ++i) {
    cl_assert_equal_m(data_out + (i * sizeof(TEST_DATA)),
                      TEST_DATA + offset, sizeof(TEST_DATA) - offset);
    cl_assert_equal_m(data_out + ((i + 1) * sizeof(TEST_DATA)) - offset,
                      TEST_DATA, offset);
  }
}

void test_session_send_queue__get_read_pointer(void) {
  cl_assert_equal_i(s_free_count, 0);

  int num_jobs = 3;
  prv_add_jobs(num_jobs);
  const uint8_t *data_out = NULL;

  for (int consumed = 0; consumed < sizeof(TEST_DATA); ++consumed) {
    cl_assert_equal_i(comm_session_send_queue_get_read_pointer(s_valid_session, &data_out),
                      sizeof(TEST_DATA) - consumed);
    cl_assert_equal_m(data_out, TEST_DATA + consumed, sizeof(TEST_DATA) - consumed);

    comm_session_send_queue_consume(s_valid_session, 1);
  }

  // Expect head to be free'd:
  cl_assert_equal_i(s_free_count, 1);

  // Next job can be read:
  cl_assert_equal_i(comm_session_send_queue_get_read_pointer(s_valid_session, &data_out),
                    sizeof(TEST_DATA));
  cl_assert_equal_m(data_out, TEST_DATA, sizeof(TEST_DATA));
}

void test_session_send_queue__get_read_pointer_no_jobs(void) {
  const uint8_t *data_out = NULL;
  cl_assert_equal_i(0, comm_session_send_queue_get_read_pointer(s_valid_session, &data_out));
}

void test_session_send_queue__consume_more_than_one_job(void) {
  int num_jobs = 3;
  prv_add_jobs(num_jobs);

  size_t consumed = sizeof(TEST_DATA) + 1;
  comm_session_send_queue_consume(s_valid_session, consumed);

  // Expect head to be free'd:
  cl_assert_equal_i(s_free_count, 1);

  cl_assert_equal_i((num_jobs * sizeof(TEST_DATA)) - consumed,
                    comm_session_send_queue_get_length(s_valid_session));
}

void test_session_send_queue__consume_all(void) {
  int num_jobs = 3;
  prv_add_jobs(num_jobs);

  comm_session_send_queue_consume(s_valid_session, UINT32_MAX);

  cl_assert_equal_i(s_free_count, num_jobs);
}

void test_session_send_queue__cleanup_calls_free_on_all_jobs(void) {
  cl_assert_equal_i(s_free_count, 0);

  // Add a couple jobs:
  int num_jobs = 3;
  prv_add_jobs(num_jobs);

  // When the session is disconnected, comm_session_send_queue_cleanup() is called:
  comm_session_send_queue_cleanup(s_valid_session);

  cl_assert_equal_i(s_free_count, num_jobs);
}

void test_session_send_queue__session_closed_when_add_is_called(void) {
  s_valid_session = NULL;

  SessionSendQueueJob *job = prv_create_test_job(TEST_DATA, sizeof(TEST_DATA));
  comm_session_send_queue_add_job(s_valid_session, &job);
  cl_assert(!job);
  cl_assert_equal_i(s_free_count, 1);
}
