/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.apache.commons.jxpath.ri.axes;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.util.HashMap;

import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.NestedTestBean;
import org.apache.commons.jxpath.Pointer;
import org.apache.commons.jxpath.TestNull;
import org.apache.commons.jxpath.ri.model.NodePointer;
import org.apache.commons.jxpath.ri.model.VariablePointer;
import org.apache.commons.jxpath.ri.model.beans.BeanPointer;
import org.apache.commons.jxpath.ri.model.beans.BeanPropertyPointer;
import org.apache.commons.jxpath.ri.model.beans.CollectionPointer;
import org.apache.commons.jxpath.ri.model.beans.NullElementPointer;
import org.apache.commons.jxpath.ri.model.beans.NullPointer;
import org.apache.commons.jxpath.ri.model.beans.NullPropertyPointer;
import org.apache.commons.jxpath.ri.model.beans.TestBeanFactory;
import org.apache.commons.jxpath.ri.model.dom.DOMNodePointer;
import org.apache.commons.jxpath.ri.model.dynamic.DynamicPointer;
import org.apache.commons.jxpath.ri.model.dynamic.DynamicPropertyPointer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class SimplePathInterpreterTest {

    private TestBeanWithNode bean;
    private JXPathContext context;

    private void assertNullPointer(final String path, final String expectedPath, final String expectedSignature) {
        final Pointer pointer = context.getPointer(path);
        assertNotNull(pointer, "Null path exists: " + path);
        assertEquals(expectedPath, pointer.asPath(), "Null path as path: " + path);
        assertEquals(expectedSignature, pointerSignature(pointer), "Checking Signature: " + path);
        final Pointer vPointer = ((NodePointer) pointer).getValuePointer();
        assertFalse(((NodePointer) vPointer).isActual(), "Null path is null: " + path);
        assertEquals(expectedSignature + "N", pointerSignature(vPointer), "Checking value pointer signature: " + path);
    }

    private void assertValueAndPointer(final String path, final Object expectedValue, final String expectedPath, final String expectedSignature) {
        assertValueAndPointer(path, expectedValue, expectedPath, expectedSignature, expectedSignature);
    }

    private void assertValueAndPointer(final String path, final Object expectedValue, final String expectedPath, final String expectedSignature,
            final String expectedValueSignature) {
        final Object value = context.getValue(path);
        assertEquals(expectedValue, value, "Checking value: " + path);
        final Pointer pointer = context.getPointer(path);
        assertEquals(expectedPath, pointer.toString(), "Checking pointer: " + path);
        assertEquals(expectedSignature, pointerSignature(pointer), "Checking signature: " + path);
        final Pointer vPointer = ((NodePointer) pointer).getValuePointer();
        assertEquals(expectedValueSignature, pointerSignature(vPointer), "Checking value pointer signature: " + path);
    }

    /**
     * Since we need to test the internal Signature of a pointer, we will get a signature which will contain a single character per pointer in the chain,
     * representing that pointer's type.
     */
    private String pointerSignature(final Pointer pointer) {
        if (pointer == null) {
            return "";
        }
        char type = '?';
        if (pointer instanceof NullPointer) {
            type = 'N';
        } else if (pointer instanceof NullPropertyPointer) {
            type = 'n';
        } else if (pointer instanceof NullElementPointer) {
            type = 'E';
        } else if (pointer instanceof VariablePointer) {
            type = 'V';
        } else if (pointer instanceof CollectionPointer) {
            type = 'C';
        } else if (pointer instanceof BeanPointer) {
            type = 'B';
        } else if (pointer instanceof BeanPropertyPointer) {
            type = 'b';
        } else if (pointer instanceof DynamicPointer) {
            type = 'D';
        } else if (pointer instanceof DynamicPropertyPointer) {
            type = 'd';
        } else if (pointer instanceof DOMNodePointer) {
            type = 'M';
        } else {
            System.err.println("UNKNOWN TYPE: " + pointer.getClass());
        }
        final NodePointer parent = ((NodePointer) pointer).getImmediateParentPointer();
        return pointerSignature(parent) + type;
    }

    @BeforeEach
    protected void setUp() throws Exception {
        bean = TestBeanWithNode.createTestBeanWithDOM();
        final HashMap submap = new HashMap();
        submap.put("key", new NestedTestBean("Name 9"));
        submap.put("strings", bean.getNestedBean().getStrings());
        bean.getList().add(new int[] { 1, 2 });
        bean.getList().add(bean.getVendor());
        bean.getMap().put("Key3", new Object[] { new NestedTestBean("some"), Integer.valueOf(2), bean.getVendor(), submap });
        bean.getMap().put("Key4", bean.getVendor());
        bean.getMap().put("Key5", submap);
        bean.getMap().put("Key6", new Object[0]);
        context = JXPathContext.newContext(null, bean);
        context.setLenient(true);
        context.setFactory(new TestBeanFactory());
    }

    @Test
    public void testDoPredicateIndex() {
        // Existing dynamic property + existing property + index
        assertValueAndPointer("/map[@name='Key2'][@name='strings'][2]", "String 2", "/map[@name='Key2']/strings[2]", "BbDdBb", "BbDdBbB");
        // existingProperty[@name=collectionProperty][index]
        assertValueAndPointer("/nestedBean[@name='strings'][2]", bean.getNestedBean().getStrings()[1], "/nestedBean/strings[2]", "BbBb", "BbBbB");
        // existingProperty[@name=missingProperty][index]
        assertNullPointer("/nestedBean[@name='foo'][3]", "/nestedBean[@name='foo'][3]", "BbBn");
        // existingProperty[@name=collectionProperty][missingIndex]
        assertNullPointer("/nestedBean[@name='strings'][5]", "/nestedBean/strings[5]", "BbBbE");
        // map[@name=collectionProperty][index]
        assertValueAndPointer("/map[@name='Key3'][2]", Integer.valueOf(2), "/map[@name='Key3'][2]", "BbDd", "BbDdB");
        // map[@name=collectionProperty][missingIndex]
        assertNullPointer("/map[@name='Key3'][5]", "/map[@name='Key3'][5]", "BbDdE");
        // map[@name=collectionProperty][missingIndex]/property
        assertNullPointer("/map[@name='Key3'][5]/foo", "/map[@name='Key3'][5]/foo", "BbDdENn");
        // map[@name=map][@name=collection][index]
        assertValueAndPointer("/map[@name='Key5'][@name='strings'][2]", "String 2", "/map[@name='Key5'][@name='strings'][2]", "BbDdDd", "BbDdDdB");
        // map[@name=map][@name=collection][missingIndex]
        assertNullPointer("/map[@name='Key5'][@name='strings'][5]", "/map[@name='Key5'][@name='strings'][5]", "BbDdDdE");
        // Existing dynamic property + indexing
        assertValueAndPointer("/map[@name='Key3'][2]", Integer.valueOf(2), "/map[@name='Key3'][2]", "BbDd", "BbDdB");
        // Existing dynamic property + indexing
        assertValueAndPointer("/map[@name='Key3'][1]/name", "some", "/map[@name='Key3'][1]/name", "BbDdBb", "BbDdBbB");
        // map[@name=missingProperty][index]
        assertNullPointer("/map[@name='foo'][3]", "/map[@name='foo'][3]", "BbDdE");
        // collectionProperty[index]
        assertValueAndPointer("/integers[2]", Integer.valueOf(2), "/integers[2]", "Bb", "BbB");
        // existingProperty/collectionProperty[index]
        assertValueAndPointer("/nestedBean/strings[2]", bean.getNestedBean().getStrings()[1], "/nestedBean/strings[2]", "BbBb", "BbBbB");
        // existingProperty[index]/existingProperty
        assertValueAndPointer("/list[3]/int", Integer.valueOf(1), "/list[3]/int", "BbBb", "BbBbB");
        // existingProperty[missingIndex]
        assertNullPointer("/list[6]", "/list[6]", "BbE");
        // existingProperty/missingProperty[index]
        assertNullPointer("/nestedBean/foo[3]", "/nestedBean/foo[3]", "BbBn");
        // map[@name=missingProperty][index]
        assertNullPointer("/map/foo[3]", "/map[@name='foo'][3]", "BbDdE");
        // existingProperty/collectionProperty[missingIndex]
        assertNullPointer("/nestedBean/strings[5]", "/nestedBean/strings[5]", "BbBbE");
        // map/collectionProperty[missingIndex]/property
        assertNullPointer("/map/Key3[5]/foo", "/map[@name='Key3'][5]/foo", "BbDdENn");
        // map[@name=map]/collection[index]
        assertValueAndPointer("/map[@name='Key5']/strings[2]", "String 2", "/map[@name='Key5'][@name='strings'][2]", "BbDdDd", "BbDdDdB");
        // map[@name=map]/collection[missingIndex]
        assertNullPointer("/map[@name='Key5']/strings[5]", "/map[@name='Key5'][@name='strings'][5]", "BbDdDdE");
        // scalarPropertyAsCollection[index]
        assertValueAndPointer("/int[1]", Integer.valueOf(1), "/int", "Bb", "BbB");
        // scalarPropertyAsCollection[index]
        assertValueAndPointer(".[1]/int", Integer.valueOf(1), "/int", "Bb", "BbB");
    }

    @Test
    public void testDoPredicateName() {
        // existingProperty[@name=existingProperty]
        assertValueAndPointer("/nestedBean[@name='int']", Integer.valueOf(1), "/nestedBean/int", "BbBb", "BbBbB");
        // /self::node()[@name=existingProperty]
        assertValueAndPointer("/.[@name='int']", Integer.valueOf(1), "/int", "Bb", "BbB");
        // dynamicProperty[@name=existingProperty]
        assertValueAndPointer("/map[@name='Key1']", "Value 1", "/map[@name='Key1']", "BbDd", "BbDdB");
        // existingProperty[@name=collectionProperty]
        assertValueAndPointer("/nestedBean[@name='strings']", bean.getNestedBean().getStrings(), "/nestedBean/strings", "BbBb", "BbBbC");
        // existingProperty[@name=missingProperty]
        assertNullPointer("/nestedBean[@name='foo']", "/nestedBean[@name='foo']", "BbBn");
        // map[@name=collectionProperty]
        assertValueAndPointer("/map[@name='Key3']", bean.getMap().get("Key3"), "/map[@name='Key3']", "BbDd", "BbDdC");
        // map[@name=missingProperty]
        assertNullPointer("/map[@name='foo']", "/map[@name='foo']", "BbDd");
        // collectionProperty[@name=...] (find node)
        assertValueAndPointer("/list[@name='fruitco']", context.getValue("/vendor"), "/list[5]", "BbCM");
        // collectionProperty[@name=...] (find map entry)
        assertValueAndPointer("/map/Key3[@name='key']/name", "Name 9", "/map[@name='Key3'][4][@name='key']/name", "BbDdCDdBb", "BbDdCDdBbB");
        // map/collectionProperty[@name...]
        assertValueAndPointer("map/Key3[@name='fruitco']", context.getValue("/vendor"), "/map[@name='Key3'][3]", "BbDdCM");
        // Bean property -> DOM Node, name match
        assertValueAndPointer("/vendor[@name='fruitco']", context.getValue("/vendor"), "/vendor", "BbM");
        // Bean property -> DOM Node, name mismatch
        assertNullPointer("/vendor[@name='foo']", "/vendor[@name='foo']", "BbMn");
        assertNullPointer("/vendor[@name='foo'][3]", "/vendor[@name='foo'][3]", "BbMn");
        // existingProperty(bean)[@name=missingProperty]/anotherStep
        assertNullPointer("/nestedBean[@name='foo']/bar", "/nestedBean[@name='foo']/bar", "BbBnNn");
        // map[@name=missingProperty]/anotherStep
        assertNullPointer("/map[@name='foo']/bar", "/map[@name='foo']/bar", "BbDdNn");
        // existingProperty(node)[@name=missingProperty]/anotherStep
        assertNullPointer("/vendor[@name='foo']/bar", "/vendor[@name='foo']/bar", "BbMnNn");
        // existingProperty(node)[@name=missingProperty][index]/anotherStep
        assertNullPointer("/vendor[@name='foo'][3]/bar", "/vendor[@name='foo'][3]/bar", "BbMnNn");
        // Existing dynamic property + existing property
        assertValueAndPointer("/map[@name='Key2'][@name='name']", "Name 6", "/map[@name='Key2']/name", "BbDdBb", "BbDdBbB");
        // Existing dynamic property + existing property + index
        assertValueAndPointer("/map[@name='Key2'][@name='strings'][2]", "String 2", "/map[@name='Key2']/strings[2]", "BbDdBb", "BbDdBbB");
        // bean/map/map/property
        assertValueAndPointer("map[@name='Key5'][@name='key']/name", "Name 9", "/map[@name='Key5'][@name='key']/name", "BbDdDdBb", "BbDdDdBbB");
        assertNullPointer("map[@name='Key2'][@name='foo']", "/map[@name='Key2'][@name='foo']", "BbDdBn");
        assertNullPointer("map[@name='Key2'][@name='foo'][@name='bar']", "/map[@name='Key2'][@name='foo'][@name='bar']", "BbDdBnNn");
        // bean/map/node
        assertValueAndPointer("map[@name='Key4'][@name='fruitco']", context.getValue("/vendor"), "/map[@name='Key4']", "BbDdM");
    }

    @Test
    public void testDoPredicatesStandard() {
        // bean/map/collection/node
        assertValueAndPointer("map[@name='Key3'][@name='fruitco']", context.getValue("/vendor"), "/map[@name='Key3'][3]", "BbDdCM");
        // bean/map/collection/missingNode
        assertNullPointer("map[@name='Key3'][@name='foo']", "/map[@name='Key3'][4][@name='foo']", "BbDdCDd");
        // bean/map/node
        assertValueAndPointer("map[@name='Key4'][@name='fruitco']", context.getValue("/vendor"), "/map[@name='Key4']", "BbDdM");
        // bean/map/emptyCollection[@name=foo]
        assertNullPointer("map[@name='Key6'][@name='fruitco']", "/map[@name='Key6'][@name='fruitco']", "BbDdCn");
        // bean/node[@name=foo][index]
        assertValueAndPointer("/vendor/contact[@name='jack'][2]", "Jack Black", "/vendor/contact[4]", "BbMM");
        // bean/node[@name=foo][missingIndex]
        assertNullPointer("/vendor/contact[@name='jack'][5]", "/vendor/contact[@name='jack'][5]", "BbMnNn");
        // bean/node/.[@name=foo][index]
        assertValueAndPointer("/vendor/contact/.[@name='jack']", "Jack", "/vendor/contact[2]", "BbMM");
    }

    @Test
    public void testDoStepNoPredicatesPropertyOwner() {
        // Existing scalar property
        assertValueAndPointer("/int", Integer.valueOf(1), "/int", "Bb", "BbB");
        // self::
        assertValueAndPointer("/./int", Integer.valueOf(1), "/int", "Bb", "BbB");
        // Missing property
        assertNullPointer("/foo", "/foo", "Bn");
        // existingProperty/existingScalarProperty
        assertValueAndPointer("/nestedBean/int", Integer.valueOf(1), "/nestedBean/int", "BbBb", "BbBbB");
        // existingProperty/collectionProperty
        assertValueAndPointer("/nestedBean/strings", bean.getNestedBean().getStrings(), "/nestedBean/strings", "BbBb", "BbBbC");
        // existingProperty/missingProperty
        assertNullPointer("/nestedBean/foo", "/nestedBean/foo", "BbBn");
        // map/missingProperty
        assertNullPointer("/map/foo", "/map[@name='foo']", "BbDd");
        // Existing property by search in collection
        assertValueAndPointer("/list/int", Integer.valueOf(1), "/list[3]/int", "BbBb", "BbBbB");
        // Missing property by search in collection
        assertNullPointer("/list/foo", "/list[1]/foo", "BbBn");
        // existingProperty/missingProperty/missingProperty
        assertNullPointer("/nestedBean/foo/bar", "/nestedBean/foo/bar", "BbBnNn");
        // collection/existingProperty/missingProperty
        assertNullPointer("/list/int/bar", "/list[3]/int/bar", "BbBbBn");
        // collectionProperty/missingProperty/missingProperty
        assertNullPointer("/list/foo/bar", "/list[1]/foo/bar", "BbBnNn");
        // map/missingProperty/anotherStep
        assertNullPointer("/map/foo/bar", "/map[@name='foo']/bar", "BbDdNn");
        // Existing dynamic property
        assertValueAndPointer("/map/Key1", "Value 1", "/map[@name='Key1']", "BbDd", "BbDdB");
        // collectionProperty
        assertValueAndPointer("/integers", bean.getIntegers(), "/integers", "Bb", "BbC");
    }

    @Test
    public void testDoStepNoPredicatesStandard() {
        // Existing DOM node
        assertValueAndPointer("/vendor/location/address/city", "Fruit Market", "/vendor/location[2]/address[1]/city[1]", "BbMMMM");
        // Missing DOM node
        assertNullPointer("/vendor/location/address/pity", "/vendor/location[1]/address[1]/pity", "BbMMMn");
        // Missing DOM node inside a missing element
        assertNullPointer("/vendor/location/address/itty/bitty", "/vendor/location[1]/address[1]/itty/bitty", "BbMMMnNn");
        // Missing DOM node by search for the best match
        assertNullPointer("/vendor/location/address/city/pretty", "/vendor/location[2]/address[1]/city[1]/pretty", "BbMMMMn");
    }

    @Test
    public void testDoStepPredicatesPropertyOwner() {
        // missingProperty[@name=foo]
        assertNullPointer("/foo[@name='foo']", "/foo[@name='foo']", "BnNn");
        // missingProperty[index]
        assertNullPointer("/foo[3]", "/foo[3]", "Bn");
    }

    @Test
    public void testDoStepPredicatesStandard() {
        // Looking for an actual XML attribute called "name"
        // nodeProperty/name[@name=value]
        assertValueAndPointer("/vendor/contact[@name='jack']", "Jack", "/vendor/contact[2]", "BbMM");
        // Indexing in XML
        assertValueAndPointer("/vendor/contact[2]", "Jack", "/vendor/contact[2]", "BbMM");
        // Indexing in XML, no result
        assertNullPointer("/vendor/contact[5]", "/vendor/contact[5]", "BbMn");
        // Combination of search by name and indexing in XML
        assertValueAndPointer("/vendor/contact[@name='jack'][2]", "Jack Black", "/vendor/contact[4]", "BbMM");
        // Combination of search by name and indexing in XML
        assertValueAndPointer("/vendor/contact[@name='jack'][2]", "Jack Black", "/vendor/contact[4]", "BbMM");
    }

    @Test
    public void testInterpretExpressionPath() {
        context.getVariables().declareVariable("array", new String[] { "Value1" });
        context.getVariables().declareVariable("testnull", new TestNull());
        assertNullPointer("$testnull/nothing[2]", "$testnull/nothing[2]", "VBbE");
    }
}
