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

import org.opensearch.Version;
import org.opensearch.cluster.metadata.IndexMetadata;
import org.opensearch.common.Explicit;
import org.opensearch.common.lucene.Lucene;
import org.opensearch.common.settings.Settings;
import org.opensearch.test.OpenSearchTestCase;

import java.util.Arrays;
import java.util.Collections;

import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;

public class FieldAliasMapperValidationTests extends OpenSearchTestCase {

    public void testDuplicateFieldAliasAndObject() {
        ObjectMapper objectMapper = createObjectMapper("some.path");
        FieldAliasMapper aliasMapper = new FieldAliasMapper("path", "some.path", "field");

        MapperParsingException e = expectThrows(
            MapperParsingException.class,
            () -> new MappingLookup(
                Collections.emptyList(),
                singletonList(objectMapper),
                singletonList(aliasMapper),
                0,
                Lucene.STANDARD_ANALYZER
            )
        );
        assertEquals("Alias [some.path] is defined both as an object and an alias", e.getMessage());
    }

    public void testDuplicateFieldAliasAndConcreteField() {
        FieldMapper field = new MockFieldMapper("field");
        FieldMapper invalidField = new MockFieldMapper("invalid");
        FieldAliasMapper invalidAlias = new FieldAliasMapper("invalid", "invalid", "field");

        MapperParsingException e = expectThrows(
            MapperParsingException.class,
            () -> new MappingLookup(
                Arrays.asList(field, invalidField),
                emptyList(),
                singletonList(invalidAlias),
                0,
                Lucene.STANDARD_ANALYZER
            )
        );

        assertEquals("Alias [invalid] is defined both as an alias and a concrete field", e.getMessage());
    }

    public void testAliasThatRefersToAlias() {
        FieldMapper field = new MockFieldMapper("field");
        FieldAliasMapper alias = new FieldAliasMapper("alias", "alias", "field");
        FieldAliasMapper invalidAlias = new FieldAliasMapper("invalid-alias", "invalid-alias", "alias");

        MappingLookup mappers = new MappingLookup(
            singletonList(field),
            emptyList(),
            Arrays.asList(alias, invalidAlias),
            0,
            Lucene.STANDARD_ANALYZER
        );
        alias.validate(mappers);

        MapperParsingException e = expectThrows(MapperParsingException.class, () -> invalidAlias.validate(mappers));

        assertEquals(
            "Invalid [path] value [alias] for field alias [invalid-alias]: an alias" + " cannot refer to another alias.",
            e.getMessage()
        );
    }

    public void testAliasThatRefersToItself() {
        FieldAliasMapper invalidAlias = new FieldAliasMapper("invalid-alias", "invalid-alias", "invalid-alias");

        MapperParsingException e = expectThrows(MapperParsingException.class, () -> {
            MappingLookup mappers = new MappingLookup(emptyList(), emptyList(), singletonList(invalidAlias), 0, null);
            invalidAlias.validate(mappers);
        });

        assertEquals(
            "Invalid [path] value [invalid-alias] for field alias [invalid-alias]: an alias" + " cannot refer to itself.",
            e.getMessage()
        );
    }

    public void testAliasWithNonExistentPath() {
        FieldAliasMapper invalidAlias = new FieldAliasMapper("invalid-alias", "invalid-alias", "non-existent");

        MapperParsingException e = expectThrows(MapperParsingException.class, () -> {
            MappingLookup mappers = new MappingLookup(emptyList(), emptyList(), singletonList(invalidAlias), 0, Lucene.STANDARD_ANALYZER);
            invalidAlias.validate(mappers);
        });

        assertEquals(
            "Invalid [path] value [non-existent] for field alias [invalid-alias]: an alias"
                + " must refer to an existing field in the mappings.",
            e.getMessage()
        );
    }

    public void testFieldAliasWithNestedScope() {
        ObjectMapper objectMapper = createNestedObjectMapper("nested");
        FieldAliasMapper aliasMapper = new FieldAliasMapper("alias", "nested.alias", "nested.field");

        MappingLookup mappers = new MappingLookup(
            singletonList(createFieldMapper("nested", "field")),
            singletonList(objectMapper),
            singletonList(aliasMapper),
            0,
            Lucene.STANDARD_ANALYZER
        );
        aliasMapper.validate(mappers);
    }

    public void testFieldAliasWithDifferentObjectScopes() {

        FieldAliasMapper aliasMapper = new FieldAliasMapper("alias", "object2.alias", "object1.field");

        MappingLookup mappers = new MappingLookup(
            singletonList(createFieldMapper("object1", "field")),
            Arrays.asList(createObjectMapper("object1"), createObjectMapper("object2")),
            singletonList(aliasMapper),
            0,
            Lucene.STANDARD_ANALYZER
        );
        aliasMapper.validate(mappers);
    }

    public void testFieldAliasWithNestedTarget() {
        ObjectMapper objectMapper = createNestedObjectMapper("nested");
        FieldAliasMapper aliasMapper = new FieldAliasMapper("alias", "alias", "nested.field");

        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
            MappingLookup mappers = new MappingLookup(
                singletonList(createFieldMapper("nested", "field")),
                Collections.singletonList(objectMapper),
                singletonList(aliasMapper),
                0,
                Lucene.STANDARD_ANALYZER
            );
            aliasMapper.validate(mappers);
        });

        String expectedMessage = "Invalid [path] value [nested.field] for field alias [alias]: "
            + "an alias must have the same nested scope as its target. The alias is not nested, "
            + "but the target's nested scope is [nested].";
        assertEquals(expectedMessage, e.getMessage());
    }

    public void testFieldAliasWithDifferentNestedScopes() {
        FieldAliasMapper aliasMapper = new FieldAliasMapper("alias", "nested2.alias", "nested1.field");

        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
            MappingLookup mappers = new MappingLookup(
                singletonList(createFieldMapper("nested1", "field")),
                Arrays.asList(createNestedObjectMapper("nested1"), createNestedObjectMapper("nested2")),
                singletonList(aliasMapper),
                0,
                Lucene.STANDARD_ANALYZER
            );
            aliasMapper.validate(mappers);
        });

        String expectedMessage = "Invalid [path] value [nested1.field] for field alias [nested2.alias]: "
            + "an alias must have the same nested scope as its target. The alias's nested scope is [nested2], "
            + "but the target's nested scope is [nested1].";
        assertEquals(expectedMessage, e.getMessage());
    }

    private static final Settings SETTINGS = Settings.builder()
        .put(IndexMetadata.SETTING_INDEX_VERSION_CREATED.getKey(), Version.CURRENT)
        .build();

    private static FieldMapper createFieldMapper(String parent, String name) {
        Mapper.BuilderContext context = new Mapper.BuilderContext(SETTINGS, new ContentPath(parent));
        return new BooleanFieldMapper.Builder(name).build(context);
    }

    private static ObjectMapper createObjectMapper(String name) {
        return new ObjectMapper(
            name,
            name,
            new Explicit<>(true, false),
            ObjectMapper.Nested.NO,
            ObjectMapper.Dynamic.FALSE,
            emptyMap(),
            SETTINGS
        );
    }

    private static ObjectMapper createNestedObjectMapper(String name) {
        return new ObjectMapper(
            name,
            name,
            new Explicit<>(true, false),
            ObjectMapper.Nested.newNested(),
            ObjectMapper.Dynamic.FALSE,
            emptyMap(),
            SETTINGS
        );
    }
}
