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

import java.math.BigDecimal ;
import java.sql.* ;
import java.util.Calendar ;

import org.apache.jena.datatypes.TypeMapper ;
import org.apache.jena.datatypes.xsd.XSDDatatype ;
import org.apache.jena.graph.Node ;
import org.apache.jena.graph.NodeFactory ;
import org.apache.jena.jdbc.results.metadata.columns.ColumnInfo ;
import org.apache.jena.rdf.model.Model ;
import org.apache.jena.rdf.model.ModelFactory ;
import org.apache.jena.sparql.util.NodeFactoryExtra ;
import org.junit.Assert ;
import org.junit.Test ;

/**
 * Tests for the various helper methods of {@link JdbcCompatibility}
 */
public class TestCompatibility {

    /**
     * Tests constants are not normalized to different values
     */
    @Test
    public void test_level_normalization_01() {
        Assert.assertEquals(JdbcCompatibility.LOW, JdbcCompatibility.normalizeLevel(JdbcCompatibility.LOW));
        Assert.assertEquals(JdbcCompatibility.MEDIUM, JdbcCompatibility.normalizeLevel(JdbcCompatibility.MEDIUM));
        Assert.assertEquals(JdbcCompatibility.HIGH, JdbcCompatibility.normalizeLevel(JdbcCompatibility.HIGH));
        Assert.assertEquals(JdbcCompatibility.MEDIUM, JdbcCompatibility.normalizeLevel(JdbcCompatibility.DEFAULT));
    }
    
    /**
     * Test values outside range are normalized to range ends
     */
    @Test
    public void test_level_normalization_02() {
        Assert.assertEquals(JdbcCompatibility.LOW, JdbcCompatibility.normalizeLevel(Integer.MIN_VALUE));
        Assert.assertEquals(JdbcCompatibility.HIGH, JdbcCompatibility.normalizeLevel(Integer.MAX_VALUE));
    }
    
    /**
     * Test all values in acceptable range are left as is
     */
    @Test
    public void test_level_normalization_03() {
        for (int i = 1; i <= 9; i++) {
            Assert.assertEquals(i, JdbcCompatibility.normalizeLevel(i));
        }
    }

    /**
     * Test that with low compatibility columns will not be typed as strings/detected types
     */
    @Test
    public void test_level_behaviours_columns_01() {
        Assert.assertFalse(JdbcCompatibility.shouldTypeColumnsAsString(JdbcCompatibility.LOW));
        Assert.assertFalse(JdbcCompatibility.shouldDetectColumnTypes(JdbcCompatibility.LOW));
    }
    
    /**
     * Test that with medium compatibility columns will be typed as strings but not detected types
     */
    @Test
    public void test_level_behaviours_columns_02() {
        Assert.assertTrue(JdbcCompatibility.shouldTypeColumnsAsString(JdbcCompatibility.MEDIUM));
        Assert.assertFalse(JdbcCompatibility.shouldDetectColumnTypes(JdbcCompatibility.MEDIUM));
        
        // This also applies to DEFAULT level
        Assert.assertTrue(JdbcCompatibility.shouldTypeColumnsAsString(JdbcCompatibility.DEFAULT));
        Assert.assertFalse(JdbcCompatibility.shouldDetectColumnTypes(JdbcCompatibility.DEFAULT));
    }
    
    /**
     * Test that with high compatibility columns will not be typed as strings but with detected types
     */
    @Test
    public void test_level_behaviours_columns_03() {
        Assert.assertFalse(JdbcCompatibility.shouldTypeColumnsAsString(JdbcCompatibility.HIGH));
        Assert.assertTrue(JdbcCompatibility.shouldDetectColumnTypes(JdbcCompatibility.HIGH));
    }
        
    /**
     * Detects a columns types and checks basic information from the detected type, returns the detected column information so tests can make further assertions on this
     * @param var Variable Name i.e. Column Label
     * @param value Example value to detect from
     * @param allowNulls Whether the column allows nulls
     * @param jdbcType Expected detected JDBC type
     * @param className Expected detected class name
     * @return Column Information
     * @throws SQLException
     */
    private ColumnInfo testColumnTypeDetection(String var, Node value, boolean allowNulls, int jdbcType, String className) throws SQLException {
        ColumnInfo info = JdbcCompatibility.detectColumnType(var, value, allowNulls);
        Assert.assertEquals(var, info.getLabel());
        if (allowNulls) {
            Assert.assertEquals(ResultSetMetaData.columnNullable, info.getNullability());
        } else {
            Assert.assertEquals(ResultSetMetaData.columnNoNulls, info.getNullability());
        }
        Assert.assertEquals(jdbcType, info.getType());
        Assert.assertEquals(className, info.getClassName());
        Assert.assertEquals(Node.class.getCanonicalName(), info.getTypeName());
        return info;
    }
    
    /**
     * Expect xsd:integer to be typed as integers
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_integer_01() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactoryExtra.intToNode(1234), true, Types.BIGINT, Long.class.getCanonicalName());
        Assert.assertEquals(0, info.getScale());
        Assert.assertTrue(info.isSigned());
    }
    
    /**
     * Expect xsd:int to be typed as integers
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_integer_02() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactory.createLiteral("1234", XSDDatatype.XSDint), true, Types.BIGINT, Long.class.getCanonicalName());
        Assert.assertEquals(0, info.getScale());
        Assert.assertTrue(info.isSigned());
    }
    
    /**
     * Expect xsd:long to be typed as integers
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_integer_03() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactory.createLiteral("1234", XSDDatatype.XSDlong), true, Types.BIGINT, Long.class.getCanonicalName());
        Assert.assertEquals(0, info.getScale());
        Assert.assertTrue(info.isSigned());
    }
    
    /**
     * Expect xsd:unsignedInteger to be typed as integers
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_integer_04() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactory.createLiteral("1234", XSDDatatype.XSDunsignedInt), true, Types.BIGINT, Long.class.getCanonicalName());
        Assert.assertEquals(0, info.getScale());
        Assert.assertFalse(info.isSigned());
    }
    
    /**
     * Expect xsd:unsignedLong to be typed as integers
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_integer_05() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactory.createLiteral("1234", XSDDatatype.XSDunsignedLong), true, Types.BIGINT, Long.class.getCanonicalName());
        Assert.assertEquals(0, info.getScale());
        Assert.assertFalse(info.isSigned());
    }
    
    /**
     * Expect xsd:short to be typed as integers
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_integer_06() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactory.createLiteral("1234", XSDDatatype.XSDshort), true, Types.INTEGER, Integer.class.getCanonicalName());
        Assert.assertEquals(0, info.getScale());
        Assert.assertTrue(info.isSigned());
    }
    
    /**
     * Expect xsd:unsignedShort to be typed as integers
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_integer_07() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactory.createLiteral("1234", XSDDatatype.XSDunsignedShort), true, Types.INTEGER, Integer.class.getCanonicalName());
        Assert.assertEquals(0, info.getScale());
        Assert.assertFalse(info.isSigned());
    }
    
    /**
     * Expect xsd:byte to be typed as bytes
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_byte_01() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactory.createLiteral("123", XSDDatatype.XSDbyte), true, Types.TINYINT, Byte.class.getCanonicalName());
        Assert.assertEquals(0, info.getScale());
        Assert.assertTrue(info.isSigned());
    }
    
    /**
     * Expect xsd:unsignedByte to be typed as bytes
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_byte_02() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactory.createLiteral("123", XSDDatatype.XSDunsignedByte), true, Types.TINYINT, Byte.class.getCanonicalName());
        Assert.assertEquals(0, info.getScale());
        Assert.assertFalse(info.isSigned());
    }
    
    /**
     * Expect xsd:boolean to be typed as booleans
     * @throws SQLException
     */
    @Test
    public void test_column_type_detection_boolean() throws SQLException {
        testColumnTypeDetection("x", NodeFactory.createLiteral("true", XSDDatatype.XSDboolean), true, Types.BOOLEAN, Boolean.class.getCanonicalName());
    }
    
    
    /**
     * Expect xsd:decimal to be typed as decimal
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_decimal() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactory.createLiteral("123.4", XSDDatatype.XSDdecimal), true, Types.DECIMAL, BigDecimal.class.getCanonicalName());
        Assert.assertEquals(16, info.getScale());
        Assert.assertEquals(16, info.getPrecision());
        Assert.assertTrue(info.isSigned());
    }
    
    /**
     * Expect xsd:double to be typed as double
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_double() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactory.createLiteral("12.3e4", XSDDatatype.XSDdouble), true, Types.DOUBLE, Double.class.getCanonicalName());
        Assert.assertEquals(16, info.getScale());
        Assert.assertEquals(16, info.getPrecision());
        Assert.assertTrue(info.isSigned());
    }
    
    /**
     * Expect xsd:float to be typed as float
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_float() throws SQLException {
        ColumnInfo info = testColumnTypeDetection("x", NodeFactory.createLiteral("12.3e4", XSDDatatype.XSDfloat), true, Types.FLOAT, Float.class.getCanonicalName());
        Assert.assertEquals(7, info.getScale());
        Assert.assertEquals(15, info.getPrecision());
        Assert.assertTrue(info.isSigned());
    }
    
    /**
     * Tests that xsd:dateTime is typed as Date
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_datetimes_01() throws SQLException {
        Model m = ModelFactory.createDefaultModel();
        testColumnTypeDetection("x", m.createTypedLiteral(Calendar.getInstance()).asNode(), true, Types.TIMESTAMP, Timestamp.class.getCanonicalName());
    }
    
    /**
     * Tests that xsd:date is typed as Date
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_datetimes_02() throws SQLException {
        testColumnTypeDetection("x", NodeFactory.createLiteral("2013-04-08", XSDDatatype.XSDdate), true, Types.DATE, Date.class.getCanonicalName());
    }
    
    /**
     * Tests that xsd:time is typed as Time
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_datetimes_03() throws SQLException {
        testColumnTypeDetection("x", NodeFactory.createLiteral("13:15:00", XSDDatatype.XSDtime), true, Types.TIME, Time.class.getCanonicalName());
    }
    
    /**
     * Tests that xsd:time is typed as Time
     * @throws SQLException 
     */
    @Test
    public void test_column_type_detection_datetimes_04() throws SQLException {
        testColumnTypeDetection("x", NodeFactory.createLiteral("13:15:00.123", XSDDatatype.XSDtime), true, Types.TIME, Time.class.getCanonicalName());
    }
    
    /**
     * Test that simple literals are typed as strings
     * @throws SQLException
     */
    @Test
    public void test_column_type_detection_strings_01() throws SQLException {
        testColumnTypeDetection("x", NodeFactory.createLiteral("simple"), true, Types.NVARCHAR, String.class.getCanonicalName());
    }
    
    /**
     * Test that literals with languages are typed as strings
     * @throws SQLException
     */
    @Test
    public void test_column_type_detection_strings_02() throws SQLException {
        testColumnTypeDetection("x", NodeFactory.createLiteral("simple", "en", false), true, Types.NVARCHAR, String.class.getCanonicalName());
    }
    
    /**
     * Test that xsd:string literals are typed as strings
     * @throws SQLException
     */
    @Test
    public void test_column_type_detection_strings_03() throws SQLException {
        testColumnTypeDetection("x", NodeFactory.createLiteral("simple", XSDDatatype.XSDstring), true, Types.NVARCHAR, String.class.getCanonicalName());
    }
    
    /**
     * Test that custom typed literals are typed as strings
     * @throws SQLException
     */
    @Test
    public void test_column_type_detection_strings_04() throws SQLException {
        testColumnTypeDetection("x", NodeFactory.createLiteral("simple", TypeMapper.getInstance().getSafeTypeByName("http://datatypes/custom")), true, Types.NVARCHAR, String.class.getCanonicalName());
    }
    
    /**
     * Test that URI are typed as strings
     * @throws SQLException
     */
    @Test
    public void test_column_type_detection_strings_05() throws SQLException {
        testColumnTypeDetection("x", NodeFactory.createURI("http://example.org"), true, Types.NVARCHAR, String.class.getCanonicalName());
    }
    
    /**
     * Test that blank nodes are typed as strings
     * @throws SQLException
     */
    @Test
    public void test_column_type_detection_strings_06() throws SQLException {
        testColumnTypeDetection("x", NodeFactory.createAnon(), true, Types.NVARCHAR, String.class.getCanonicalName());
    }
    
    /**
     * Tests treatment of nulls in type detection
     * @throws SQLException
     */
    @Test
    public void test_column_type_detection_nulls_01() throws SQLException {
        testColumnTypeDetection("x", null, true, Types.NVARCHAR, String.class.getCanonicalName());
    }
    
    /**
     * Tests treatment of nulls in type detection
     * @throws SQLException
     */
    @Test(expected=SQLException.class)
    public void test_column_type_detection_nulls_02() throws SQLException {
        // Expect an error to be thrown if a null is provided when the column should not allow nulls
        testColumnTypeDetection("x", null, false, Types.NVARCHAR, String.class.getCanonicalName());
    }
}