/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */
package org.teiid.translator.odata;

import static org.junit.Assert.*;

import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Properties;

import org.junit.Before;
import org.junit.Test;
import org.odata4j.edm.EdmDataServices;
import org.odata4j.format.xml.EdmxFormatParser;
import org.odata4j.stax2.util.StaxUtil;
import org.teiid.cdk.api.TranslationUtility;
import org.teiid.core.util.ObjectConverterUtil;
import org.teiid.core.util.UnitTestUtil;
import org.teiid.language.Call;
import org.teiid.language.Command;
import org.teiid.language.Select;
import org.teiid.metadata.MetadataFactory;
import org.teiid.query.function.FunctionTree;
import org.teiid.query.function.UDFSource;
import org.teiid.query.metadata.MetadataValidator;
import org.teiid.query.metadata.SystemMetadata;
import org.teiid.query.metadata.TransformationMetadata;
import org.teiid.query.unittest.RealMetadataFactory;
import org.teiid.query.validator.ValidatorReport;
import org.teiid.translator.TranslatorException;

@SuppressWarnings("nls")
public class TestODataSQLVistor {
	private static final boolean printPayload = false;
	private ODataExecutionFactory translator;
	private TranslationUtility utility;
	
    @Before
    public void setUp() throws Exception {
    	translator = new ODataExecutionFactory();
    	translator.start();
    	
		String csdl = ObjectConverterUtil.convertFileToString(UnitTestUtil.getTestDataFile("northwind.xml"));
		EdmDataServices eds = new EdmxFormatParser().parseMetadata(StaxUtil.newXMLEventReader(new InputStreamReader(new ByteArrayInputStream(csdl.getBytes()))));
		ODataMetadataProcessor processor = new ODataMetadataProcessor();
		Properties props = new Properties();
		props.setProperty("schemaNamespace", "ODataWeb.Northwind.Model");
		props.setProperty("entityContainer", "NorthwindEntities");
		MetadataFactory mf = new MetadataFactory("vdb", 1, "nw", SystemMetadata.getInstance().getRuntimeTypeMap(), props, null);
		
		processor.getMetadata(mf, eds);
		
		TransformationMetadata metadata = RealMetadataFactory.createTransformationMetadata(mf.asMetadataStore(), "northwind", new FunctionTree("foo", new UDFSource(translator.getPushDownFunctions())));
    	ValidatorReport report = new MetadataValidator().validate(metadata.getVdbMetaData(), metadata.getMetadataStore());
    	if (report.hasItems()) {
    		throw new RuntimeException(report.getFailureMessage());
    	}
    	//TransformationMetadata metadata = RealMetadataFactory.fromDDL(ObjectConverterUtil.convertFileToString(UnitTestUtil.getTestDataFile("northwind.ddl")), "northwind", "nw");
    	utility = new TranslationUtility(metadata);
    }
    
    private ArrayList<TranslatorException> helpExecute(String query, String expected) throws Exception {
    	Select cmd = (Select)this.utility.parseCommand(query);
    	ODataSQLVisitor visitor = new ODataSQLVisitor(this.translator, utility.createRuntimeMetadata());
    	visitor.visitNode(cmd); 
    	String actual = URLDecoder.decode(visitor.buildURL(), "UTF-8");
    	assertEquals(expected, actual);
    	return visitor.exceptions;
    }

    @Test
    public void testSelectStar() throws Exception {
    	helpExecute("select * from customers", "Customers?$select=CustomerID,CompanyName,ContactName,ContactTitle,Mailing,Shipping");
    }
    
    @Test
    public void testSelectSpecificColumns() throws Exception {
    	helpExecute("select CustomerID from customers", "Customers?$select=CustomerID");
    }

    @Test
    public void testPKBasedFilter() throws Exception {
    	helpExecute("select CompanyName from Customers where CustomerID = 'ALSK'", "Customers('ALSK')?$select=CompanyName");
    }
    
    @Test
    public void testMultiKeyKeyBasedFilter() throws Exception {
    	helpExecute("select UnitPrice from Order_Details where OrderID = 1 and ProductID = 12 and Quantity = 2", "Order_Details(OrderID=1,ProductID=12)?$filter=Quantity eq 2&$select=UnitPrice");
    }    
    
    @Test
    public void testAddFilter() throws Exception {
    	helpExecute("select UnitPrice from Order_Details where OrderID = 1 and (Quantity+2) > OrderID", "Order_Details?$filter=OrderID eq 1 and (cast(Quantity,'integer') add 2) gt OrderID&$select=UnitPrice");
    }
    
    @Test
    public void testEndsWithFilter() throws Exception {
    	helpExecute("select CompanyName from Customers where endswith('k', CustomerID)", "Customers?$filter=endswith(CustomerID,'k') eq true&$select=CompanyName");
    } 
    
    @Test
    public void testMultiKeyKeyBasedFilterOr() throws Exception {
    	helpExecute("select UnitPrice from Order_Details where (OrderID = 1 and ProductID = 12) or Quantity = 2", "Order_Details?$filter=(OrderID eq 1 and ProductID eq 12) or Quantity eq 2&$select=UnitPrice");
    }   
    
    @Test
    public void testPartialPK() throws Exception {
    	helpExecute("select UnitPrice from Order_Details where Quantity >= 2 and ProductID = 12", "Order_Details?$filter=Quantity ge 2 and ProductID eq 12&$select=UnitPrice");
    }    
    
    @Test
    public void testSimpleJoin() throws Exception {
    	helpExecute("SELECT od.UnitPrice FROM Orders o JOIN Order_Details od ON o.OrderID=od.OrderID and o.OrderID=12", "Orders(12)/Order_Details?$select=UnitPrice");
    }    
    
    @Test
    public void testJoinBasedTwoPK() throws Exception {
    	helpExecute("SELECT od.UnitPrice FROM Orders o JOIN Order_Details od ON o.OrderID=od.OrderID and o.OrderID=12 WHERE od.ProductID=1", "Orders(12)/Order_Details(OrderID=12,ProductID=1)?$select=UnitPrice");
    }   
    
    @Test
    public void testJoinBasedTwoPKOnKey() throws Exception {
    	helpExecute("SELECT od.UnitPrice FROM Orders o JOIN Order_Details od ON o.OrderID=od.OrderID and o.OrderID=12 and od.ProductID=1", "Orders(12)/Order_Details(OrderID=12,ProductID=1)?$select=UnitPrice");
    }     
    
    @Test
    public void testEmbeddedJoin() throws Exception {
    	testSelectStar(); // customer table is embedded
    }
    
    @Test
    public void testFunction() throws Exception {
    	helpExecute("SELECT ContactName FROM Customers WHERE odata.startswith(CompanyName, 'CN')", "Customers?$filter=startswith(CompanyName,'CN') eq true&$select=ContactName");
    }
    
    @Test
    public void testBooleanFunction() throws Exception {
    	this.translator.setSupportsOdataBooleanFunctionsWithComparison(false);
    	try {
    		helpExecute("SELECT ContactName FROM Customers WHERE odata.startswith(CompanyName, 'CN')", "Customers?$filter=startswith(CompanyName,'CN')&$select=ContactName");
    		helpExecute("SELECT ContactName FROM Customers WHERE odata.startswith(CompanyName, 'CN') = 0", "Customers?$filter=NOT (startswith(CompanyName,'CN'))&$select=ContactName");
    		helpExecute("SELECT ContactName FROM Customers WHERE odata.startswith(CompanyName, 'CN') <> 1", "Customers?$filter=NOT (startswith(CompanyName,'CN'))&$select=ContactName");
    		helpExecute("SELECT ContactName FROM Customers WHERE odata.startswith(CompanyName, 'CN') <> 0", "Customers?$filter=startswith(CompanyName,'CN')&$select=ContactName");
    		
    		helpExecute("SELECT ContactName FROM Customers WHERE endswith('CN', CompanyName)", "Customers?$filter=endswith(CompanyName,'CN')&$select=ContactName");
    		helpExecute("SELECT ContactName FROM Customers WHERE odata.substringof(CompanyName, 'CN')", "Customers?$filter=substringof(CompanyName,'CN')&$select=ContactName");
    		
    		
    		ArrayList<TranslatorException> exceptions = 
    				helpExecute("SELECT ContactName FROM Customers WHERE odata.startswith(CompanyName, 'CN') < 1", "Customers?$filter=startswith(CompanyName,'CN')&$select=ContactName");
    		assertTrue(!exceptions.isEmpty());
    		assertTrue(exceptions.get(0).getMessage().contains(ODataPlugin.Event.TEIID17018.name()));    		
    	} finally {
    		this.translator.setSupportsOdataBooleanFunctionsWithComparison(true);
    	}
    }
    
    @Test
    public void testLimit() throws Exception {
    	helpExecute("SELECT ContactName FROM Customers limit 10", "Customers?$select=ContactName&$top=10");
    }  
    
    @Test
    public void testLimitOffset() throws Exception {
    	helpExecute("SELECT ContactName FROM Customers limit 10, 19", "Customers?$select=ContactName&$skip=10&$top=19");
    }  
    
    @Test
    public void testUseAirthmaticFunction() throws Exception {
    	helpExecute("SELECT LastName FROM Employees WHERE EmployeeID/10 > EmployeeID", "Employees?$filter=(EmployeeID div 10) gt EmployeeID&$select=LastName");
    }  
    
    @Test
    public void testOrderBy() throws Exception {
    	helpExecute("SELECT LastName FROM Employees Order By LastName", "Employees?$orderby=LastName&$select=LastName");
    }  
    
    @Test
    public void testOrderByDESC() throws Exception {
    	helpExecute("SELECT LastName FROM Employees Order By LastName DESC", "Employees?$orderby=LastName desc&$select=LastName");
    }    
    
    @Test
    public void testOrderByMultiple() throws Exception {
    	helpExecute("SELECT LastName FROM Employees Order By LastName DESC, EmployeeId", "Employees?$orderby=LastName desc,EmployeeID&$select=LastName");
    }     
    
    @Test
    public void testisNotNull() throws Exception {
    	helpExecute("SELECT LastName FROM Employees WHERE LastName is NOT NULL", "Employees?$filter=not(LastName eq null)&$select=LastName");
    }    
    
    @Test
    public void testisNull() throws Exception {
    	helpExecute("SELECT LastName FROM Employees WHERE LastName is NULL", "Employees?$filter=LastName eq null&$select=LastName");
    }     
    
    @Test
    public void testCountStar() throws Exception {
    	helpExecute("SELECT count(*) FROM Employees", "Employees/$count");
    }      
    
    private void helpFunctionExecute(String query, String expected) throws Exception {
    	Call cmd = (Call)this.utility.parseCommand(query);
		ODataProcedureVisitor visitor = new ODataProcedureVisitor(translator, utility.createRuntimeMetadata());
    	visitor.visitNode(cmd); 
    	String odataCmd = visitor.buildURL();
    	
    	assertEquals(expected, odataCmd);
    	assertEquals("GET", visitor.getMethod());
    }
    
    @Test
    public void testProcedureExec() throws Exception {
    	helpFunctionExecute("Exec TopCustomers('newyork')", "TopCustomers?city=%27newyork%27");
    }  
    
    @Test
    public void testProcedureExecEncoded() throws Exception {
    	helpFunctionExecute("Exec CommonCustomers('new york', 'los angeles')", "CommonCustomers?city1=%27new%20york%27&city2=%27los%20angeles%27");
    }  
    
    private void helpUpdateExecute(String query, String expected, String expectedMethod, boolean checkPayload) throws Exception {
    	Command cmd = this.utility.parseCommand(query);
    	
		ODataUpdateVisitor visitor = new ODataUpdateVisitor(translator, utility.createRuntimeMetadata());
    	visitor.visitNode(cmd); 
    	
		if (!visitor.exceptions.isEmpty()) {
			throw visitor.exceptions.get(0);
		}
		
    	String odataCmd = visitor.buildURL();
    	
    	if (checkPayload) {
    		assertNotNull(visitor.getPayload());
    	}
    	
    	if (printPayload) {
    		System.out.println(visitor.getPayload());
    	}
    	
    	assertEquals(expected, odataCmd);
    	assertEquals(expectedMethod, visitor.getMethod());
    }   
    
    @Test
    public void testInsert() throws Exception {
    	helpUpdateExecute("INSERT INTO Regions (RegionID,RegionDescription) VALUES (10,'Asian')", "Regions", "POST", true);
    }     
    
    @Test(expected=TranslatorException.class)
    public void testDeletewithoutPK() throws Exception {
    	helpUpdateExecute("Delete From Regions", "Regions", "DELETE", false);
    }    
    
    @Test
    public void testDelete() throws Exception {
    	helpUpdateExecute("Delete From Regions where RegionID=10", "Regions(10)", "DELETE", false);
    }     
    
    @Test(expected=TranslatorException.class)
    public void testDeleteOtherClause() throws Exception {
    	helpUpdateExecute("Delete From Regions where RegionDescription='foo'", "Regions", "DELETE", false);
    }     
    
    @Test
    public void testUpdate() throws Exception {
    	helpUpdateExecute("UPDATE Regions SET RegionDescription='foo' WHERE RegionID=10", "Regions(10)", "PUT", true);
    }     
    
    @Test(expected=TranslatorException.class)
    public void testUpdatewithoutPK() throws Exception {
    	helpUpdateExecute("UPDATE Regions SET RegionDescription='foo'", "Regions(10)", "PATCH", true);
    }   
    
    @Test(expected=TranslatorException.class)
    public void testUpdateOtherClause() throws Exception {
    	helpUpdateExecute("UPDATE Regions SET RegionID=10 WHERE RegionDescription='foo'", "Regions(10)", "PATCH", true);
    }    
}
