/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * vim: set ts=8 sts=4 et sw=4 tw=99:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef jsapi_tests_tests_h
#define jsapi_tests_tests_h

#include "mozilla/ArrayUtils.h"
#include "mozilla/TypeTraits.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "jsalloc.h"
#include "jscntxt.h"
#include "jsgc.h"

#include "js/Vector.h"

/* Note: Aborts on OOM. */
class JSAPITestString {
    js::Vector<char, 0, js::SystemAllocPolicy> chars;
  public:
    JSAPITestString() {}
    explicit JSAPITestString(const char* s) { *this += s; }
    JSAPITestString(const JSAPITestString& s) { *this += s; }

    const char* begin() const { return chars.begin(); }
    const char* end() const { return chars.end(); }
    size_t length() const { return chars.length(); }

    JSAPITestString & operator +=(const char* s) {
        if (!chars.append(s, strlen(s)))
            abort();
        return *this;
    }

    JSAPITestString & operator +=(const JSAPITestString& s) {
        if (!chars.append(s.begin(), s.length()))
            abort();
        return *this;
    }
};

inline JSAPITestString operator+(JSAPITestString a, const char* b) { return a += b; }
inline JSAPITestString operator+(JSAPITestString a, const JSAPITestString& b) { return a += b; }

class JSAPITest
{
  public:
    static JSAPITest* list;
    JSAPITest* next;

    JSContext* cx;
    JS::PersistentRootedObject global;
    bool knownFail;
    JSAPITestString msgs;
    JSCompartment* oldCompartment;

    JSAPITest() : cx(nullptr), knownFail(false), oldCompartment(nullptr) {
        next = list;
        list = this;
    }

    virtual ~JSAPITest() {
        MOZ_RELEASE_ASSERT(!cx);
        MOZ_RELEASE_ASSERT(!global);
    }

    virtual bool init();
    virtual void uninit();

    virtual const char * name() = 0;
    virtual bool run(JS::HandleObject global) = 0;

#define EXEC(s) do { if (!exec(s, __FILE__, __LINE__)) return false; } while (false)

    bool exec(const char* bytes, const char* filename, int lineno);

    // Like exec(), but doesn't call fail() if JS::Evaluate returns false.
    bool execDontReport(const char* bytes, const char* filename, int lineno);

#define EVAL(s, vp) do { if (!evaluate(s, __FILE__, __LINE__, vp)) return false; } while (false)

    bool evaluate(const char* bytes, const char* filename, int lineno, JS::MutableHandleValue vp);

    JSAPITestString jsvalToSource(JS::HandleValue v) {
        JSString* str = JS_ValueToSource(cx, v);
        if (str) {
            JSAutoByteString bytes(cx, str);
            if (!!bytes)
                return JSAPITestString(bytes.ptr());
        }
        JS_ClearPendingException(cx);
        return JSAPITestString("<<error converting value to string>>");
    }

    JSAPITestString toSource(long v) {
        char buf[40];
        sprintf(buf, "%ld", v);
        return JSAPITestString(buf);
    }

    JSAPITestString toSource(unsigned long v) {
        char buf[40];
        sprintf(buf, "%lu", v);
        return JSAPITestString(buf);
    }

    JSAPITestString toSource(long long v) {
        char buf[40];
        sprintf(buf, "%lld", v);
        return JSAPITestString(buf);
    }

    JSAPITestString toSource(unsigned long long v) {
        char buf[40];
        sprintf(buf, "%llu", v);
        return JSAPITestString(buf);
    }

    JSAPITestString toSource(unsigned int v) {
        return toSource((unsigned long)v);
    }

    JSAPITestString toSource(int v) {
        return toSource((long)v);
    }

    JSAPITestString toSource(bool v) {
        return JSAPITestString(v ? "true" : "false");
    }

    JSAPITestString toSource(JSAtom* v) {
        JS::RootedValue val(cx, JS::StringValue((JSString*)v));
        return jsvalToSource(val);
    }

    JSAPITestString toSource(JSVersion v) {
        return JSAPITestString(JS_VersionToString(v));
    }

    // Note that in some still-supported GCC versions (we think anything before
    // GCC 4.6), this template does not work when the second argument is
    // nullptr. It infers type U = long int. Use CHECK_NULL instead.
    template <typename T, typename U>
    bool checkEqual(const T& actual, const U& expected,
                    const char* actualExpr, const char* expectedExpr,
                    const char* filename, int lineno)
    {
        static_assert(mozilla::IsSigned<T>::value == mozilla::IsSigned<U>::value,
                      "using CHECK_EQUAL with different-signed inputs triggers compiler warnings");
        static_assert(mozilla::IsUnsigned<T>::value == mozilla::IsUnsigned<U>::value,
                      "using CHECK_EQUAL with different-signed inputs triggers compiler warnings");
        return (actual == expected) ||
            fail(JSAPITestString("CHECK_EQUAL failed: expected (") +
                 expectedExpr + ") = " + toSource(expected) +
                 ", got (" + actualExpr + ") = " + toSource(actual), filename, lineno);
    }

#define CHECK_EQUAL(actual, expected) \
    do { \
        if (!checkEqual(actual, expected, #actual, #expected, __FILE__, __LINE__)) \
            return false; \
    } while (false)

    template <typename T>
    bool checkNull(const T* actual, const char* actualExpr,
                   const char* filename, int lineno) {
        return (actual == nullptr) ||
            fail(JSAPITestString("CHECK_NULL failed: expected nullptr, got (") +
                 actualExpr + ") = " + toSource(actual),
                 filename, lineno);
    }

#define CHECK_NULL(actual) \
    do { \
        if (!checkNull(actual, #actual, __FILE__, __LINE__)) \
            return false; \
    } while (false)

    bool checkSame(const JS::Value& actualArg, const JS::Value& expectedArg,
                   const char* actualExpr, const char* expectedExpr,
                   const char* filename, int lineno) {
        bool same;
        JS::RootedValue actual(cx, actualArg), expected(cx, expectedArg);
        return (JS_SameValue(cx, actual, expected, &same) && same) ||
               fail(JSAPITestString("CHECK_SAME failed: expected JS_SameValue(cx, ") +
                    actualExpr + ", " + expectedExpr + "), got !JS_SameValue(cx, " +
                    jsvalToSource(actual) + ", " + jsvalToSource(expected) + ")", filename, lineno);
    }

#define CHECK_SAME(actual, expected) \
    do { \
        if (!checkSame(actual, expected, #actual, #expected, __FILE__, __LINE__)) \
            return false; \
    } while (false)

#define CHECK(expr) \
    do { \
        if (!(expr)) \
            return fail(JSAPITestString("CHECK failed: " #expr), __FILE__, __LINE__); \
    } while (false)

    bool fail(JSAPITestString msg = JSAPITestString(), const char* filename = "-", int lineno = 0) {
        if (JS_IsExceptionPending(cx)) {
            js::gc::AutoSuppressGC gcoff(cx);
            JS::RootedValue v(cx);
            JS_GetPendingException(cx, &v);
            JS_ClearPendingException(cx);
            JSString* s = JS::ToString(cx, v);
            if (s) {
                JSAutoByteString bytes(cx, s);
                if (!!bytes)
                    msg += bytes.ptr();
            }
        }
        fprintf(stderr, "%s:%d:%.*s\n", filename, lineno, (int) msg.length(), msg.begin());
        msgs += msg;
        return false;
    }

    JSAPITestString messages() const { return msgs; }

    static const JSClass * basicGlobalClass() {
        static const JSClassOps cOps = {
            nullptr, nullptr, nullptr, nullptr,
            nullptr, nullptr, nullptr, nullptr,
            nullptr, nullptr, nullptr,
            JS_GlobalObjectTraceHook
        };
        static const JSClass c = {
            "global", JSCLASS_GLOBAL_FLAGS,
            &cOps
        };
        return &c;
    }

  protected:
    static bool
    print(JSContext* cx, unsigned argc, JS::Value* vp)
    {
        JS::CallArgs args = JS::CallArgsFromVp(argc, vp);

        for (unsigned i = 0; i < args.length(); i++) {
            JSString* str = JS::ToString(cx, args[i]);
            if (!str)
                return false;
            char* bytes = JS_EncodeString(cx, str);
            if (!bytes)
                return false;
            printf("%s%s", i ? " " : "", bytes);
            JS_free(cx, bytes);
        }

        putchar('\n');
        fflush(stdout);
        args.rval().setUndefined();
        return true;
    }

    bool definePrint();

    static void setNativeStackQuota(JSContext* cx)
    {
        const size_t MAX_STACK_SIZE =
/* Assume we can't use more than 5e5 bytes of C stack by default. */
#if (defined(DEBUG) && defined(__SUNPRO_CC))  || defined(JS_CPU_SPARC)
            /*
             * Sun compiler uses a larger stack space for js::Interpret() with
             * debug.  Use a bigger gMaxStackSize to make "make check" happy.
             */
            5000000
#else
            500000
#endif
        ;

        JS_SetNativeStackQuota(cx, MAX_STACK_SIZE);
    }

    virtual JSContext* createContext() {
        JSContext* cx = JS_NewContext(8L * 1024 * 1024);
        if (!cx)
            return nullptr;
        JS::SetWarningReporter(cx, &reportWarning);
        setNativeStackQuota(cx);
        return cx;
    }

    virtual void destroyContext() {
        MOZ_RELEASE_ASSERT(cx);
        JS_DestroyContext(cx);
        cx = nullptr;
    }

    static void reportWarning(JSContext* cx, JSErrorReport* report) {
        MOZ_RELEASE_ASSERT(report);
        MOZ_RELEASE_ASSERT(JSREPORT_IS_WARNING(report->flags));

        fprintf(stderr, "%s:%u:%s\n",
                report->filename ? report->filename : "<no filename>",
                (unsigned int) report->lineno,
                report->message().c_str());
    }

    virtual const JSClass * getGlobalClass() {
        return basicGlobalClass();
    }

    virtual JSObject * createGlobal(JSPrincipals* principals = nullptr);
};

#define BEGIN_TEST(testname)                                            \
    class cls_##testname : public JSAPITest {                           \
      public:                                                           \
        virtual const char * name() override { return #testname; }      \
        virtual bool run(JS::HandleObject global) override

#define END_TEST(testname)                                              \
    };                                                                  \
    static cls_##testname cls_##testname##_instance;

/*
 * A "fixture" is a subclass of JSAPITest that holds common definitions for a
 * set of tests. Each test that wants to use the fixture should use
 * BEGIN_FIXTURE_TEST and END_FIXTURE_TEST, just as one would use BEGIN_TEST and
 * END_TEST, but include the fixture class as the first argument. The fixture
 * class's declarations are then in scope for the test bodies.
 */

#define BEGIN_FIXTURE_TEST(fixture, testname)                           \
    class cls_##testname : public fixture {                             \
      public:                                                           \
        virtual const char * name() override { return #testname; }      \
        virtual bool run(JS::HandleObject global) override

#define END_FIXTURE_TEST(fixture, testname)                             \
    };                                                                  \
    static cls_##testname cls_##testname##_instance;

/*
 * A class for creating and managing one temporary file.
 *
 * We could use the ISO C temporary file functions here, but those try to
 * create files in the root directory on Windows, which fails for users
 * without Administrator privileges.
 */
class TempFile {
    const char* name;
    FILE* stream;

  public:
    TempFile() : name(), stream() { }
    ~TempFile() {
        if (stream)
            close();
        if (name)
            remove();
    }

    /*
     * Return a stream for a temporary file named |fileName|. Infallible.
     * Use only once per TempFile instance. If the file is not explicitly
     * closed and deleted via the member functions below, this object's
     * destructor will clean them up.
     */
    FILE* open(const char* fileName)
    {
        stream = fopen(fileName, "wb+");
        if (!stream) {
            fprintf(stderr, "error opening temporary file '%s': %s\n",
                    fileName, strerror(errno));
            exit(1);
        }
        name = fileName;
        return stream;
    }

    /* Close the temporary file's stream. */
    void close() {
        if (fclose(stream) == EOF) {
            fprintf(stderr, "error closing temporary file '%s': %s\n",
                    name, strerror(errno));
            exit(1);
        }
        stream = nullptr;
    }

    /* Delete the temporary file. */
    void remove() {
        if (::remove(name) != 0) {
            fprintf(stderr, "error deleting temporary file '%s': %s\n",
                    name, strerror(errno));
            exit(1);
        }
        name = nullptr;
    }
};

// Just a wrapper around JSPrincipals that allows static construction.
class TestJSPrincipals : public JSPrincipals
{
  public:
    explicit TestJSPrincipals(int rc = 0)
      : JSPrincipals()
    {
        refcount = rc;
    }

    bool write(JSContext* cx, JSStructuredCloneWriter* writer) override {
        MOZ_ASSERT(false, "not implemented");
        return false;
    }
};

#endif /* jsapi_tests_tests_h */