/*
 * Copyright (c) 2008-2014 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 org.bson.codecs;

import org.bson.BsonBinaryReader;
import org.bson.BsonBinarySubType;
import org.bson.BsonBinaryWriter;
import org.bson.BsonInt32;
import org.bson.BsonObjectId;
import org.bson.ByteBufNIO;
import org.bson.Document;
import org.bson.io.BasicOutputBuffer;
import org.bson.io.BsonInput;
import org.bson.io.ByteBufferBsonInput;
import org.bson.types.Binary;
import org.bson.types.Code;
import org.bson.types.CodeWithScope;
import org.bson.types.Decimal128;
import org.bson.types.MaxKey;
import org.bson.types.MinKey;
import org.bson.types.ObjectId;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.HashSet;
import java.util.List;

import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class DocumentCodecTest {
    private BasicOutputBuffer buffer;
    private BsonBinaryWriter writer;

    @Before
    public void setUp() throws Exception {
        buffer = new BasicOutputBuffer();
        writer = new BsonBinaryWriter(buffer);
    }

    @After
    public void tearDown() {
        writer.close();
    }

    @Test
    public void testPrimitiveBSONTypeCodecs() throws IOException {
        DocumentCodec documentCodec = new DocumentCodec();
        Document doc = new Document();
        doc.put("oid", new ObjectId());
        doc.put("integer", 1);
        doc.put("long", 2L);
        doc.put("string", "hello");
        doc.put("double", 3.2);
        doc.put("decimal", Decimal128.parse("0.100"));
        doc.put("binary", new Binary(BsonBinarySubType.USER_DEFINED, new byte[]{0, 1, 2, 3}));
        doc.put("date", new Date(1000));
        doc.put("boolean", true);
        doc.put("code", new Code("var i = 0"));
        doc.put("minkey", new MinKey());
        doc.put("maxkey", new MaxKey());
        //        doc.put("pattern", Pattern.compile("^hello"));  // TODO: Pattern doesn't override equals method!
        doc.put("null", null);

        documentCodec.encode(writer, doc, EncoderContext.builder().build());

        BsonInput bsonInput = createInputBuffer();
        Document decodedDocument = documentCodec.decode(new BsonBinaryReader(bsonInput), DecoderContext.builder().build());
        assertEquals(doc, decodedDocument);
    }

    @Test
    public void testIterableEncoding() throws IOException {
        DocumentCodec documentCodec = new DocumentCodec();
        Document doc = new Document()
                       .append("list", asList(1, 2, 3, 4, 5))
                       .append("set", new HashSet<Integer>(asList(1, 2, 3, 4)));

        documentCodec.encode(writer, doc, EncoderContext.builder().build());

        BsonInput bsonInput = createInputBuffer();
        Document decodedDocument = documentCodec.decode(new BsonBinaryReader(bsonInput), DecoderContext.builder().build());
        assertEquals(new Document()
                     .append("list", asList(1, 2, 3, 4, 5))
                     .append("set", asList(1, 2, 3, 4)),
                     decodedDocument);
    }

    @Test
    public void testCodeWithScopeEncoding() throws IOException {
        DocumentCodec documentCodec = new DocumentCodec();
        Document doc = new Document();
        doc.put("theCode", new CodeWithScope("javaScript code", new Document("fieldNameOfScope", "valueOfScope")));

        documentCodec.encode(writer, doc, EncoderContext.builder().build());

        Document decodedDocument = documentCodec.decode(new BsonBinaryReader(createInputBuffer()), DecoderContext.builder().build());
        assertEquals(doc, decodedDocument);
    }

    @Test
    public void testIterableContainingOtherIterableEncoding() throws IOException {
        DocumentCodec documentCodec = new DocumentCodec();
        Document doc = new Document();
        @SuppressWarnings("unchecked")
        List<List<Integer>> listOfLists = asList(asList(1), asList(2));
        doc.put("array", listOfLists);

        documentCodec.encode(writer, doc, EncoderContext.builder().build());

        BsonInput bsonInput = createInputBuffer();
        Document decodedDocument = documentCodec.decode(new BsonBinaryReader(bsonInput), DecoderContext.builder().build());
        assertEquals(doc, decodedDocument);
    }

    @Test
    public void testIterableContainingDocumentsEncoding() throws IOException {
        DocumentCodec documentCodec = new DocumentCodec();
        Document doc = new Document();
        List<Document> listOfDocuments = asList(new Document("intVal", 1), new Document("anotherInt", 2));
        doc.put("array", listOfDocuments);

        documentCodec.encode(writer, doc, EncoderContext.builder().build());

        BsonInput bsonInput = createInputBuffer();
        Document decodedDocument = documentCodec.decode(new BsonBinaryReader(bsonInput), DecoderContext.builder().build());
        assertEquals(doc, decodedDocument);
    }

    @Test
    public void testNestedDocumentEncoding() throws IOException {
        DocumentCodec documentCodec = new DocumentCodec();
        Document doc = new Document();
        doc.put("nested", new Document("x", 1));

        documentCodec.encode(writer, doc, EncoderContext.builder().build());

        BsonInput bsonInput = createInputBuffer();
        Document decodedDocument = documentCodec.decode(new BsonBinaryReader(bsonInput), DecoderContext.builder().build());
        assertEquals(doc, decodedDocument);
    }

    @Test
    public void shouldNotGenerateIdIfPresent() {
        DocumentCodec documentCodec = new DocumentCodec();
        Document document = new Document("_id", 1);
        assertTrue(documentCodec.documentHasId(document));
        document = documentCodec.generateIdIfAbsentFromDocument(document);
        assertTrue(documentCodec.documentHasId(document));
        assertEquals(new BsonInt32(1), documentCodec.getDocumentId(document));
    }

    @Test
    public void shouldGenerateIdIfAbsent() {
        DocumentCodec documentCodec = new DocumentCodec();
        Document document = new Document();
        assertFalse(documentCodec.documentHasId(document));
        document = documentCodec.generateIdIfAbsentFromDocument(document);
        assertTrue(documentCodec.documentHasId(document));
        assertEquals(BsonObjectId.class, documentCodec.getDocumentId(document).getClass());
    }

    // TODO: factor into common base class;
    private BsonInput createInputBuffer() throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        buffer.pipe(baos);
        return new ByteBufferBsonInput(new ByteBufNIO(ByteBuffer.wrap(baos.toByteArray())));
    }
}
