/* 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.
 */

#include <string.h>

#include "jerryscript.h"

#include "test-common.h"

static void
compare_specifier (jerry_value_t specifier, /* string value */
                   int id) /* module id */
{
  jerry_char_t string[] = "XX_module.mjs";

  TEST_ASSERT (id >= 1 && id <= 99 && string[0] == 'X' && string[1] == 'X');

  string[0] = (jerry_char_t) ((id / 10) + '0');
  string[1] = (jerry_char_t) ((id % 10) + '0');

  jerry_size_t length = (jerry_size_t) (sizeof (string) - 1);
  jerry_char_t buffer[sizeof (string) - 1];

  TEST_ASSERT (jerry_value_is_string (specifier));
  TEST_ASSERT (jerry_get_string_size (specifier) == length);

  TEST_ASSERT (jerry_string_to_char_buffer (specifier, buffer, length) == length);
  TEST_ASSERT (memcmp (buffer, string, length) == 0);
} /* compare_specifier */

static void
compare_property (jerry_value_t namespace_object, /**< namespace object */
                  const char *name_p, /**< property name */
                  double expected_value) /**< property value (number for simplicity) */
{
  jerry_value_t name = jerry_create_string ((const jerry_char_t *) name_p);
  jerry_value_t result = jerry_get_property (namespace_object, name);

  TEST_ASSERT (jerry_value_is_number (result));
  TEST_ASSERT (jerry_get_number_value (result) == expected_value);

  jerry_release_value (result);
  jerry_release_value (name);
} /* compare_property */

static jerry_value_t
create_module (int id) /**< module id */
{
  jerry_parse_options_t module_parse_options;
  module_parse_options.options = JERRY_PARSE_MODULE;

  jerry_value_t result;

  if (id == 0)
  {
    jerry_char_t source[] = "export var a = 7";

    result = jerry_parse (source, sizeof (source) - 1, &module_parse_options);
  }
  else
  {
    jerry_char_t source[] = "export {a} from 'XX_module.mjs'";

    TEST_ASSERT (id >= 1 && id <= 99 && source[17] == 'X' && source[18] == 'X');

    source[17] = (jerry_char_t) ((id / 10) + '0');
    source[18] = (jerry_char_t) ((id % 10) + '0');

    result = jerry_parse (source, sizeof (source) - 1, &module_parse_options);
  }

  TEST_ASSERT (!jerry_value_is_error (result));
  return result;
} /* create_module */

static int counter = 0;
static jerry_value_t module;

static jerry_value_t
resolve_callback1 (const jerry_value_t specifier, /**< module specifier */
                   const jerry_value_t referrer, /**< parent module */
                   void *user_p) /**< user data */
{
  TEST_ASSERT (user_p == (void *) &module);
  TEST_ASSERT (referrer == module);
  compare_specifier (specifier, 1);

  counter++;
  return counter == 1 ? jerry_create_number (7) : jerry_create_object ();
} /* resolve_callback1 */

static jerry_value_t prev_module;
static bool terminate_with_error;

static jerry_value_t
resolve_callback2 (const jerry_value_t specifier, /**< module specifier */
                   const jerry_value_t referrer, /**< parent module */
                   void *user_p) /**< user data */
{
  TEST_ASSERT (prev_module == referrer);
  TEST_ASSERT (user_p == NULL);

  compare_specifier (specifier, ++counter);

  if (counter >= 32)
  {
    if (terminate_with_error)
    {
      return jerry_create_error (JERRY_ERROR_RANGE, (const jerry_char_t *) "Module not found");
    }

    return create_module (0);
  }

  prev_module = create_module (counter + 1);
  return prev_module;
} /* resolve_callback2 */

static jerry_value_t
resolve_callback3 (const jerry_value_t specifier, /**< module specifier */
                   const jerry_value_t referrer, /**< parent module */
                   void *user_p) /**< user data */
{
  (void) specifier;
  (void) referrer;
  (void) user_p;

  TEST_ASSERT (false);
} /* resolve_callback3 */

static jerry_value_t
native_module_evaluate (const jerry_value_t native_module) /**< native module */
{
  ++counter;

  TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_EVALUATING);

  jerry_value_t exp_val = jerry_create_string ((const jerry_char_t *) "exp");
  jerry_value_t other_exp_val = jerry_create_string ((const jerry_char_t *) "other_exp");
  /* The native module has no such export. */
  jerry_value_t no_exp_val = jerry_create_string ((const jerry_char_t *) "no_exp");

  jerry_value_t result = jerry_native_module_get_export (native_module, exp_val);
  TEST_ASSERT (jerry_value_is_undefined (result));
  jerry_release_value (result);

  result = jerry_native_module_get_export (native_module, other_exp_val);
  TEST_ASSERT (jerry_value_is_undefined (result));
  jerry_release_value (result);

  result = jerry_native_module_get_export (native_module, no_exp_val);
  TEST_ASSERT (jerry_value_is_error (result));
  TEST_ASSERT (jerry_get_error_type (result) == JERRY_ERROR_REFERENCE);
  jerry_release_value (result);

  jerry_value_t export = jerry_create_number (3.5);
  result = jerry_native_module_set_export (native_module, exp_val, export);
  TEST_ASSERT (jerry_value_is_boolean (result) && jerry_value_is_true (result));
  jerry_release_value (result);
  jerry_release_value (export);

  export = jerry_create_string ((const jerry_char_t *) "str");
  result = jerry_native_module_set_export (native_module, other_exp_val, export);
  TEST_ASSERT (jerry_value_is_boolean (result) && jerry_value_is_true (result));
  jerry_release_value (result);
  jerry_release_value (export);

  result = jerry_native_module_set_export (native_module, no_exp_val, no_exp_val);
  TEST_ASSERT (jerry_value_is_error (result));
  TEST_ASSERT (jerry_get_error_type (result) == JERRY_ERROR_REFERENCE);
  jerry_release_value (result);

  result = jerry_native_module_get_export (native_module, exp_val);
  TEST_ASSERT (jerry_value_is_number (result) && jerry_get_number_value (result) == 3.5);
  jerry_release_value (result);

  result = jerry_native_module_get_export (native_module, other_exp_val);
  TEST_ASSERT (jerry_value_is_string (result));
  jerry_release_value (result);

  jerry_release_value (exp_val);
  jerry_release_value (other_exp_val);
  jerry_release_value (no_exp_val);

  if (counter == 4)
  {
    ++counter;
    return jerry_create_error (JERRY_ERROR_COMMON, (const jerry_char_t *) "Ooops!");
  }

  return jerry_create_undefined ();
} /* native_module_evaluate */

static jerry_value_t
resolve_callback4 (const jerry_value_t specifier, /**< module specifier */
                   const jerry_value_t referrer, /**< parent module */
                   void *user_p) /**< user data */
{
  (void) specifier;
  (void) referrer;

  ++counter;

  jerry_value_t exports[2] = { jerry_create_string ((const jerry_char_t *) "exp"),
                               jerry_create_string ((const jerry_char_t *) "other_exp") };

  jerry_value_t native_module = jerry_native_module_create (native_module_evaluate, exports, 2);
  TEST_ASSERT (!jerry_value_is_error (native_module));

  jerry_release_value (exports[0]);
  jerry_release_value (exports[1]);

  *((jerry_value_t *) user_p) = jerry_acquire_value (native_module);
  return native_module;
} /* resolve_callback4 */

static void
module_state_changed (jerry_module_state_t new_state, /**< new state of the module */
                      const jerry_value_t module_val, /**< a module whose state is changed */
                      const jerry_value_t value, /**< value argument */
                      void *user_p) /**< user pointer */
{
  TEST_ASSERT (jerry_module_get_state (module_val) == new_state);
  TEST_ASSERT (module_val == module);
  TEST_ASSERT (user_p == (void *) &counter);

  ++counter;

  switch (counter)
  {
    case 1:
    case 3:
    {
      TEST_ASSERT (new_state == JERRY_MODULE_STATE_LINKED);
      TEST_ASSERT (jerry_value_is_undefined (value));
      break;
    }
    case 2:
    {
      TEST_ASSERT (new_state == JERRY_MODULE_STATE_EVALUATED);
      TEST_ASSERT (jerry_value_is_number (value) && jerry_get_number_value (value) == 33.5);
      break;
    }
    default:
    {
      TEST_ASSERT (counter == 4);
      TEST_ASSERT (new_state == JERRY_MODULE_STATE_ERROR);
      TEST_ASSERT (jerry_value_is_number (value) && jerry_get_number_value (value) == -5.5);
      break;
    }
  }
} /* module_state_changed */

static jerry_value_t
resolve_callback5 (const jerry_value_t specifier, /**< module specifier */
                   const jerry_value_t referrer, /**< parent module */
                   void *user_p) /**< user data */
{
  (void) specifier;
  (void) user_p;

  /* This circular reference is valid. However, import resolving triggers
   * a SyntaxError, because the module does not export a default binding. */
  return referrer;
} /* resolve_callback5 */

int
main (void)
{
  jerry_init (JERRY_INIT_EMPTY);

  if (!jerry_is_feature_enabled (JERRY_FEATURE_MODULE))
  {
    jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Module is disabled!\n");
    jerry_cleanup ();
    return 0;
  }

  jerry_value_t number = jerry_create_number (5);
  jerry_value_t object = jerry_create_object ();

  jerry_value_t result = jerry_module_link (number, resolve_callback1, NULL);
  TEST_ASSERT (jerry_value_is_error (result));
  jerry_release_value (result);

  result = jerry_module_link (object, resolve_callback1, NULL);
  TEST_ASSERT (jerry_value_is_error (result));
  jerry_release_value (result);

  module = create_module (1);

  /* After an error, module must remain in unlinked mode. */
  result = jerry_module_link (module, resolve_callback1, (void *) &module);
  TEST_ASSERT (jerry_value_is_error (result));
  TEST_ASSERT (counter == 1);
  jerry_release_value (result);

  result = jerry_module_link (module, resolve_callback1, (void *) &module);
  TEST_ASSERT (jerry_value_is_error (result));
  TEST_ASSERT (counter == 2);
  jerry_release_value (result);

  prev_module = module;
  counter = 0;
  terminate_with_error = true;
  result = jerry_module_link (module, resolve_callback2, NULL);
  TEST_ASSERT (jerry_value_is_error (result));
  TEST_ASSERT (counter == 32);
  jerry_release_value (result);

  /* The successfully resolved modules is kept around in unlinked state. */
  jerry_gc (JERRY_GC_PRESSURE_HIGH);

  counter = 31;
  terminate_with_error = false;
  result = jerry_module_link (module, resolve_callback2, NULL);
  TEST_ASSERT (jerry_value_is_boolean (result) && jerry_value_is_true (result));
  TEST_ASSERT (counter == 32);
  jerry_release_value (result);

  TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_LINKED);
  TEST_ASSERT (jerry_module_get_number_of_requests (module) == 1);
  result = jerry_module_get_request (module, 0);
  TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_LINKED);
  jerry_release_value (result);

  jerry_release_value (module);

  module = create_module (1);

  prev_module = module;
  counter = 0;
  terminate_with_error = false;
  result = jerry_module_link (module, resolve_callback2, NULL);
  TEST_ASSERT (jerry_value_is_boolean (result) && jerry_value_is_true (result));
  TEST_ASSERT (counter == 32);
  jerry_release_value (result);
  jerry_release_value (module);

  TEST_ASSERT (jerry_module_get_state (number) == JERRY_MODULE_STATE_INVALID);

  jerry_parse_options_t module_parse_options;
  module_parse_options.options = JERRY_PARSE_MODULE;

  jerry_char_t source1[] = TEST_STRING_LITERAL ("import a from '16_module.mjs'\n"
                                                "export * from '07_module.mjs'\n"
                                                "export * from '44_module.mjs'\n"
                                                "import * as b from '36_module.mjs'\n");
  module = jerry_parse (source1, sizeof (source1) - 1, &module_parse_options);
  TEST_ASSERT (!jerry_value_is_error (module));
  TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_UNLINKED);

  TEST_ASSERT (jerry_module_get_number_of_requests (number) == 0);
  TEST_ASSERT (jerry_module_get_number_of_requests (module) == 4);

  result = jerry_module_get_request (object, 0);
  TEST_ASSERT (jerry_value_is_error (result));
  jerry_release_value (result);

  result = jerry_module_get_request (module, 0);
  compare_specifier (result, 16);
  jerry_release_value (result);

  result = jerry_module_get_request (module, 1);
  compare_specifier (result, 7);
  jerry_release_value (result);

  result = jerry_module_get_request (module, 2);
  compare_specifier (result, 44);
  jerry_release_value (result);

  result = jerry_module_get_request (module, 3);
  compare_specifier (result, 36);
  jerry_release_value (result);

  result = jerry_module_get_request (module, 4);
  TEST_ASSERT (jerry_value_is_error (result));
  jerry_release_value (result);

  jerry_release_value (module);

  result = jerry_module_get_namespace (number);
  TEST_ASSERT (jerry_value_is_error (result));
  jerry_release_value (result);

  jerry_char_t source2[] = TEST_STRING_LITERAL ("export let a = 6\n"
                                                "export let b = 8.5\n");
  module = jerry_parse (source2, sizeof (source2) - 1, &module_parse_options);
  TEST_ASSERT (!jerry_value_is_error (module));
  TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_UNLINKED);

  result = jerry_module_link (module, resolve_callback3, NULL);
  TEST_ASSERT (!jerry_value_is_error (result));
  jerry_release_value (result);

  TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_LINKED);

  result = jerry_module_evaluate (module);
  TEST_ASSERT (!jerry_value_is_error (result));
  jerry_release_value (result);

  TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_EVALUATED);

  result = jerry_module_get_namespace (module);
  TEST_ASSERT (jerry_value_is_object (result));
  compare_property (result, "a", 6);
  compare_property (result, "b", 8.5);
  jerry_release_value (result);

  jerry_release_value (module);

  module = jerry_native_module_create (NULL, &object, 1);
  TEST_ASSERT (jerry_value_is_error (module));
  jerry_release_value (module);

  module = jerry_native_module_create (NULL, NULL, 0);
  TEST_ASSERT (!jerry_value_is_error (module));
  TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_UNLINKED);

  result = jerry_native_module_get_export (object, number);
  TEST_ASSERT (jerry_value_is_error (result));
  jerry_release_value (result);

  result = jerry_native_module_set_export (module, number, number);
  TEST_ASSERT (jerry_value_is_error (result));
  jerry_release_value (result);

  jerry_release_value (module);

  /* Valid identifier. */
  jerry_value_t export = jerry_create_string ((const jerry_char_t *) "\xed\xa0\x83\xed\xb2\x80");

  module = jerry_native_module_create (NULL, &export, 1);
  TEST_ASSERT (!jerry_value_is_error (module));
  TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_UNLINKED);

  result = jerry_module_link (module, NULL, NULL);
  TEST_ASSERT (jerry_value_is_boolean (result) && jerry_value_is_true (result));
  jerry_release_value (result);

  result = jerry_module_evaluate (module);
  TEST_ASSERT (jerry_value_is_undefined (result));
  jerry_release_value (result);

  jerry_release_value (module);
  jerry_release_value (export);

  /* Invalid identifiers. */
  export = jerry_create_string ((const jerry_char_t *) "a+");
  module = jerry_native_module_create (NULL, &export, 1);
  TEST_ASSERT (jerry_value_is_error (module));
  jerry_release_value (module);
  jerry_release_value (export);

  export = jerry_create_string ((const jerry_char_t *) "\xed\xa0\x80");
  module = jerry_native_module_create (NULL, &export, 1);
  TEST_ASSERT (jerry_value_is_error (module));
  jerry_release_value (module);
  jerry_release_value (export);

  counter = 0;

  for (int i = 0; i < 2; i++)
  {
    jerry_char_t source3[] = TEST_STRING_LITERAL (
      "import {exp, other_exp as other} from 'native.js'\n"
      "import * as namespace from 'native.js'\n"
      "if (exp !== 3.5 || other !== 'str') { throw 'Assertion failed!' }\n"
      "if (namespace.exp !== 3.5 || namespace.other_exp !== 'str') { throw 'Assertion failed!' }\n");
    module = jerry_parse (source3, sizeof (source3) - 1, &module_parse_options);
    TEST_ASSERT (!jerry_value_is_error (module));
    TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_UNLINKED);

    jerry_value_t native_module;

    result = jerry_module_link (module, resolve_callback4, (void *) &native_module);
    TEST_ASSERT (!jerry_value_is_error (result));
    jerry_release_value (result);

    TEST_ASSERT (counter == i * 2 + 1);
    TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_LINKED);
    TEST_ASSERT (jerry_module_get_state (native_module) == JERRY_MODULE_STATE_LINKED);

    result = jerry_module_evaluate (module);

    if (i == 0)
    {
      TEST_ASSERT (!jerry_value_is_error (result));
      TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_EVALUATED);
      TEST_ASSERT (jerry_module_get_state (native_module) == JERRY_MODULE_STATE_EVALUATED);
      TEST_ASSERT (counter == 2);
    }
    else
    {
      TEST_ASSERT (jerry_value_is_error (result));
      TEST_ASSERT (jerry_module_get_state (module) == JERRY_MODULE_STATE_ERROR);
      TEST_ASSERT (jerry_module_get_state (native_module) == JERRY_MODULE_STATE_ERROR);
      TEST_ASSERT (counter == 5);
    }

    jerry_release_value (result);
    jerry_release_value (module);
    jerry_release_value (native_module);
  }

  jerry_release_value (object);
  jerry_release_value (number);

  counter = 0;
  jerry_module_set_state_changed_callback (module_state_changed, (void *) &counter);

  jerry_char_t source4[] = TEST_STRING_LITERAL ("33.5\n");
  module = jerry_parse (source4, sizeof (source4) - 1, &module_parse_options);

  result = jerry_module_link (module, NULL, NULL);
  TEST_ASSERT (!jerry_value_is_error (result));
  jerry_release_value (result);

  result = jerry_module_evaluate (module);
  TEST_ASSERT (!jerry_value_is_error (result));
  jerry_release_value (result);

  jerry_release_value (module);

  jerry_char_t source5[] = TEST_STRING_LITERAL ("throw -5.5\n");
  module = jerry_parse (source5, sizeof (source5) - 1, &module_parse_options);

  result = jerry_module_link (module, NULL, NULL);
  TEST_ASSERT (!jerry_value_is_error (result));
  jerry_release_value (result);

  result = jerry_module_evaluate (module);
  TEST_ASSERT (jerry_value_is_error (result));
  jerry_release_value (result);

  jerry_release_value (module);

  jerry_module_set_state_changed_callback (NULL, NULL);

  TEST_ASSERT (counter == 4);

  jerry_char_t source6[] = TEST_STRING_LITERAL ("import a from 'self'\n");
  module = jerry_parse (source6, sizeof (source6) - 1, &module_parse_options);

  result = jerry_module_link (module, resolve_callback5, NULL);
  TEST_ASSERT (jerry_value_is_error (result) && jerry_get_error_type (result) == JERRY_ERROR_SYNTAX);
  jerry_release_value (result);

  jerry_cleanup ();

  return 0;
} /* main */
