/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * The OpenSearch Contributors require contributions made to
 * this file be licensed under the Apache-2.0 license or a
 * compatible open source license.
 */

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

/*
 * Modifications Copyright OpenSearch Contributors. See
 * GitHub history for details.
 */

package org.opensearch.index.query;

import org.apache.lucene.document.LatLonPoint;
import org.apache.lucene.document.LongPoint;
import org.apache.lucene.search.Query;
import org.opensearch.common.geo.GeoPoint;
import org.opensearch.common.geo.GeoUtils;
import org.opensearch.common.lucene.search.Queries;
import org.opensearch.common.unit.DistanceUnit;
import org.opensearch.common.unit.TimeValue;
import org.opensearch.index.mapper.DateFieldMapper;
import org.opensearch.index.mapper.DateFieldMapper.DateFieldType;
import org.opensearch.index.mapper.MapperService;
import org.opensearch.index.query.DistanceFeatureQueryBuilder.Origin;
import org.opensearch.test.AbstractQueryTestCase;
import org.joda.time.DateTime;

import java.io.IOException;
import java.time.Instant;

import static org.hamcrest.Matchers.containsString;

public class DistanceFeatureQueryBuilderTests extends AbstractQueryTestCase<DistanceFeatureQueryBuilder> {
    @Override
    protected DistanceFeatureQueryBuilder doCreateTestQueryBuilder() {
        String field = randomFrom(DATE_FIELD_NAME, DATE_NANOS_FIELD_NAME, GEO_POINT_FIELD_NAME);
        Origin origin;
        String pivot;
        switch (field) {
            case GEO_POINT_FIELD_NAME:
                GeoPoint point = new GeoPoint(randomDouble(), randomDouble());
                origin = randomBoolean() ? new Origin(point) : new Origin(point.geohash());
                pivot = randomFrom(DistanceUnit.values()).toString(randomDouble());
                break;
            case DATE_FIELD_NAME:
                long randomDateMills = randomLongBetween(0, 2_000_000_000_000L);
                origin = randomBoolean() ? new Origin(randomDateMills) : new Origin(new DateTime(randomDateMills).toString());
                pivot = randomTimeValue(1, 1000, "d", "h", "ms", "s", "m");
                break;
            default: // DATE_NANOS_FIELD_NAME
                randomDateMills = randomLongBetween(0, 2_000_000_000_000L);
                if (randomBoolean()) {
                    origin = new Origin(randomDateMills); // nano_dates long accept milliseconds since epoch
                } else {
                    long randomNanos = randomLongBetween(0, 1_000_000L);
                    Instant randomDateNanos = Instant.ofEpochMilli(randomDateMills).plusNanos(randomNanos);
                    origin = new Origin(randomDateNanos.toString());
                }
                pivot = randomTimeValue(1, 100_000_000, "nanos");
                break;
        }
        return new DistanceFeatureQueryBuilder(field, origin, pivot);
    }

    @Override
    protected void doAssertLuceneQuery(DistanceFeatureQueryBuilder queryBuilder, Query query, QueryShardContext context)
        throws IOException {
        String fieldName = expectedFieldName(queryBuilder.fieldName());
        Object origin = queryBuilder.origin().origin();
        String pivot = queryBuilder.pivot();
        final Query expectedQuery;
        if (fieldName.equals(GEO_POINT_FIELD_NAME)) {
            GeoPoint originGeoPoint = (origin instanceof GeoPoint) ? (GeoPoint) origin : GeoUtils.parseFromString((String) origin);
            double pivotDouble = DistanceUnit.DEFAULT.parse(pivot, DistanceUnit.DEFAULT);
            expectedQuery = LatLonPoint.newDistanceFeatureQuery(fieldName, 1.0f, originGeoPoint.lat(), originGeoPoint.lon(), pivotDouble);
        } else { // if (fieldName.equals(DATE_FIELD_NAME))
            MapperService mapperService = context.getMapperService();
            DateFieldType fieldType = (DateFieldType) mapperService.fieldType(fieldName);
            long originLong = fieldType.parseToLong(origin, true, null, null, context::nowInMillis);
            TimeValue pivotVal = TimeValue.parseTimeValue(pivot, DistanceFeatureQueryBuilder.class.getSimpleName() + ".pivot");
            long pivotLong;
            if (fieldType.resolution() == DateFieldMapper.Resolution.MILLISECONDS) {
                pivotLong = pivotVal.getMillis();
            } else { // NANOSECONDS
                pivotLong = pivotVal.getNanos();
            }
            expectedQuery = LongPoint.newDistanceFeatureQuery(fieldName, 1.0f, originLong, pivotLong);
        }
        assertEquals(expectedQuery, query);
    }

    public void testFromJsonDateFieldType() throws IOException {
        // origin as string
        String origin = "2018-01-01T13:10:30Z";
        String pivot = "7d";
        String json = "{\n"
            + "    \"distance_feature\" : {\n"
            + "            \"field\": \""
            + DATE_FIELD_NAME
            + "\",\n"
            + "            \"origin\": \""
            + origin
            + "\",\n"
            + "            \"pivot\" : \""
            + pivot
            + "\",\n"
            + "            \"boost\" : 1.0\n"
            + "    }\n"
            + "}";
        DistanceFeatureQueryBuilder parsed = (DistanceFeatureQueryBuilder) parseQuery(json);
        checkGeneratedJson(json, parsed);
        assertEquals(json, origin, parsed.origin().origin());
        assertEquals(json, pivot, parsed.pivot());
        assertEquals(json, 1.0, parsed.boost(), 0.0001);

        // origin as long
        long originLong = 1514812230999L;
        json = "{\n"
            + "    \"distance_feature\" : {\n"
            + "            \"field\": \""
            + DATE_FIELD_NAME
            + "\",\n"
            + "            \"origin\": "
            + originLong
            + ",\n"
            + "            \"pivot\" : \""
            + pivot
            + "\",\n"
            + "            \"boost\" : 1.0\n"
            + "    }\n"
            + "}";
        parsed = (DistanceFeatureQueryBuilder) parseQuery(json);
        assertEquals(json, originLong, parsed.origin().origin());
    }

    public void testFromJsonDateNanosFieldType() throws IOException {
        // origin as string
        String origin = "2018-01-01T13:10:30.323456789Z";
        String pivot = "100000000nanos";
        String json = "{\n"
            + "    \"distance_feature\" : {\n"
            + "            \"field\": \""
            + DATE_NANOS_FIELD_NAME
            + "\",\n"
            + "            \"origin\": \""
            + origin
            + "\",\n"
            + "            \"pivot\" : \""
            + pivot
            + "\",\n"
            + "            \"boost\" : 1.0\n"
            + "    }\n"
            + "}";
        DistanceFeatureQueryBuilder parsed = (DistanceFeatureQueryBuilder) parseQuery(json);
        checkGeneratedJson(json, parsed);
        assertEquals(json, origin, parsed.origin().origin());
        assertEquals(json, pivot, parsed.pivot());
        assertEquals(json, 1.0, parsed.boost(), 0.0001);

        // origin as long
        long originLong = 1514812230999L;
        json = "{\n"
            + "    \"distance_feature\" : {\n"
            + "            \"field\": \""
            + DATE_NANOS_FIELD_NAME
            + "\",\n"
            + "            \"origin\": "
            + originLong
            + ",\n"
            + "            \"pivot\" : \""
            + pivot
            + "\",\n"
            + "            \"boost\" : 1.0\n"
            + "    }\n"
            + "}";
        parsed = (DistanceFeatureQueryBuilder) parseQuery(json);
        assertEquals(json, originLong, parsed.origin().origin());
    }

    public void testFromJsonGeoFieldType() throws IOException {
        final GeoPoint origin = new GeoPoint(41.12, -71.34);
        final String pivot = "1km";

        // origin as string
        String json = "{\n"
            + "    \"distance_feature\" : {\n"
            + "            \"field\": \""
            + GEO_POINT_FIELD_NAME
            + "\",\n"
            + "            \"origin\": \""
            + origin.toString()
            + "\",\n"
            + "            \"pivot\" : \""
            + pivot
            + "\",\n"
            + "            \"boost\" : 2.0\n"
            + "    }\n"
            + "}";
        DistanceFeatureQueryBuilder parsed = (DistanceFeatureQueryBuilder) parseQuery(json);
        checkGeneratedJson(json, parsed);
        assertEquals(json, origin.toString(), parsed.origin().origin());
        assertEquals(json, pivot, parsed.pivot());
        assertEquals(json, 2.0, parsed.boost(), 0.0001);

        // origin as array
        json = "{\n"
            + "    \"distance_feature\" : {\n"
            + "            \"field\": \""
            + GEO_POINT_FIELD_NAME
            + "\",\n"
            + "            \"origin\": ["
            + origin.lon()
            + ", "
            + origin.lat()
            + "],\n"
            + "            \"pivot\" : \""
            + pivot
            + "\",\n"
            + "            \"boost\" : 2.0\n"
            + "    }\n"
            + "}";
        parsed = (DistanceFeatureQueryBuilder) parseQuery(json);
        assertEquals(json, origin, parsed.origin().origin());

        // origin as object
        json = "{\n"
            + "    \"distance_feature\" : {\n"
            + "            \"field\": \""
            + GEO_POINT_FIELD_NAME
            + "\",\n"
            + "            \"origin\": {"
            + "\"lat\":"
            + origin.lat()
            + ", \"lon\":"
            + origin.lon()
            + "},\n"
            + "            \"pivot\" : \""
            + pivot
            + "\",\n"
            + "            \"boost\" : 2.0\n"
            + "    }\n"
            + "}";
        parsed = (DistanceFeatureQueryBuilder) parseQuery(json);
        assertEquals(json, origin, parsed.origin().origin());
    }

    public void testQueryMatchNoDocsQueryWithUnmappedField() throws IOException {
        Query expectedQuery = Queries.newMatchNoDocsQuery("Can't run [" + DistanceFeatureQueryBuilder.NAME + "] query on unmapped fields!");
        String queryString = "{\n"
            + "    \"distance_feature\" : {\n"
            + "            \"field\": \"random_unmapped_field\",\n"
            + "            \"origin\": \"random_string\",\n"
            + "            \"pivot\" : \"random_string\"\n"
            + "    }\n"
            + "}";
        Query query = parseQuery(queryString).toQuery(createShardContext());
        assertEquals(expectedQuery, query);
    }

    public void testQueryFailsWithWrongFieldType() {
        String query = "{\n"
            + "    \"distance_feature\" : {\n"
            + "            \"field\": \""
            + INT_FIELD_NAME
            + "\",\n"
            + "            \"origin\": 40,\n"
            + "            \"pivot\" : \"random_string\"\n"
            + "    }\n"
            + "}";
        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> parseQuery(query).toQuery(createShardContext()));
        assertThat(e.getMessage(), containsString("query can only be run on a date, date_nanos or geo_point field type!"));
    }
}