/*
 * 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 "test_jerry_port_common.h"
#include "test_rocky_common.h"

#include "applib/graphics/gpath.h"
#include "applib/preferred_content_size.h"
#include "applib/rockyjs/api/rocky_api.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_timers.h"
#include "applib/rockyjs/api/rocky_api_graphics.h"
#include "applib/rockyjs/api/rocky_api_tickservice.h"
#include "applib/rockyjs/api/rocky_api_util.h"
#include "applib/rockyjs/pbl_jerry_port.h"

#include "syscall/syscall.h"

// Standard
#include "string.h"
#include "applib/rockyjs/rocky.h"

// Fakes
#include "fake_app_timer.h"
#include "fake_pbl_malloc.h"
#include "fake_time.h"
#include "fake_logging.h"

// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"

const RockyGlobalAPI APP_MESSAGE_APIS = {};
const RockyGlobalAPI WATCHINFO_APIS = {};

size_t heap_bytes_free(void) {
  return 123456;
}

void sys_analytics_inc(AnalyticsMetric metric, AnalyticsClient client) {
}

bool sys_get_current_app_is_rocky_app(void) {
  return true;
}

void tick_timer_service_subscribe(TimeUnits tick_units, TickHandler handler) {}

static Window s_app_window_stack_get_top_window;
Window *app_window_stack_get_top_window() {
  return &s_app_window_stack_get_top_window;
}

PreferredContentSize preferred_content_size(void) {
  return PreferredContentSizeMedium;
}

static MockCallRecordings s_layer_mark_dirty;
void layer_mark_dirty(Layer *layer) {
  s_layer_mark_dirty.call_count++;
  s_layer_mark_dirty.last_call = (MockCallRecording){.layer = layer};
}

static MockCallRecordings s_graphics_context_set_fill_color;
void graphics_context_set_fill_color(GContext* ctx, GColor color) {
  s_graphics_context_set_fill_color.call_count++;
  s_graphics_context_set_fill_color.last_call = (MockCallRecording){.ctx = ctx, .color = color};
}

static MockCallRecordings s_graphics_context_set_stroke_color;
void graphics_context_set_stroke_color(GContext* ctx, GColor color) {
  s_graphics_context_set_stroke_color.call_count++;
  s_graphics_context_set_stroke_color.last_call = (MockCallRecording){.ctx = ctx, .color = color};
}

static MockCallRecordings s_graphics_context_set_stroke_width;
void graphics_context_set_stroke_width(GContext* ctx, uint8_t stroke_width) {
  s_graphics_context_set_stroke_width.call_count++;
  s_graphics_context_set_stroke_width.last_call =
    (MockCallRecording){.ctx = ctx, .width =stroke_width};
}


static MockCallRecordings s_graphics_line_draw_precise_stroked;
void graphics_line_draw_precise_stroked(GContext* ctx, GPointPrecise p0, GPointPrecise p1) {
  record_mock_call(s_graphics_line_draw_precise_stroked) {
    .ctx = ctx, .pp0 = p0, .pp1 = p1,
  };
}

static MockCallRecordings s_graphics_draw_line;
void graphics_draw_line(GContext* ctx, GPoint p0, GPoint p1) {
  s_graphics_draw_line.call_count++;
  s_graphics_draw_line.last_call =
    (MockCallRecording){.ctx = ctx, .p0 = p0, .p1 = p1};
}

static MockCallRecordings s_graphics_fill_rect;
void graphics_fill_rect(GContext *ctx, const GRect *rect) {
  s_graphics_fill_rect.call_count++;
  s_graphics_fill_rect.last_call = (MockCallRecording){.ctx = ctx, .rect = *rect};
}

static MockCallRecordings s_graphics_fill_rect;
void graphics_fill_round_rect_by_value(GContext* ctx, GRect rect, uint16_t radius,
                                       GCornerMask corner_mask) {
  s_graphics_fill_rect.call_count++;
  s_graphics_fill_rect.last_call = (MockCallRecording) {
    .ctx = ctx,
    .rect = rect,
    .radius = radius,
    .corner_mask = corner_mask,
  };
}

GPointPrecise gpoint_from_polar_precise(const GPointPrecise *precise_center,
                                        uint16_t precise_radius, int32_t angle) {
  return GPointPreciseFromGPoint(GPointZero);
}

void graphics_draw_arc_precise_internal(GContext *ctx, GPointPrecise center, Fixed_S16_3 radius,
                                        int32_t angle_start, int32_t angle_end) {}

void graphics_draw_rect_precise(GContext *ctx, const GRectPrecise *rect) {}

void graphics_fill_radial_precise_internal(GContext *ctx, GPointPrecise center,
                                           Fixed_S16_3 radius_inner, Fixed_S16_3 radius_outer,
                                           int32_t angle_start, int32_t angle_end) {}

void gpath_draw_filled(GContext* ctx, GPath *path) {}

void layer_get_unobstructed_bounds(const Layer *layer, GRect *bounds_out) {
  *bounds_out = layer->bounds;
}

GFont fonts_get_system_font(const char *font_key) {
  return (GFont)123;
}

void graphics_draw_text(GContext *ctx, const char *text, GFont const font, const GRect box,
                        const GTextOverflowMode overflow_mode, const GTextAlignment alignment,
                        GTextAttributes *text_attributes) {}

void graphics_text_attributes_destroy(GTextAttributes *text_attributes) {}

GSize graphics_text_layout_get_max_used_size(GContext *ctx, const char *text,
                                             GFont const font, const GRect box,
                                             const GTextOverflowMode overflow_mode,
                                             const GTextAlignment alignment,
                                             GTextLayoutCacheRef layout) {
  return GSizeZero;
}

uint32_t resource_storage_get_num_entries(ResAppNum app_num, uint32_t resource_id) {
  return 0;
}

static bool s_skip_mem_leak_check;

static void prv_init(void) {
  rocky_runtime_context_init();
  jerry_init(JERRY_INIT_EMPTY);
}

static void prv_deinit(void) {
  jerry_cleanup();
  rocky_runtime_context_deinit();
}

void test_js__initialize(void) {
  fake_pbl_malloc_clear_tracking();
  s_skip_mem_leak_check = false;

  fake_app_timer_init();
  prv_init();
  s_app_window_stack_get_top_window = (Window){};
  s_app_state_get_graphics_context = NULL;

  s_layer_mark_dirty = (MockCallRecordings){};
  s_graphics_context_set_fill_color = (MockCallRecordings){};
  s_graphics_context_set_stroke_color = (MockCallRecordings){};
  s_graphics_context_set_stroke_width = (MockCallRecordings){};
  s_graphics_line_draw_precise_stroked = (MockCallRecordings){};
  s_graphics_draw_line = (MockCallRecordings){};
  s_graphics_fill_rect = (MockCallRecordings){};

  s_app_event_loop_callback = NULL;
  s_log_internal__expected = NULL;
  s_app_heap_analytics_log_stats_to_app_heartbeat_call_count = 0;
  s_app_heap_analytics_log_rocky_heap_oom_fault_call_count = 0;
}

void test_js__cleanup(void) {
  fake_app_timer_deinit();
  s_log_internal__expected = NULL;

  // some tests deinitialize the engine, avoid double de-init
  if (app_state_get_rocky_runtime_context() != NULL) {
    prv_deinit();
  }

  // PBL-40702: test_js__init_deinit is leaking memory...
  if (!s_skip_mem_leak_check) {
     fake_pbl_malloc_check_net_allocs();
  }
}

void test_js__addition(void) {
  char script[] = "var a = 1; var b = 2; var c = a + b;";
  EXECUTE_SCRIPT(script);
  ASSERT_JS_GLOBAL_EQUALS_I("c", 3.0);
}

void test_js__eval_error(void) {
  prv_deinit();  // engine will be re-initialized in rocky_event_loop~
  char script[] = "function f({;";
  s_log_internal__expected = (const char *[]){
    "Not a snapshot, interpreting buffer as JS source code",
    "Exception while Evaluating JS",
    "SyntaxError: Identifier expected. [line: 1, column: 12]",
    NULL };
  rocky_event_loop_with_string_or_snapshot(script, sizeof(script));
  cl_assert(*s_log_internal__expected == NULL);
}

AppTimer *rocky_timer_get_app_timer(void *data);

void test_js__init_deinit(void) {
  // PBL-40702: test_js__init_deinit is leaking memory...
  s_skip_mem_leak_check = true;

  prv_deinit();

  char *script =
    "var num_times = 0;"
    "var extra_arg = 0;"
    "var timer = setInterval(function(extra) {"
      "num_times++;"
      "extra_arg = extra;"
    "}, 1000, 5);";

  for (int i = 0; i < 30; ++i) {
    prv_init();
    TIMER_APIS.init();
    EXECUTE_SCRIPT(script);
    prv_deinit();
  }

  prv_init();
}

static char *prv_load_js(char *suffix) {
  char path[512] = {0};
  snprintf(path, sizeof(path), "%s/js/tictoc~rect~%s.js", CLAR_FIXTURE_PATH, suffix);
  FILE *f = fopen(path, "r");
  cl_assert(f);
  fseek(f, 0, SEEK_END);
  size_t length = (size_t)ftell(f);
  fseek (f, 0, SEEK_SET);
  char *buffer = malloc(length + 1);
  memset(buffer, 0, length + 1);
  cl_assert(buffer);
  fread (buffer, 1, length, f);
  fclose (f);
  return buffer;
}

void test_js__call_cleanup_twice(void) {
  prv_deinit();
  char *script = "function f(i) { return i * 4; } f(5);";
  bool result = rocky_event_loop_with_string_or_snapshot(script, strlen(script));
  cl_assert(result);
}

static bool s_tictoc_callback_is_color;
static void prv_rocky_tictoc_callback(void) {
  Layer *root_layer = &s_app_window_stack_get_top_window.layer;
  root_layer->bounds = GRect(10, 20, 30, 40);
  cl_assert(root_layer->update_proc);
  GContext ctx = {.lock = true};
  root_layer->update_proc(root_layer, &ctx);

  if (s_tictoc_callback_is_color) {
    cl_assert_equal_i(1, s_graphics_fill_rect.call_count);
    cl_assert_equal_i(4, s_graphics_line_draw_precise_stroked.call_count);
    cl_assert_equal_i(0, s_graphics_draw_line.call_count);
    cl_assert_equal_i(1, s_graphics_context_set_fill_color.call_count);
    cl_assert_equal_i(4, s_graphics_context_set_stroke_color.call_count);
    cl_assert_equal_i(4, s_graphics_context_set_stroke_width.call_count);
  } else {
    cl_assert_equal_i(2, s_graphics_fill_rect.call_count);
    cl_assert_equal_i(0, s_graphics_line_draw_precise_stroked.call_count);
    cl_assert_equal_i(0, s_graphics_draw_line.call_count);
    cl_assert_equal_i(1, s_graphics_context_set_fill_color.call_count);
    cl_assert_equal_i(0, s_graphics_context_set_stroke_color.call_count);
    cl_assert_equal_i(0, s_graphics_context_set_stroke_width.call_count);
  }

  // run update proc multiple times to verify we don't have a memory leak
  for (int i = 1024; i >=0; i--) {
    root_layer->update_proc(root_layer, &ctx);
  }
}

void test_js__rocky_tictoc_color(void) {
  prv_deinit();  // engine will be re-initialized in rocky_event_loop~
  char *script = prv_load_js("color");
  s_tictoc_callback_is_color = true;
  s_app_event_loop_callback = prv_rocky_tictoc_callback;
  bool result = rocky_event_loop_with_string_or_snapshot(script, strlen(script));
  cl_assert(result);
}

void test_js__rocky_tictoc_bw(void) {
  GContext ctx = {};
  s_app_state_get_graphics_context = &ctx;
  prv_deinit();  // engine will be re-initialized in rocky_event_loop~
  char *script = prv_load_js("bw");
  s_tictoc_callback_is_color = false;
  s_app_event_loop_callback = prv_rocky_tictoc_callback;
  bool result = rocky_event_loop_with_string_or_snapshot(script, strlen(script));
  cl_assert(result);
}

void test_js__recursion(void) {
  const char script[] =
    "function f(i) { \n"
    "  if (i == 0) {_rocky.requestDraw();} \n"
    "  else {f(i-1)}\n"
    "}\n"
    "f(10)";
  static const RockyGlobalAPI *apis[] = {
    &GRAPHIC_APIS,
    NULL,
  };
  rocky_global_init(apis);
  EXECUTE_SCRIPT(script);

  cl_assert_equal_i(1, s_layer_mark_dirty.call_count);
}

void test_js__no_print_builtin(void) {
  JS_VAR global_obj = jerry_get_global_object();
  JS_VAR print_builtin = jerry_get_object_field(global_obj, "print");
  cl_assert_equal_b(true, jerry_value_is_undefined(print_builtin));
}

void test_js__sin_cos(void) {
  EXECUTE_SCRIPT(
    "var s1 = 100 + 50 * Math.sin(0);\n"
    "var s2 = 100 + 50 * Math.sin(2 * Math.PI);\n"
    "var c1 = 100 + 50 * Math.cos(0);\n"
    "var c2 = 100 + 50 * Math.cos(2 * Math.PI);\n"
  );
  cl_assert_equal_i(100, (int32_t)jerry_get_number_value(prv_js_global_get_value("s1")));
  cl_assert_equal_i(99, (int32_t)jerry_get_number_value(prv_js_global_get_value("s2")));
  cl_assert_equal_i(150, (int32_t)jerry_get_number_value(prv_js_global_get_value("c1")));
  cl_assert_equal_i(150, (int32_t)jerry_get_number_value(prv_js_global_get_value("c2")));

  cl_assert_equal_i(100, jerry_get_int32_value(prv_js_global_get_value("s1")));
  cl_assert_equal_i(100, jerry_get_int32_value(prv_js_global_get_value("s2")));
  cl_assert_equal_i(150, jerry_get_int32_value(prv_js_global_get_value("c1")));
  cl_assert_equal_i(150, jerry_get_int32_value(prv_js_global_get_value("c2")));
}

void test_js__date(void) {
  const time_t cur_time = 1458250851; // Thu Mar 17 21:40:51 2016 UTC
                                      // Thu Mar 17 14:40:51 2016 PDT
  const uint16_t cur_millis = 123;
  fake_time_init(cur_time, cur_millis);
  fake_time_set_gmtoff(-8 * 60 * 60); // PST
  fake_time_set_dst(1 * 60 * 60, 1458111600, 1465628400); // PDT 3/16 -> 11/6 2016

  char *script =
    "var date_now = new Date();"
    "var now = date_now.getTime();"
    "var local_day = date_now.getDay();"
    "var local_hour = date_now.getHours();";
  EXECUTE_SCRIPT(script);

  ASSERT_JS_GLOBAL_EQUALS_D("now", (double)cur_time * 1000.0 + (double)cur_millis);
  ASSERT_JS_GLOBAL_EQUALS_D("local_day", 4.0); // Thursday
  ASSERT_JS_GLOBAL_EQUALS_D("local_hour", 14.0); // 1pm
}

void test_js__log_exception(void) {
  char *script =
    "var e1;\n"
    "var f1 = function(){throw new Error('test')};\n"
    "var f2 = function(){throw new 'test';};\n"
    "var f2 = function(){throw new 123;};\n"
    "try {f1();} catch(e) {e1 = e;}\n"
    "try {f2();} catch(e) {e2 = e;}\n"
    "try {f3();} catch(e) {e3 = e;}\n";

  EXECUTE_SCRIPT(script);
  jerry_value_t e1 = prv_js_global_get_value("e1");
  jerry_value_t e2 = prv_js_global_get_value("e2");
  jerry_value_t e3 = prv_js_global_get_value("e3");

  // error
  s_log_internal__expected = (const char *[]){
    "Exception while e1", "Error: test", NULL,
  };
  rocky_log_exception("e1", e1);
  cl_assert(*s_log_internal__expected == NULL);

  // string
  s_log_internal__expected = (const char *[]){
    "Exception while e2", "TypeError", NULL,
  };
  rocky_log_exception("e2", e2);
  cl_assert(*s_log_internal__expected == NULL);

  // number
  s_log_internal__expected = (const char *[]){
    "Exception while e3", "ReferenceError", NULL,
  };
  rocky_log_exception("e3", e3);
  cl_assert(*s_log_internal__expected == NULL);
}

/*
 * FIXME: JS Tests should be built in a 32-bit env
void test_js__size(void) {
  cl_assert_equal_i(4, sizeof(size_t));
}
*/

void test_js__snapshot(void) {
  prv_deinit();
  rocky_runtime_context_init();
  jerry_init(JERRY_INIT_SHOW_OPCODES);
  char *const script = prv_load_js("color");
  uint8_t snapshot[65536] = { 0 };

  // make sure snapshot data starts with expected Rocky header
  const size_t header_size = sizeof(ROCKY_EXPECTED_SNAPSHOT_HEADER);
  cl_assert_equal_i(8, header_size);
  // NOTE: the snapshot header in this unit test is fixed to
  //       CAPABILITY_JAVASCRIPT_BYTECODE_VERSION=1 only use the resulting binary
  //       if the true JS version matches
  memcpy(snapshot, &ROCKY_EXPECTED_SNAPSHOT_HEADER, header_size);
  const size_t snapshot_size = jerry_parse_and_save_snapshot((const jerry_char_t *)script,
                                                             strlen(script),
                                                             true,  /* is_for_global */
                                                             false, /* is_strict */
                                                             snapshot + header_size,
                                                             sizeof(snapshot) - header_size);
  cl_assert(snapshot_size > 512); // make sure it contains "something" and compiling didn't fail

  prv_deinit();

  bool result = rocky_event_loop_with_string_or_snapshot(snapshot, snapshot_size);
  cl_assert(result);
}

static int s_cleanup_calls;
static void prv_cleanup_cb(const uintptr_t native_p) {
  ++s_cleanup_calls;
}

void test_js__js_value_cleanup(void) {
  s_cleanup_calls = 0;

  {
    // Sanity check:
    // we don't clean up when a bare jerry_value_t goes out of scope.
    jerry_value_t value = jerry_create_object();
    jerry_set_object_native_handle(value, 0, prv_cleanup_cb);
  }
  jerry_gc(); // Perform GC in case refcount = 0
  cl_assert_equal_i(s_cleanup_calls, 0); // Never release()d, so wasn't cleaned.

  {
    // When this goes out of scope, we do clean up
    JS_VAR value = jerry_create_object();
    jerry_set_object_native_handle(value, 0, prv_cleanup_cb);
  }
  jerry_gc(); // Perform GC so it will be cleaned up if refcount = 0
  cl_assert_equal_i(s_cleanup_calls, 1); // Make sure that it was cleaned up

  {
    // Create a regular value, attach the native handle
    jerry_value_t value = jerry_create_object();
    jerry_set_object_native_handle(value, 0, prv_cleanup_cb);

    // Create an autoreleased variable that points to the same, it will be cleaned up.
    JS_UNUSED_VAL = value;
  }
  jerry_gc();
  cl_assert_equal_i(s_cleanup_calls, 2);

  {
    // Naming check on unused variables, shouldn't clash.
    // This is really just a compile-time test.
    JS_UNUSED_VAL = jerry_create_object();
    JS_UNUSED_VAL = jerry_create_object();
  }
}

void test_js__get_global_builtin(void) {
  jerry_value_t date_builtin = jerry_get_global_builtin((const jerry_char_t *)"Date");
  cl_assert(!jerry_value_is_undefined(date_builtin));
  cl_assert(jerry_value_is_constructor(date_builtin));
  jerry_release_value(date_builtin);

  jerry_value_t json_builtin = jerry_get_global_builtin((const jerry_char_t *)"JSON");
  cl_assert(jerry_value_is_object(json_builtin));
  jerry_release_value(json_builtin);

  jerry_value_t not_builtin = jerry_get_global_builtin((const jerry_char_t *)"_not_builtin_");
  cl_assert(jerry_value_is_undefined(not_builtin));
}

void test_js__get_global_builtin_compare(void) {
  jerry_value_t date_builtin = jerry_get_global_builtin((const jerry_char_t *)"Date");
  jerry_value_t global_object = jerry_get_global_object();

  // Compare that the global Date is the same object as the builtin
  jerry_value_t global_date = jerry_get_object_field(global_object, "Date");
  cl_assert(date_builtin == global_date);

  jerry_release_value(global_date);
  jerry_release_value(global_object);
  jerry_release_value(date_builtin);
}

void test_js__get_global_builtin_changed(void) {
  jerry_value_t date_builtin = jerry_get_global_builtin((const jerry_char_t *)"Date");
  jerry_value_t global_object = jerry_get_global_object();

  const char *source = "Date = 'some string';";
  jerry_eval((const jerry_char_t *)source, strlen(source), false);

  // After changing the global date object, it should not match our builtin
  jerry_value_t global_date = jerry_get_object_field(global_object, "Date");
  cl_assert(jerry_value_is_string(global_date));
  cl_assert(date_builtin != global_date);

  jerry_release_value(global_date);
  jerry_release_value(global_object);
  jerry_release_value(date_builtin);
}

void test_js__capture_mem_stats_upon_exiting_event_loop(void) {
  prv_deinit();

  s_app_event_loop_callback = NULL;
  const char *source = ";";
  cl_assert_equal_b(true, rocky_event_loop_with_string_or_snapshot(source, strlen(source)));
  cl_assert_equal_i(s_app_heap_analytics_log_stats_to_app_heartbeat_call_count, 1);
}

void test_js__jmem_heap_stats_largest_free_block_bytes(void) {
  jmem_heap_stats_t stats = {};
  jmem_heap_get_stats(&stats);
  // Note: this might fail in the future if JerryScript would happen to cause fragmentation right
  // upon initializing the engine:
  cl_assert_equal_i(stats.size - stats.allocated_bytes, stats.largest_free_block_bytes);
}

void test_js__capture_jerry_heap_oom_stats(void) {
  const char *source = "var big = []; for (;;) { big += 'bigger'; };";
  cl_assert_passert(jerry_eval((const jerry_char_t *)source, strlen(source), false));
  cl_assert_equal_i(s_app_heap_analytics_log_rocky_heap_oom_fault_call_count, 1);
}