/*
 * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * Licensed 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.
 *
 * Contributors:
 *     Florent Guillaume
 */

package org.nuxeo.ecm.core.query.sql.model;

import static org.junit.Assert.assertEquals;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.Test;
import org.nuxeo.ecm.core.query.sql.SQLQueryParser;

/**
 * Simple test of the visitor using a dumb printer.
 * <p>
 * Also tests SQLQuery.toString() while we're at it.
 *
 * @author Florent Guillaume
 */
public class TestQueryVisitor {

    private static void check(String sql, String expected) {
        PrintVisitor v = new PrintVisitor();
        SQLQuery query = SQLQueryParser.parse(sql);
        assertEquals(sql, query.toString());
        v.visitQuery(query);
        assertEquals(expected, v.toString());
    }

    @Test
    public void testRemoveTZSuffixes() {
        assertEquals("000", removeTzSuffix("000+00:00')"));
        assertEquals("000", removeTzSuffix("000Z')"));
    }

    @Test
    public void testVisitor() throws Exception {
        String sql;
        String expected;

        sql = "select p as p from t where title=\"%test\"";
        expected = "SELECT p FROM t WHERE (title = '%test')";
        check(sql, expected);

        sql = "select p from t where foo in (1, 2)";
        expected = "SELECT p FROM t WHERE (foo IN (1, 2))";
        check(sql, expected);

        sql = "SELECT p, q AS qq, f(x) FROM t, u, v" + //
                " WHERE title = 'ab' AND des = 'cd'" + //
                " ORDER BY x DESC,y,z  DESC" + //
                " LIMIT 8   OFFSET 43";
        expected = "SELECT p, q AS qq, f(x) FROM t, u, v" + //
                " WHERE ((title = 'ab') AND (des = 'cd'))" + //
                " ORDER BY x DESC, y, z DESC" + //
                " LIMIT 8 OFFSET 43";
        check(sql, expected);

        sql = "select foo from docs";
        expected = "SELECT foo FROM docs";
        check(sql, expected);

        sql = "select * from d where foo <> DATE '2008-01-01'";
        expected = "SELECT * FROM d WHERE (foo <> DATE '2008-01-01')";
        check(sql, expected);

        sql = "select * from d where foo between DATE '2008-01-01' and DATE '2008-02-01'";
        expected = "SELECT * FROM d WHERE (foo BETWEEN DATE '2008-01-01' AND DATE '2008-02-01')";
        check(sql, expected);

        // hack around timezone variations for this test
        sql = "select * from d where foo = TIMESTAMP '2008-08-08 12:34:56'";
        expected = "SELECT * FROM d WHERE (foo = TIMESTAMP '2008-08-08T12:34:56.000+00:00')";

        PrintVisitor v = new PrintVisitor();
        v.visitQuery(SQLQueryParser.parse(sql));
        String got = v.toString();
        assertEquals(removeTzSuffix(expected), removeTzSuffix(got));

        sql = "select * from d where a = 2 OR NOT b = 5";
        expected = "SELECT * FROM d WHERE ((a = 2) OR (NOT (b = 5)))";
        check(sql, expected);

        sql = "select * from d where NOT (a = 2 OR b = 5)";
        expected = "SELECT * FROM d WHERE (NOT ((a = 2) OR (b = 5)))";
        check(sql, expected);

        sql = "select foo from docs where x = 1 AND x=2 AND x = 3";
        expected = "SELECT foo FROM docs WHERE AND(x = 1, x = 2, x = 3)";
        SQLQuery query = SQLQueryParser.parse(sql);
        // AndExpression is not generated by the parser, build it by hand
        Expression pred = query.where.predicate;
        List<Predicate> predicates = new LinkedList<Predicate>();
        predicates.add((Predicate) ((Expression) pred.lvalue).lvalue);
        predicates.add((Predicate) ((Expression) pred.lvalue).rvalue);
        predicates.add((Predicate) pred.rvalue);
        query = new SQLQuery(query.select, query.from, new WhereClause(new MultiExpression(Operator.AND, predicates)),
                query.groupBy, query.having, query.orderBy);
        v = new PrintVisitor();
        v.visitQuery(query);
        assertEquals(expected, v.toString());

        sql = "select * from d where b IS NULL or b IS NOT NULL";
        expected = "SELECT * FROM d WHERE ((IS NULL b) OR (IS NOT NULL b))";
        check(sql, expected);
    }

    private static final Pattern REMOVE_TZ_PATTERN = Pattern.compile("(.*)((\\+|-).*|Z)'\\)$");

    private String removeTzSuffix(String value) {
        Matcher matcher = REMOVE_TZ_PATTERN.matcher(value);
        if (!matcher.matches()) {
            throw new AssertionError(REMOVE_TZ_PATTERN + " pattern does not match " + value);
        }
        return matcher.group(1);
    }

}

class PrintVisitor extends DefaultQueryVisitor {

    public StringBuilder buf = new StringBuilder();

    @Override
    public String toString() {
        return buf.toString();
    }

    @Override
    public void visitQuery(SQLQuery node) {
        super.visitQuery(node);
        if (node.limit != 0) {
            buf.append(" LIMIT ");
            buf.append(node.limit);
            if (node.offset != 0) {
                buf.append(" OFFSET ");
                buf.append(node.offset);
            }
        }
    }

    @Override
    public void visitSelectClause(SelectClause node) {
        buf.append("SELECT ");
        SelectList elements = node.elements;
        if (elements.isEmpty()) {
            buf.append("*");
        } else {
            boolean first = true;
            for (Entry<String, Operand> entry : elements.entrySet()) {
                if (!first) {
                    buf.append(", ");
                }
                String alias = entry.getKey();
                Operand op = entry.getValue();
                op.accept(PrintVisitor.this);
                if (!alias.equals(op.toString())) {
                    buf.append(" AS ");
                    buf.append(alias);
                }
                first = false;
            }
        }
    }

    @Override
    public void visitFromClause(FromClause node) {
        buf.append(" FROM ");
        FromList elements = node.elements;
        boolean first = true;
        for (String element : elements.values()) {
            if (!first) {
                buf.append(", ");
            }
            buf.append(element);
            first = false;
        }
    }

    @Override
    public void visitWhereClause(WhereClause node) {
        buf.append(" WHERE ");
        super.visitWhereClause(node);
    }

    @Override
    public void visitGroupByClause(GroupByClause node) {
        String[] elements = node.elements;
        if (elements.length == 0) {
            return;
        }
        buf.append(" GROUP BY ");
        for (int i = 0; i < elements.length; i++) {
            if (i != 0) {
                buf.append(", ");
            }
            buf.append(elements[i]);
        }
    }

    @Override
    public void visitHavingClause(HavingClause node) {
        if (node.predicate != null) {
            buf.append(" HAVING ");
            super.visitHavingClause(node);
        }
    }

    @Override
    public void visitOrderByClause(OrderByClause node) {
        if (node.elements.size() == 0) {
            return;
        }
        buf.append(" ORDER BY ");
        super.visitOrderByClause(node);
    }

    @Override
    public void visitOrderByList(OrderByList node) {
        for (int i = 0; i < node.size(); i++) {
            if (i != 0) {
                buf.append(", ");
            }
            node.get(i).accept(this);
        }
    }

    @Override
    public void visitOrderByExpr(OrderByExpr node) {
        super.visitOrderByExpr(node);
        if (node.isDescending) {
            buf.append(" DESC");
        }
    }

    @Override
    public void visitExpression(Expression node) {
        buf.append('(');
        if (node.rvalue == null) {
            // NOT
            node.operator.accept(this);
            buf.append(' ');
            node.lvalue.accept(this);
        } else if (node.operator == Operator.BETWEEN) {
            LiteralList l = (LiteralList) node.rvalue;
            node.lvalue.accept(this);
            buf.append(' ');
            node.operator.accept(this);
            buf.append(' ');
            l.get(0).accept(this);
            buf.append(" AND ");
            l.get(1).accept(this);
        } else {
            node.lvalue.accept(this);
            buf.append(' ');
            node.operator.accept(this);
            buf.append(' ');
            node.rvalue.accept(this);
        }
        buf.append(')');
    }

    @Override
    public void visitMultiExpression(MultiExpression node) {
        node.operator.accept(this);
        buf.append('(');
        for (Iterator<Predicate> it = node.predicates.iterator(); it.hasNext();) {
            StringBuilder bak = buf;
            buf = new StringBuilder();
            it.next().accept(this);
            if (buf.charAt(0) == '(' && buf.charAt(buf.length() - 1) == ')') {
                buf.deleteCharAt(0);
                buf.deleteCharAt(buf.length() - 1);
            }
            bak.append(buf);
            buf = bak;
            if (it.hasNext()) {
                buf.append(", ");
            }
        }
        buf.append(')');
    }

    @Override
    public void visitOperator(Operator node) {
        buf.append(node.toString());
    }

    @Override
    public void visitReference(Reference node) {
        buf.append(node.name);
    }

    @Override
    public void visitReferenceList(ReferenceList node) {
        for (int i = 0; i < node.size(); i++) {
            if (i != 0) {
                buf.append(", ");
            }
            node.get(i).accept(this);
        }
    }

    @Override
    public void visitLiteral(Literal node) {
        buf.append(node.toString());
    }

    @Override
    public void visitLiteralList(LiteralList node) {
        buf.append('(');
        for (Iterator<Literal> it = node.iterator(); it.hasNext();) {
            it.next().accept(this);
            if (it.hasNext()) {
                buf.append(", ");
            }
        }
        buf.append(')');
    }

    @Override
    public void visitFunction(Function node) {
        buf.append(node.name);
        buf.append("(");
        for (Iterator<Operand> it = node.args.iterator(); it.hasNext();) {
            it.next().accept(this);
            if (it.hasNext()) {
                buf.append(", ");
            }
        }
        buf.append(")");
    }

    @Override
    public void visitOperandList(OperandList node) {
        for (Iterator<Operand> it = node.iterator(); it.hasNext();) {
            it.next().accept(this);
            if (it.hasNext()) {
                buf.append(", ");
            }
        }
    }

}
