/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you 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.elasticsearch.index.mapper.copyto;

import com.google.common.collect.ImmutableList;
import org.apache.lucene.index.IndexableField;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.index.mapper.*;
import org.elasticsearch.index.mapper.ParseContext.Document;
import org.elasticsearch.index.mapper.core.LongFieldMapper;
import org.elasticsearch.index.mapper.core.StringFieldMapper;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.test.ElasticsearchSingleNodeTest;
import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.index.mapper.DocumentMapper.MergeFlags.mergeFlags;
import static org.hamcrest.Matchers.*;

/**
 *
 */
public class CopyToMapperTests extends ElasticsearchSingleNodeTest {

    @SuppressWarnings("unchecked")
    @Test
    public void testCopyToFieldsParsing() throws Exception {
        String mapping = jsonBuilder().startObject().startObject("type1").startObject("properties")
                .startObject("copy_test")
                .field("type", "string")
                .array("copy_to", "another_field", "cyclic_test")
                .endObject()

                .startObject("another_field")
                .field("type", "string")
                .endObject()

                .startObject("cyclic_test")
                .field("type", "string")
                .array("copy_to", "copy_test")
                .endObject()

                .startObject("int_to_str_test")
                .field("type", "integer")
                .array("copy_to",  "another_field", "new_field")
                .endObject()
                .endObject().endObject().endObject().string();

        DocumentMapper docMapper = createIndex("test").mapperService().documentMapperParser().parse(mapping);
        FieldMapper fieldMapper = docMapper.mappers().name("copy_test").mapper();
        assertThat(fieldMapper, instanceOf(StringFieldMapper.class));

        // Check json serialization
        StringFieldMapper stringFieldMapper = (StringFieldMapper) fieldMapper;
        XContentBuilder builder = jsonBuilder().startObject();
        stringFieldMapper.toXContent(builder, ToXContent.EMPTY_PARAMS).endObject();
        builder.close();
        Map<String, Object> serializedMap = JsonXContent.jsonXContent.createParser(builder.bytes()).mapAndClose();
        Map<String, Object> copyTestMap = (Map<String, Object>) serializedMap.get("copy_test");
        assertThat(copyTestMap.get("type").toString(), is("string"));
        List<String> copyToList = (List<String>) copyTestMap.get("copy_to");
        assertThat(copyToList.size(), equalTo(2));
        assertThat(copyToList.get(0).toString(), equalTo("another_field"));
        assertThat(copyToList.get(1).toString(), equalTo("cyclic_test"));

        // Check data parsing
        BytesReference json = jsonBuilder().startObject()
                .field("copy_test", "foo")
                .field("cyclic_test", "bar")
                .field("int_to_str_test", 42)
                .endObject().bytes();

        ParseContext.Document doc = docMapper.parse("type1", "1", json).rootDoc();
        assertThat(doc.getFields("copy_test").length, equalTo(2));
        assertThat(doc.getFields("copy_test")[0].stringValue(), equalTo("foo"));
        assertThat(doc.getFields("copy_test")[1].stringValue(), equalTo("bar"));

        assertThat(doc.getFields("another_field").length, equalTo(2));
        assertThat(doc.getFields("another_field")[0].stringValue(), equalTo("foo"));
        assertThat(doc.getFields("another_field")[1].stringValue(), equalTo("42"));

        assertThat(doc.getFields("cyclic_test").length, equalTo(2));
        assertThat(doc.getFields("cyclic_test")[0].stringValue(), equalTo("foo"));
        assertThat(doc.getFields("cyclic_test")[1].stringValue(), equalTo("bar"));

        assertThat(doc.getFields("int_to_str_test").length, equalTo(1));
        assertThat(doc.getFields("int_to_str_test")[0].numericValue().intValue(), equalTo(42));

        assertThat(doc.getFields("new_field").length, equalTo(1));
        assertThat(doc.getFields("new_field")[0].numericValue().intValue(), equalTo(42));

        fieldMapper = docMapper.mappers().name("new_field").mapper();
        assertThat(fieldMapper, instanceOf(LongFieldMapper.class));
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testCopyToFieldsInnerObjectParsing() throws Exception {
        String mapping = jsonBuilder().startObject().startObject("type1").startObject("properties")

                .startObject("copy_test")
                .field("type", "string")
                .field("copy_to", "very.inner.field")
                .endObject()

                .startObject("very")
                .field("type", "object")
                .startObject("properties")
                .startObject("inner")
                .field("type", "object")
                .endObject()
                .endObject()
                .endObject()

                .endObject().endObject().endObject().string();

        DocumentMapper docMapper = createIndex("test").mapperService().documentMapperParser().parse(mapping);

        BytesReference json = jsonBuilder().startObject()
                .field("copy_test", "foo")
                .startObject("foo").startObject("bar").field("baz", "zoo").endObject().endObject()
                .endObject().bytes();

        ParseContext.Document doc = docMapper.parse("type1", "1", json).rootDoc();
        assertThat(doc.getFields("copy_test").length, equalTo(1));
        assertThat(doc.getFields("copy_test")[0].stringValue(), equalTo("foo"));

        assertThat(doc.getFields("very.inner.field").length, equalTo(1));
        assertThat(doc.getFields("very.inner.field")[0].stringValue(), equalTo("foo"));

    }

    @SuppressWarnings("unchecked")
    @Test
    public void testCopyToFieldsNonExistingInnerObjectParsing() throws Exception {
        String mapping = jsonBuilder().startObject().startObject("type1").startObject("properties")

                .startObject("copy_test")
                .field("type", "string")
                .field("copy_to", "very.inner.field")
                .endObject()

                .endObject().endObject().endObject().string();

        DocumentMapper docMapper = createIndex("test").mapperService().documentMapperParser().parse(mapping);

        BytesReference json = jsonBuilder().startObject()
                .field("copy_test", "foo")
                .endObject().bytes();

        try {
            docMapper.parse("type1", "1", json).rootDoc();
            fail();
        } catch (MapperParsingException ex) {
            assertThat(ex.getMessage(), startsWith("attempt to copy value to non-existing object"));
        }
    }

    @Test
    public void testCopyToFieldMerge() throws Exception {

        String mappingBefore = jsonBuilder().startObject().startObject("type1").startObject("properties")

                .startObject("copy_test")
                .field("type", "string")
                .array("copy_to", "foo", "bar")
                .endObject()

                .endObject().endObject().endObject().string();

        String mappingAfter = jsonBuilder().startObject().startObject("type1").startObject("properties")

                .startObject("copy_test")
                .field("type", "string")
                .array("copy_to", "baz", "bar")
                .endObject()

                .endObject().endObject().endObject().string();

        DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser();
        DocumentMapper docMapperBefore = parser.parse(mappingBefore);

        ImmutableList<String> fields = docMapperBefore.mappers().name("copy_test").mapper().copyTo().copyToFields();

        assertThat(fields.size(), equalTo(2));
        assertThat(fields.get(0), equalTo("foo"));
        assertThat(fields.get(1), equalTo("bar"));


        DocumentMapper docMapperAfter = parser.parse(mappingAfter);

        DocumentMapper.MergeResult mergeResult = docMapperBefore.merge(docMapperAfter, mergeFlags().simulate(true));

        assertThat(Arrays.toString(mergeResult.conflicts()), mergeResult.hasConflicts(), equalTo(false));

        docMapperBefore.merge(docMapperAfter, mergeFlags().simulate(false));

        fields = docMapperBefore.mappers().name("copy_test").mapper().copyTo().copyToFields();

        assertThat(fields.size(), equalTo(2));
        assertThat(fields.get(0), equalTo("baz"));
        assertThat(fields.get(1), equalTo("bar"));
    }

    public void testCopyToNestedField() throws Exception {
        IndexService indexService = createIndex("test");
        DocumentMapperParser parser = indexService.mapperService().documentMapperParser();
        for (boolean mapped : new boolean[] {true, false}) {
            XContentBuilder mapping = jsonBuilder().startObject()
                    .startObject("type")
                        .startObject("properties")
                            .startObject("n1")
                                .field("type", "nested")
                                .startObject("properties")
                                    .startObject("n2")
                                        .field("type", "nested")
                                        .startObject("properties")
                                            .startObject("source")
                                                .field("type", "long")
                                                .startArray("copy_to")
                                                    .value("target") // should go to the root doc
                                                    .value("n1.target") // should go to the parent doc
                                                    .value("n1.n2.target") // should go to the current doc
                                                .endArray()
                                            .endObject();
            for (int i = 0; i < 3; ++i) {
                if (mapped) {
                    mapping = mapping.startObject("target").field("type", "long").endObject();
                }
                mapping = mapping.endObject().endObject();
            }
            mapping = mapping.endObject();

            DocumentMapper mapper = parser.parse(mapping.string());

            XContentBuilder jsonDoc = XContentFactory.jsonBuilder()
                    .startObject()
                        .startArray("n1")
                            .startObject()
                                .startArray("n2")
                                    .startObject()
                                        .field("source", 3)
                                    .endObject()
                                    .startObject()
                                        .field("source", 5)
                                    .endObject()
                                .endArray()
                            .endObject()
                            .startObject()
                                .startArray("n2")
                                    .startObject()
                                        .field("source", 7)
                                    .endObject()
                                .endArray()
                            .endObject()
                        .endArray()
                    .endObject();

            ParsedDocument doc = mapper.parse("type", "1", jsonDoc.bytes());
            assertEquals(6, doc.docs().size());

            Document nested = doc.docs().get(0);
            assertFieldValue(nested, "n1.n2.target", 7L);
            assertFieldValue(nested, "n1.target");
            assertFieldValue(nested, "target");

            nested = doc.docs().get(2);
            assertFieldValue(nested, "n1.n2.target", 5L);
            assertFieldValue(nested, "n1.target");
            assertFieldValue(nested, "target");

            nested = doc.docs().get(3);
            assertFieldValue(nested, "n1.n2.target", 3L);
            assertFieldValue(nested, "n1.target");
            assertFieldValue(nested, "target");

            Document parent = doc.docs().get(1);
            assertFieldValue(parent, "target");
            assertFieldValue(parent, "n1.target", 7L);
            assertFieldValue(parent, "n1.n2.target");

            parent = doc.docs().get(4);
            assertFieldValue(parent, "target");
            assertFieldValue(parent, "n1.target", 3L, 5L);
            assertFieldValue(parent, "n1.n2.target");

            Document root = doc.docs().get(5);
            assertFieldValue(root, "target", 3L, 5L, 7L);
            assertFieldValue(root, "n1.target");
            assertFieldValue(root, "n1.n2.target");
        }
    }

    private void assertFieldValue(Document doc, String field, Number... expected) {
        IndexableField[] values = doc.getFields(field);
        if (values == null) {
            values = new IndexableField[0];
        }
        Number[] actual = new Number[values.length];
        for (int i = 0; i < values.length; ++i) {
            actual[i] = values[i].numericValue();
        }
        assertArrayEquals(expected, actual);
    }

}
