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

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;

import org.apache.commons.jxpath.ClassFunctions;
import org.apache.commons.jxpath.ExpressionContext;
import org.apache.commons.jxpath.Function;
import org.apache.commons.jxpath.FunctionLibrary;
import org.apache.commons.jxpath.Functions;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathTestCase;
import org.apache.commons.jxpath.NodeSet;
import org.apache.commons.jxpath.PackageFunctions;
import org.apache.commons.jxpath.Pointer;
import org.apache.commons.jxpath.TestBean;
import org.apache.commons.jxpath.Variables;
import org.apache.commons.jxpath.ri.model.NodePointer;
import org.apache.commons.jxpath.util.JXPath11CompatibleTypeConverter;
import org.apache.commons.jxpath.util.TypeConverter;
import org.apache.commons.jxpath.util.TypeUtils;

/**
 * Test extension functions.
 *
 * @author Dmitri Plotnikov
 * @version $Revision: 652845 $ $Date: 2008-05-02 12:46:46 -0500 (Fri, 02 May 2008) $
 */
public class ExtensionFunctionTest extends JXPathTestCase {
    private Functions functions;
    private JXPathContext context;
    private TestBean testBean;
    private TypeConverter typeConverter;

    public void setUp() {
        if (context == null) {
            testBean = new TestBean();
            context = JXPathContext.newContext(testBean);
            Variables vars = context.getVariables();
            vars.declareVariable("test", new TestFunctions(4, "test"));

            FunctionLibrary lib = new FunctionLibrary();
            lib.addFunctions(new ClassFunctions(TestFunctions.class, "test"));
            lib.addFunctions(new ClassFunctions(TestFunctions2.class, "test"));
            lib.addFunctions(new PackageFunctions("", "call"));
            lib.addFunctions(
                new PackageFunctions(
                    "org.apache.commons.jxpath.ri.compiler.",
                    "jxpathtest"));
            lib.addFunctions(new PackageFunctions("", null));
            context.setFunctions(lib);
            context.getVariables().declareVariable("List.class", List.class);
            context.getVariables().declareVariable("NodeSet.class", NodeSet.class);
        }
        functions = new ClassFunctions(TestFunctions.class, "test");
        typeConverter = TypeUtils.getTypeConverter();
    }

    public void tearDown() {
        TypeUtils.setTypeConverter(typeConverter);
    }

    public void testConstructorLookup() {
        Object[] args = new Object[] { new Integer(1), "x" };
        Function func = functions.getFunction("test", "new", args);

        assertEquals(
            "test:new(1, x)",
            func.invoke(new Context(null), args).toString(),
            "foo=1; bar=x");
    }

    public void testConstructorLookupWithExpressionContext() {
        Object[] args = new Object[] { "baz" };
        Function func = functions.getFunction("test", "new", args);
        assertEquals(
            "test:new('baz')",
            func.invoke(new Context(new Integer(1)), args).toString(),
            "foo=1; bar=baz");
    }

    public void testStaticMethodLookup() {
        Object[] args = new Object[] { new Integer(1), "x" };
        Function func = functions.getFunction("test", "build", args);
        assertEquals(
            "test:build(1, x)",
            func.invoke(new Context(null), args).toString(),
            "foo=1; bar=x");
    }

    public void testStaticMethodLookupWithConversion() {
        Object[] args = new Object[] { "7", new Integer(1)};
        Function func = functions.getFunction("test", "build", args);
        assertEquals(
            "test:build('7', 1)",
            func.invoke(new Context(null), args).toString(),
            "foo=7; bar=1");
    }

    public void testMethodLookup() {
        Object[] args = new Object[] { new TestFunctions()};
        Function func = functions.getFunction("test", "getFoo", args);
        assertEquals(
            "test:getFoo($test, 1, x)",
            func.invoke(new Context(null), args).toString(),
            "0");
    }

    public void testStaticMethodLookupWithExpressionContext() {
        Object[] args = new Object[0];
        Function func = functions.getFunction("test", "path", args);
        assertEquals(
            "test:path()",
            func.invoke(new Context(new Integer(1)), args),
            "1");
    }

    public void testMethodLookupWithExpressionContext() {
        Object[] args = new Object[] { new TestFunctions()};
        Function func = functions.getFunction("test", "instancePath", args);
        assertEquals(
            "test:instancePath()",
            func.invoke(new Context(new Integer(1)), args),
            "1");
    }

    public void testMethodLookupWithExpressionContextAndArgument() {
        Object[] args = new Object[] { new TestFunctions(), "*" };
        Function func = functions.getFunction("test", "pathWithSuffix", args);
        assertEquals(
            "test:pathWithSuffix('*')",
            func.invoke(new Context(new Integer(1)), args),
            "1*");
    }

    public void testAllocation() {
        
        // Allocate new object using the default constructor
        assertXPathValue(context, "string(test:new())", "foo=0; bar=null");

        // Allocate new object using PackageFunctions and class name
        assertXPathValue(
            context,
            "string(jxpathtest:TestFunctions.new())",
            "foo=0; bar=null");

        // Allocate new object using a fully qualified class name
        assertXPathValue(
            context,
            "string(" + TestFunctions.class.getName() + ".new())",
            "foo=0; bar=null");

        // Allocate new object using a custom constructor
        assertXPathValue(
            context,
            "string(test:new(3, 'baz'))",
            "foo=3; bar=baz");

        // Allocate new object using a custom constructor - type conversion
        assertXPathValue(context, "string(test:new('3', 4))", "foo=3; bar=4.0");
        
        context.getVariables().declareVariable("A", "baz");        
        assertXPathValue(
                context,
                "string(test:new(2, $A, false))",
                "foo=2; bar=baz");
    }

    public void testMethodCall() {
        assertXPathValue(context, "length('foo')", new Integer(3));

        // We are just calling a method - prefix is ignored
        assertXPathValue(context, "call:substring('foo', 1, 2)", "o");

        // Invoke a function implemented as a regular method
        assertXPathValue(context, "string(test:getFoo($test))", "4");
        
        // Note that the prefix is ignored anyway, we are just calling a method
        assertXPathValue(context, "string(call:getFoo($test))", "4");

        // We don't really need to supply a prefix in this case
        assertXPathValue(context, "string(getFoo($test))", "4");

        // Method with two arguments
        assertXPathValue(
            context,
            "string(test:setFooAndBar($test, 7, 'biz'))",
            "foo=7; bar=biz");
    }
    
    public void testCollectionMethodCall() {
        
        List list = new ArrayList();
        list.add("foo");
        context.getVariables().declareVariable("myList", list);

        assertXPathValue(
            context, 
            "size($myList)", 
            new Integer(1));
    
        assertXPathValue(
            context, 
            "size(beans)", 
            new Integer(2));
            
        context.getValue("add($myList, 'hello')");
        assertEquals("After adding an element", 2, list.size());
        
        JXPathContext context = JXPathContext.newContext(new ArrayList());
        assertEquals("Extension function on root collection", "0", String
                .valueOf(context.getValue("size(/)")));
    }

    public void testStaticMethodCall() {

        assertXPathValue(
            context,
            "string(test:build(8, 'goober'))",
            "foo=8; bar=goober");

        // Call a static method using PackageFunctions and class name
        assertXPathValue(
            context,
            "string(jxpathtest:TestFunctions.build(8, 'goober'))",
            "foo=8; bar=goober");

        // Call a static method with a fully qualified class name
        assertXPathValue(
            context,
            "string(" + TestFunctions.class.getName() + ".build(8, 'goober'))",
            "foo=8; bar=goober");

        // Two ClassFunctions are sharing the same prefix.
        // This is TestFunctions2
        assertXPathValue(context, "string(test:increment(8))", "9");
        
        // See that a NodeSet gets properly converted to a string
        assertXPathValue(context, "test:string(/beans/name)", "Name 1");
    }

    public void testExpressionContext() {
        // Execute an extension function for each node while searching
        // The function uses ExpressionContext to get to the current
        // node.
        assertXPathValue(
            context, 
            "//.[test:isMap()]/Key1", 
            "Value 1");

        // The function gets all
        // nodes in the context that match the pattern.
        assertXPathValue(
            context,
            "count(//.[test:count(strings) = 3])",
            new Double(7));

        // The function receives a collection of strings
        // and checks their type for testing purposes            
        assertXPathValue(
            context,
            "test:count(//strings)",
            new Integer(21));

        
        // The function receives a collection of pointers
        // and checks their type for testing purposes            
        assertXPathValue(
            context,
            "test:countPointers(//strings)",
            new Integer(21));
            
        // The function uses ExpressionContext to get to the current
        // pointer and returns its path.
        assertXPathValue(
            context,
            "/beans[contains(test:path(), '[2]')]/name",
            "Name 2");
    }
    
    public void testCollectionReturn() {
        assertXPathValueIterator(
            context,
            "test:collection()/name",
            list("foo", "bar"));

        assertXPathPointerIterator(
            context,
            "test:collection()/name",
            list("/.[1]/name", "/.[2]/name"));
            
        assertXPathValue(
            context,
            "test:collection()/name",
            "foo");        

        assertXPathValue(
            context,
            "test:collection()/@name",
            "foo");   
        
        List list = new ArrayList();
        list.add("foo");
        list.add("bar");
        context.getVariables().declareVariable("list", list);
        Object values = context.getValue("test:items($list)");
        assertTrue("Return type: ", values instanceof Collection);
        assertEquals(
            "Return values: ",
            list,
            new ArrayList((Collection) values));
    }

    public void testNodeSetReturn() {
        assertXPathValueIterator(
            context,
            "test:nodeSet()/name",
            list("Name 1", "Name 2"));

        assertXPathValueIterator(
            context,
            "test:nodeSet()",
            list(testBean.getBeans()[0], testBean.getBeans()[1]));

        assertXPathPointerIterator(
            context,
            "test:nodeSet()/name",
            list("/beans[1]/name", "/beans[2]/name"));
            
        assertXPathValueAndPointer(
            context,
            "test:nodeSet()/name",
            "Name 1",
            "/beans[1]/name");        

        assertXPathValueAndPointer(
            context,
            "test:nodeSet()/@name",
            "Name 1",
            "/beans[1]/@name");

        assertEquals(2, ((Number) context.getValue("count(test:nodeSet())")).intValue());

        assertXPathValue(context, "test:nodeSet()", testBean.getBeans()[0]);
    }

    public void testEstablishNodeSetBaseline() {
        assertXPathValue(
            context,
            "test:isInstance(//strings, $List.class)",
            Boolean.TRUE);
        assertXPathValue(
            context,
            "test:isInstance(//strings, $NodeSet.class)",
            Boolean.FALSE);
    }

    public void testBCNodeSetHack() {
        TypeUtils.setTypeConverter(new JXPath11CompatibleTypeConverter());
        assertXPathValue(
            context,
            "test:isInstance(//strings, $List.class)",
            Boolean.FALSE);
        assertXPathValue(
            context,
            "test:isInstance(//strings, $NodeSet.class)",
            Boolean.TRUE);
    }

    private static class Context implements ExpressionContext {
        private Object object;

        public Context(Object object) {
            this.object = object;
        }

        public Pointer getContextNodePointer() {
            return NodePointer
                    .newNodePointer(null, object, Locale.getDefault());
        }

        public List getContextNodeList() {
            return null;
        }

        public JXPathContext getJXPathContext() {
            return null;
        }

        public int getPosition() {
            return 0;
        }
    }
}