/*
 * 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;

import org.apache.lucene.index.IndexableField;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.index.mapper.ParseContext.Document;
import org.elasticsearch.test.ESSingleNodeTestCase;

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

import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;

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

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

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

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

        IndexService index = createIndex("test");
        client().admin().indices().preparePutMapping("test").setType("type1").setSource(mapping, XContentType.JSON).get();
        DocumentMapper docMapper = index.mapperService().documentMapper("type1");
        FieldMapper fieldMapper = docMapper.mappers().getMapper("copy_test");

        // Check json serialization
        TextFieldMapper stringFieldMapper = (TextFieldMapper) fieldMapper;
        XContentBuilder builder = jsonBuilder().startObject();
        stringFieldMapper.toXContent(builder, ToXContent.EMPTY_PARAMS).endObject();
        builder.close();
        Map<String, Object> serializedMap;
        try (XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes())) {
            serializedMap = parser.map();
        }
        Map<String, Object> copyTestMap = (Map<String, Object>) serializedMap.get("copy_test");
        assertThat(copyTestMap.get("type").toString(), is("text"));
        List<String> copyToList = (List<String>) copyTestMap.get("copy_to");
        assertThat(copyToList.size(), equalTo(2));
        assertThat(copyToList.get(0), equalTo("another_field"));
        assertThat(copyToList.get(1), 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();

        ParsedDocument parsedDoc = docMapper.parse(SourceToParse.source("test", "type1", "1", json, XContentType.JSON));
        ParseContext.Document doc = parsedDoc.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(2)); // new field has doc values
        assertThat(doc.getFields("new_field")[0].numericValue().intValue(), equalTo(42));

        assertNotNull(parsedDoc.dynamicMappingsUpdate());
        client().admin().indices().preparePutMapping("test").setType("type1")
            .setSource(parsedDoc.dynamicMappingsUpdate().toString(), XContentType.JSON).get();

        docMapper = index.mapperService().documentMapper("type1");
        fieldMapper = docMapper.mappers().getMapper("new_field");
        assertThat(fieldMapper.fieldType().typeName(), equalTo("long"));
    }

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

                .startObject("copy_test")
                .field("type", "text")
                .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("type1", new CompressedXContent(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(SourceToParse.source("test", "type1", "1", json, 
                XContentType.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"));

    }

    public void testCopyToDynamicInnerObjectParsing() throws Exception {
        String mapping = jsonBuilder().startObject().startObject("type1")
            .startObject("properties")
                .startObject("copy_test")
                    .field("type", "text")
                    .field("copy_to", "very.inner.field")
                .endObject()
            .endObject()
            .endObject().endObject().string();

        DocumentMapper docMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping));

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

        ParseContext.Document doc = docMapper.parse(SourceToParse.source("test", "type1", "1", json,
                XContentType.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"));

        assertThat(doc.getFields("new_field").length, equalTo(1));
        assertThat(doc.getFields("new_field")[0].stringValue(), equalTo("bar"));
    }

    public void testCopyToDynamicInnerInnerObjectParsing() throws Exception {
        String mapping = jsonBuilder().startObject().startObject("type1")
            .startObject("properties")
                .startObject("copy_test")
                    .field("type", "text")
                    .field("copy_to", "very.far.inner.field")
                .endObject()
                .startObject("very")
                    .field("type", "object")
                    .startObject("properties")
                        .startObject("far")
                            .field("type", "object")
                        .endObject()
                    .endObject()
                .endObject()
            .endObject()
            .endObject().endObject().string();

        DocumentMapper docMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping));

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

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

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

        assertThat(doc.getFields("new_field").length, equalTo(1));
        assertThat(doc.getFields("new_field")[0].stringValue(), equalTo("bar"));
    }

    public void testCopyToStrictDynamicInnerObjectParsing() throws Exception {
        String mapping = jsonBuilder().startObject().startObject("type1")
            .field("dynamic", "strict")
                .startObject("properties")
                    .startObject("copy_test")
                        .field("type", "text")
                        .field("copy_to", "very.inner.field")
                    .endObject()
                .endObject()
            .endObject().endObject().string();

        DocumentMapper docMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping));

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

        try {
            docMapper.parse(SourceToParse.source("test", "type1", "1", json, XContentType.JSON)).rootDoc();
            fail();
        } catch (MapperParsingException ex) {
            assertThat(ex.getMessage(), startsWith("mapping set to strict, dynamic introduction of [very] within [type1] is not allowed"));
        }
    }

    public void testCopyToInnerStrictDynamicInnerObjectParsing() throws Exception {
        String mapping = jsonBuilder().startObject().startObject("type1")
            .startObject("properties")
                .startObject("copy_test")
                    .field("type", "text")
                    .field("copy_to", "very.far.field")
                .endObject()
                .startObject("very")
                    .field("type", "object")
                    .startObject("properties")
                        .startObject("far")
                            .field("type", "object")
                            .field("dynamic", "strict")
                        .endObject()
                    .endObject()
                .endObject()

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

        DocumentMapper docMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping));

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

        try {
            docMapper.parse(SourceToParse.source("test", "type1", "1", json, XContentType.JSON)).rootDoc();
            fail();
        } catch (MapperParsingException ex) {
          assertThat(ex.getMessage(), startsWith("mapping set to strict, dynamic introduction of [field] within [very.far] is not allowed"));
        }
    }

    public void testCopyToFieldMerge() throws Exception {
        String mappingBefore = jsonBuilder().startObject().startObject("type1").startObject("properties")

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

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

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

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

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

        MapperService mapperService = createIndex("test").mapperService();
        DocumentMapper docMapperBefore = mapperService.merge("type1", new CompressedXContent(mappingBefore), MapperService.MergeReason.MAPPING_UPDATE, false);

        assertEquals(Arrays.asList("foo", "bar"), docMapperBefore.mappers().getMapper("copy_test").copyTo().copyToFields());

        DocumentMapper docMapperAfter = mapperService.merge("type1", new CompressedXContent(mappingAfter), MapperService.MergeReason.MAPPING_UPDATE, false);

        assertEquals(Arrays.asList("baz", "bar"), docMapperAfter.mappers().getMapper("copy_test").copyTo().copyToFields());
        assertEquals(Arrays.asList("foo", "bar"), docMapperBefore.mappers().getMapper("copy_test").copyTo().copyToFields());
    }

    public void testCopyToNestedField() throws Exception {
        IndexService indexService = createIndex("test");
        DocumentMapperParser parser = indexService.mapperService().documentMapperParser();
        XContentBuilder mapping = jsonBuilder().startObject()
                .startObject("type")
                    .startObject("properties")
                        .startObject("target")
                            .field("type", "long")
                            .field("doc_values", false)
                        .endObject()
                        .startObject("n1")
                            .field("type", "nested")
                            .startObject("properties")
                                .startObject("target")
                                    .field("type", "long")
                                    .field("doc_values", false)
                                .endObject()
                                .startObject("n2")
                                    .field("type", "nested")
                                    .startObject("properties")
                                        .startObject("target")
                                            .field("type", "long")
                                            .field("doc_values", false)
                                        .endObject()
                                        .startObject("source")
                                            .field("type", "long")
                                            .field("doc_values", false)
                                            .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()
                                    .endObject()
                                .endObject()
                            .endObject()
                        .endObject()
                    .endObject()
                .endObject()
            .endObject();

            DocumentMapper mapper = parser.parse("type", new CompressedXContent(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(SourceToParse.source("test", "type", "1", jsonDoc.bytes(), XContentType.JSON));
        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");
    }

    public void testCopyToDynamicNestedObjectParsing() throws Exception {
        String mapping = jsonBuilder().startObject().startObject("type1")
            .startArray("dynamic_templates")
                .startObject()
                    .startObject("objects")
                        .field("match_mapping_type", "object")
                        .startObject("mapping")
                            .field("type", "nested")
                        .endObject()
                    .endObject()
                .endObject()
            .endArray()
            .startObject("properties")
                .startObject("copy_test")
                    .field("type", "text")
                    .field("copy_to", "very.inner.field")
                .endObject()
            .endObject()
            .endObject().endObject().string();

        DocumentMapper docMapper = createIndex("test").mapperService().documentMapperParser().parse("type1", new CompressedXContent(mapping));

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

        try {
          docMapper.parse(SourceToParse.source("test", "type1", "1", json, XContentType.JSON)).rootDoc();
          fail();
        } catch (MapperParsingException ex) {
            assertThat(ex.getMessage(), startsWith("It is forbidden to create dynamic nested objects ([very]) through `copy_to`"));
        }
    }

    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);
    }

}
