/*    Copyright 2012 10gen Inc.
 *
 *    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.
 */

#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kDefault

#include <boost/scoped_array.hpp>

#include "mongo/base/data_view.h"
#include "mongo/db/jsobj.h"
#include "mongo/unittest/unittest.h"
#include "mongo/platform/random.h"
#include "mongo/bson/bson_validate.h"
#include "mongo/util/log.h"

namespace {

using std::endl;

using namespace mongo;

void appendInvalidStringElement(const char* fieldName, BufBuilder* bb) {
    // like a BSONObj string, but without a NUL terminator.
    bb->appendChar(String);
    bb->appendStr(fieldName, /*withNUL*/ true);
    bb->appendNum(4);
    bb->appendStr("asdf", /*withNUL*/ false);
}

TEST(BSONValidate, Basic) {
    BSONObj x;
    ASSERT_TRUE(x.valid());

    x = BSON("x" << 1);
    ASSERT_TRUE(x.valid());
}

TEST(BSONValidate, RandomData) {
    PseudoRandom r(17);

    int numValid = 0;
    int numToRun = 1000;
    long long jsonSize = 0;

    for (int i = 0; i < numToRun; i++) {
        int size = 1234;

        char* x = new char[size];
        DataView(x).writeLE(size);

        for (int i = 4; i < size; i++) {
            x[i] = r.nextInt32(255);
        }

        x[size - 1] = 0;

        BSONObj o(x);

        ASSERT_EQUALS(size, o.objsize());

        if (o.valid()) {
            numValid++;
            jsonSize += o.jsonString().size();
            ASSERT_OK(validateBSON(o.objdata(), o.objsize()));
        } else {
            ASSERT_NOT_OK(validateBSON(o.objdata(), o.objsize()));
        }

        delete[] x;
    }

    log() << "RandomData: didn't crash valid/total: " << numValid << "/" << numToRun
          << " (want few valid ones)"
          << " jsonSize: " << jsonSize << endl;
}

TEST(BSONValidate, MuckingData1) {
    BSONObj theObject;

    {
        BSONObjBuilder b;
        b.append("name", "eliot was here");
        b.append("yippee", "asd");
        BSONArrayBuilder a(b.subarrayStart("arr"));
        for (int i = 0; i < 100; i++) {
            a.append(BSON("x" << i << "who"
                              << "me"
                              << "asd"
                              << "asd"));
        }
        a.done();
        b.done();

        theObject = b.obj();
    }

    int numValid = 0;
    int numToRun = 1000;
    long long jsonSize = 0;

    for (int i = 4; i < theObject.objsize() - 1; i++) {
        BSONObj mine = theObject.copy();

        unsigned char* data = reinterpret_cast<unsigned char*>(const_cast<char*>(mine.objdata()));

        data[i] = 200;

        numToRun++;
        if (mine.valid()) {
            numValid++;
            jsonSize += mine.jsonString().size();
            ASSERT_OK(validateBSON(mine.objdata(), mine.objsize()));
        } else {
            ASSERT_NOT_OK(validateBSON(mine.objdata(), mine.objsize()));
        }
    }

    log() << "MuckingData1: didn't crash valid/total: " << numValid << "/" << numToRun
          << " (want few valid ones) "
          << " jsonSize: " << jsonSize << endl;
}

TEST(BSONValidate, Fuzz) {
    int64_t seed = time(0);
    log() << "BSONValidate Fuzz random seed: " << seed << endl;
    PseudoRandom randomSource(seed);

    BSONObj original =
        BSON("one" << 3 << "two" << 5 << "three" << BSONObj() << "four"
                   << BSON("five" << BSON("six" << 11)) << "seven" << BSON_ARRAY("a"
                                                                                 << "bb"
                                                                                 << "ccc" << 5)
                   << "eight" << BSONDBRef("rrr", OID("01234567890123456789aaaa")) << "_id"
                   << OID("deadbeefdeadbeefdeadbeef") << "nine"
                   << BSONBinData("\x69\xb7", 2, BinDataGeneral) << "ten" << Date_t(44) << "eleven"
                   << BSONRegEx("foooooo", "i"));

    int32_t fuzzFrequencies[] = {2, 10, 20, 100, 1000};
    for (size_t i = 0; i < sizeof(fuzzFrequencies) / sizeof(int32_t); ++i) {
        int32_t fuzzFrequency = fuzzFrequencies[i];

        // Copy the 'original' BSONObj to 'buffer'.
        boost::scoped_array<char> buffer(new char[original.objsize()]);
        memcpy(buffer.get(), original.objdata(), original.objsize());

        // Randomly flip bits in 'buffer', with probability determined by 'fuzzFrequency'. The
        // first four bytes, representing the size of the object, are excluded from bit
        // flipping.
        for (int32_t byteIdx = 4; byteIdx < original.objsize(); ++byteIdx) {
            for (int32_t bitIdx = 0; bitIdx < 8; ++bitIdx) {
                if (randomSource.nextInt32(fuzzFrequency) == 0) {
                    reinterpret_cast<unsigned char&>(buffer[byteIdx]) ^= (1U << bitIdx);
                }
            }
        }
        BSONObj fuzzed(buffer.get());

        // Check that the two validation implementations agree (and neither crashes).
        ASSERT_EQUALS(fuzzed.valid(), validateBSON(fuzzed.objdata(), fuzzed.objsize()).isOK());
    }
}

TEST(BSONValidateFast, Empty) {
    BSONObj x;
    ASSERT_OK(validateBSON(x.objdata(), x.objsize()));
}

TEST(BSONValidateFast, RegEx) {
    BSONObjBuilder b;
    b.appendRegex("foo", "i");
    BSONObj x = b.obj();
    ASSERT_OK(validateBSON(x.objdata(), x.objsize()));
}

TEST(BSONValidateFast, Simple0) {
    BSONObj x;
    ASSERT_OK(validateBSON(x.objdata(), x.objsize()));

    x = BSON("foo" << 17 << "bar"
                   << "eliot");
    ASSERT_OK(validateBSON(x.objdata(), x.objsize()));
}

TEST(BSONValidateFast, Simple2) {
    char buf[64];
    for (int i = 1; i <= JSTypeMax; i++) {
        BSONObjBuilder b;
        sprintf(buf, "foo%d", i);
        b.appendMinForType(buf, i);
        sprintf(buf, "bar%d", i);
        b.appendMaxForType(buf, i);
        BSONObj x = b.obj();
        ASSERT_OK(validateBSON(x.objdata(), x.objsize()));
    }
}


TEST(BSONValidateFast, Simple3) {
    BSONObjBuilder b;
    char buf[64];
    for (int i = 1; i <= JSTypeMax; i++) {
        sprintf(buf, "foo%d", i);
        b.appendMinForType(buf, i);
        sprintf(buf, "bar%d", i);
        b.appendMaxForType(buf, i);
    }
    BSONObj x = b.obj();
    ASSERT_OK(validateBSON(x.objdata(), x.objsize()));
}

TEST(BSONValidateFast, NestedObject) {
    BSONObj x = BSON("a" << 1 << "b" << BSON("c" << 2 << "d" << BSONArrayBuilder().obj() << "e"
                                                 << BSON_ARRAY("1" << 2 << 3)));
    ASSERT_OK(validateBSON(x.objdata(), x.objsize()));
    ASSERT_NOT_OK(validateBSON(x.objdata(), x.objsize() / 2));
}

TEST(BSONValidateFast, ErrorWithId) {
    BufBuilder bb;
    BSONObjBuilder ob(bb);
    ob.append("_id", 1);
    appendInvalidStringElement("not_id", &bb);
    const BSONObj x = ob.done();
    const Status status = validateBSON(x.objdata(), x.objsize());
    ASSERT_NOT_OK(status);
    ASSERT_EQUALS(status.reason(), "not null terminated string in object with _id: 1");
}

TEST(BSONValidateFast, ErrorBeforeId) {
    BufBuilder bb;
    BSONObjBuilder ob(bb);
    appendInvalidStringElement("not_id", &bb);
    ob.append("_id", 1);
    const BSONObj x = ob.done();
    const Status status = validateBSON(x.objdata(), x.objsize());
    ASSERT_NOT_OK(status);
    ASSERT_EQUALS(status.reason(), "not null terminated string in object with unknown _id");
}

TEST(BSONValidateFast, ErrorNoId) {
    BufBuilder bb;
    BSONObjBuilder ob(bb);
    appendInvalidStringElement("not_id", &bb);
    const BSONObj x = ob.done();
    const Status status = validateBSON(x.objdata(), x.objsize());
    ASSERT_NOT_OK(status);
    ASSERT_EQUALS(status.reason(), "not null terminated string in object with unknown _id");
}

TEST(BSONValidateFast, ErrorIsInId) {
    BufBuilder bb;
    BSONObjBuilder ob(bb);
    appendInvalidStringElement("_id", &bb);
    const BSONObj x = ob.done();
    const Status status = validateBSON(x.objdata(), x.objsize());
    ASSERT_NOT_OK(status);
    ASSERT_EQUALS(status.reason(), "not null terminated string in object with unknown _id");
}

TEST(BSONValidateFast, NonTopLevelId) {
    BufBuilder bb;
    BSONObjBuilder ob(bb);
    ob.append("not_id1",
              BSON("_id"
                   << "not the real _id"));
    appendInvalidStringElement("not_id2", &bb);
    const BSONObj x = ob.done();
    const Status status = validateBSON(x.objdata(), x.objsize());
    ASSERT_NOT_OK(status);
    ASSERT_EQUALS(status.reason(), "not null terminated string in object with unknown _id");
}

TEST(BSONValidateFast, StringHasSomething) {
    BufBuilder bb;
    BSONObjBuilder ob(bb);
    bb.appendChar(String);
    bb.appendStr("x", /*withNUL*/ true);
    bb.appendNum(0);
    const BSONObj x = ob.done();
    ASSERT_EQUALS(5  // overhead
                      +
                      1  // type
                      +
                      2  // name
                      +
                      4  // size
                  ,
                  x.objsize());
    ASSERT_NOT_OK(validateBSON(x.objdata(), x.objsize()));
}
}
