/**
 *    Copyright (C) 2013 MongoDB Inc.
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program 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 Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *    As a special exception, the copyright holders give permission to link the
 *    code of portions of this program with the OpenSSL library under certain
 *    conditions as described in each individual source file and distribute
 *    linked combinations including the program with the OpenSSL library. You
 *    must comply with the GNU Affero General Public License in all respects for
 *    all of the code used other than as permitted herein. If you modify file(s)
 *    with this exception, you may extend this exception to your version of the
 *    file(s), but you are not obligated to do so. If you do not wish to do so,
 *    delete this exception statement from your version. If you delete this
 *    exception statement from all source files in the program, then also delete
 *    it in the license file.
 */

/**
 * tests for BSONObjBuilder
 */

#include "mongo/db/jsobj.h"
#include "mongo/db/json.h"

#include <sstream>
#include "mongo/unittest/unittest.h"

namespace {

    using std::string;
    using std::stringstream;
    using mongo::BSONElement;
    using mongo::BSONObj;
    using mongo::BSONObjBuilder;
    using mongo::BSONType;
    using mongo::OpTime;

    const long long maxEncodableInt = (1 << 30) - 1;
    const long long minEncodableInt = -maxEncodableInt;

    const long long maxInt = (std::numeric_limits<int>::max)();
    const long long minInt = (std::numeric_limits<int>::min)();

    const long long maxEncodableDouble = (1LL << 40) - 1;
    const long long minEncodableDouble = -maxEncodableDouble;

    const long long maxDouble = (1LL << std::numeric_limits<double>::digits) - 1;
    const long long minDouble = -maxDouble;

    const long long maxLongLong = (std::numeric_limits<long long>::max)();
    const long long minLongLong = (std::numeric_limits<long long>::min)();

    template<typename T> void assertBSONTypeEquals(BSONType actual, BSONType expected, T value, int i) {
        if (expected != actual) {
            stringstream ss;
            ss << "incorrect type in bson object for " << (i+1) << "-th test value " << value
               << ". actual: " << mongo::typeName(actual)
               << "; expected: " << mongo::typeName(expected);
            const string msg = ss.str();
            FAIL(msg);
        }
    }

    /**
     * current conversion ranges in append(unsigned n)
     * dbl/int max/min in comments refer to max/min encodable constants
     *                  0 <= n <= uint_max          -----> int
     */

    TEST(BSONObjBuilderTest, AppendUnsignedInt) {
        struct { unsigned int v; BSONType t; } data[] = {
            {                                           0, mongo::NumberInt },
            {                                         100, mongo::NumberInt },
            {                             maxEncodableInt, mongo::NumberInt },
            {                         maxEncodableInt + 1, mongo::NumberInt },
            {           static_cast<unsigned int>(maxInt), mongo::NumberInt },
            {      static_cast<unsigned int>(maxInt) + 1U, mongo::NumberInt },
            {  (std::numeric_limits<unsigned int>::max)(), mongo::NumberInt },
            {                                           0, mongo::Undefined }
        };
        for (int i=0; data[i].t != mongo::Undefined; i++) {
            unsigned int v = data[i].v;
            BSONObjBuilder b;
            b.append("a", v);
            BSONObj o = b.obj();
            ASSERT_EQUALS(o.nFields(), 1);
            BSONElement e = o.getField("a");
            unsigned int n = e.numberLong();
            ASSERT_EQUALS(n, v);
            assertBSONTypeEquals(e.type(), data[i].t, v, i);
        }
    }

    /**
     * current conversion ranges in appendIntOrLL(long long n)
     * dbl/int max/min in comments refer to max/min encodable constants
     *                       n <  dbl_min            -----> long long
     *            dbl_min <= n <  int_min            -----> double
     *            int_min <= n <= int_max            -----> int
     *            int_max <  n <= dbl_max            -----> double
     *            dbl_max <  n                       -----> long long
     */

    TEST(BSONObjBuilderTest, AppendIntOrLL) {
        struct { long long v; BSONType t; } data[] = {
            {                   0, mongo::NumberInt  },
            {                -100, mongo::NumberInt  },
            {                 100, mongo::NumberInt  },
            {   -(maxInt / 2 - 1), mongo::NumberInt  },
            {      maxInt / 2 - 1, mongo::NumberInt  },
            {       -(maxInt / 2), mongo::NumberLong },
            {          maxInt / 2, mongo::NumberLong },
            {     minEncodableInt, mongo::NumberLong  },
            {     maxEncodableInt, mongo::NumberLong  },
            { minEncodableInt - 1, mongo::NumberLong  },
            { maxEncodableInt + 1, mongo::NumberLong  },
            {              minInt, mongo::NumberLong },
            {              maxInt, mongo::NumberLong },
            {          minInt - 1, mongo::NumberLong  },
            {          maxInt + 1, mongo::NumberLong  },
            {         minLongLong, mongo::NumberLong },
            {         maxLongLong, mongo::NumberLong },
            {                   0, mongo::Undefined  }
        };
        for (int i=0; data[i].t != mongo::Undefined; i++) {
            long long v = data[i].v;
            BSONObjBuilder b;
            b.appendIntOrLL("a", v);
            BSONObj o = b.obj();
            ASSERT_EQUALS(o.nFields(), 1);
            BSONElement e = o.getField("a");
            long long n = e.numberLong();
            ASSERT_EQUALS(n, v);
            assertBSONTypeEquals(e.type(), data[i].t, v, i);
        }
    }

    /**
     * current conversion ranges in appendNumber(size_t n)
     * dbl/int max/min in comments refer to max/min encodable constants
     *                  0 <= n <= int_max            -----> int
     *            int_max <  n                       -----> long long
     */
 
    TEST(BSONObjBuilderTest, AppendNumberSizeT) {
        struct { size_t v; BSONType t; } data[] = {
            {                                     0, mongo::NumberInt    },
            {                                   100, mongo::NumberInt    },
            {                       maxEncodableInt, mongo::NumberInt    },
            {                   maxEncodableInt + 1, mongo::NumberLong   },
            {                        size_t(maxInt), mongo::NumberLong   },
            {                   size_t(maxInt) + 1U, mongo::NumberLong   },
            {  (std::numeric_limits<size_t>::max)(), mongo::NumberLong   },
            {                                     0, mongo::Undefined    }
        };
        for (int i=0; data[i].t != mongo::Undefined; i++) {
            size_t v = data[i].v;
            BSONObjBuilder b;
            b.appendNumber("a", v);
            BSONObj o = b.obj();
            ASSERT_EQUALS(o.nFields(), 1);
            BSONElement e = o.getField("a");
            size_t n = e.numberLong();
            ASSERT_EQUALS(n, v);
            assertBSONTypeEquals(e.type(), data[i].t, v, i);
        }
    }

    /**
     * current conversion ranges in appendNumber(long long n)
     * dbl/int max/min in comments refer to max/min encodable constants
     *                       n <  dbl_min            -----> long long
     *            dbl_min <= n <  int_min            -----> double
     *            int_min <= n <= int_max            -----> int
     *            int_max <  n <= dbl_max            -----> double
     *            dbl_max <  n                       -----> long long
     */

    TEST(BSONObjBuilderTest, AppendNumberLongLong) {
        struct { long long v; BSONType t; } data[] = {
            {                      0, mongo::NumberInt    },
            {                   -100, mongo::NumberInt    },
            {                    100, mongo::NumberInt    },
            {        minEncodableInt, mongo::NumberInt    },
            {        maxEncodableInt, mongo::NumberInt    },
            {    minEncodableInt - 1, mongo::NumberDouble },
            {    maxEncodableInt + 1, mongo::NumberDouble },
            {                 minInt, mongo::NumberDouble },
            {                 maxInt, mongo::NumberDouble },
            {             minInt - 1, mongo::NumberDouble },
            {             maxInt + 1, mongo::NumberDouble },
            {     minEncodableDouble, mongo::NumberDouble },
            {     maxEncodableDouble, mongo::NumberDouble },
            { minEncodableDouble - 1, mongo::NumberLong   },
            { maxEncodableDouble + 1, mongo::NumberLong   },
            {              minDouble, mongo::NumberLong   },
            {              maxDouble, mongo::NumberLong   },
            {          minDouble - 1, mongo::NumberLong   },
            {          maxDouble + 1, mongo::NumberLong   },
            {            minLongLong, mongo::NumberLong   },
            {            maxLongLong, mongo::NumberLong   },
            {                      0, mongo::Undefined    }
        };
        for (int i=0; data[i].t != mongo::Undefined; i++) {
            long long v = data[i].v;
            BSONObjBuilder b;
            b.appendNumber("a", v);
            BSONObj o = b.obj();
            ASSERT_EQUALS(o.nFields(), 1);
            BSONElement e = o.getField("a");
            if (data[i].t != mongo::NumberDouble) {
                long long n = e.numberLong(); 
                ASSERT_EQUALS(n, v);
            }
            else {
                double n = e.numberDouble(); 
                ASSERT_APPROX_EQUAL(n, static_cast<double>(v), 0.001);
            }
            assertBSONTypeEquals(e.type(), data[i].t, v, i);
        }
    }

    TEST(BSONObjBuilderTest, StreamLongLongMin) {
        BSONObj o = BSON("a" << std::numeric_limits<long long>::min());
        ASSERT_EQUALS(o.nFields(), 1);
        BSONElement e = o.getField("a");
        long long n = e.numberLong(); 
        ASSERT_EQUALS(n, std::numeric_limits<long long>::min());
    }

    TEST(BSONObjBuilderTest, AppendNumberLongLongMinCompareObject) {
        BSONObjBuilder b;
        b.appendNumber("a", std::numeric_limits<long long>::min());
        BSONObj o1 = b.obj();
 
        BSONObj o2 = BSON("a" << std::numeric_limits<long long>::min());
 
        ASSERT_EQUALS(o1, o2);
    }

    TEST(BSONObjBuilderTest, AppendMaxTimestampOpTimeConversion) {
        BSONObjBuilder b;
        b.appendMaxForType("a", mongo::Timestamp);
        BSONObj o1 = b.obj();

        BSONElement e = o1.getField("a");
        ASSERT_FALSE(e.eoo());

        OpTime opTime = e._opTime();
        ASSERT_FALSE(opTime.isNull());
    }

} // unnamed namespace
