/*
 * 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.document.InetAddressPoint;
import org.apache.lucene.index.IndexableField;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.network.InetAddresses;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;

import java.io.IOException;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;

import static org.elasticsearch.index.query.RangeQueryBuilder.GT_FIELD;
import static org.elasticsearch.index.query.RangeQueryBuilder.GTE_FIELD;
import static org.elasticsearch.index.query.RangeQueryBuilder.LT_FIELD;
import static org.elasticsearch.index.query.RangeQueryBuilder.LTE_FIELD;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;

public class RangeFieldMapperTests extends AbstractNumericFieldMapperTestCase {
    private static String FROM_DATE = "2016-10-31";
    private static String TO_DATE = "2016-11-01 20:00:00";
    private static String FROM_IP = "::ffff:c0a8:107";
    private static String TO_IP = "2001:db8::";
    private static int FROM = 5;
    private static String FROM_STR = FROM + "";
    private static int TO = 10;
    private static String TO_STR = TO + "";
    private static String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis";

    @Override
    protected void setTypeList() {
        TYPES = new HashSet<>(Arrays.asList("date_range", "ip_range", "float_range", "double_range", "integer_range", "long_range"));
    }

    private Object getFrom(String type) {
        if (type.equals("date_range")) {
            return FROM_DATE;
        } else if (type.equals("ip_range")) {
            return FROM_IP;
        }
        return random().nextBoolean() ? FROM : FROM_STR;
    }

    private String getFromField() {
        return random().nextBoolean() ? GT_FIELD.getPreferredName() : GTE_FIELD.getPreferredName();
    }

    private String getToField() {
        return random().nextBoolean() ? LT_FIELD.getPreferredName() : LTE_FIELD.getPreferredName();
    }

    private Object getTo(String type) {
        if (type.equals("date_range")) {
            return TO_DATE;
        } else if (type.equals("ip_range")) {
            return TO_IP;
        }
        return random().nextBoolean() ? TO : TO_STR;
    }

    private Object getMax(String type) {
        if (type.equals("date_range") || type.equals("long_range")) {
            return Long.MAX_VALUE;
        } else if (type.equals("ip_range")) {
            return InetAddressPoint.MAX_VALUE;
        } else if (type.equals("integer_range")) {
            return Integer.MAX_VALUE;
        } else if (type.equals("float_range")) {
            return Float.POSITIVE_INFINITY;
        }
        return Double.POSITIVE_INFINITY;
    }

    @Override
    public void doTestDefaults(String type) throws Exception {
        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type")
            .startObject("properties").startObject("field").field("type", type);
        if (type.equals("date_range")) {
            mapping = mapping.field("format", DATE_FORMAT);
        }
        mapping = mapping.endObject().endObject().endObject().endObject();

        DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping.string()));
        assertEquals(mapping.string(), mapper.mappingSource().toString());

        ParsedDocument doc = mapper.parse(SourceToParse.source("test", "type", "1", XContentFactory.jsonBuilder()
            .startObject()
            .startObject("field")
            .field(getFromField(), getFrom(type))
            .field(getToField(), getTo(type))
            .endObject()
            .endObject().bytes(),
            XContentType.JSON));

        IndexableField[] fields = doc.rootDoc().getFields("field");
        assertEquals(1, fields.length);
        IndexableField pointField = fields[0];
        assertEquals(2, pointField.fieldType().pointDimensionCount());
        assertFalse(pointField.fieldType().stored());
    }

    @Override
    protected void doTestNotIndexed(String type) throws Exception {
        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type")
            .startObject("properties").startObject("field").field("type", type).field("index", false);
        if (type.equals("date_range")) {
            mapping = mapping.field("format", DATE_FORMAT);
        }
        mapping = mapping.endObject().endObject().endObject().endObject();

        DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping.string()));
        assertEquals(mapping.string(), mapper.mappingSource().toString());

        ParsedDocument doc = mapper.parse(SourceToParse.source("test", "type", "1", XContentFactory.jsonBuilder()
            .startObject()
            .startObject("field")
            .field(getFromField(), getFrom(type))
            .field(getToField(), getTo(type))
            .endObject()
            .endObject().bytes(),
            XContentType.JSON));

        IndexableField[] fields = doc.rootDoc().getFields("field");
        assertEquals(0, fields.length);
    }

    @Override
    protected void doTestNoDocValues(String type) throws Exception {
        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type")
            .startObject("properties").startObject("field").field("type", type).field("doc_values", false);
        if (type.equals("date_range")) {
            mapping = mapping.field("format", DATE_FORMAT);
        }
        mapping = mapping.endObject().endObject().endObject().endObject();
        DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping.string()));
        assertEquals(mapping.string(), mapper.mappingSource().toString());

        ParsedDocument doc = mapper.parse(SourceToParse.source("test", "type", "1", XContentFactory.jsonBuilder()
            .startObject()
            .startObject("field")
            .field(getFromField(), getFrom(type))
            .field(getToField(), getTo(type))
            .endObject()
            .endObject().bytes(),
            XContentType.JSON));

        IndexableField[] fields = doc.rootDoc().getFields("field");
        assertEquals(1, fields.length);
        IndexableField pointField = fields[0];
        assertEquals(2, pointField.fieldType().pointDimensionCount());
    }

    @Override
    protected void doTestStore(String type) throws Exception {
        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type")
            .startObject("properties").startObject("field").field("type", type).field("store", true);
        if (type.equals("date_range")) {
            mapping = mapping.field("format", DATE_FORMAT);
        }
        mapping = mapping.endObject().endObject().endObject().endObject();
        DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping.string()));
        assertEquals(mapping.string(), mapper.mappingSource().toString());

        ParsedDocument doc = mapper.parse(SourceToParse.source("test", "type", "1", XContentFactory.jsonBuilder()
            .startObject()
            .startObject("field")
            .field(getFromField(), getFrom(type))
            .field(getToField(), getTo(type))
            .endObject()
            .endObject().bytes(),
            XContentType.JSON));

        IndexableField[] fields = doc.rootDoc().getFields("field");
        assertEquals(2, fields.length);
        IndexableField pointField = fields[0];
        assertEquals(2, pointField.fieldType().pointDimensionCount());
        IndexableField storedField = fields[1];
        assertTrue(storedField.fieldType().stored());
        String strVal = "5";
        if (type.equals("date_range")) {
            strVal = "1477872000000";
        } else if (type.equals("ip_range")) {
            strVal = InetAddresses.toAddrString(InetAddresses.forString("192.168.1.7")) + " : "
                + InetAddresses.toAddrString(InetAddresses.forString("2001:db8:0:0:0:0:0:0"));
        }
        assertThat(storedField.stringValue(), containsString(strVal));
    }

    @Override
    public void doTestCoerce(String type) throws IOException {
        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type")
            .startObject("properties").startObject("field").field("type", type);
        if (type.equals("date_range")) {
            mapping = mapping.field("format", DATE_FORMAT);
        }
        mapping = mapping.endObject().endObject().endObject().endObject();
        DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping.string()));

        assertEquals(mapping.string(), mapper.mappingSource().toString());

        ParsedDocument doc = mapper.parse(SourceToParse.source("test", "type", "1", XContentFactory.jsonBuilder()
            .startObject()
            .startObject("field")
            .field(getFromField(), getFrom(type))
            .field(getToField(), getTo(type))
            .endObject()
            .endObject().bytes(),
            XContentType.JSON));

        IndexableField[] fields = doc.rootDoc().getFields("field");
        assertEquals(1, fields.length);
        IndexableField pointField = fields[0];
        assertEquals(2, pointField.fieldType().pointDimensionCount());

        mapping = XContentFactory.jsonBuilder().startObject().startObject("type")
            .startObject("properties").startObject("field").field("type", type).field("coerce", false).endObject().endObject()
            .endObject().endObject();
        DocumentMapper mapper2 = parser.parse("type", new CompressedXContent(mapping.string()));

        assertEquals(mapping.string(), mapper2.mappingSource().toString());

        ThrowingRunnable runnable = () -> mapper2.parse(SourceToParse.source("test", "type", "1", XContentFactory.jsonBuilder()
            .startObject()
            .startObject("field")
            .field(getFromField(), "5.2")
            .field(getToField(), "10")
            .endObject()
            .endObject().bytes(),
            XContentType.JSON));
        MapperParsingException e = expectThrows(MapperParsingException.class, runnable);
        assertThat(e.getCause().getMessage(), anyOf(containsString("passed as String"),
            containsString("failed to parse date"), containsString("is not an IP string literal")));
    }

    @Override
    protected void doTestNullValue(String type) throws IOException {
        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type")
            .startObject("properties").startObject("field").field("type", type).field("store", true);
        if (type.equals("date_range")) {
            mapping = mapping.field("format", DATE_FORMAT);
        }
        mapping = mapping.endObject().endObject().endObject().endObject();

        DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping.string()));
        assertEquals(mapping.string(), mapper.mappingSource().toString());

        // test null value for min and max
        ParsedDocument doc = mapper.parse(SourceToParse.source("test", "type", "1", XContentFactory.jsonBuilder()
            .startObject()
            .startObject("field")
            .nullField(getFromField())
            .nullField(getToField())
            .endObject()
            .endObject().bytes(),
            XContentType.JSON));
        assertEquals(2, doc.rootDoc().getFields("field").length);
        IndexableField[] fields = doc.rootDoc().getFields("field");
        IndexableField storedField = fields[1];
        String expected = type.equals("ip_range") ? InetAddresses.toAddrString((InetAddress)getMax(type)) : getMax(type) +"";
        assertThat(storedField.stringValue(), containsString(expected));

        // test null max value
        doc = mapper.parse(SourceToParse.source("test", "type", "1", XContentFactory.jsonBuilder()
            .startObject()
            .startObject("field")
            .field(getFromField(), getFrom(type))
            .nullField(getToField())
            .endObject()
            .endObject().bytes(),
            XContentType.JSON));

        fields = doc.rootDoc().getFields("field");
        assertEquals(2, fields.length);
        IndexableField pointField = fields[0];
        assertEquals(2, pointField.fieldType().pointDimensionCount());
        assertFalse(pointField.fieldType().stored());
        storedField = fields[1];
        assertTrue(storedField.fieldType().stored());
        String strVal = "5";
        if (type.equals("date_range")) {
            strVal = "1477872000000";
        } else if (type.equals("ip_range")) {
            strVal = InetAddresses.toAddrString(InetAddresses.forString("192.168.1.7")) + " : "
                + InetAddresses.toAddrString(InetAddresses.forString("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
        }
        assertThat(storedField.stringValue(), containsString(strVal));
    }

    public void testNoBounds() throws Exception {
        for (String type : TYPES) {
            doTestNoBounds(type);
        }
    }

    public void doTestNoBounds(String type) throws IOException {
        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type")
            .startObject("properties").startObject("field").field("type", type).field("store", true);
        if (type.equals("date_range")) {
            mapping = mapping.field("format", DATE_FORMAT);
        }
        mapping = mapping.endObject().endObject().endObject().endObject();

        DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping.string()));
        assertEquals(mapping.string(), mapper.mappingSource().toString());

        // test no bounds specified
        ParsedDocument doc = mapper.parse(SourceToParse.source("test", "type", "1", XContentFactory.jsonBuilder()
            .startObject()
            .startObject("field")
            .endObject()
            .endObject().bytes(),
            XContentType.JSON));

        IndexableField[] fields = doc.rootDoc().getFields("field");
        assertEquals(2, fields.length);
        IndexableField pointField = fields[0];
        assertEquals(2, pointField.fieldType().pointDimensionCount());
        assertFalse(pointField.fieldType().stored());
        IndexableField storedField = fields[1];
        assertTrue(storedField.fieldType().stored());
        String expected = type.equals("ip_range") ? InetAddresses.toAddrString((InetAddress)getMax(type)) : getMax(type) +"";
        assertThat(storedField.stringValue(), containsString(expected));
    }

    public void testIllegalArguments() throws Exception {
        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type")
            .startObject("properties").startObject("field").field("type", RangeFieldMapper.RangeType.INTEGER.name)
            .field("format", DATE_FORMAT).endObject().endObject().endObject().endObject();

        ThrowingRunnable runnable = () -> parser.parse("type", new CompressedXContent(mapping.string()));
        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, runnable);
        assertThat(e.getMessage(), containsString("should not define a dateTimeFormatter"));
    }

    public void testSerializeDefaults() throws Exception {
        for (String type : TYPES) {
            String mapping = XContentFactory.jsonBuilder().startObject().startObject("type")
                .startObject("properties").startObject("field").field("type", type).endObject().endObject()
                .endObject().endObject().string();

            DocumentMapper docMapper = parser.parse("type", new CompressedXContent(mapping));
            RangeFieldMapper mapper = (RangeFieldMapper) docMapper.root().getMapper("field");
            XContentBuilder builder = XContentFactory.jsonBuilder().startObject();
            mapper.doXContentBody(builder, true, ToXContent.EMPTY_PARAMS);
            String got = builder.endObject().string();

            // if type is date_range we check that the mapper contains the default format and locale
            // otherwise it should not contain a locale or format
            assertTrue(got, got.contains("\"format\":\"strict_date_optional_time||epoch_millis\"") == type.equals("date_range"));
            assertTrue(got, got.contains("\"locale\":" + "\"" + Locale.ROOT + "\"") == type.equals("date_range"));
        }
    }
}
