package adql.db;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.junit.AfterClass;
import org.junit.Test;

import adql.db.DBType.DBDatatype;
import adql.db.FunctionDef.FunctionParam;
import adql.parser.ParseException;
import adql.query.operand.ADQLColumn;
import adql.query.operand.ADQLOperand;
import adql.query.operand.NumericConstant;
import adql.query.operand.StringConstant;
import adql.query.operand.function.ADQLFunction;
import adql.query.operand.function.DefaultUDF;
import adql.query.operand.function.geometry.PointFunction;

public class TestFunctionDef {

	@AfterClass
	public static void tearDownAfterClass() throws Exception{
		DBType.DBDatatype.UNKNOWN.setCustomType(null);
	}

	@Test
	public void testIsString(){
		for(DBDatatype type : DBDatatype.values()){
			switch(type){
				case CHAR:
				case VARCHAR:
				case TIMESTAMP:
				case CLOB:
					assertTrue(new FunctionDef("foo", new DBType(type)).isString);
					break;
				default:
					assertFalse(new FunctionDef("foo", new DBType(type)).isString);
			}
		}
	}

	@Test
	public void testIsGeometry(){
		for(DBDatatype type : DBDatatype.values()){
			switch(type){
				case POINT:
				case REGION:
					assertTrue(new FunctionDef("foo", new DBType(type)).isGeometry);
					break;
				default:
					assertFalse(new FunctionDef("foo", new DBType(type)).isGeometry);
			}
		}
	}

	@Test
	public void testIsNumeric(){
		for(DBDatatype type : DBDatatype.values()){
			switch(type){
				case CHAR:
				case VARCHAR:
				case TIMESTAMP:
				case POINT:
				case REGION:
				case CLOB:
				case UNKNOWN:
					assertFalse(new FunctionDef("foo", new DBType(type)).isNumeric);
					break;
				case UNKNOWN_NUMERIC:
				default:
					assertTrue(new FunctionDef("foo", new DBType(type)).isNumeric);
			}
		}
	}

	@Test
	public void testToString(){
		assertEquals("fct1()", new FunctionDef("fct1").toString());
		assertEquals("fct1() -> VARCHAR", new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR)).toString());
		assertEquals("fct1(foo DOUBLE) -> VARCHAR", new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo", new DBType(DBDatatype.DOUBLE))}).toString());
		assertEquals("fct1(foo DOUBLE)", new FunctionDef("fct1", new FunctionParam[]{new FunctionParam("foo", new DBType(DBDatatype.DOUBLE))}).toString());
		assertEquals("fct1(foo DOUBLE, pt POINT) -> VARCHAR", new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo", new DBType(DBDatatype.DOUBLE)),new FunctionParam("pt", new DBType(DBDatatype.POINT))}).toString());
		assertEquals("fct1(foo DOUBLE, pt POINT)", new FunctionDef("fct1", null, new FunctionParam[]{new FunctionParam("foo", new DBType(DBDatatype.DOUBLE)),new FunctionParam("pt", new DBType(DBDatatype.POINT))}).toString());
	}

	@Test
	public void testParse(){
		final String WRONG_FULL_SYNTAX = "Wrong function definition syntax! Expected syntax: \"<regular_identifier>(<parameters>?) <return_type>?\", where <regular_identifier>=\"[a-zA-Z]+[a-zA-Z0-9_]*\", <return_type>=\" -> <type_name>\", <parameters>=\"(<regular_identifier> <type_name> (, <regular_identifier> <type_name>)*)\", <type_name> should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"foo()\", \"foo() -> VARCHAR\", \"foo(param INTEGER)\", \"foo(param1 INTEGER, param2 DOUBLE) -> DOUBLE\"";
		final String WRONG_PARAM_SYNTAX = "Wrong parameters syntax! Expected syntax: \"(<regular_identifier> <type_name> (, <regular_identifier> <type_name>)*)\", where <regular_identifier>=\"[a-zA-Z]+[a-zA-Z0-9_]*\", <type_name> should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"()\", \"(param INTEGER)\", \"(param1 INTEGER, param2 DOUBLE)\"";

		// NULL test:
		try{
			FunctionDef.parse(null);
			fail("A NULL string is not valide!");
		}catch(Exception ex){
			assertTrue(ex instanceof NullPointerException);
			assertEquals("Missing string definition to build a FunctionDef!", ex.getMessage());
		}

		// EMPTY STRING test:
		try{
			FunctionDef.parse("");
			fail("An empty string is not valide!");
		}catch(Exception ex){
			assertTrue(ex instanceof ParseException);
			assertEquals(WRONG_FULL_SYNTAX, ex.getMessage());
		}

		// CORRECT string definitions:
		try{
			assertEquals("foo()", FunctionDef.parse("foo()").toString());
			assertEquals("foo() -> VARCHAR", FunctionDef.parse("foo() -> string").toString());
			assertEquals("foo() -> VARCHAR", FunctionDef.parse("foo()->string").toString());
			assertEquals("foo(toto VARCHAR) -> SMALLINT", FunctionDef.parse("foo(toto varchar) -> boolean").toString());
			assertEquals("foo(param1 DOUBLE, param2 INTEGER) -> DOUBLE", FunctionDef.parse(" foo ( param1	numeric,	param2    int )	->	DOUBLE ").toString());
			assertEquals("foo_ALTernative2first(p POINT, d TIMESTAMP) -> TIMESTAMP", FunctionDef.parse("foo_ALTernative2first	(p POINT,d date) -> time").toString());
			assertEquals("blabla_123(toto INTEGER, bla SMALLINT, truc CLOB, bidule CHAR, smurph POINT, date TIMESTAMP) -> SMALLINT", FunctionDef.parse("blabla_123(toto int4, bla bool, truc text, bidule character, smurph point, date timestamp) -> BOOLEAN").toString());
		}catch(Exception ex){
			ex.printStackTrace(System.err);
			fail("All this string definitions are correct.");
		}

		// TYPE PARAMETER test:
		try{
			for(DBDatatype t : DBDatatype.values()){
				switch(t){
					case CHAR:
					case VARCHAR:
					case BINARY:
					case VARBINARY:
						assertEquals("foo() -> " + t.toString() + "(10)", FunctionDef.parse("foo() -> " + t.toString() + "(10)").toString());
						break;
					default:
						assertEquals("foo() -> " + t.toString(), FunctionDef.parse("foo() -> " + t.toString() + "(10)").toString());
				}
			}
		}catch(Exception ex){
			ex.printStackTrace(System.err);
			fail("Wrong type parsing!");
		}

		// TYPE WITH SPACES AND/OR PARAMETER test:
		try{
			assertEquals("foo() -> DOUBLE", FunctionDef.parse("foo() -> double precision").toString());
			assertEquals("foo(bar DOUBLE)", FunctionDef.parse("foo(bar   DOUBLE  Precision  )").toString());
			assertEquals("foo() -> VARCHAR", FunctionDef.parse("foo() -> character varying").toString());
			assertEquals("foo(bar VARBINARY)", FunctionDef.parse("foo(bar bit   varying)").toString());
			assertEquals("foo(bar VARCHAR(12))", FunctionDef.parse("foo(bar varchar  (12))").toString());
			assertEquals("foo(bar VARCHAR(12))", FunctionDef.parse("foo(bar character varying (12))").toString());
			assertEquals("foo() -> DOUBLE", FunctionDef.parse("foo() -> double precision (2)").toString());
		}catch(Exception ex){
			ex.printStackTrace(System.err);
			fail("Wrong type parsing!");
		}

		// WRONG string definitions:
		try{
			FunctionDef.parse("123()");
			fail("No number is allowed as first character of a function name!");
		}catch(Exception ex){
			assertTrue(ex instanceof ParseException);
			assertEquals(WRONG_FULL_SYNTAX, ex.getMessage());
		}
		try{
			FunctionDef.parse("1foo()");
			fail("No number is allowed as first character of a function name!");
		}catch(Exception ex){
			assertTrue(ex instanceof ParseException);
			assertEquals(WRONG_FULL_SYNTAX, ex.getMessage());
		}
		try{
			FunctionDef.parse("foo,truc()");
			fail("No other character than [a-zA-Z0-9_] is allowed after a first character [a-zA-Z] in a function name!");
		}catch(Exception ex){
			assertTrue(ex instanceof ParseException);
			assertEquals(WRONG_FULL_SYNTAX, ex.getMessage());
		}
		try{
			FunctionDef.parse("foo");
			fail("A function definition must contain at list parenthesis even if there is no parameter.");
		}catch(Exception ex){
			assertTrue(ex instanceof ParseException);
			assertEquals(WRONG_FULL_SYNTAX, ex.getMessage());
		}
		try{
			FunctionDef.parse("foo(param)");
			fail("A parameter must always have a type!");
		}catch(Exception ex){
			assertTrue(ex instanceof ParseException);
			assertEquals("Wrong syntax for the 1-th parameter: \"param\"! Expected syntax: \"(<regular_identifier> <type_name> (, <regular_identifier> <type_name>)*)\", where <regular_identifier>=\"[a-zA-Z]+[a-zA-Z0-9_]*\", <type_name> should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"()\", \"(param INTEGER)\", \"(param1 INTEGER, param2 DOUBLE)\"", ex.getMessage());
		}
		try{
			FunctionDef fct = FunctionDef.parse("foo()->aType");
			assertTrue(fct.isUnknown);
			assertFalse(fct.isString);
			assertFalse(fct.isNumeric);
			assertFalse(fct.isGeometry);
			assertEquals("?aType?", fct.returnType.type.toString());
		}catch(Exception ex){
			ex.printStackTrace(System.err);
			fail("Unknown types MUST be allowed!");
		}
		try{
			FunctionDef fct = FunctionDef.parse("foo()->aType(10)");
			assertTrue(fct.isUnknown);
			assertFalse(fct.isString);
			assertFalse(fct.isNumeric);
			assertFalse(fct.isGeometry);
			assertEquals("?aType(10)?", fct.returnType.type.toString());
		}catch(Exception ex){
			ex.printStackTrace(System.err);
			fail("Unknown types MUST be allowed!");
		}
		try{
			FunctionDef.parse("foo() -> ");
			fail("The return type is missing!");
		}catch(Exception ex){
			assertTrue(ex instanceof ParseException);
			assertEquals(WRONG_FULL_SYNTAX, ex.getMessage());
		}
		try{
			FunctionDef.parse("foo(,)");
			fail("Missing parameter definition!");
		}catch(Exception ex){
			assertTrue(ex instanceof ParseException);
			assertEquals(WRONG_PARAM_SYNTAX, ex.getMessage());
		}
		try{
			FunctionDef.parse("foo(param1 int,)");
			fail("Missing parameter definition!");
		}catch(Exception ex){
			assertTrue(ex instanceof ParseException);
			assertEquals(WRONG_PARAM_SYNTAX, ex.getMessage());
		}
		try{
			FunctionDef fct = FunctionDef.parse("foo(param1 aType)");
			assertTrue(fct.getParam(0).type.isUnknown());
			assertFalse(fct.getParam(0).type.isString());
			assertFalse(fct.getParam(0).type.isNumeric());
			assertFalse(fct.getParam(0).type.isGeometry());
			assertEquals("?aType?", fct.getParam(0).type.toString());
		}catch(Exception ex){
			ex.printStackTrace(System.err);
			fail("Unknown types MUST be allowed!");
		}
		try{
			FunctionDef fct = FunctionDef.parse("foo(param1 aType(10))");
			assertTrue(fct.getParam(0).type.isUnknown());
			assertFalse(fct.getParam(0).type.isString());
			assertFalse(fct.getParam(0).type.isNumeric());
			assertFalse(fct.getParam(0).type.isGeometry());
			assertEquals("?aType(10)?", fct.getParam(0).type.toString());
		}catch(Exception ex){
			ex.printStackTrace(System.err);
			fail("Unknown types MUST be allowed!");
		}
		try{
			FunctionDef fct = FunctionDef.parse("INTERSECTION(region1 region, region2 region) -> region");
			assertEquals(DBType.DBDatatype.REGION, fct.getParam(0).type.type);
			assertEquals(DBType.DBDatatype.REGION, fct.getParam(1).type.type);
			assertEquals(DBType.DBDatatype.REGION, fct.returnType.type);
		}catch(Exception ex){
			ex.printStackTrace(System.err);
			fail("Impossible to parse this REGION based FunctionDef! (see console for more details)");
		}
	}

	@Test
	public void testCompareToFunctionDef(){
		// DEFINITION 1 :: fct1() -> VARCHAR
		FunctionDef def1 = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR));

		// TEST :: Identity test (def1 with def1): [EQUAL]
		assertEquals(0, def1.compareTo(def1));

		// TEST :: With a function having a different name and also no parameter: [GREATER]
		assertEquals(1, def1.compareTo(new FunctionDef("fct0", new DBType(DBDatatype.VARCHAR))));

		// TEST :: With a function having the same name, but a different return type: [EQUAL}
		assertEquals(0, def1.compareTo(new FunctionDef("fct1", new DBType(DBDatatype.INTEGER))));

		// TEST :: With a function having the same name, but 2 parameters: [LESS (6 characters: ø against 100100)]
		assertEquals(-6, def1.compareTo(new FunctionDef("fct1", new DBType(DBDatatype.INTEGER), new FunctionParam[]{new FunctionParam("foo", new DBType(DBDatatype.INTEGER)),new FunctionParam("foo", new DBType(DBDatatype.INTEGER))})));

		// DEFINITION 1 :: fct1(foo1 CHAR(12), foo2 DOUBLE) -> VARCHAR
		def1 = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo1", new DBType(DBDatatype.CHAR, 12)),new FunctionParam("foo2", new DBType(DBDatatype.DOUBLE))});

		// TEST :: Identity test (def1 with def1): [EQUAL]
		assertEquals(0, def1.compareTo(def1));

		// DEFINITION 2 :: fct1(foo1 CHAR(12), foo2 VARCHAR) -> VARCHAR
		FunctionDef def2 = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo1", new DBType(DBDatatype.CHAR, 12)),new FunctionParam("foo2", new DBType(DBDatatype.VARCHAR))});

		// TEST :: Identity test (def2 with def2): [EQUAL]
		assertEquals(0, def2.compareTo(def2));

		// TEST :: Same name, but different type for the last parameter only: [GREATER (because Numeric = 10 > String = 01)]
		assertEquals(1, def1.compareTo(def2));

		// DEFINITION 2 :: fct2(foo1 CHAR(12), foo2 DOUBLE) -> VARCHAR
		def2 = new FunctionDef("fct2", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo1", new DBType(DBDatatype.CHAR, 12)),new FunctionParam("foo2", new DBType(DBDatatype.DOUBLE))});

		// TEST :: Identity test (def2 with def2): [EQUAL]
		assertEquals(0, def2.compareTo(def2));

		// TEST :: Different name but same parameters: [LESS]
		assertEquals(-1, def1.compareTo(def2));

		// DEFINITION 2 :: fct1(foo1 CHAR(12), foo2 POINT) -> VARCHAR
		def2 = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo1", new DBType(DBDatatype.CHAR, 12)),new FunctionParam("foo2", new DBType(DBDatatype.POINT))});

		// TEST :: Identity test (def2 with def2): [EQUAL]
		assertEquals(0, def2.compareTo(def2));

		// TEST :: Same name, but different type for the last parameter only: [GREATER]
		assertEquals(1, def1.compareTo(def2));
	}

	@Test
	public void testCompareToADQLFunction(){
		// DEFINITION :: fct1() -> VARCHAR
		FunctionDef def = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR));

		// TEST :: NULL:
		try{
			def.compareTo((ADQLFunction)null);
			fail("Missing ADQL function for comparison with FunctionDef!");
		}catch(Exception e){
			assertTrue(e instanceof NullPointerException);
			assertEquals("Missing ADQL function with which comparing this function definition!", e.getMessage());
		}

		// TEST :: "fct1()": [EQUAL]
		assertEquals(0, def.compareTo(new DefaultUDF("fct1", null)));

		// TEST :: "fct0()": [GREATER]
		assertEquals(1, def.compareTo(new DefaultUDF("fct0", null)));

		// TEST :: "fct1(12.3, 3.14)": [LESS (of 2 params)]
		assertEquals(-2, def.compareTo(new DefaultUDF("fct1", new ADQLOperand[]{new NumericConstant(12.3),new NumericConstant(3.14)})));

		// DEFINITION :: fct1(foo1 CHAR(12), foo2 DOUBLE) -> VARCHAR
		def = new FunctionDef("fct1", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("foo1", new DBType(DBDatatype.CHAR, 12)),new FunctionParam("foo2", new DBType(DBDatatype.DOUBLE))});

		// TEST :: "fct1('blabla', 'blabla2')": [GREATER (because the second param is numeric and Numeric = 10 > String = 01)]
		assertEquals(1, def.compareTo(new DefaultUDF("fct1", new ADQLOperand[]{new StringConstant("blabla"),new StringConstant("blabla2")})));

		// TEST :: "fct1('blabla', POINT('COORDSYS', 1.2, 3.4))": [GREATER (same reason ; POINT is considered as a String)]
		try{
			assertEquals(1, def.compareTo(new DefaultUDF("fct1", new ADQLOperand[]{new StringConstant("blabla"),new PointFunction(new StringConstant("COORDSYS"), new NumericConstant(1.2), new NumericConstant(3.4))})));
		}catch(Exception e){
			e.printStackTrace();
			fail();
		}

		// Test with an UNKNOWN numeric type:
		// TEST :: "fct0(foo)", where foo is a simple UNKNOWN [EQUAL]
		FunctionDef def0 = new FunctionDef("fct0", null, new FunctionParam[]{new FunctionParam("whatever", new DBType(DBDatatype.VARCHAR))});
		DefaultDBColumn dbcol = new DefaultDBColumn("foo", new DefaultDBTable("toto"));
		dbcol.setDatatype(new DBType(DBDatatype.UNKNOWN));
		ADQLColumn col = new ADQLColumn("foo");
		col.setDBLink(dbcol);
		assertEquals(0, def0.compareTo(new DefaultUDF("fct0", new ADQLOperand[]{col})));
		// TEST :: "fct0(foo)", where foo is an UNKNOWN NUMERIC [LESS]
		dbcol.setDatatype(new DBType(DBDatatype.UNKNOWN_NUMERIC));
		assertEquals(-1, def0.compareTo(new DefaultUDF("fct0", new ADQLOperand[]{col})));
	}

}
