// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

#include <cstdio>
#include <cstdlib>
#include <cstring>

// TODO(dartbug.com/40579): This requires static linking to either link
// dart.exe or dartaotruntime.exe on Windows.
// The sample currently fails on Windows in AOT mode.
#include "include/dart_api.h"
#include "include/dart_native_api.h"

static bool is_dartaotruntime = true;

bool IsTreeShaken(const char* name, Dart_Handle handle, const char* error) {
  // No tree shaking in the JIT runtime.
  if (!is_dartaotruntime) return false;
  if (Dart_IsApiError(handle)) {
    // All tree-shaking related API errors should include the expected name.
    if (strstr(error, name) == nullptr) return false;
    // Node was tree shaken (e.g., 'Class C not found in library...').
    if (strstr(error, " not found in ") != nullptr) return true;
    // Constructor was tree shaken.
    if (strstr(error, "Dart_New: could not find ") != nullptr) return true;
  } else if (Dart_IsUnhandledExceptionError(handle)) {
    // TFA replaces operations with a throw in some cases. If obfuscation
    // is turned on and the member has no entry point annotation, then its
    // name may be obfuscated in the result and so cannot be depended on.
    if (strstr(error, "Attempt to execute code removed by ") != nullptr) {
      return true;
    }
    // All other tree-shaking related unhandled exceptions are NSM errors
    // that should include the expected name.
    if (strstr(error, name) == nullptr) return false;
    if (strstr(error, "NoSuchMethodError: ") == nullptr) {
      return false;
    }
    if (strstr(error, "No top-level method '") != nullptr) return true;
    if (strstr(error, "No top-level getter '") != nullptr) return true;
    if (strstr(error, "No top-level setter '") != nullptr) return true;
    if (strstr(error, "No static method '") != nullptr) return true;
    if (strstr(error, "No static getter '") != nullptr) return true;
    if (strstr(error, "No static setter '") != nullptr) return true;
    if (strstr(error, "' has no instance method '") != nullptr) return true;
    if (strstr(error, "' has no instance getter '") != nullptr) return true;
    if (strstr(error, "' has no instance setter '") != nullptr) return true;
  }
  // Not an tree shaking-related error.
  return false;
}

#define FATAL(fmt, ...)                                                        \
  do {                                                                         \
    fprintf(stderr, "Failed at %s:%d: " fmt "!\n", __FILE__, __LINE__,         \
            ##__VA_ARGS__);                                                    \
    abort();                                                                   \
  } while (false)

#define CHECK(H)                                                               \
  do {                                                                         \
    fprintf(stderr, "Checking %s...\n", #H);                                   \
    Dart_Handle __handle__ = H;                                                \
    if (Dart_IsError(__handle__)) {                                            \
      const char* message = Dart_GetError(__handle__);                         \
      FATAL("\n%s", message);                                                  \
    } else {                                                                   \
      fprintf(stderr, "  Check passed.\n\n");                                  \
    }                                                                          \
  } while (false)

#define ASSERT_SUBSTRING(needle, haystack)                                     \
  do {                                                                         \
    if (strstr(haystack, needle) == nullptr) {                                 \
      FATAL("expected '%s' within:\n%s\n", needle, haystack);                  \
    }                                                                          \
  } while (false)

#define FAIL(name, result)                                                     \
  do {                                                                         \
    fprintf(stderr, "Expect failure to access '%s'\n", name);                  \
    if (!Dart_IsError(result)) {                                               \
      FATAL("No error for accessing %s", name);                                \
    }                                                                          \
    const char* error = Dart_GetError(result);                                 \
    if (IsTreeShaken(name, result, error)) {                                   \
      fprintf(stderr, "  Received error due to tree shaking: %s\n\n", error);  \
    } else if (!Dart_IsApiError(result)) {                                     \
      FATAL("Not an API error for accessing %s: %s", name, error);             \
    } else {                                                                   \
      ASSERT_SUBSTRING(name, error);                                           \
      ASSERT_SUBSTRING("ERROR: ", error);                                      \
      ASSERT_SUBSTRING("' from native code, it must be annotated.", error);    \
      fprintf(stderr, "  Received expected error: %s\n\n", error);             \
    }                                                                          \
  } while (false)

// Some invalid accesses are allowed in AOT since we don't retain @pragma
// annotations. Only use this if there's no other way to detect missing
// annotations, e.g., a function with no code signals that the precompiler
// did not preserve the code object because the function is not annotated
// as a "call" entry point.
#define FAIL_UNLESS_PRECOMPILED(name, result)                                  \
  do {                                                                         \
    if (!is_dartaotruntime) {                                                  \
      FAIL(name, result);                                                      \
    } else {                                                                   \
      CHECK(result);                                                           \
    }                                                                          \
  } while (false);

#define FAIL_CLOSURIZE_CONSTRUCTOR(name, result)                               \
  do {                                                                         \
    fprintf(stderr, "Expect failure to closurize constructor '%s'\n", name);   \
    if (!Dart_IsError(result)) {                                               \
      FATAL("No error for closurizing %s", name);                              \
    }                                                                          \
    const char* error = Dart_GetError(result);                                 \
    if (!Dart_IsUnhandledExceptionError(result)) {                             \
      FATAL("Not an unhandled exception error for closurizing %s: %s", name,   \
            error);                                                            \
    } else {                                                                   \
      ASSERT_SUBSTRING(name, error);                                           \
      ASSERT_SUBSTRING("No static getter", error);                             \
      fprintf(stderr, "  Received expected error: %s\n\n", error);             \
    }                                                                          \
  } while (false)

#define TEST_FIELDS(target)                                                    \
  do {                                                                         \
    /* Since the fields start off initialized to a non-null value and then */  \
    /* are updated with a null value, check invocation prior to set.       */  \
    FAIL("fld0", Dart_GetField(target, Dart_NewStringFromCString("fld0")));    \
    FAIL("fld0",                                                               \
         Dart_Invoke(target, Dart_NewStringFromCString("fld0"), 0, nullptr));  \
    FAIL("fld0", Dart_SetField(target, Dart_NewStringFromCString("fld0"),      \
                               Dart_Null()));                                  \
    CHECK(Dart_GetField(target, Dart_NewStringFromCString("fld1")));           \
    CHECK(Dart_Invoke(target, Dart_NewStringFromCString("fld1"), 0, nullptr)); \
    CHECK(Dart_SetField(target, Dart_NewStringFromCString("fld1"),             \
                        Dart_Null()));                                         \
    CHECK(Dart_GetField(target, Dart_NewStringFromCString("fld2")));           \
    CHECK(Dart_Invoke(target, Dart_NewStringFromCString("fld2"), 0, nullptr)); \
    if (Dart_IsLibrary(target) || Dart_IsType(target)) {                       \
      /* There are no implicit setters for static fields, so the pragma     */ \
      /* must be checked; in precompiled mode, that means a false positive. */ \
      FAIL_UNLESS_PRECOMPILED(                                                 \
          "fld2", Dart_SetField(target, Dart_NewStringFromCString("fld2"),     \
                                Dart_Null()));                                 \
    } else {                                                                   \
      FAIL("fld2", Dart_SetField(target, Dart_NewStringFromCString("fld2"),    \
                                 Dart_Null()));                                \
    }                                                                          \
    FAIL("fld3", Dart_GetField(target, Dart_NewStringFromCString("fld3")));    \
    FAIL("fld3",                                                               \
         Dart_Invoke(target, Dart_NewStringFromCString("fld3"), 0, nullptr));  \
    CHECK(Dart_SetField(target, Dart_NewStringFromCString("fld3"),             \
                        Dart_Null()));                                         \
  } while (false)

#define TEST_GETTERS(target)                                                   \
  do {                                                                         \
    FAIL("get1", Dart_GetField(target, Dart_NewStringFromCString("get1")));    \
    CHECK(Dart_GetField(target, Dart_NewStringFromCString("get2")));           \
    CHECK(Dart_GetField(target, Dart_NewStringFromCString("get3")));           \
  } while (false)

#define TEST_SETTERS(target, value)                                            \
  do {                                                                         \
    FAIL("set1",                                                               \
         Dart_SetField(target, Dart_NewStringFromCString("set1"), value));     \
    CHECK(Dart_SetField(target, Dart_NewStringFromCString("set2"), value));    \
    CHECK(Dart_SetField(target, Dart_NewStringFromCString("set3"), value));    \
  } while (false)

DART_EXPORT void RunTests() {
  is_dartaotruntime = Dart_IsPrecompiledRuntime();

  Dart_Handle lib = Dart_RootLibrary();

  //////// Test class access.

  FAIL("C", Dart_GetClass(lib, Dart_NewStringFromCString("C")));

  Dart_Handle D_class = Dart_GetClass(lib, Dart_NewStringFromCString("D"));
  CHECK(D_class);

  Dart_Handle F_class = Dart_GetClass(lib, Dart_NewStringFromCString("F"));
  CHECK(F_class);

  //////// Test allocation and constructor invocation.

  CHECK(Dart_Allocate(D_class));

  FAIL("D.", Dart_New(D_class, Dart_Null(), 0, nullptr));

  CHECK(Dart_New(D_class, Dart_NewStringFromCString("defined"), 0, nullptr));
  Dart_Handle D =
      Dart_New(D_class, Dart_NewStringFromCString("fact"), 0, nullptr);
  CHECK(D);

  //////// Test actions against methods

  fprintf(stderr, "\n\nTesting methods with library target\n\n\n");

  FAIL("noop", Dart_Invoke(lib, Dart_NewStringFromCString("noop"), 0, nullptr));

  FAIL("fn0", Dart_Invoke(lib, Dart_NewStringFromCString("fn0"), 0, nullptr));

  CHECK(Dart_Invoke(lib, Dart_NewStringFromCString("fn1"), 0, nullptr));
  FAIL("fn1_get",
       Dart_Invoke(lib, Dart_NewStringFromCString("fn1_get"), 0, nullptr));
  CHECK(Dart_Invoke(lib, Dart_NewStringFromCString("fn1_call"), 0, nullptr));

  FAIL("fn0", Dart_GetField(lib, Dart_NewStringFromCString("fn0")));

  CHECK(Dart_GetField(lib, Dart_NewStringFromCString("fn1")));
  CHECK(Dart_GetField(lib, Dart_NewStringFromCString("fn1_get")));
  FAIL("fn1_call", Dart_GetField(lib, Dart_NewStringFromCString("fn1_call")));

  fprintf(stderr, "\n\nTesting methods with class target\n\n\n");

  FAIL_CLOSURIZE_CONSTRUCTOR(
      "defined", Dart_GetField(D_class, Dart_NewStringFromCString("defined")));
  FAIL_CLOSURIZE_CONSTRUCTOR(
      "fact", Dart_GetField(D_class, Dart_NewStringFromCString("fact")));

  FAIL("fn0", Dart_Invoke(D, Dart_NewStringFromCString("fn0"), 0, nullptr));

  FAIL("fn2",
       Dart_Invoke(D_class, Dart_NewStringFromCString("fn2"), 0, nullptr));

  CHECK(Dart_Invoke(D_class, Dart_NewStringFromCString("fn3"), 0, nullptr));
  CHECK(
      Dart_Invoke(D_class, Dart_NewStringFromCString("fn3_call"), 0, nullptr));
  FAIL("fn3_get",
       Dart_Invoke(D_class, Dart_NewStringFromCString("fn3_get"), 0, nullptr));

  FAIL("fn2", Dart_GetField(D_class, Dart_NewStringFromCString("fn2")));

  CHECK(Dart_GetField(D_class, Dart_NewStringFromCString("fn3")));
  FAIL("fn3_call",
       Dart_GetField(D_class, Dart_NewStringFromCString("fn3_call")));
  CHECK(Dart_GetField(D_class, Dart_NewStringFromCString("fn3_get")));

  fprintf(stderr, "\n\nTesting methods with instance target\n\n\n");

  CHECK(Dart_Invoke(D, Dart_NewStringFromCString("fn1"), 0, nullptr));
  FAIL("fn1_get",
       Dart_Invoke(D, Dart_NewStringFromCString("fn1_get"), 0, nullptr));
  CHECK(Dart_Invoke(D, Dart_NewStringFromCString("fn1_call"), 0, nullptr));

  FAIL("fn0", Dart_GetField(D, Dart_NewStringFromCString("fn0")));

  CHECK(Dart_GetField(D, Dart_NewStringFromCString("fn1")));
  CHECK(Dart_GetField(D, Dart_NewStringFromCString("fn1_get")));
  FAIL("fn1_call", Dart_GetField(D, Dart_NewStringFromCString("fn1_call")));

  //////// Test actions against fields

  fprintf(stderr, "\n\nTesting fields with library target\n\n\n");
  TEST_FIELDS(lib);

  fprintf(stderr, "\n\nTesting fields with class target\n\n\n");
  TEST_FIELDS(F_class);

  fprintf(stderr, "\n\nTesting fields with instance target\n\n\n");
  TEST_FIELDS(D);

  //////// Test actions against getter and setter functions.

  fprintf(stderr, "\n\nTesting getters with library target\n\n\n");
  TEST_GETTERS(lib);

  fprintf(stderr, "\n\nTesting getters with class target\n\n\n");
  TEST_GETTERS(F_class);

  fprintf(stderr, "\n\nTesting getters with instance target\n\n\n");
  TEST_GETTERS(D);

  Dart_Handle test_value =
      Dart_GetField(lib, Dart_NewStringFromCString("testValue"));
  CHECK(test_value);

  fprintf(stderr, "\n\nTesting setters with library target\n\n\n");
  TEST_SETTERS(lib, test_value);

  fprintf(stderr, "\n\nTesting setters with class target\n\n\n");
  TEST_SETTERS(F_class, test_value);

  fprintf(stderr, "\n\nTesting setters with instance target\n\n\n");
  TEST_SETTERS(D, test_value);
}
