/*
 * 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.TEST_PROPERTIES;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.util.ByteUtil;
import org.apache.phoenix.util.PropertiesUtil;
import org.apache.phoenix.util.TestUtil;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.phoenix.thirdparty.com.google.common.collect.Lists;

/**
 * Tests for the LPAD built-in function.
 */

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

  /**
   * Helper to test LPAD function connection to be used name of column to query length of the output
   * string fill characters to be used while prepending sort order of the pk column expected output
   * of LPAD function base name of the table
   */
  private void testLpadHelper(Connection conn, String colName, int length,
    List<String> fillStringList, List<String> expectedOutputList, String tableName,
    String sortOrder) throws Exception {
    assertEquals("fillStringList and expectedOutputList should be of equal size",
      fillStringList.size(), expectedOutputList.size());
    for (int id = 0; id < fillStringList.size(); ++id) {
      String fillString = fillStringList.get(id);
      String lPadExpr = fillString != null ? "LPAD(%s,?,?)" : "LPAD(%s,?)";
      String sql = String.format("SELECT " + lPadExpr + " FROM " + tableName + "_%s WHERE id=?",
        colName, sortOrder);
      PreparedStatement stmt = conn.prepareStatement(sql);
      int index = 1;
      stmt.setInt(index++, length);
      if (fillString != null) stmt.setString(index++, fillString);
      stmt.setInt(index++, id);

      ResultSet rs = stmt.executeQuery();
      assertTrue("Expected exactly one row to be returned ", rs.next());
      assertEquals("LPAD returned incorrect result ", expectedOutputList.get(id), rs.getString(1));
      assertFalse("Expected exactly one row to be returned ", rs.next());
    }
  }

  /**
   * Helper to test LPAD function connection to phoenix list of values to test length to be used in
   * lpad function list of fill string to be used while testing name of column to be used as
   * function input list of expected output values expected output of lpad function
   */
  private void testLpad(Connection conn, List<String> inputList, int length,
    List<String> fillStringList, String colName, List<String> expectedOutputList) throws Exception {
    String tableName = TestUtil.initTables(conn, "VARCHAR", new ArrayList<Object>(inputList));
    testLpadHelper(conn, colName, length, fillStringList, expectedOutputList, tableName, "ASC");
    testLpadHelper(conn, colName, length, fillStringList, expectedOutputList, tableName, "DESC");
  }

  private void testLpad(Connection conn, List<String> inputList, int length,
    List<String> fillStringList, List<String> expectedOutputList) throws Exception {
    testLpad(conn, inputList, length, fillStringList, "pk", expectedOutputList);
  }

  @Test
  public void testCharPadding() throws Exception {
    ResultSet rs;
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();
    conn.createStatement().execute("CREATE TABLE " + tableName + " (k CHAR(3) PRIMARY KEY)");
    conn.createStatement().execute("UPSERT INTO " + tableName + " VALUES('a')");
    conn.createStatement().execute("UPSERT INTO " + tableName + " VALUES('ab')");
    conn.commit();
    rs = conn.createStatement().executeQuery("SELECT * FROM " + tableName + " ORDER BY k");
    assertTrue(rs.next());
    assertEquals("a", rs.getString(1));
    assertTrue(rs.next());
    assertEquals("ab", rs.getString(1));
    assertFalse(rs.next());
    String tableNameDesc = generateUniqueName();
    conn.createStatement()
      .execute("CREATE TABLE " + tableNameDesc + " (k CHAR(3) PRIMARY KEY DESC)");
    conn.createStatement().execute("UPSERT INTO " + tableNameDesc + " VALUES('a')");
    conn.createStatement().execute("UPSERT INTO " + tableNameDesc + " VALUES('ab')");
    conn.commit();
    rs = conn.createStatement().executeQuery("SELECT * FROM " + tableNameDesc + " ORDER BY k DESC");
    assertTrue(rs.next());
    assertEquals("ab", rs.getString(1));
    assertTrue(rs.next());
    assertEquals("a", rs.getString(1));
    assertFalse(rs.next());
  }

  @Test
  public void testBinaryPadding() throws Exception {
    ResultSet rs;
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();
    conn.createStatement().execute("CREATE TABLE " + tableName + " (k BINARY(3) PRIMARY KEY)");
    conn.createStatement().execute("UPSERT INTO " + tableName + " VALUES('a')");
    conn.createStatement().execute("UPSERT INTO " + tableName + " VALUES('ab')");
    conn.commit();
    rs = conn.createStatement().executeQuery("SELECT * FROM " + tableName + " ORDER BY k");
    assertTrue(rs.next());
    assertArrayEquals(ByteUtil.concat(Bytes.toBytes("a"), QueryConstants.SEPARATOR_BYTE_ARRAY,
      QueryConstants.SEPARATOR_BYTE_ARRAY), rs.getBytes(1));
    assertTrue(rs.next());
    assertArrayEquals(ByteUtil.concat(Bytes.toBytes("ab"), QueryConstants.SEPARATOR_BYTE_ARRAY),
      rs.getBytes(1));
    assertFalse(rs.next());

    String tableNameDesc = generateUniqueName();
    conn.createStatement()
      .execute("CREATE TABLE " + tableNameDesc + " (k BINARY(3) PRIMARY KEY DESC)");
    conn.createStatement().execute("UPSERT INTO " + tableNameDesc + " VALUES('a')");
    conn.createStatement().execute("UPSERT INTO " + tableNameDesc + " VALUES('ab')");
    conn.commit();
    rs = conn.createStatement().executeQuery("SELECT * FROM " + tableNameDesc + " ORDER BY k DESC");
    assertTrue(rs.next());
    assertArrayEquals(ByteUtil.concat(Bytes.toBytes("ab"), QueryConstants.SEPARATOR_BYTE_ARRAY),
      rs.getBytes(1));
    assertTrue(rs.next());
    assertArrayEquals(ByteUtil.concat(Bytes.toBytes("a"), QueryConstants.SEPARATOR_BYTE_ARRAY,
      QueryConstants.SEPARATOR_BYTE_ARRAY), rs.getBytes(1));
    assertFalse(rs.next());
  }

  @Test
  public void testNullInputStringSB() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    testLpad(conn, Lists.newArrayList("X", "X"), 4, Lists.newArrayList("", ""), "kv",
      Lists.<String> newArrayList(null, null));
  }

  @Test
  public void testEmptyFillExpr() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    testLpad(conn, Lists.newArrayList("ABCD", "ണഫɰɸ"), 6, Lists.newArrayList("", ""),
      Lists.<String> newArrayList(null, null));
  }

  @Test
  public void testDefaultFill() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    testLpad(conn, Lists.newArrayList("ABCD", "ണഫɰɸ"), 6, Lists.<String> newArrayList(null, null),
      Lists.newArrayList("  ABCD", "  ണഫɰɸ"));
  }

  @Test
  public void testLpadFillLengthGreaterThanPadLength() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    testLpad(conn, Lists.newArrayList("ABCD", "ണഫɰɸ", "ണഫɰɸ", "ABCD"), 8,
      Lists.newArrayList("123456", "ɚɚɦɚɚɦ", "123456", "ണഫɰɸണഫ"),
      Lists.newArrayList("1234ABCD", "ɚɚɦɚണഫɰɸ", "1234ണഫɰɸ", "ണഫɰɸABCD"));
  }

  @Test
  public void testLpadFillLengthLessThanPadLength() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    testLpad(conn, Lists.newArrayList("ABCD", "ɰɸɰɸ", "ɰɸɰɸ", "ABCD"), 8,
      Lists.newArrayList("12", "ഫɰ", "12", "ഫɰ"),
      Lists.newArrayList("1212ABCD", "ഫɰഫɰɰɸɰɸ", "1212ɰɸɰɸ", "ഫɰഫɰABCD"));
  }

  @Test
  public void testLpadFillLengthEqualPadLength() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    testLpad(conn, Lists.newArrayList("ABCD", "ɰɸɰɸ", "ɰɸɰɸ", "ABCD"), 8,
      Lists.newArrayList("1234", "ണഫɰɸ", "1234", "ണഫɰɸ"),
      Lists.newArrayList("1234ABCD", "ണഫɰɸɰɸɰɸ", "1234ɰɸɰɸ", "ണഫɰɸABCD"));
  }

  @Test
  public void testLpadZeroPadding() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    ArrayList<String> inputList = Lists.newArrayList("ABCD", "ണഫɰɸ", "ണഫɰɸ", "ABCD");
    testLpad(conn, inputList, 4, Lists.newArrayList("1234", "ɚɦɚɦ", "1234", "ɚɦɚɦ"), inputList);
  }

  @Test
  public void testLpadTrucate() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    testLpad(conn, Lists.newArrayList("ABCD", "ണഫɰɸ", "ണഫɰɸ", "ABCD"), 2,
      Lists.newArrayList("12", "ɚɦ", "12", "ɚɦ"), Lists.newArrayList("AB", "ണഫ", "ണഫ", "AB"));
  }

  @Test
  public void testLpadZeroOutputStringLength() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    testLpad(conn, Lists.newArrayList("ABCD", "ണഫɰɸ", "ണഫɰɸ", "ABCD"), 0,
      Lists.newArrayList("12", "ɚɦ", "12", "ɚɦ"),
      Lists.<String> newArrayList(null, null, null, null));
  }

  @Test
  public void testNegativeOutputStringLength() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    testLpad(conn, Lists.newArrayList("ABCD", "ണഫɰɸ", "ണഫɰɸ", "ABCD"), -1,
      Lists.newArrayList("12", "ɚɦ", "12", "ɚɦ"),
      Lists.<String> newArrayList(null, null, null, null));
  }

  @Test
  public void testStrConcat() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();
    conn.createStatement().execute("create table " + tableName
      + " (PK1 integer, F1 varchar, F2 varchar, F3 varchar, F4 varchar, constraint PK primary key (PK1))");
    conn.createStatement()
      .execute("upsert into " + tableName + "(PK1, F1,F3) values(0, 'tortilla', 'chip')");
    conn.commit();

    ResultSet rs = conn.createStatement()
      .executeQuery("select * from " + tableName + " where (F1||F2||F3||F4)='tortillachip'");
    assertTrue(rs.next());
    assertEquals(0, rs.getInt(1));
    assertFalse(rs.next());
  }

  @Test
  public void testLpadWithNullArgs() throws Exception {
    ResultSet rs;
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = generateUniqueName();
    conn.createStatement()
      .execute("CREATE TABLE " + tableName + " (k CHAR(3) PRIMARY KEY, v1 VARCHAR, v2 INTEGER)");
    conn.createStatement().execute("UPSERT INTO " + tableName + "(k) VALUES('a')");
    conn.commit();

    rs = conn.createStatement().executeQuery("SELECT LPAD(v1, 5, 'ab') FROM " + tableName);
    assertTrue(rs.next());
    assertEquals("ababa", rs.getString(1));
    assertFalse(rs.next());

    rs = conn.createStatement().executeQuery("SELECT LPAD('abc', v2, 'a') FROM " + tableName);
    assertTrue(rs.next());
    rs.getString(1);
    assertTrue(rs.wasNull());
    assertFalse(rs.next());

    rs = conn.createStatement().executeQuery("SELECT LPAD('abc', 5, v1) FROM " + tableName);
    assertTrue(rs.next());
    rs.getString(1);
    assertTrue(rs.wasNull());
    assertFalse(rs.next());
  }

  @Test
  public void testValidStringConcatExpression() throws Exception {// test fails with stack overflow
                                                                  // wee
    Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
    Date date = new Date(System.currentTimeMillis());
    Connection conn = DriverManager.getConnection(getUrl(), props);
    String tableName = initATableValues(generateUniqueName(), getOrganizationId(),
      getDefaultSplits(getOrganizationId()), date, null, getUrl(), "COLUMN_ENCODED_BYTES=0");
    int counter = 0;
    String[] answers = new String[] { "00D300000000XHP5bar", "a5bar", "15bar", "5bar", "5bar" };
    String[] queries =
      new String[] { "SELECT  organization_id || 5 || 'bar' FROM " + tableName + " limit 1",
        "SELECT a_string || 5 || 'bar' FROM " + tableName + "  order by a_string  limit 1",
        "SELECT a_integer||5||'bar' FROM " + tableName + " order by a_integer  limit 1",
        "SELECT x_decimal||5||'bar' FROM " + tableName + " limit 1",
        "SELECT x_long||5||'bar' FROM " + tableName + " limit 1" };

    for (String query : queries) {
      PreparedStatement statement = conn.prepareStatement(query);
      ResultSet rs = statement.executeQuery();
      assertTrue(rs.next());
      assertEquals(answers[counter++], rs.getString(1));
      assertFalse(rs.next());
    }
    conn.close();
  }

}
