/*
 * Copyright (C) 2011 Intel Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) version 3.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#ifndef INCL_CLIENTTESTASSERT
#define INCL_CLIENTTESTASSERT

#include <cppunit/Exception.h>
#include <syncevo/util.h>

#include <syncevo/declarations.h>
SE_BEGIN_CXX

/**
 * All of the macros below include the behavior of the corresponding
 * CPUNIT assertion. In addition they catch exceptions and turn them
 * into the following, extended CPPUnit exception by preserving the
 * information from the original exception and adding the current
 * source code line.
 *
 * Source information is listed as inner first (as part of the error
 * preamble), followed by error at that location and all locations
 * that the error passed through until caught at the top level of a
 * test.
 */
class CTException : public CppUnit::Exception
{

 public:
    CTException(const CppUnit::Message &message = CppUnit::Message(),
                const std::string &currentMessage = "",
                const CppUnit::SourceLine &currentSourceLine = CppUnit::SourceLine(),
                const CppUnit::SourceLine &previousSourceLine = CppUnit::SourceLine()) :
    CppUnit::Exception(message, previousSourceLine)
    {
        CppUnit::Message extendedMessage = message;
        if (!currentMessage.empty()) {
            extendedMessage.addDetail(currentMessage);
        }
        if (currentSourceLine.isValid()) {
            extendedMessage.addDetail(StringPrintf("%s:%d",
                                                   getBasename(currentSourceLine.fileName()).c_str(),
                                                   currentSourceLine.lineNumber()));
        }
        setMessage(extendedMessage);
    }
};

// Avoid clang 'error: attributes are not allowed on a function-definition' by putting the SE_NORETURN
// into a separate declaration.
static void ClientTestExceptionHandle(const char *file, int line, const std::string &message = "") SE_NORETURN;
static void ClientTestExceptionHandle(const char *file, int line, const std::string &message)
{
    CppUnit::SourceLine here(file, line);
    try {
        throw;
    }  catch (const CTException &ex) {
        throw(CTException(ex.message(),
                          message,
                          here,
                          ex.sourceLine()));
    } catch (const CppUnit::Exception &ex) {
        if (ex.sourceLine() == CPPUNIT_SOURCELINE()) {
            /* failure in condition expression itself, already includes source info, pass through */
            throw;
        } else {
            throw(CTException(ex.message(),
                              message,
                              here,
                              ex.sourceLine()));
        }
    } catch (const Exception &ex) {
        throw(CTException(CppUnit::Message(ex.what()),
                          message,
                          here,
                          CppUnit::SourceLine(ex.m_file, ex.m_line)));
    } catch (const std::exception &ex) {
        CppUnit::Message msg(CPPUNIT_EXTRACT_EXCEPTION_TYPE_(ex,
                                                             "std::exception or derived"));
        msg.addDetail(std::string("What(): ") + ex.what());
        throw(CTException(msg,
                          message,
                          here));
    }
}

// The only purpose of this here is to teach clang's scan-build that certain
// code paths are not taken. Because the program just exists on failed
// asserts, the resulting binary should not be used for real testing.
// (http://clang-analyzer.llvm.org/annotations.html#custom_assertions).
#ifdef CT_ASSERT_SIMPLE
// Disable CPPUNIT, use simple assert.
# define CT_ASSERT_NO_CLANG_BUILD(_assert)
# define CT_ASSERT_TRUE_CLANG_BUILD(_expression) if (!(_expression)) { exit(1); }
# define CT_ASSERT_EXE_CLANG_BUILD(_statement) _statement
#else
// Use CPPUNIT assert, disable simple assert.
# define CT_ASSERT_NO_CLANG_BUILD(_assert) _assert
# define CT_ASSERT_TRUE_CLANG_BUILD(_expression)
# define CT_ASSERT_EXE_CLANG_BUILD(_statement)
#endif

#define CT_WRAP_ASSERT(_file, _line, _assert, _expression) \
    do { \
       try { \
           SE_LOG_DEBUG(NULL, "%s:%d: starting %s", getBasename(_file).c_str(), _line, #_assert); \
           CT_ASSERT_NO_CLANG_BUILD(_assert); \
           _expression; \
           SE_LOG_DEBUG(NULL, "%s:%d: ending %s", getBasename(_file).c_str(), _line, #_assert); \
       } catch (...) { \
           ClientTestExceptionHandle(_file, _line); \
       } \
    } while (false)

#define CT_WRAP_ASSERT_MESSAGE(_file, _line, _message, _assert, _expression) \
    do { \
       try { \
           SE_LOG_DEBUG(NULL, "%s:%d: starting %s %s", \
                        getBasename(_file).c_str(), _line, \
                        std::string(_message).c_str(), \
                        #_assert); \
           CT_ASSERT_NO_CLANG_BUILD(_assert); \
           _expression; \
           SE_LOG_DEBUG(NULL, "%s:%d: ending %s", getBasename(_file).c_str(), _line, #_assert); \
       } catch (...) { \
           ClientTestExceptionHandle(_file, _line, _message); \
       } \
    } while (false)

#define CT_ASSERT(condition) CT_WRAP_ASSERT(__FILE__, __LINE__, CPPUNIT_ASSERT(condition), CT_ASSERT_TRUE_CLANG_BUILD(condition))
#define CT_ASSERT_NO_THROW(expression) CT_WRAP_ASSERT(__FILE__, __LINE__, expression, CT_ASSERT_EXE_CLANG_BUILD(expression))
#define CT_ASSERT_NO_THROW_MESSAGE(message, expression) CT_WRAP_ASSERT_MESSAGE(__FILE__, __LINE__, message, expression, CT_ASSERT_EXE_CLANG_BUILD(expression))
#define CT_ASSERT_MESSAGE(message,condition) CT_WRAP_ASSERT(__FILE__, __LINE__, CPPUNIT_ASSERT_MESSAGE(message, (condition)), CT_ASSERT_TRUE_CLANG_BUILD(condition))
#define CT_FAIL(message) CPPUNIT_FAIL(message)
#define CT_ASSERT_EQUAL(expected,actual) CT_WRAP_ASSERT(__FILE__, __LINE__, CPPUNIT_ASSERT_EQUAL((expected),(actual)), CT_ASSERT_TRUE_CLANG_BUILD((expected) == (actual)))
#define CT_ASSERT_EQUAL_MESSAGE(message,expected,actual) CT_WRAP_ASSERT(__FILE__, __LINE__, CPPUNIT_ASSERT_EQUAL_MESSAGE(message,(expected),(actual)), CT_ASSERT_TRUE_CLANG_BUILD((expected) == (actual)))
#define CT_ASSERT_DOUBLES_EQUAL(expected,actual,delta) CT_WRAP_ASSERT(__FILE__, __LINE__, CPPUNIT_ASSERT_DOUBLES_EQUAL((expected),(actual),(delta)), CT_ASSERT_TRUE_CLANG_BUILD(fabs((expected) - (actual)) < (delta)))
#define CT_ASSERT_DOUBLES_EQUAL_MESSAGE(message,expected,actual,delta) CT_WRAP_ASSERT(__FILE__, __LINE__, CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(__FILE__, __LINE__, message,(expected),(actual),(delta)), CT_ASSERT_TRUE_CLANG_BUILD(fabs((expected) - (actual)) < (delta)))

SE_END_CXX

#endif  // INCL_CLIENTTESTASSERT
