/*
 * Copyright 2008-2015 MongoDB, 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.
 */

package com.mongodb;

import org.bson.BSON;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonDocumentWriter;
import org.bson.BsonInt32;
import org.bson.BsonNull;
import org.bson.BsonObjectId;
import org.bson.LazyBSONCallback;
import org.bson.Transformer;
import org.bson.codecs.BsonValueCodecProvider;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.ValueCodecProvider;
import org.junit.Test;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import static java.util.Arrays.asList;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class DBObjectCodecTest extends DatabaseTestCase {

    @Test
    public void testTransformers() {
        try {
            collection.save(new BasicDBObject("_id", 1).append("x", 1.1));
            assertEquals(Double.class, collection.findOne().get("x").getClass());

            BSON.addEncodingHook(Double.class, new Transformer() {
                public Object transform(final Object o) {
                    return o.toString();
                }
            });

            collection.save(new BasicDBObject("_id", 1).append("x", 1.1));
            assertEquals(String.class, collection.findOne().get("x").getClass());

            BSON.clearAllHooks();
            collection.save(new BasicDBObject("_id", 1).append("x", 1.1));
            assertEquals(Double.class, collection.findOne().get("x").getClass());

            BSON.addDecodingHook(Double.class, new Transformer() {
                public Object transform(final Object o) {
                    return o.toString();
                }
            });
            assertEquals(String.class, collection.findOne().get("x").getClass());
            BSON.clearAllHooks();
            assertEquals(Double.class, collection.findOne().get("x").getClass());
        } finally {
            BSON.clearAllHooks();
        }
    }

    @Test
    public void testDBListEncoding() {
        BasicDBList list = new BasicDBList();
        list.add(new BasicDBObject("a", 1).append("b", true));
        list.add(new BasicDBObject("c", "string").append("d", 0.1));
        collection.save(new BasicDBObject("l", list));
        assertEquals(list, collection.findOne().get("l"));
    }

    @Test
    public void shouldNotGenerateIdIfPresent() {
        DBObjectCodec dbObjectCodec = new DBObjectCodec(fromProviders(asList(new ValueCodecProvider(), new DBObjectCodecProvider(),
                new BsonValueCodecProvider())));
        DBObject document = new BasicDBObject("_id", 1);
        assertTrue(dbObjectCodec.documentHasId(document));
        document = dbObjectCodec.generateIdIfAbsentFromDocument(document);
        assertTrue(dbObjectCodec.documentHasId(document));
        assertEquals(new BsonInt32(1), dbObjectCodec.getDocumentId(document));
    }

    @Test
    public void shouldGenerateIdIfAbsent() {
        DBObjectCodec dbObjectCodec = new DBObjectCodec(fromProviders(asList(new ValueCodecProvider(), new DBObjectCodecProvider(),
                new BsonValueCodecProvider())));
        DBObject document = new BasicDBObject();
        assertFalse(dbObjectCodec.documentHasId(document));
        document = dbObjectCodec.generateIdIfAbsentFromDocument(document);
        assertTrue(dbObjectCodec.documentHasId(document));
        assertEquals(BsonObjectId.class, dbObjectCodec.getDocumentId(document).getClass());
    }

    @Test
    public void shouldRespectEncodeIdFirstPropertyInEncoderContext() {
        DBObjectCodec dbObjectCodec = new DBObjectCodec(fromProviders(asList(new ValueCodecProvider(), new DBObjectCodecProvider(),
                new BsonValueCodecProvider())));
        // given
        DBObject doc = new BasicDBObject("x", 2).append("_id", 2);

        // when
        BsonDocument encodedDocument = new BsonDocument();
        dbObjectCodec.encode(new BsonDocumentWriter(encodedDocument),
                             doc,
                             EncoderContext.builder().isEncodingCollectibleDocument(true).build());

        // then
        assertEquals(new ArrayList<String>(encodedDocument.keySet()), asList("_id", "x"));

        // when
        encodedDocument.clear();
        dbObjectCodec.encode(new BsonDocumentWriter(encodedDocument),
                             doc,
                             EncoderContext.builder().isEncodingCollectibleDocument(false).build());

        // then
        assertEquals(new ArrayList<String>(encodedDocument.keySet()), asList("x", "_id"));
    }

    @Test
    public void shouldEncodeNull() {
        DBObjectCodec dbObjectCodec = new DBObjectCodec(fromProviders(asList(new ValueCodecProvider(), new DBObjectCodecProvider(),
                new BsonValueCodecProvider())));

        DBObject doc = new BasicDBObject("null", null);

        BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
        dbObjectCodec.encode(writer, doc, EncoderContext.builder().build());

        assertEquals(new BsonDocument("null", BsonNull.VALUE), writer.getDocument());
    }

    @Test
    public void shouldEncodedNestedMapsListsAndDocuments() {
        byte[] zeroOneDocumentBytes = new byte[]{19, 0, 0, 0, 16, 48, 0, 0, 0, 0, 0, 16, 49, 0, 1, 0, 0, 0, 0}; //  {"0" : 0, "1", 1}
        Map<String, Object> zeroOneMap = new HashMap<String, Object>();
        zeroOneMap.put("0", 0);
        zeroOneMap.put("1", 1);
        DBObject zeroOneDBObject = new BasicDBObject();
        zeroOneDBObject.putAll(zeroOneMap);
        DBObject zeroOneDBList = new BasicDBList();
        zeroOneDBList.putAll(zeroOneMap);
        List<Integer> zeroOneList = asList(0, 1);

        DBObjectCodec dbObjectCodec = new DBObjectCodec(fromProviders(asList(new ValueCodecProvider(), new DBObjectCodecProvider(),
                new BsonValueCodecProvider())));

        DBObject doc = new BasicDBObject()
                       .append("map", zeroOneMap)
                       .append("dbDocument", zeroOneDBObject)
                       .append("dbList", zeroOneDBList)
                       .append("list", zeroOneList)
                       .append("array", new int[] {0, 1})
                       .append("lazyDoc", new LazyDBObject(zeroOneDocumentBytes, new LazyBSONCallback()))
                       .append("lazyArray", new LazyDBList(zeroOneDocumentBytes, new LazyBSONCallback()));

        BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
        dbObjectCodec.encode(writer, doc, EncoderContext.builder().build());

        BsonDocument zeroOneBsonDocument = new BsonDocument().append("0", new BsonInt32(0)).append("1", new BsonInt32(1));
        BsonArray zeroOneBsonArray = new BsonArray(asList(new BsonInt32(0), new BsonInt32(1)));

        assertEquals(new BsonDocument("map", zeroOneBsonDocument)
                     .append("dbDocument", zeroOneBsonDocument)
                     .append("dbList", zeroOneBsonArray)
                     .append("list", zeroOneBsonArray)
                     .append("array", zeroOneBsonArray)
                     .append("lazyDoc", zeroOneBsonDocument)
                     .append("lazyArray", zeroOneBsonArray), writer.getDocument());
    }

    @Test
    public void shouldEncodeIterableMapAsMap() {
        IterableMap iterableMap = new IterableMap();
        iterableMap.put("first", 1);

        DBObjectCodec dbObjectCodec = new DBObjectCodec(fromProviders(asList(new ValueCodecProvider(), new DBObjectCodecProvider(),
                new BsonValueCodecProvider())));

        DBObject doc = new BasicDBObject("map", iterableMap);

        BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
        dbObjectCodec.encode(writer, doc, EncoderContext.builder().build());

        assertEquals(new BsonDocument("map", new BsonDocument("first", new BsonInt32(1))), writer.getDocument());
    }

    static class IterableMap extends HashMap<String, Integer> implements Iterable<Integer> {
        private static final long serialVersionUID = -5090421898469363392L;

        @Override
        public Iterator<Integer> iterator() {
            return values().iterator();
        }
    }
}
