/*
 * 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.openjpa.persistence.criteria;

import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.List;

import jakarta.persistence.Query;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;

import org.apache.openjpa.kernel.Filters;

/**
 * Test JDBC Escape Syntax date/time strings in query.
 *
 *
 * @author Pinaki Poddar
 *
 */
public class TestDateStringConversion extends CriteriaTest {
    private static final String OPEN_BRACKET  = "{";
    private static final String CLOSE_BRACKET = "}";
    private static final String SINGLE_QUOTE  = "'";

    void createData(String name) {
        long now = System.currentTimeMillis();
        long tomorrow = now + 24*60*60*1000+1;

        Date date = new Date(tomorrow);
        DependentId id = new DependentId();
        id.setEffDate(date);
        id.setName(name);
        Dependent pc = new Dependent();
        pc.setId(id);
        pc.setEndDate(date);
        em.getTransaction().begin();
        em.persist(pc);
        em.getTransaction().commit();
    }

    /**
     * The persistent property Dependent.endDate being tested is declared as Date but mapped with
     * @Temporal(TemporalType.TIMESTAMP).
     *
     * When executing JPQL, the query string directly uses a JDBC Escape Syntax that is passed
     * to JDBC Driver and hence that string should encode a Timestamp.
     *
     * Criteria Query, on the other hand, must conform to the API signature and hence the expression
     * that takes the JDBC Escape Syntax must be cast to Date.class to match the declared type of
     * Dependent.endDate.
     *
     */
    public void testDateString() {
        createData("testDateString");
        long now = System.currentTimeMillis();

        String dateString = createJDBCEscapeString(new Date(now));
        String tsString = createJDBCEscapeString(new Timestamp(now));

        // add Timestamp string because Dependent.endDate is mapped to TemporalType.TimeStamp
        String jpql = "select d from Dependent d where d.endDate >= " + tsString + " ORDER BY d.endDate";


        CriteriaQuery<Dependent> c = cb.createQuery(Dependent.class);
        Root<Dependent> d = c.from(Dependent.class);
        // the literal is cast to Date.class because Dependent.endDate is declared as Date
        c.where(cb.greaterThanOrEqualTo(d.get(Dependent_.endDate), cb.literal(dateString).as(Date.class)));
        c.orderBy(cb.asc(d.get(Dependent_.endDate)));

        // can not compare SQL because JPQL and Criteria handles JDBC escape syntax in
        // a different way.
        assertSameResult(c, jpql);
    }

    /**
     * A similar query but the JDBC Escape String is passed as an parameter.
     * Parameter value binding is relaxed to accept the string as a value
     * of temporal type instance.
     * Hence does not require, as in the the earlier case, to use different
     * escape string.
     */
    public void testDateStringAsParameter() {
        createData("testDateStringAsParameter");
        long now = System.currentTimeMillis();

        Date earlier = new Date(now - 1000);

        final String dateString = createJDBCEscapeString(earlier);
        String jpql = "select d from Dependent d where d.endDate >= :dateString ORDER BY d.endDate";
        CriteriaQuery<Dependent> c = cb.createQuery(Dependent.class);
        Root<Dependent> d = c.from(Dependent.class);

        c.where(cb.greaterThanOrEqualTo(d.get(Dependent_.endDate), cb.parameter(String.class, "dateString")
                .as(Date.class)));
        c.orderBy(cb.asc(d.get(Dependent_.endDate)));

        assertEquivalence(new QueryDecorator() {
            @Override
            public void decorate(Query q) {
                q.setParameter("dateString", dateString);
            }
        }, c, jpql);

    }

    String createJDBCEscapeString(Object time) {
        String key = "";
        if (time instanceof Date)
            key = "d ";
        else if (time instanceof Time)
            key = "t ";
        else if (time instanceof Timestamp)
            key = "ts ";
        else {
            fail("Wrong object " + time + " of " + time.getClass() + " for JDBC conversion");
        }
        return OPEN_BRACKET + key + SINGLE_QUOTE + time.toString() + SINGLE_QUOTE + CLOSE_BRACKET;
    }

    public void testJDBCEscapeSyntaxTimestamp() {
        Timestamp ts = new Timestamp(System.currentTimeMillis());
        String s = "{ts '" + ts.toString() + "'}";
        assertTrue(Filters.isJDBCTemporalSyntax(s));
        assertNotNull(Filters.parseJDBCTemporalSyntax(s));
        Object converted = Filters.convert(s, Timestamp.class);
        assertTrue(converted instanceof Timestamp);
        assertEquals("Original=" + s + " Converted " + converted, ts.toString(), converted.toString());
    }

    public void testJDBCEscapeSyntaxTime() {
        Time t = new Time(System.currentTimeMillis());
        String s = "{t '" + t.toString() + "'}";
        assertTrue(Filters.isJDBCTemporalSyntax(s));
        assertNotNull(Filters.parseJDBCTemporalSyntax(s));
        Object converted = Filters.convert(s, Time.class);
        assertTrue(converted instanceof Time);
        assertEquals("Original=" + s + " Converted " + converted, t.toString(), converted.toString());
    }

    public void testJDBCEscapeSyntaxDate() {
        Date d = new Date(System.currentTimeMillis());
        String s = "{d '" + d.toString() + "'}";
        assertTrue(Filters.isJDBCTemporalSyntax(s));
        assertNotNull(Filters.parseJDBCTemporalSyntax(s));
        Object converted = Filters.convert(s, Date.class);
        assertTrue(converted instanceof Date);
        assertEquals("Original=" + s + " Converted " + converted, d.toString(), converted.toString());
    }

    void assertSameResult(CriteriaQuery<Dependent> c, String jpql) {
        List<Dependent> jResult = em.createQuery(jpql).getResultList();
        List<Dependent> cResult = em.createQuery(c).getResultList();
        assertFalse(jResult.isEmpty());
        assertEquals(cResult.size(), jResult.size());
        for (int i = 0; i < jResult.size(); i++) {
            assertEquals(jResult.get(i).getEndDate(), cResult.get(i).getEndDate());
        }
    }
}
