/* Copyright JS Foundation and other contributors, http://js.foundation
 *
 * 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.
 */

/**
 * Unit test for jerry-ext/handler property registration
 */

#include "jerryscript.h"
#include "jerryscript-ext/handler.h"
#include "test-common.h"

#include <string.h>

static jerry_value_t
method_hello (const jerry_value_t jfunc,  /**< function object */
              const jerry_value_t jthis,  /**< function this */
              const jerry_value_t jargv[], /**< arguments */
              const jerry_length_t jargc) /**< number of arguments */
{
  (void) jfunc;
  (void) jthis;
  (void) jargv;
  return jerry_create_number (jargc);
} /* method_hello */

/**
 * Helper method to create a non-configurable property on an object
 */
static void
freeze_property (jerry_value_t target_obj, /**< target object */
                 const char *target_prop) /**< target property name */
{
  // "freeze" property
  jerry_property_descriptor_t prop_desc;
  jerry_init_property_descriptor_fields (&prop_desc);
  prop_desc.is_configurable_defined = true;
  prop_desc.is_configurable = false;

  jerry_value_t prop_name = jerry_create_string ((const jerry_char_t *) target_prop);
  jerry_value_t return_value = jerry_define_own_property (target_obj, prop_name, &prop_desc);
  TEST_ASSERT (jerry_value_is_boolean (return_value));
  jerry_release_value (return_value);
  jerry_release_value (prop_name);

  jerry_free_property_descriptor_fields (&prop_desc);
} /* freeze_property */

/**
 * Test registration of various property values.
 */
static void
test_simple_registration (void)
{
  jerry_init (JERRY_INIT_EMPTY);

  jerry_value_t target_object = jerry_create_object ();

  // Test simple registration
  jerryx_property_entry methods[] =
  {
    JERRYX_PROPERTY_FUNCTION ("hello", method_hello),
    JERRYX_PROPERTY_NUMBER ("my_number", 42.5),
    JERRYX_PROPERTY_STRING ("my_str", "super_str"),
    JERRYX_PROPERTY_STRING_SZ ("my_str_sz", "super_str", 6),
    JERRYX_PROPERTY_BOOLEAN ("my_bool", true),
    JERRYX_PROPERTY_BOOLEAN ("my_bool_false", false),
    JERRYX_PROPERTY_UNDEFINED ("my_non_value"),
    JERRYX_PROPERTY_LIST_END (),
  };

  jerryx_register_result register_result = jerryx_set_properties (target_object, methods);

  TEST_ASSERT (register_result.registered == 7);
  TEST_ASSERT (jerry_value_is_undefined (register_result.result));

  jerryx_release_property_entry (methods, register_result);
  jerry_release_value (register_result.result);

  jerry_value_t global_obj = jerry_get_global_object ();
  jerryx_set_property_str (global_obj, "test", target_object);
  jerry_release_value (target_object);
  jerry_release_value (global_obj);

  {
    const char *test_A = "test.my_number";
    jerry_value_t result = jerry_eval ((const jerry_char_t *) test_A, strlen (test_A), 0);
    TEST_ASSERT (jerry_value_is_number (result));
    TEST_ASSERT (jerry_get_number_value (result) == 42.5);
    jerry_release_value (result);
  }

  {
    const char *test_A = "test.my_str_sz === 'super_'";
    jerry_value_t result = jerry_eval ((const jerry_char_t *) test_A, strlen (test_A), 0);
    TEST_ASSERT (jerry_value_is_boolean (result));
    TEST_ASSERT (jerry_get_boolean_value (result) == true);
    jerry_release_value (result);
  }

  {
    const char *test_A = "test.my_str === 'super_str'";
    jerry_value_t result = jerry_eval ((const jerry_char_t *) test_A, strlen (test_A), 0);
    TEST_ASSERT (jerry_value_is_boolean (result));
    TEST_ASSERT (jerry_get_boolean_value (result) == true);
    jerry_release_value (result);
  }

  {
    const char *test_A = "test.my_bool";
    jerry_value_t result = jerry_eval ((const jerry_char_t *) test_A, strlen (test_A), 0);
    TEST_ASSERT (jerry_value_is_boolean (result));
    TEST_ASSERT (jerry_get_boolean_value (result) == true);
    jerry_release_value (result);
  }

  {
    const char *test_A = "test.my_bool_false";
    jerry_value_t result = jerry_eval ((const jerry_char_t *) test_A, strlen (test_A), 0);
    TEST_ASSERT (jerry_value_is_boolean (result));
    TEST_ASSERT (jerry_get_boolean_value (result) == false);
    jerry_release_value (result);
  }

  {
    const char *test_A = "test.my_non_value";
    jerry_value_t result = jerry_eval ((const jerry_char_t *) test_A, strlen (test_A), 0);
    TEST_ASSERT (jerry_value_is_undefined (result));
    jerry_release_value (result);
  }

  {
    const char *test_A = "test.hello(33, 42, 2);";
    jerry_value_t result = jerry_eval ((const jerry_char_t *) test_A, strlen (test_A), 0);
    TEST_ASSERT (jerry_value_is_number (result));
    TEST_ASSERT ((uint32_t) jerry_get_number_value (result) == 3u);
    jerry_release_value (result);
  }

  {
    const char *test_A = "test.hello();";
    jerry_value_t result = jerry_eval ((const jerry_char_t *) test_A, strlen (test_A), 0);
    TEST_ASSERT (jerry_value_is_number (result));
    TEST_ASSERT ((uint32_t) jerry_get_number_value (result) == 0u);
    jerry_release_value (result);
  }

  jerry_cleanup ();
} /* test_simple_registration */

/**
 * Test registration error.
 *
 * Trying to register a property which is already a non-configurable property
 * should result in an error.
 */
static void
test_error_setvalue (void)
{
  jerry_init (JERRY_INIT_EMPTY);

  const char *target_prop = "test_err";
  jerry_value_t global_obj = jerry_get_global_object ();
  freeze_property (global_obj, target_prop);

  jerry_value_t new_object = jerry_create_object ();
  jerry_value_t set_result = jerryx_set_property_str (global_obj, target_prop, new_object);
  TEST_ASSERT (jerry_value_is_error (set_result));

  jerry_release_value (set_result);
  jerry_release_value (new_object);
  jerry_release_value (global_obj);

  jerry_cleanup ();
} /* test_error_setvalue */


/**
 * Test registration error with jerryx_set_properties.
 *
 * Trying to register a property which is already a non-configurable property
 * should result in an error.
 */
static void
test_error_single_function (void)
{
  jerry_init (JERRY_INIT_EMPTY);

  const char *target_prop = "test_err";
  jerry_value_t target_object = jerry_create_object ();
  freeze_property (target_object, target_prop);

  jerryx_property_entry methods[] =
  {
    JERRYX_PROPERTY_FUNCTION (target_prop, method_hello), // This registration should fail
    JERRYX_PROPERTY_LIST_END (),
  };

  jerryx_register_result register_result = jerryx_set_properties (target_object, methods);

  TEST_ASSERT (register_result.registered == 0);
  TEST_ASSERT (jerry_value_is_error (register_result.result));
  jerryx_release_property_entry (methods, register_result);
  jerry_release_value (register_result.result);

  jerry_release_value (target_object);

  jerry_cleanup ();
} /* test_error_single_function */

/**
 * Test to see if jerryx_set_properties exits at the first error.
 */
static void
test_error_multiple_functions (void)
{
  jerry_init (JERRY_INIT_EMPTY);

  const char *prop_ok = "prop_ok";
  const char *prop_err = "prop_err";
  const char *prop_not = "prop_not";
  jerry_value_t target_object = jerry_create_object ();
  freeze_property (target_object, prop_err);

  jerryx_property_entry methods[] =
  {
    JERRYX_PROPERTY_FUNCTION (prop_ok, method_hello), // This registration is ok
    JERRYX_PROPERTY_FUNCTION (prop_err, method_hello), // This registration should fail
    JERRYX_PROPERTY_FUNCTION (prop_not, method_hello), // This registration is not done
    JERRYX_PROPERTY_LIST_END (),
  };

  jerryx_register_result register_result = jerryx_set_properties (target_object, methods);

  TEST_ASSERT (register_result.registered == 1);
  TEST_ASSERT (jerry_value_is_error (register_result.result));

  jerryx_release_property_entry (methods, register_result);
  jerry_release_value (register_result.result);

  {
    // Test if property "prop_ok" is correctly registered.
    jerry_value_t prop_ok_val = jerry_create_string ((const jerry_char_t *) prop_ok);
    jerry_value_t prop_ok_exists = jerry_has_own_property (target_object, prop_ok_val);
    TEST_ASSERT (jerry_get_boolean_value (prop_ok_exists) == true);
    jerry_release_value (prop_ok_exists);

    // Try calling the method
    jerry_value_t prop_ok_func = jerry_get_property (target_object, prop_ok_val);
    TEST_ASSERT (jerry_value_is_function (prop_ok_func) == true);
    jerry_value_t args[2] =
    {
      jerry_create_number (22),
      jerry_create_number (-3),
    };
    jerry_size_t args_cnt = sizeof (args) / sizeof (jerry_value_t);
    jerry_value_t func_result = jerry_call_function (prop_ok_func,
                                                     jerry_create_undefined (),
                                                     args,
                                                     args_cnt);
    TEST_ASSERT (jerry_value_is_number (func_result) == true);
    TEST_ASSERT ((uint32_t) jerry_get_number_value (func_result) == 2u);
    jerry_release_value (func_result);
    for (jerry_size_t idx = 0; idx < args_cnt; idx++)
    {
      jerry_release_value (args[idx]);
    }
    jerry_release_value (prop_ok_func);
    jerry_release_value (prop_ok_val);
  }

  {
    // The "prop_err" should exist - as it was "freezed" - but it should not be a function
    jerry_value_t prop_err_val = jerry_create_string ((const jerry_char_t *) prop_err);
    jerry_value_t prop_err_exists = jerry_has_own_property (target_object, prop_err_val);
    TEST_ASSERT (jerry_get_boolean_value (prop_err_exists) == true);
    jerry_release_value (prop_err_exists);

    jerry_value_t prop_err_func = jerry_value_is_function (prop_err_val);
    TEST_ASSERT (jerry_value_is_function (prop_err_func) == false);
    jerry_release_value (prop_err_val);
  }

  { // The "prop_not" is not available on the target object
    jerry_value_t prop_not_val = jerry_create_string ((const jerry_char_t *) prop_not);
    jerry_value_t prop_not_exists = jerry_has_own_property (target_object, prop_not_val);
    TEST_ASSERT (jerry_get_boolean_value (prop_not_exists) == false);
    jerry_release_value (prop_not_exists);
    jerry_release_value (prop_not_val);
  }

  jerry_release_value (target_object);

  jerry_cleanup ();
} /* test_error_multiple_functions */

int
main (void)
{
  test_simple_registration ();
  test_error_setvalue ();
  test_error_single_function ();
  test_error_multiple_functions ();
  return 0;
} /* main */
