/*
 * 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.index.Term;
import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.Query;
import org.opensearch.OpenSearchParseException;
import org.opensearch.common.unit.Fuzziness;
import org.opensearch.core.common.ParsingException;
import org.opensearch.test.AbstractQueryTestCase;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;

public class FuzzyQueryBuilderTests extends AbstractQueryTestCase<FuzzyQueryBuilder> {

    @Override
    protected FuzzyQueryBuilder doCreateTestQueryBuilder() {
        String fieldName = randomFrom(TEXT_FIELD_NAME, TEXT_ALIAS_FIELD_NAME);
        FuzzyQueryBuilder query = new FuzzyQueryBuilder(fieldName, getRandomValueForFieldName(fieldName));
        if (randomBoolean()) {
            query.fuzziness(randomFuzziness(query.fieldName()));
        }
        if (randomBoolean()) {
            query.prefixLength(randomIntBetween(0, 10));
        }
        if (randomBoolean()) {
            query.maxExpansions(randomIntBetween(1, 10));
        }
        if (randomBoolean()) {
            query.transpositions(randomBoolean());
        }
        if (randomBoolean()) {
            query.rewrite(getRandomRewriteMethod());
        }
        return query;
    }

    @Override
    protected Map<String, FuzzyQueryBuilder> getAlternateVersions() {
        Map<String, FuzzyQueryBuilder> alternateVersions = new HashMap<>();
        FuzzyQueryBuilder fuzzyQuery = new FuzzyQueryBuilder(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10));
        String contentString = "{\n"
            + "    \"fuzzy\" : {\n"
            + "        \""
            + fuzzyQuery.fieldName()
            + "\" : \""
            + fuzzyQuery.value()
            + "\"\n"
            + "    }\n"
            + "}";
        alternateVersions.put(contentString, fuzzyQuery);
        return alternateVersions;
    }

    @Override
    protected void doAssertLuceneQuery(FuzzyQueryBuilder queryBuilder, Query query, QueryShardContext context) throws IOException {
        assertThat(query, instanceOf(FuzzyQuery.class));

        FuzzyQuery fuzzyQuery = (FuzzyQuery) query;
        String expectedFieldName = expectedFieldName(queryBuilder.fieldName());
        String actualFieldName = fuzzyQuery.getTerm().field();
        assertThat(actualFieldName, equalTo(expectedFieldName));
    }

    public void testIllegalArguments() {
        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new FuzzyQueryBuilder(null, "text"));
        assertEquals("field name cannot be null or empty", e.getMessage());

        e = expectThrows(IllegalArgumentException.class, () -> new FuzzyQueryBuilder("", "text"));
        assertEquals("field name cannot be null or empty", e.getMessage());

        e = expectThrows(IllegalArgumentException.class, () -> new FuzzyQueryBuilder("field", null));
        assertEquals("query value cannot be null", e.getMessage());
    }

    public void testUnsupportedFuzzinessForStringType() throws IOException {
        QueryShardContext context = createShardContext();
        context.setAllowUnmappedFields(true);
        FuzzyQueryBuilder fuzzyQueryBuilder = new FuzzyQueryBuilder(TEXT_FIELD_NAME, "text");
        fuzzyQueryBuilder.fuzziness(Fuzziness.build(randomFrom("a string which is not auto", "3h", "200s")));
        NumberFormatException e = expectThrows(NumberFormatException.class, () -> fuzzyQueryBuilder.toQuery(context));
        assertThat(e.getMessage(), containsString("For input string"));
    }

    public void testToQueryWithStringField() throws IOException {
        String query = "{\n"
            + "    \"fuzzy\":{\n"
            + "        \""
            + TEXT_FIELD_NAME
            + "\":{\n"
            + "            \"value\":\"sh\",\n"
            + "            \"fuzziness\": \"AUTO\",\n"
            + "            \"prefix_length\":1,\n"
            + "            \"boost\":2.0\n"
            + "        }\n"
            + "    }\n"
            + "}";
        Query parsedQuery = parseQuery(query).toQuery(createShardContext());
        assertThat(parsedQuery, instanceOf(BoostQuery.class));
        BoostQuery boostQuery = (BoostQuery) parsedQuery;
        assertThat(boostQuery.getBoost(), equalTo(2.0f));
        assertThat(boostQuery.getQuery(), instanceOf(FuzzyQuery.class));
        FuzzyQuery fuzzyQuery = (FuzzyQuery) boostQuery.getQuery();
        assertThat(fuzzyQuery.getTerm(), equalTo(new Term(TEXT_FIELD_NAME, "sh")));
        assertThat(fuzzyQuery.getMaxEdits(), equalTo(Fuzziness.AUTO.asDistance("sh")));
        assertThat(fuzzyQuery.getPrefixLength(), equalTo(1));
    }

    public void testToQueryWithStringFieldDefinedFuzziness() throws IOException {
        String query = "{\n"
            + "    \"fuzzy\":{\n"
            + "        \""
            + TEXT_FIELD_NAME
            + "\":{\n"
            + "            \"value\":\"sh\",\n"
            + "            \"fuzziness\": \"AUTO:2,5\",\n"
            + "            \"prefix_length\":1,\n"
            + "            \"boost\":2.0\n"
            + "        }\n"
            + "    }\n"
            + "}";
        Query parsedQuery = parseQuery(query).toQuery(createShardContext());
        assertThat(parsedQuery, instanceOf(BoostQuery.class));
        BoostQuery boostQuery = (BoostQuery) parsedQuery;
        assertThat(boostQuery.getBoost(), equalTo(2.0f));
        assertThat(boostQuery.getQuery(), instanceOf(FuzzyQuery.class));
        FuzzyQuery fuzzyQuery = (FuzzyQuery) boostQuery.getQuery();
        assertThat(fuzzyQuery.getTerm(), equalTo(new Term(TEXT_FIELD_NAME, "sh")));
        assertThat(fuzzyQuery.getMaxEdits(), equalTo(1));
        assertThat(fuzzyQuery.getPrefixLength(), equalTo(1));
    }

    public void testToQueryWithStringFieldDefinedWrongFuzziness() throws IOException {
        String queryMissingFuzzinessUpLimit = "{\n"
            + "    \"fuzzy\":{\n"
            + "        \""
            + TEXT_FIELD_NAME
            + "\":{\n"
            + "            \"value\":\"sh\",\n"
            + "            \"fuzziness\": \"AUTO:2\",\n"
            + "            \"prefix_length\":1,\n"
            + "            \"boost\":2.0\n"
            + "        }\n"
            + "    }\n"
            + "}";
        OpenSearchParseException e = expectThrows(
            OpenSearchParseException.class,
            () -> parseQuery(queryMissingFuzzinessUpLimit).toQuery(createShardContext())
        );
        String msg = "failed to find low and high distance values";
        assertTrue(e.getMessage() + " didn't contain: " + msg + " but: " + e.getMessage(), e.getMessage().contains(msg));

        String queryHavingNegativeFuzzinessLowLimit = "{\n"
            + "    \"fuzzy\":{\n"
            + "        \""
            + TEXT_FIELD_NAME
            + "\":{\n"
            + "            \"value\":\"sh\",\n"
            + "            \"fuzziness\": \"AUTO:-1,6\",\n"
            + "            \"prefix_length\":1,\n"
            + "            \"boost\":2.0\n"
            + "        }\n"
            + "    }\n"
            + "}";
        String msg2 = "fuzziness wrongly configured";
        IllegalArgumentException e2 = expectThrows(
            IllegalArgumentException.class,
            () -> parseQuery(queryHavingNegativeFuzzinessLowLimit).toQuery(createShardContext())
        );
        assertTrue(e2.getMessage() + " didn't contain: " + msg2 + " but: " + e.getMessage(), e.getMessage().contains(msg));

        String queryMissingFuzzinessUpLimit2 = "{\n"
            + "    \"fuzzy\":{\n"
            + "        \""
            + TEXT_FIELD_NAME
            + "\":{\n"
            + "            \"value\":\"sh\",\n"
            + "            \"fuzziness\": \"AUTO:1,\",\n"
            + "            \"prefix_length\":1,\n"
            + "            \"boost\":2.0\n"
            + "        }\n"
            + "    }\n"
            + "}";
        e = expectThrows(OpenSearchParseException.class, () -> parseQuery(queryMissingFuzzinessUpLimit2).toQuery(createShardContext()));
        assertTrue(e.getMessage() + " didn't contain: " + msg + " but: " + e.getMessage(), e.getMessage().contains(msg));

        String queryMissingFuzzinessLowLimit = "{\n"
            + "    \"fuzzy\":{\n"
            + "        \""
            + TEXT_FIELD_NAME
            + "\":{\n"
            + "            \"value\":\"sh\",\n"
            + "            \"fuzziness\": \"AUTO:,5\",\n"
            + "            \"prefix_length\":1,\n"
            + "            \"boost\":2.0\n"
            + "        }\n"
            + "    }\n"
            + "}";
        e = expectThrows(OpenSearchParseException.class, () -> parseQuery(queryMissingFuzzinessLowLimit).toQuery(createShardContext()));
        msg = "failed to parse [AUTO:,5] as a \"auto:int,int\"";
        assertTrue(e.getMessage() + " didn't contain: " + msg + " but: " + e.getMessage(), e.getMessage().contains(msg));
    }

    public void testToQueryWithNumericField() throws IOException {
        String query = "{\n"
            + "    \"fuzzy\":{\n"
            + "        \""
            + INT_FIELD_NAME
            + "\":{\n"
            + "            \"value\":12,\n"
            + "            \"fuzziness\":5\n"
            + "        }\n"
            + "    }\n"
            + "}\n";
        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> parseQuery(query).toQuery(createShardContext()));
        assertEquals(
            "Can only use fuzzy queries on keyword and text fields - not on [mapped_int] which is of type [integer]",
            e.getMessage()
        );
    }

    public void testFromJson() throws IOException {
        String json = "{\n"
            + "  \"fuzzy\" : {\n"
            + "    \"user\" : {\n"
            + "      \"value\" : \"ki\",\n"
            + "      \"fuzziness\" : \"2\",\n"
            + "      \"prefix_length\" : 0,\n"
            + "      \"max_expansions\" : 100,\n"
            + "      \"transpositions\" : false,\n"
            + "      \"boost\" : 42.0\n"
            + "    }\n"
            + "  }\n"
            + "}";
        FuzzyQueryBuilder parsed = (FuzzyQueryBuilder) parseQuery(json);
        checkGeneratedJson(json, parsed);
        assertEquals(json, 42.0, parsed.boost(), 0.00001);
        assertEquals(json, 2, parsed.fuzziness().asFloat(), 0f);
        assertEquals(json, false, parsed.transpositions());
    }

    public void testParseFailsWithMultipleFields() throws IOException {
        String json1 = "{\n"
            + "  \"fuzzy\" : {\n"
            + "    \"message1\" : {\n"
            + "      \"value\" : \"this is a test\"\n"
            + "    }\n"
            + "  }\n"
            + "}";
        parseQuery(json1); // should be all good

        String json2 = "{\n"
            + "  \"fuzzy\" : {\n"
            + "    \"message1\" : {\n"
            + "      \"value\" : \"this is a test\"\n"
            + "    },\n"
            + "    \"message2\" : {\n"
            + "      \"value\" : \"this is a test\"\n"
            + "    }\n"
            + "  }\n"
            + "}";

        ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(json2));
        assertEquals("[fuzzy] query doesn't support multiple fields, found [message1] and [message2]", e.getMessage());

        String shortJson = "{\n"
            + "  \"fuzzy\" : {\n"
            + "    \"message1\" : \"this is a test\",\n"
            + "    \"message2\" : \"value\" : \"this is a test\"\n"
            + "  }\n"
            + "}";

        e = expectThrows(ParsingException.class, () -> parseQuery(shortJson));
        assertEquals("[fuzzy] query doesn't support multiple fields, found [message1] and [message2]", e.getMessage());
    }

    public void testParseFailsWithValueArray() {
        String query = "{\n"
            + "  \"fuzzy\" : {\n"
            + "    \"message1\" : {\n"
            + "      \"value\" : [ \"one\", \"two\", \"three\"]\n"
            + "    }\n"
            + "  }\n"
            + "}";

        ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(query));
        assertEquals("[fuzzy] unexpected token [START_ARRAY] after [value]", e.getMessage());
    }

    public void testToQueryWithTranspositions() throws Exception {
        Query query = new FuzzyQueryBuilder(TEXT_FIELD_NAME, "text").toQuery(createShardContext());
        assertThat(query, instanceOf(FuzzyQuery.class));
        assertEquals(FuzzyQuery.defaultTranspositions, ((FuzzyQuery) query).getTranspositions());

        query = new FuzzyQueryBuilder(TEXT_FIELD_NAME, "text").transpositions(true).toQuery(createShardContext());
        assertThat(query, instanceOf(FuzzyQuery.class));
        assertEquals(true, ((FuzzyQuery) query).getTranspositions());

        query = new FuzzyQueryBuilder(TEXT_FIELD_NAME, "text").transpositions(false).toQuery(createShardContext());
        assertThat(query, instanceOf(FuzzyQuery.class));
        assertEquals(false, ((FuzzyQuery) query).getTranspositions());
    }
}
