/*
 * 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 java.util.HashMap;

import junit.framework.TestCase;

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;

public class SimplePathInterpreterTest extends TestCase {

    private TestBeanWithNode bean;
    private JXPathContext context;

    protected void setUp() throws Exception {
        bean = TestBeanWithNode.createTestBeanWithDOM();
        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"),
                new Integer(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());
    }

    public void testDoStepNoPredicatesPropertyOwner() {
        // Existing scalar property
        assertValueAndPointer("/int",
                new Integer(1),
                "/int",
                "Bb",
                "BbB");

        // self::
        assertValueAndPointer("/./int",
                new Integer(1),
                "/int",
                "Bb",
                "BbB");

        // Missing property
        assertNullPointer("/foo",
                "/foo",
                "Bn");

        // existingProperty/existingScalarProperty
        assertValueAndPointer("/nestedBean/int",
                new Integer(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",
                new Integer(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");
    }

    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");
    }

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

        // missingProperty[index]
        assertNullPointer("/foo[3]",
                "/foo[3]",
                "Bn");
    }

    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");
    }

    public void testDoPredicateName() {
        // existingProperty[@name=existingProperty]
        assertValueAndPointer("/nestedBean[@name='int']",
                new Integer(1),
                "/nestedBean/int",
                "BbBb",
                "BbBbB");

        // /self::node()[@name=existingProperty]
        assertValueAndPointer("/.[@name='int']",
                new Integer(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");
    }

    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");
    }

    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]",
                new Integer(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]",
                new Integer(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]",
                new Integer(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",
                new Integer(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]",
                new Integer(1),
                "/int",
                "Bb",
                "BbB");

        // scalarPropertyAsCollection[index]
        assertValueAndPointer(".[1]/int",
                new Integer(1),
                "/int",
                "Bb",
                "BbB");
    }

    public void testInterpretExpressionPath() {
        context.getVariables().declareVariable("array", new String[]{"Value1"});
        context.getVariables().declareVariable("testnull", new TestNull());

        assertNullPointer("$testnull/nothing[2]",
                "$testnull/nothing[2]",
                "VBbE");
    }

    private void assertValueAndPointer(
            String path, Object expectedValue, String expectedPath,
            String expectedSignature)
    {
        assertValueAndPointer(
            path,
            expectedValue,
            expectedPath,
            expectedSignature,
            expectedSignature);
    }
    
    private void assertValueAndPointer(
            String path, Object expectedValue, String expectedPath,
            String expectedSignature, String expectedValueSignature)
    {
        Object value = context.getValue(path);
        assertEquals("Checking value: " + path, expectedValue, value);

        Pointer pointer = context.getPointer(path);
        assertEquals("Checking pointer: " + path,
                expectedPath, pointer.toString());

        assertEquals("Checking signature: " + path,
                expectedSignature, pointerSignature(pointer));
        
        Pointer vPointer = ((NodePointer) pointer).getValuePointer();
        assertEquals("Checking value pointer signature: " + path,
                expectedValueSignature, pointerSignature(vPointer));
    }

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

    /**
     * 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(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());
        }
        NodePointer parent = 
            ((NodePointer) pointer).getImmediateParentPointer();
        return pointerSignature(parent) + type;
    }
}

