/**
 * Copyright (C) 2014 Stratio (http://stratio.com)
 *
 * 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.stratio.ingestion.sink.mongodb;

import static org.fest.assertions.Assertions.assertThat;

import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.flume.event.EventBuilder;
import org.bson.types.ObjectId;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.ISODateTimeFormat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import com.google.common.base.Charsets;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.util.JSONParseException;

@RunWith(JUnit4.class)
public class EventParserTest {

    private FieldDefinition definition(MongoDataType type) {
        FieldDefinition def;
        switch (type) {
        case DATE:
            def = new DateFieldDefinition();
            break;
        case DOCUMENT:
            def = new DocumentFieldDefinition();
            break;
        default:
            def = new SimpleFieldDefinition(type);
        }

        return def;
    }

    @Test
    public void parseDocumentTypeWithValidRegularExpressionSeparator() {
        EventParser eventParser = new EventParser();
        DBObject dbObject = buildExpectedObject();
        DocumentFieldDefinition fieldDefinition = (DocumentFieldDefinition) definition(MongoDataType.DOCUMENT);
        fieldDefinition.setDelimiter("(#¬**#)");
        Map<String, FieldDefinition> documentMapping = new LinkedHashMap<String, FieldDefinition>();
        documentMapping.put("field1", new SimpleFieldDefinition(MongoDataType.STRING));
        documentMapping.put("field2", new SimpleFieldDefinition(MongoDataType.ARRAY));
        DocumentFieldDefinition fieldDefinition2 = (DocumentFieldDefinition) definition(MongoDataType.DOCUMENT);
        fieldDefinition2.setDelimiter("(#¬**#)");
        documentMapping.put("field3", fieldDefinition2);
        documentMapping.put("field6", new SimpleFieldDefinition(MongoDataType.STRING));
        Map<String, FieldDefinition> documentMapping2 = new LinkedHashMap<String, FieldDefinition>();
        documentMapping2.put("field4", new SimpleFieldDefinition(MongoDataType.STRING));
        documentMapping2.put("field5", new SimpleFieldDefinition(MongoDataType.ARRAY));
        fieldDefinition.setDocumentMapping(documentMapping);
        fieldDefinition2.setDocumentMapping(documentMapping2);

        assertThat(
                eventParser.parseValue(fieldDefinition, "Point1(#¬**#)[111.11,222.22](#¬**#)Point2(#¬**#)[111.11,222.22](#¬**#)Point3"))
                .isEqualTo(dbObject);
    }

    @Test
    public void parseValue() {
        final EventParser eventParser = new EventParser();
        assertThat(eventParser.parseValue(definition(MongoDataType.STRING), "foo")).isEqualTo("foo");
        assertThat(eventParser.parseValue(definition(MongoDataType.INT32), "32")).isEqualTo(32);
        assertThat(eventParser.parseValue(definition(MongoDataType.INT64), "64")).isEqualTo(64L);
        assertThat(eventParser.parseValue(definition(MongoDataType.DOUBLE), "1.0")).isEqualTo(1.0);
        assertThat(eventParser.parseValue(definition(MongoDataType.BOOLEAN), "true")).isEqualTo(true);
        final DateTime now = DateTime.now().toDateTime(DateTimeZone.UTC);
        assertThat(eventParser.parseValue(definition(MongoDataType.DATE), Long.toString(now.getMillis()))).isEqualTo(
                now.toDate());
              
        assertThat((eventParser.parseValue(definition(MongoDataType.DATE), ISODateTimeFormat.dateTime().print(now))))
                .isEqualTo(
                        now.toDate());
        assertThat(eventParser.parseValue(definition(MongoDataType.NULL), "full")).isNull();
        assertThat(eventParser.parseValue(definition(MongoDataType.OBJECTID), "507c7f79bcf86cd7994f6c0e")).isEqualTo(
                new ObjectId("507c7f79bcf86cd7994f6c0e"));

        BasicDBList dbList = new BasicDBList();
        dbList.add(1);
        dbList.add(2);
        dbList.add(3);
        DBObject dbObject = new BasicDBObject();
        dbObject.put("abc", 123);
        dbObject.put("myArray", dbList);
        assertThat(eventParser.parseValue(definition(MongoDataType.OBJECT), "{ \"abc\": 123, \"myArray\": [1, 2, 3] }"))
                .isEqualTo(
                        dbObject);

        assertThat(eventParser.parseValue(definition(MongoDataType.BINARY), "U3RyYXRpbw==")).isEqualTo(
                "Stratio".getBytes(Charsets.UTF_8));
    }

    @Test
    public void parseValueForDate() {
        final EventParser eventParser = new EventParser();
        DateFieldDefinition fd = (DateFieldDefinition) definition(MongoDataType.DATE);
        fd.setDateFormat("yyyy/MM/dd");
        assertThat(eventParser.parseValue(fd, "2004/03/13")).isEqualTo(new Date(104, 2, 13));
    }
    
    @Test
    public void parseNegativeValueForDate() {
    	final EventParser eventParser = new EventParser();
    	long before1970Millis = -100;
    	final DateTime pastDate = new DateTime(before1970Millis);
    	assertThat(eventParser.parseValue(definition(MongoDataType.DATE), Long.toString(before1970Millis))).isEqualTo(pastDate.toDate());
    }

    @Test(expected = MongoSinkException.class)
    public void eventParserWithBadType() {
        new EventParser(MappingDefinition.load("/mapping_definition_bad_type.json"));
    }

    @Test
    public void parseRawBodyToRow() {
        final EventParser eventParser = new EventParser(MappingDefinition.load("/simple_body_row_raw.json"));
        assertThat(eventParser.parse(EventBuilder.withBody("TEST".getBytes(Charsets.UTF_8))).get("data")).isEqualTo(
                "TEST".getBytes(Charsets.UTF_8));
    }

    @Test
    public void parseJsonBodyToRow() {
        final EventParser eventParser = new EventParser(MappingDefinition.load("/simple_body_row_json.json"));
        assertThat(
                eventParser.parse(EventBuilder.withBody("{ \"foo\": \"bar\" }".getBytes(Charsets.UTF_8))).get("data"))
                .isEqualTo(
                        new BasicDBObject("foo", "bar"));
    }

    @Test(expected = JSONParseException.class)
    public void parseBadJsonBodyToRow() {
        final EventParser eventParser = new EventParser(MappingDefinition.load("/simple_body_row_json.json"));
        eventParser.parse(EventBuilder.withBody("{???? \"foo\": \"bar\" }".getBytes(Charsets.UTF_8)));
    }

    @Test
    public void parseWithoutMapping() {
        final EventParser eventParser = new EventParser(MappingDefinition.load("/full_map.json"));
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("myString", "\"bar\""); // Overwrites the value defined in JSON body
        headers.put("myInt64", "64");
        headers.put("myBoolean", "true");
        headers.put("myDouble", "1.0");
        headers.put("myNull", "\"foobar\"");
        headers.put("myObj", "{ \"foo\": \"bar\" }");
        headers.put("myArr", "[1,1.0,\"str\"]");
        DBObject dbObject = eventParser.parse(EventBuilder.withBody(new byte[0], headers));
        assertThat(dbObject.get("myString")).isEqualTo("bar");
        assertThat(dbObject.get("myInt64"))
                .isEqualTo(64); // XXX: If auto-mapped, 64 will be recognized as int32, not int64
        assertThat(dbObject.get("myBoolean")).isEqualTo(true);
        assertThat(dbObject.get("myDouble")).isEqualTo(1.0);
        assertThat(dbObject.get("myNull")).isEqualTo("foobar");
        assertThat(dbObject.get("myObj")).isEqualTo(new BasicDBObject("foo", "bar"));
        BasicDBList dbList = new BasicDBList();
        dbList.add(1);
        dbList.add(1.0);
        dbList.add("str");
        assertThat(dbObject.get("myArr")).isEqualTo(dbList);
    }

    @Test(expected = MongoSinkException.class)
    public void parseDocumentTypeWithNoValidSeparator() {
        EventParser eventParser = new EventParser();
        DBObject dbObject = buildExpectedObject();
        DocumentFieldDefinition fieldDefinition = (DocumentFieldDefinition) definition(MongoDataType.DOCUMENT);

        Map<String, FieldDefinition> documentMapping = new LinkedHashMap<String, FieldDefinition>();
        documentMapping.put("field1", new SimpleFieldDefinition(MongoDataType.STRING));
        documentMapping.put("field2", new SimpleFieldDefinition(MongoDataType.ARRAY));
        DocumentFieldDefinition fieldDefinition2 = (DocumentFieldDefinition) definition(MongoDataType.DOCUMENT);
        documentMapping.put("field3", fieldDefinition2);
        documentMapping.put("field6", new SimpleFieldDefinition(MongoDataType.STRING));
        Map<String, FieldDefinition> documentMapping2 = new LinkedHashMap<String, FieldDefinition>();
        documentMapping2.put("field4", new SimpleFieldDefinition(MongoDataType.STRING));
        documentMapping2.put("field5", new SimpleFieldDefinition(MongoDataType.ARRAY));
        fieldDefinition.setDocumentMapping(documentMapping);
        fieldDefinition2.setDocumentMapping(documentMapping2);

        assertThat(eventParser.parseValue(fieldDefinition, "Point1#[111.11,222.22]#Point2#[111.11,222.22]#Point3"))
                .isEqualTo(
                        dbObject);
    }

    @Test
    public void parseDocumentTypeWithValidSeparator() {
        EventParser eventParser = new EventParser();
        DBObject dbObject = buildExpectedObject();
        DocumentFieldDefinition fieldDefinition = (DocumentFieldDefinition) definition(MongoDataType.DOCUMENT);
        fieldDefinition.setDelimiter("#");
        Map<String, FieldDefinition> documentMapping = new LinkedHashMap<String, FieldDefinition>();
        documentMapping.put("field1", new SimpleFieldDefinition(MongoDataType.STRING));
        documentMapping.put("field2", new SimpleFieldDefinition(MongoDataType.ARRAY));
        DocumentFieldDefinition fieldDefinition2 = (DocumentFieldDefinition) definition(MongoDataType.DOCUMENT);
        fieldDefinition2.setDelimiter("#");
        documentMapping.put("field3", fieldDefinition2);
        documentMapping.put("field6", new SimpleFieldDefinition(MongoDataType.STRING));
        Map<String, FieldDefinition> documentMapping2 = new LinkedHashMap<String, FieldDefinition>();
        documentMapping2.put("field4", new SimpleFieldDefinition(MongoDataType.STRING));
        documentMapping2.put("field5", new SimpleFieldDefinition(MongoDataType.ARRAY));
        fieldDefinition.setDocumentMapping(documentMapping);
        fieldDefinition2.setDocumentMapping(documentMapping2);

        assertThat(eventParser.parseValue(fieldDefinition, "Point1#[111.11,222.22]#Point2#[111.11,222.22]#Point3"))
                .isEqualTo(
                        dbObject);
    }

    private DBObject buildExpectedObject() {
        DBObject dbObject = new BasicDBObject();
        BasicDBObject object2 = new BasicDBObject();
        BasicDBList locList = new BasicDBList();
        locList.add(111.11);
        locList.add(222.22);
        dbObject.put("field1", "Point1");
        dbObject.put("field2", locList);
        dbObject.put("field3", object2);
        dbObject.put("field6", "Point3");

        object2.put("field4", "Point2");
        object2.put("field5", locList);

        return dbObject;
    }

}
