/*
 * 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.iotdb.db.qp;

import org.apache.iotdb.db.auth.entity.PrivilegeType;
import org.apache.iotdb.db.conf.IoTDBConstant;
import org.apache.iotdb.db.exception.StorageEngineException;
import org.apache.iotdb.db.exception.metadata.IllegalPathException;
import org.apache.iotdb.db.exception.metadata.MetadataException;
import org.apache.iotdb.db.exception.query.QueryProcessException;
import org.apache.iotdb.db.exception.runtime.SQLParserException;
import org.apache.iotdb.db.metadata.MManager;
import org.apache.iotdb.db.metadata.path.PartialPath;
import org.apache.iotdb.db.qp.executor.PlanExecutor;
import org.apache.iotdb.db.qp.logical.Operator.OperatorType;
import org.apache.iotdb.db.qp.physical.PhysicalPlan;
import org.apache.iotdb.db.qp.physical.crud.InsertRowPlan;
import org.apache.iotdb.db.service.IoTDB;
import org.apache.iotdb.db.utils.EnvironmentUtils;
import org.apache.iotdb.service.rpc.thrift.TSLastDataQueryReq;
import org.apache.iotdb.service.rpc.thrift.TSRawDataQueryReq;
import org.apache.iotdb.tsfile.common.conf.TSFileDescriptor;
import org.apache.iotdb.tsfile.exception.filter.QueryFilterOptimizationException;
import org.apache.iotdb.tsfile.file.metadata.enums.CompressionType;
import org.apache.iotdb.tsfile.file.metadata.enums.TSDataType;
import org.apache.iotdb.tsfile.file.metadata.enums.TSEncoding;
import org.apache.iotdb.tsfile.read.query.dataset.QueryDataSet;

import org.antlr.v4.runtime.misc.ParseCancellationException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class PlannerTest {

  private CompressionType compressionType =
      TSFileDescriptor.getInstance().getConfig().getCompressor();
  private MManager mManager = IoTDB.metaManager;
  private Planner processor = new Planner();

  static {
    IoTDB.metaManager.init();
  }

  @Before
  public void setUp() throws Exception {
    EnvironmentUtils.envSetUp();
    mManager.setStorageGroup(new PartialPath("root.vehicle"));
    mManager.setStorageGroup(new PartialPath("root.vehicle1"));
    mManager.createTimeseries(
        new PartialPath("root.vehicle.device1.sensor1"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
    mManager.createTimeseries(
        new PartialPath("root.vehicle.device1.sensor2"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
    mManager.createTimeseries(
        new PartialPath("root.vehicle.device1.sensor3"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
    mManager.createTimeseries(
        new PartialPath("root.vehicle.device2.sensor1"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
    mManager.createTimeseries(
        new PartialPath("root.vehicle.device2.sensor2"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
    mManager.createTimeseries(
        new PartialPath("root.vehicle.device2.sensor3"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
    mManager.createTimeseries(
        new PartialPath("root.vehicle1.device1.sensor1"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
    mManager.createTimeseries(
        new PartialPath("root.vehicle1.device1.sensor2"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
    mManager.createTimeseries(
        new PartialPath("root.vehicle1.device1.sensor3"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
    mManager.createTimeseries(
        new PartialPath("root.vehicle1.device2.sensor1"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
    mManager.createTimeseries(
        new PartialPath("root.vehicle1.device2.sensor2"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
    mManager.createTimeseries(
        new PartialPath("root.vehicle1.device2.sensor3"),
        TSDataType.valueOf("INT32"),
        TSEncoding.valueOf("RLE"),
        compressionType,
        Collections.emptyMap());
  }

  @After
  public void tearDown() throws Exception {
    EnvironmentUtils.cleanEnv();
  }

  @Test
  public void parseSQLToPhysicalPlan() throws Exception {
    String createSGStatement = "set storage group to root.vehicle";
    PhysicalPlan plan1 = processor.parseSQLToPhysicalPlan(createSGStatement);
    assertEquals(OperatorType.SET_STORAGE_GROUP, plan1.getOperatorType());

    String createTSStatement1 =
        "create timeseries root.vehicle.d1.s1 with datatype=INT32,encoding=RLE";
    PhysicalPlan plan2 = processor.parseSQLToPhysicalPlan(createTSStatement1);
    assertEquals(OperatorType.CREATE_TIMESERIES, plan2.getOperatorType());

    String deleteTSStatement = "delete timeseries root.vehicle.d1.s1";
    PhysicalPlan plan3 = processor.parseSQLToPhysicalPlan(deleteTSStatement);
    assertEquals(OperatorType.DELETE_TIMESERIES, plan3.getOperatorType());

    String insertStatement = "insert into root.vehicle.d0(timestamp,s0) values(10,100)";
    PhysicalPlan plan4 = processor.parseSQLToPhysicalPlan(insertStatement);
    assertEquals(OperatorType.INSERT, plan4.getOperatorType());

    String deleteStatement =
        "DELETE FROM root.device0.sensor0,root.device0.sensor1 WHERE time <= 5000";
    PhysicalPlan plan6 = processor.parseSQLToPhysicalPlan(deleteStatement);
    assertEquals(OperatorType.DELETE, plan6.getOperatorType());

    String queryStatement1 =
        "select * from root.vehicle.** where root.vehicle.device1.sensor1 > 50";
    PhysicalPlan plan7 = processor.parseSQLToPhysicalPlan(queryStatement1);
    assertEquals(OperatorType.QUERY, plan7.getOperatorType());

    String aggregationStatement =
        "select sum(*) from root.vehicle where root.vehicle.device1.sensor1 > 50";
    PhysicalPlan plan8 = processor.parseSQLToPhysicalPlan(aggregationStatement);
    assertEquals(OperatorType.AGGREGATION, plan8.getOperatorType());

    String groupbyStatement =
        "select sum(*) from root.vehicle where root.vehicle.device1.sensor1 > 50 group by ([100,1100), 20ms)";
    PhysicalPlan plan9 = processor.parseSQLToPhysicalPlan(groupbyStatement);
    assertEquals(OperatorType.GROUP_BY_TIME, plan9.getOperatorType());

    String fillStatement =
        "select sensor1 from root.vehicle.device1 where time = 50 Fill(int32[linear, 5m, 5m], boolean[previous, 5m])";
    PhysicalPlan plan10 = processor.parseSQLToPhysicalPlan(fillStatement);
    assertEquals(OperatorType.FILL, plan10.getOperatorType());

    String insertTimeStatement = "insert into root.vehicle.d0(time,s0) values(10,100)";
    PhysicalPlan plan11 = processor.parseSQLToPhysicalPlan(insertTimeStatement);
    assertEquals(OperatorType.INSERT, plan11.getOperatorType());

    String createTSStatement2 =
        "create timeseries root.a.b.d_1.`1s` with datatype=FLOAT,encoding=RLE";
    PhysicalPlan plan12 = processor.parseSQLToPhysicalPlan(createTSStatement2);
    assertEquals(OperatorType.CREATE_TIMESERIES, plan12.getOperatorType());

    String queryStatement2 =
        "select windDirection10min from root.national.`4`.`5`.`585`.`9_6666`.`9_333`.`88_9`";
    PhysicalPlan plan13 = processor.parseSQLToPhysicalPlan(queryStatement2);
    assertEquals(OperatorType.QUERY, plan13.getOperatorType());

    String insertStatementException = "insert into root.vehicle.d0(timestamp,s0,s1) values(10,100)";
    try {
      processor.parseSQLToPhysicalPlan(insertStatementException);
    } catch (Exception e) {
      assertEquals(
          new SQLParserException(
                  "the measurementList's size 2 is not consistent with the valueList's size 1")
              .getMessage(),
          e.getMessage());
    }
  }

  @Test
  public void parseShowChildNodeToPhysicalPlan() throws Exception {
    String showChildNodesStatement = "show child nodes root.vehicle1.device1";
    PhysicalPlan plan14 = processor.parseSQLToPhysicalPlan(showChildNodesStatement);
    assertEquals(OperatorType.SHOW, plan14.getOperatorType());
  }

  @Test(expected = ParseCancellationException.class)
  public void parseErrorSQLToPhysicalPlan() throws QueryProcessException {
    String createTSStatement =
        "create timeseriess root.vehicle.d1.s1 with datatype=INT32,encoding=RLE";
    processor.parseSQLToPhysicalPlan(createTSStatement);
  }

  @Test
  public void insertStatementWithNullValue() throws QueryProcessException {
    String createTSStatement = "insert into root.vehicle.d0(time,s0) values(10,NaN)";
    PhysicalPlan physicalPlan = processor.parseSQLToPhysicalPlan(createTSStatement);

    assertTrue(physicalPlan instanceof InsertRowPlan);
    assertEquals("NaN", ((InsertRowPlan) physicalPlan).getValues()[0]);
    // Later we will use Double.parseDouble so we have to ensure that it is parsed right
    assertEquals(Double.NaN, Double.parseDouble("NaN"), 1e-15);
  }

  @Test
  public void rawDataQueryReqToPhysicalPlanTest()
      throws QueryProcessException, IllegalPathException {
    TSRawDataQueryReq tsRawDataQueryReq = new TSRawDataQueryReq();
    List<String> paths = new ArrayList<>();
    paths.add("root.vehicle.device1.sensor1");
    paths.add("root.vehicle.device1.sensor2");
    tsRawDataQueryReq.setPaths(paths);
    tsRawDataQueryReq.setStartTime(0);
    tsRawDataQueryReq.setEndTime(100);
    tsRawDataQueryReq.setFetchSize(1000);
    PhysicalPlan physicalPlan =
        processor.rawDataQueryReqToPhysicalPlan(
            tsRawDataQueryReq, ZoneId.of("Asia/Shanghai"), IoTDBConstant.ClientVersion.V_0_13);
    assertEquals(OperatorType.QUERY, physicalPlan.getOperatorType());
    assertEquals(paths.get(0), physicalPlan.getPaths().get(0).getFullPath());
    assertEquals(paths.get(1), physicalPlan.getPaths().get(1).getFullPath());
  }

  @Test
  public void lastDataQueryReqToPhysicalPlanTest()
      throws QueryProcessException, IllegalPathException {
    TSLastDataQueryReq tsLastDataQueryReq = new TSLastDataQueryReq();
    List<String> paths = new ArrayList<>();
    paths.add("root.vehicle.device1.sensor1");
    tsLastDataQueryReq.setPaths(paths);
    tsLastDataQueryReq.setTime(0);
    tsLastDataQueryReq.setFetchSize(1000);
    PhysicalPlan physicalPlan =
        processor.lastDataQueryReqToPhysicalPlan(
            tsLastDataQueryReq, ZoneId.of("Asia/Shanghai"), IoTDBConstant.ClientVersion.V_0_13);
    assertEquals(OperatorType.LAST, physicalPlan.getOperatorType());
    assertEquals(paths.get(0), physicalPlan.getPaths().get(0).getFullPath());
  }

  @Test
  public void testRootPrivilege()
      throws QueryProcessException, StorageEngineException, IOException, InterruptedException,
          QueryFilterOptimizationException, MetadataException {
    String listRootPrivilegeStatement = "list user privileges root";
    PhysicalPlan physicalPlan = processor.parseSQLToPhysicalPlan(listRootPrivilegeStatement);
    PlanExecutor executor = new PlanExecutor();
    QueryDataSet queryDataSet = executor.processQuery(physicalPlan, null);
    for (PrivilegeType privilegeType : PrivilegeType.values()) {
      if (queryDataSet.hasNext()) {
        assertEquals(String.valueOf(queryDataSet.next().getFields()), "[" + privilegeType + "]");
      }
    }
  }
}
