/*
 * 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.phoenix.end2end;

import static org.apache.phoenix.util.TestUtil.ROW1;
import static org.apache.phoenix.util.TestUtil.ROW2;
import static org.apache.phoenix.util.TestUtil.ROW3;
import static org.apache.phoenix.util.TestUtil.ROW4;
import static org.apache.phoenix.util.TestUtil.ROW5;
import static org.apache.phoenix.util.TestUtil.ROW6;
import static org.apache.phoenix.util.TestUtil.ROW7;
import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES;
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 java.math.BigDecimal;
import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.util.PropertiesUtil;
import org.junit.Assert;
import org.junit.Test;
import org.junit.experimental.categories.Category;

@Category(ParallelStatsDisabledTest.class)
public class CoalesceFunctionIT extends ParallelStatsDisabledIT {

  @Test
  public void testCoalesce() throws Exception {
    String tenantId = getOrganizationId();
    String tableName = initATableValues(tenantId, getDefaultSplits(tenantId), getUrl());
    Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
    Connection conn = DriverManager.getConnection(getUrl(), props);
    String query = "SELECT entity_id, a_integer + COALESCE(x_integer,1) FROM " + tableName
      + " WHERE organization_id = ? AND a_integer >= 6 AND a_integer <= 7";
    PreparedStatement statement = conn.prepareStatement(query);
    statement.setString(1, tenantId);
    ResultSet rs = statement.executeQuery();

    assertTrue(rs.next());
    assertEquals(ROW6, rs.getString(1));
    assertEquals(7, rs.getInt(2));

    assertTrue(rs.next());
    assertEquals(ROW7, rs.getString(1));
    assertEquals(12, rs.getInt(2));

    assertFalse(rs.next());
    conn.close();
  }

  @Test
  public void coalesceWithSumExplicitLong() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();
    String ddl = "CREATE TABLE " + tableName + "(" + "    ID BIGINT NOT NULL, "
      + "    \"COUNT\" BIGINT " + "    CONSTRAINT pk PRIMARY KEY(ID))";
    conn.createStatement().execute(ddl);

    conn.createStatement().execute("UPSERT INTO " + tableName + "(ID, \"COUNT\") VALUES(2, null)");
    conn.commit();

    ResultSet rs =
      conn.createStatement().executeQuery("SELECT " + "COALESCE(SUM(\"COUNT\"), CAST(0 AS BIGINT)) " // explicitly
                                                                                                     // def
                                                                                                     // long
        + "FROM  " + tableName + " GROUP BY ID");

    assertTrue(rs.next());
    assertEquals(0, rs.getLong(1));
    assertFalse(rs.wasNull());
  }

  @Test
  public void coalesceWithSumImplicitLong() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();
    String ddl = "CREATE TABLE " + tableName + "(" + "    ID BIGINT NOT NULL, "
      + "    \"COUNT\" BIGINT " + "    CONSTRAINT pk PRIMARY KEY(ID))";
    conn.createStatement().execute(ddl);

    conn.createStatement().execute("UPSERT INTO " + tableName + "(ID, \"COUNT\") VALUES(2, null)");
    conn.commit();

    ResultSet rs = conn.createStatement().executeQuery("SELECT " + "COALESCE(SUM(\"COUNT\"), 0) " // no
                                                                                                  // long
                                                                                                  // def
      + "FROM " + tableName + " GROUP BY ID");

    assertTrue(rs.next());
    assertEquals(0, rs.getLong(1));
    assertFalse(rs.wasNull());
  }

  @Test
  public void coalesceWithSecondParamAsExpression() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();
    String ddl = "CREATE TABLE " + tableName + "(" + "    ID BIGINT NOT NULL, "
      + "    \"COUNT\" BIGINT " + "    CONSTRAINT pk PRIMARY KEY(ID))";
    conn.createStatement().execute(ddl);

    conn.createStatement().execute("UPSERT INTO " + tableName + "(ID, \"COUNT\") VALUES(2, null)");
    conn.commit();

    ResultSet rs =
      conn.createStatement().executeQuery("SELECT " + "COALESCE(SUM(\"COUNT\"), SUM(ID)) " // second
                                                                                           // param
                                                                                           // as
                                                                                           // expression
        + "FROM " + tableName + " GROUP BY ID");

    assertTrue(rs.next());
    assertEquals(2, rs.getLong(1));
    assertFalse(rs.wasNull());
  }

  @Test
  public void nonTypedSecondParameterLong() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();

    String ddl =
      "CREATE TABLE " + tableName + "(" + "    ID BIGINT NOT NULL, " + "    \"COUNT\" BIGINT " // first
                                                                                               // parameter
                                                                                               // to
                                                                                               // coalesce
        + "    CONSTRAINT pk PRIMARY KEY(ID))";
    conn.createStatement().execute(ddl);

    conn.createStatement().execute("UPSERT INTO " + tableName + "(ID, \"COUNT\") VALUES(2, null)");
    conn.commit();

    ResultSet rs = conn.createStatement().executeQuery(
      "SELECT " + "COALESCE(NTH_VALUE(\"COUNT\", 100) WITHIN GROUP (ORDER BY \"COUNT\" DESC), 0) " // second
                                                                                                   // param
                                                                                                   // is
                                                                                                   // int
        + "FROM " + tableName + " GROUP BY ID");

    assertTrue(rs.next());
    assertEquals(0, rs.getLong(1));
    assertFalse(rs.wasNull());
  }

  @Test
  public void nonTypedSecondParameterUnsignedDataTypes() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();
    String ddl =
      "CREATE TABLE " + tableName + "(" + "    ID BIGINT NOT NULL, " + "    \"COUNT\" UNSIGNED_INT " // first
                                                                                                     // parameter
                                                                                                     // to
                                                                                                     // coalesce
        + "    CONSTRAINT pk PRIMARY KEY(ID))";
    conn.createStatement().execute(ddl);

    conn.createStatement().execute("UPSERT INTO " + tableName + " (ID, \"COUNT\") VALUES(2, null)");
    conn.commit();

    // second param to coalesce is signed int
    ResultSet rs = conn.createStatement()
      .executeQuery("SELECT "
        + " COALESCE(NTH_VALUE(\"COUNT\", 100) WITHIN GROUP (ORDER BY \"COUNT\" DESC), 1) "
        + " FROM " + tableName + " GROUP BY ID");

    assertTrue(rs.next());
    assertEquals(1, rs.getInt(1));
    assertFalse(rs.wasNull());
  }

  @Test
  public void testWithNthValueAggregationFunction() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();

    String ddl = "CREATE TABLE " + tableName + "(" + "    ID BIGINT NOT NULL, "
      + "    \"DATE\" TIMESTAMP NOT NULL, " + "    \"COUNT\" BIGINT "
      + "    CONSTRAINT pk PRIMARY KEY(ID, \"DATE\"))";
    conn.createStatement().execute(ddl);

    conn.createStatement().execute(
      "UPSERT INTO " + tableName + "(ID, \"DATE\", \"COUNT\") VALUES(1, CURRENT_TIME(), 1)");
    conn.createStatement().execute(
      "UPSERT INTO " + tableName + "(ID, \"DATE\", \"COUNT\") VALUES(1, CURRENT_TIME(), 2)");
    conn.createStatement().execute(
      "UPSERT INTO " + tableName + "(ID, \"DATE\", \"COUNT\") VALUES(2, CURRENT_TIME(), 1)");
    conn.commit();

    // second param to coalesce is signed int
    ResultSet rs = conn.createStatement()
      .executeQuery("SELECT " + " COALESCE(" + "            NTH_VALUE(\"COUNT\", 2000)" // should
                                                                                        // evaluate
                                                                                        // null
        + "            WITHIN GROUP (ORDER BY \"COUNT\" DESC)," + "       0)" + "FROM  " + tableName
        + " GROUP BY ID");

    assertTrue(rs.next());
    assertEquals(0, rs.getLong(1));
    assertFalse(rs.wasNull());
  }

  @Test
  public void wrongDataTypeOfSecondParameter() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();

    String ddl = "CREATE TABLE " + tableName + "(" + "    ID UNSIGNED_INT NOT NULL, "
      + "    \"COUNT\" UNSIGNED_INT " + "    CONSTRAINT pk PRIMARY KEY(ID))";
    conn.createStatement().execute(ddl);

    conn.createStatement().execute("UPSERT INTO " + tableName + "(ID, \"COUNT\") VALUES(2, null)");
    conn.commit();

    try {
      conn.createStatement().executeQuery("SELECT " + "COALESCE(MIN(\"COUNT\"), -1) " // invalid
                                                                                      // value for
                                                                                      // UNSIGNED_INT
        + "FROM " + tableName + " GROUP BY ID");

      Assert.fail("CANNOT CONVERT TYPE exception expected");
    } catch (SQLException e) {

    }
  }

  @Test
  public void testImplicitSecondArgCastingException() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();

    String ddl = "CREATE TABLE " + tableName + "(" + "    ID INTEGER NOT NULL, "
      + "    \"COUNT\" UNSIGNED_INT " // first parameter to coalesce
      + "    CONSTRAINT pk PRIMARY KEY(ID))";
    conn.createStatement().execute(ddl);

    conn.createStatement().execute("UPSERT INTO " + tableName + "(ID, \"COUNT\") VALUES(-2, null)");
    conn.commit();

    try {
      ResultSet rs = conn.createStatement().executeQuery(
        "SELECT " + "COALESCE(MIN(\"COUNT\"), ID) " + "FROM " + tableName + " GROUP BY ID");

      assertTrue(rs.next());
      assertEquals(0, rs.getLong(1));
      fail("Should not cast -2 to UNSIGNED_INT");
    } catch (SQLException e) {
      assertEquals(SQLExceptionCode.ILLEGAL_DATA.getErrorCode(), e.getErrorCode());
    }
  }

  @Test
  public void testImplicitSecondArgCasting() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();

    String ddl =
      "CREATE TABLE " + tableName + "(" + "    ID DOUBLE NOT NULL, " + "    \"COUNT\" INTEGER " // first
                                                                                                // parameter
                                                                                                // to
                                                                                                // coalesce
        + "    CONSTRAINT pk PRIMARY KEY(ID))";
    conn.createStatement().execute(ddl);

    conn.createStatement()
      .execute("UPSERT INTO " + tableName + "(ID, \"COUNT\") VALUES(2.0, null)");
    conn.commit();

    ResultSet rs = conn.createStatement().executeQuery(
      "SELECT " + "COALESCE(MIN(\"COUNT\"), ID) " + "FROM " + tableName + " GROUP BY ID");

    assertTrue(rs.next());
    assertEquals(2, rs.getLong(1));
    assertFalse(rs.wasNull());
  }

  @Test
  public void testCoalesceInRowKeyColumn() throws Exception {
    Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
    Connection conn = DriverManager.getConnection(getUrl(), props);
    String tableName = generateUniqueName();
    conn.createStatement().execute(
      "CREATE TABLE " + tableName + "(k1 decimal, k2 decimal, constraint pk primary key (k1,k2))");
    conn.createStatement().execute("UPSERT INTO " + tableName + "(k2) VALUES (1)");
    conn.createStatement().execute("UPSERT INTO " + tableName + " VALUES (2,2)");
    conn.createStatement().execute("UPSERT INTO " + tableName + " VALUES (3,3)");
    conn.commit();

    ResultSet rs =
      conn.createStatement().executeQuery("SELECT coalesce(k1, 1) ,k2 FROM " + tableName);
    assertTrue(rs.next());
    assertEquals(1, rs.getInt(1));
    assertEquals(1, rs.getInt(2));
    assertTrue(rs.next());
    assertEquals(2, rs.getInt(1));
    assertEquals(2, rs.getInt(2));
    assertTrue(rs.next());
    assertEquals(3, rs.getInt(1));
    assertEquals(3, rs.getInt(2));
    assertFalse(rs.next());
  }

  @Test
  public void testNull() throws Exception {
    Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
    Connection conn = DriverManager.getConnection(getUrl(), props);
    String tableName = generateUniqueName();
    conn.createStatement().execute(
      "CREATE TABLE " + tableName + "(k1 decimal, k2 decimal, constraint pk primary key (k1))");
    conn.createStatement().execute("UPSERT INTO " + tableName + " VALUES (1,1)");
    conn.commit();

    ResultSet rs =
      conn.createStatement().executeQuery("SELECT coalesce(null, null) FROM " + tableName);
    assertTrue(rs.next());
    rs.getInt(1);
    assertTrue(rs.wasNull());
    assertFalse(rs.next());
  }

  @Test
  public void testCoalesceFunction() throws Exception {
    String tenantId = getOrganizationId();
    String tableName = initATableValues(generateUniqueName(), tenantId, getDefaultSplits(tenantId),
      new Date(System.currentTimeMillis()), null, getUrl(), null);
    String query = "SELECT entity_id FROM " + tableName + " WHERE coalesce(X_DECIMAL,0.0) = 0.0";
    Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
    Connection conn = DriverManager.getConnection(getUrl(), props);
    PreparedStatement stmt = conn.prepareStatement(
      "UPSERT INTO  " + tableName + " (organization_id,entity_id,x_decimal) values(?,?,?)");
    stmt.setString(1, getOrganizationId());
    stmt.setString(2, ROW1);
    stmt.setBigDecimal(3, BigDecimal.valueOf(1.0));
    stmt.execute();
    stmt.setString(2, ROW3);
    stmt.setBigDecimal(3, BigDecimal.valueOf(2.0));
    stmt.execute();
    stmt.setString(2, ROW4);
    stmt.setBigDecimal(3, BigDecimal.valueOf(3.0));
    stmt.execute();
    stmt.setString(2, ROW5);
    stmt.setBigDecimal(3, BigDecimal.valueOf(0.0));
    stmt.execute();
    stmt.setString(2, ROW6);
    stmt.setBigDecimal(3, BigDecimal.valueOf(4.0));
    stmt.execute();
    conn.commit();

    conn = DriverManager.getConnection(getUrl(), props);
    try {
      PreparedStatement statement = conn.prepareStatement(query);
      ResultSet rs = statement.executeQuery();
      assertTrue(rs.next());
      assertEquals(rs.getString(1), ROW2);
      assertTrue(rs.next());
      assertEquals(rs.getString(1), ROW5);
      assertFalse(rs.next());
    } finally {
      conn.close();
    }
  }

}
