/*
 * 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
 *
 *   https://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.
 *
 *   SPDX-License-Identifier: Apache-2.0
 */

package org.apache.jena.sparql.function.library;
import static org.apache.jena.sparql.expr.LibTestExpr.test;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;

import org.junit.jupiter.api.Test;

import org.apache.jena.datatypes.xsd.XSDDatatype;
import org.apache.jena.query.ARQ;
import org.apache.jena.sparql.expr.*;
import org.apache.jena.sparql.expr.nodevalue.XSDFuncOp;
import org.apache.jena.sparql.util.ExprUtils;

public class TestFnFunctionsDateTimeDuration {

    @Test
    public void datetime_1() {
        test("fn:dateTime('2017-09-14'^^xsd:date, '10:11:23'^^xsd:time) = '2017-09-14T10:11:23'^^xsd:dateTime");
    }

    @Test
    public void datetime_2() {
        test("fn:dateTime('2017-09-14+01:00'^^xsd:date, '10:11:23'^^xsd:time) = '2017-09-14T10:11:23+01:00'^^xsd:dateTime");
    }

    @Test
    public void datetime_3() {
        test("fn:dateTime('2017-09-14'^^xsd:date, '10:11:23+01:00'^^xsd:time) = '2017-09-14T10:11:23+01:00'^^xsd:dateTime");
    }

    @Test
    public void datetime_4() {
        test("fn:dateTime('2017-09-14+01:00'^^xsd:date, '10:11:23+01:00'^^xsd:time) = '2017-09-14T10:11:23+01:00'^^xsd:dateTime");
    }

    @Test
    public void datetime_5() {
        // Incompatible timezones.
        assertThrows(ExprEvalException.class,
					 ()-> test("fn:dateTime('2017-09-14+09:00'^^xsd:date, '10:11:23+01:00'^^xsd:time) = '2017-09-14T10:11:23+01:00'^^xsd:dateTime") );
    }

    @Test
    public void datetime_6() {
        // Bad date
        assertThrows(ExprEvalException.class,
					 ()-> test("fn:dateTime('xyz', '10:11:23+01:00'^^xsd:time) = '2017-09-14T10:11:23+01:00'^^xsd:dateTime") );
    }

    @Test
    public void datetime_7() {
        // Bad time: 'now'^^xsd:time
        boolean systemValue = NodeValue.VerboseWarnings;
        NodeValue.VerboseWarnings = false;
        try {
            assertThrows(ExprEvalException.class,
                         ()-> test("fn:dateTime('2017-09-14+09:00', 'now'^^xsd:time) = '2017-09-14T10:11:23+01:00'^^xsd:dateTime") );
        } finally {
            NodeValue.VerboseWarnings = systemValue;
        }
    }

    @Test public void fromDateTime() {
        test("fn:year-from-dateTime('2017-09-14T17:01:02'^^xsd:dateTime)",   "2017");
        test("fn:month-from-dateTime('2017-09-14T17:01:02'^^xsd:dateTime)",  "09");
        test("fn:day-from-dateTime('2017-09-14T17:01:02'^^xsd:dateTime)",    "14");
        test("fn:hours-from-dateTime('2017-09-14T17:01:02'^^xsd:dateTime)",   "17");
        test("fn:minutes-from-dateTime('2017-09-14T17:01:02'^^xsd:dateTime)", "01");
        test("fn:seconds-from-dateTime('2017-09-14T17:01:02.5'^^xsd:dateTime)", "02.5");
        test("fn:timezone-from-dateTime('2017-09-14T17:01:02+01:00'^^xsd:dateTime)", "'PT1H'^^xsd:dayTimeDuration");
    }

    @Test public void fromDate() {
        test("fn:year-from-date('2017-09-14'^^xsd:date)",   "2017");
        test("fn:month-from-date('2017-09-14'^^xsd:date)",  "09");
        test("fn:day-from-date('2017-09-14'^^xsd:date)",    "14");
        test("fn:timezone-from-date('2017-09-14+01:00'^^xsd:date)", "'PT1H'^^xsd:dayTimeDuration");
    }

    @Test public void fromTime() {
        test("fn:hours-from-time('17:01:02'^^xsd:time)",   "17");
        test("fn:minutes-from-time('17:01:02'^^xsd:time)", "01");
        test("fn:seconds-from-time('17:01:02.5'^^xsd:time)", "02.5");
        test("fn:timezone-from-time('17:01:02+01:00'^^xsd:time)", "'PT1H'^^xsd:dayTimeDuration");
    }

    @Test public void fromDuration() {
        String s = "'P1Y2M3DT4H5M6.7S'^^xsd:duration";
        test("fn:years-from-duration("+s+")",    "1");
        test("fn:months-from-duration("+s+")",   "2");
        test("fn:days-from-duration("+s+")",     "3");
        test("fn:hours-from-duration("+s+")",    "4");
        test("fn:minutes-from-duration("+s+")",  "5");
        test("fn:seconds-from-duration("+s+")",  "6.7");
    }

    @Test public void fromMisc() {
        test("fn:hours-from-time('17:01:02'^^xsd:time)",   "17");
        test("fn:minutes-from-time('17:01:02'^^xsd:time)", "01");
        test("fn:seconds-from-time('17:01:02.5'^^xsd:time)", "02.5");
        test("fn:timezone-from-time('17:01:02+01:00'^^xsd:time)", "'PT1H'^^xsd:dayTimeDuration");
    }

    @Test public void from_strict() {
        // ARQ is lax about exact datatypes types unless in strict mode.
        // If the datatytpe has "days" (xsd:date, xsd;dateTime, xsd:duration and derived types)
        // the normal operating mode for "days-from-*" is to return the days
        // regardless of the specific datatype.
        ARQ.getContext().set(ARQ.strictSPARQL, true);
        try {
            testException("fn:year-from-dateTime('2017-09-14'^^xsd:date)");
            testException("fn:month-from-date('P1Y2M3DT4H5M6.7S'^^xsd:duration)");
            testException("fn:day-from-time('2017-09-14T17:01:02'^^xsd:dateTime)");
        } finally {
            ARQ.getContext().set(ARQ.strictSPARQL, false);
        }
    }

    private static void testException(String exprStr) {
        boolean systemWarnFlag = E_Function.WarnOnUnknownFunction;
        Expr expr = ExprUtils.parse(exprStr);
        try {
            E_Function.WarnOnUnknownFunction = false;
            // Possible warning.
            NodeValue rExpected = expr.eval(null, LibTestExpr.createTest());
            fail("Expected exception: "+exprStr);
        } catch ( ExprEvalException ex) {
        } finally { E_Function.WarnOnUnknownFunction = systemWarnFlag; }

    }

    @Test public void exprAdjustDatetimeToTz_01(){
        test(
            "fn:adjust-dateTime-to-timezone('2002-03-07T10:00:00'^^xsd:dateTime)",
            "fn:adjust-dateTime-to-timezone('2002-03-07T10:00:00'^^xsd:dateTime,"+getTimezoneDurationString()+")");
    }

    @Test public void exprAdjustDatetimeToTz_02(){
        test(
            "fn:adjust-dateTime-to-timezone('2002-03-07T10:00:00-07:00'^^xsd:dateTime)",
            "fn:adjust-dateTime-to-timezone('2002-03-07T10:00:00-07:00'^^xsd:dateTime,"+getTimezoneDurationString()+")");
    }

    @Test public void exprAdjustDatetimeToTz_03() { test("fn:adjust-dateTime-to-timezone('2002-03-07T10:00:00'^^xsd:dateTime,'-PT10H'^^xsd:dayTimeDuration)",NodeValue.makeDateTime("2002-03-07T10:00:00-10:00"));}

    @Test public void exprAdjustDatetimeToTz_04() { test("fn:adjust-dateTime-to-timezone('2002-03-07T10:00:00-07:00'^^xsd:dateTime,'-PT10H'^^xsd:dayTimeDuration)",NodeValue.makeDateTime("2002-03-07T07:00:00-10:00"));}

    @Test public void exprAdjustDatetimeToTz_05() { test("fn:adjust-dateTime-to-timezone('2002-03-07T10:00:00-07:00'^^xsd:dateTime,'PT10H'^^xsd:dayTimeDuration)",NodeValue.makeDateTime("2002-03-08T03:00:00+10:00"));}

    @Test public void exprAdjustDatetimeToTz_06() { test("fn:adjust-dateTime-to-timezone('2002-03-07T00:00:00+01:00'^^xsd:dateTime,'-PT8H'^^xsd:dayTimeDuration)",NodeValue.makeDateTime("2002-03-06T15:00:00-08:00"));}

    @Test public void exprAdjustDatetimeToTz_07() { test("fn:adjust-dateTime-to-timezone('2002-03-07T10:00:00'^^xsd:dateTime,'')",NodeValue.makeDateTime("2002-03-07T10:00:00"));}

    @Test public void exprAdjustDatetimeToTz_08() { test("fn:adjust-dateTime-to-timezone('2002-03-07T10:00:00-07:00'^^xsd:dateTime,'')",NodeValue.makeDateTime("2002-03-07T10:00:00"));}

    @Test public void exprAdjustDateToTz_01(){
        test(
            "fn:adjust-date-to-timezone('2002-03-07'^^xsd:date)",
            "fn:adjust-date-to-timezone('2002-03-07'^^xsd:date,"+getTimezoneDurationString()+")");
    }

    @Test public void exprAdjustDateToTz_02(){
        test(
            "fn:adjust-date-to-timezone('2002-03-07-07:00'^^xsd:date)",
            "fn:adjust-date-to-timezone('2002-03-07-07:00'^^xsd:date,"+getTimezoneDurationString()+")");
    }

    @Test public void exprAdjustDateToTz_03() { test("fn:adjust-date-to-timezone('2002-03-07'^^xsd:date,'-PT10H'^^xsd:dayTimeDuration)",NodeValue.makeDate("2002-03-07-10:00"));}

    @Test public void exprAdjustDateToTz_04() { test("fn:adjust-date-to-timezone('2002-03-07-07:00'^^xsd:date,'-PT10H'^^xsd:dayTimeDuration)",NodeValue.makeDate("2002-03-06-10:00"));}

    @Test public void exprAdjustDateToTz_05() { test("fn:adjust-date-to-timezone('2002-03-07'^^xsd:date,'')",NodeValue.makeDate("2002-03-07"));}

    @Test public void exprAdjustDateToTz_06() { test("fn:adjust-date-to-timezone('2002-03-07-07:00'^^xsd:date,'')",NodeValue.makeDate("2002-03-07"));}

    @Test public void exprAdjustTimeToTz_01(){
        test(
            "fn:adjust-time-to-timezone('10:00:00'^^xsd:time)",
            "fn:adjust-time-to-timezone('10:00:00'^^xsd:time,"+getTimezoneDurationString()+")");
    }

    @Test public void exprAdjustTimeToTz_02(){
        test(
            "fn:adjust-time-to-timezone('10:00:00-07:00'^^xsd:time)",
            "fn:adjust-time-to-timezone('10:00:00-07:00'^^xsd:time,"+getTimezoneDurationString()+")");
    }

    @Test public void exprAdjustTimeToTz_03() { test("fn:adjust-time-to-timezone('10:00:00'^^xsd:time,'-PT10H'^^xsd:dayTimeDuration)",NodeValue.makeNode("10:00:00-10:00",XSDDatatype.XSDtime));}

    @Test public void exprAdjustTimeToTz_04() { test("fn:adjust-time-to-timezone('10:00:00-07:00'^^xsd:time,'-PT10H'^^xsd:dayTimeDuration)",NodeValue.makeNode("07:00:00-10:00",XSDDatatype.XSDtime));}

    @Test public void exprAdjustTimeToTz_05() { test("fn:adjust-time-to-timezone('10:00:00'^^xsd:time,'')",NodeValue.makeNode("10:00:00",XSDDatatype.XSDtime));}

    @Test public void exprAdjustTimeToTz_06() { test("fn:adjust-time-to-timezone('10:00:00-07:00'^^xsd:time,'')",NodeValue.makeNode("10:00:00",XSDDatatype.XSDtime));}

    @Test public void exprAdjustTimeToTz_07() { test("fn:adjust-time-to-timezone('10:00:00-07:00'^^xsd:time,'PT10H'^^xsd:dayTimeDuration)",NodeValue.makeNode("03:00:00+10:00",XSDDatatype.XSDtime));}
    //@Test public void exprStrJoin()      { test("fn:string-join('a', 'b')", NodeValue.makeString("ab")); }

    @Test public void localTimezone_1() { test("fn:implicit-timezone()", nv->nv.isDayTimeDuration()); }

    /** Return syntax for the timezone as a day-time duration */
    private String getTimezoneDurationString(){
        // Timezone for ARQ is not the JVM timezone.
        return XSDFuncOp.implicitTimezone().asQuotedString();
    }
}
