/*
 * Copyright (c) 2015, 2023, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is also distributed with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms,
 * as designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have included with MySQL.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "find_statement_builder.h"
#include "query_string_builder.h"
#include "mysqld_error.h"
#include "expr_generator.h"
#include "ngs_common/protocol_protobuf.h"

#include <gtest/gtest.h>

namespace xpl
{
namespace test
{

class Find_statement_builder_impl: public Find_statement_builder
{
public:
  Find_statement_builder_impl(Expression_generator &gen) : Find_statement_builder(gen) {}
  using Find_statement_builder::add_table_projection;
  using Find_statement_builder::add_document_projection;
  using Find_statement_builder::add_grouping;
  using Find_statement_builder::add_grouping_criteria;
  using Find_statement_builder::Grouping_list;
  using Find_statement_builder::Grouping_criteria;
};


class Find_statement_builder_test : public ::testing::Test
{
public:
  Find_statement_builder_test()
  : args(*msg.mutable_args()),
    expr_gen(query, args, schema, true),
    builder(expr_gen)
    {}
  Find_statement_builder::Find msg;
  Expression_generator::Args &args;
  Query_string_builder query;
  std::string schema;
  Expression_generator expr_gen;
  Find_statement_builder_impl builder;

  enum {DM_DOCUMENT = 0, DM_TABLE = 1};

  typedef ::google::protobuf::RepeatedPtrField< ::Mysqlx::Crud::Projection > Projection_list;
};


namespace
{
void operator<< (::google::protobuf::Message &msg, const std::string& txt)
{
  ASSERT_TRUE(::google::protobuf::TextFormat::ParseFromString(txt, &msg));
}
} // namespace


TEST_F(Find_statement_builder_test, add_projection_table_empty)
{
  Projection_list projection;
  ASSERT_NO_THROW(builder.add_table_projection(projection));
  EXPECT_EQ("*", query.get());
}


TEST_F(Find_statement_builder_test, add_document_projection_empty)
{
  Projection_list projection;
  ASSERT_NO_THROW(builder.add_document_projection(projection));
  EXPECT_EQ("doc", query.get());
}


TEST_F(Find_statement_builder_test, add_projection_table_one_member_item)
{
  Projection_list projection;
  *projection.Add() << "source { type: IDENT identifier "
      "{ document_path { type: MEMBER value: \"alpha\" } } }";
  ASSERT_NO_THROW(builder.add_table_projection(projection));
  EXPECT_EQ("JSON_EXTRACT(doc,'$.alpha')", query.get());
}


TEST_F(Find_statement_builder_test, add_projection_table_one_item)
{
  Projection_list projection;
  *projection.Add() << "source { type: IDENT identifier "
      "{ name: 'alpha' } }";
  ASSERT_NO_THROW(builder.add_table_projection(projection));
  EXPECT_EQ("`alpha`", query.get());
}


TEST_F(Find_statement_builder_test, add_projection_table_two_items)
{
  Projection_list projection;
  *projection.Add() << "source { type: IDENT identifier "
      "{ name: 'alpha' } }";
  *projection.Add() <<  "source { type: IDENT identifier "
      "{ name: 'beta' } }";
  ASSERT_NO_THROW(builder.add_table_projection(projection));
  EXPECT_EQ("`alpha`,`beta`", query.get());
}


TEST_F(Find_statement_builder_test, add_projection_table_two_items_placeholder)
{
  *args.Add() << "type: V_DOUBLE v_double: 2.2";

  Projection_list projection;
  *projection.Add() << "source { type: IDENT identifier "
      "{ name: 'alpha' } }";
  *projection.Add() <<  "source { type: PLACEHOLDER position: 0 }";
  ASSERT_NO_THROW(builder.add_table_projection(projection));
  EXPECT_EQ("`alpha`,2.2", query.get());
}


TEST_F(Find_statement_builder_test, add_projection_table_one_item_with_alias)
{
  Projection_list projection;
  *projection.Add() << "source { type: IDENT identifier "
      "{ name: 'alpha' } } alias: 'beta'";
  ASSERT_NO_THROW(builder.add_table_projection(projection));
  EXPECT_EQ("`alpha` AS `beta`", query.get());
}


TEST_F(Find_statement_builder_test, add_projection_document_one_item_no_alias)
{
  Projection_list projection;
  *projection.Add() << "source { type: IDENT identifier "
      "{ name: 'alpha' } }";
  EXPECT_THROW(builder.add_document_projection(projection), ngs::Error_code);
}


TEST_F(Find_statement_builder_test, add_projection_document_one_member_item)
{
  Projection_list projection;
  *projection.Add() << "source { type: IDENT identifier "
      "{ document_path { type: MEMBER value: \"alpha\" } } }"
      "alias: \"beta\"";
  ASSERT_NO_THROW(builder.add_document_projection(projection));
  EXPECT_EQ("JSON_OBJECT('beta', JSON_EXTRACT(doc,'$.alpha')) AS doc",
            query.get());
}


TEST_F(Find_statement_builder_test, add_projection_document_two_member_items)
{
  Projection_list projection;
  *projection.Add() << "source { type: IDENT identifier "
      "{ document_path { type: MEMBER value: \"alpha\" } } }"
      "alias: \"beta\"";
  *projection.Add() << "source { type: IDENT identifier "
      "{ document_path { type: MEMBER value: \"first\" } } }"
      "alias: \"second\"";
  ASSERT_NO_THROW(builder.add_document_projection(projection));
  EXPECT_EQ("JSON_OBJECT('beta', JSON_EXTRACT(doc,'$.alpha'),"
      "'second', JSON_EXTRACT(doc,'$.first')) AS doc",
      query.get());
}


TEST_F(Find_statement_builder_test, add_projection_document_two_member_items_placeholder)
{
  *args.Add() << "type: V_DOUBLE v_double: 2.2";
  Projection_list projection;
  *projection.Add() << "source { type: IDENT identifier "
      "{ document_path { type: MEMBER value: \"alpha\" } } }"
      "alias: \"beta\"";
  *projection.Add() << "source {type: PLACEHOLDER position: 0}"
      "alias: \"second\"";
  ASSERT_NO_THROW(builder.add_document_projection(projection));
  EXPECT_EQ("JSON_OBJECT('beta', JSON_EXTRACT(doc,'$.alpha'),"
      "'second', 2.2) AS doc",
      query.get());
}


TEST_F(Find_statement_builder_test, add_gruping_empty)
{
  Find_statement_builder_impl::Grouping_list group;
  ASSERT_NO_THROW(builder.add_grouping(group));
  EXPECT_EQ("", query.get());
}


TEST_F(Find_statement_builder_test, add_gruping_one_item)
{
  Find_statement_builder_impl::Grouping_list group;
  *group.Add() << "type: IDENT identifier { name: 'alpha' }";
  ASSERT_NO_THROW(builder.add_grouping(group));
  EXPECT_EQ(" GROUP BY `alpha`", query.get());
}


TEST_F(Find_statement_builder_test, add_gruping_two_items)
{
  Find_statement_builder_impl::Grouping_list group;
  *group.Add() << "type: IDENT identifier { name: 'alpha' }";
  *group.Add() << "type: IDENT identifier { name: 'beta' }";
  ASSERT_NO_THROW(builder.add_grouping(group));
  EXPECT_EQ(" GROUP BY `alpha`,`beta`", query.get());
}


TEST_F(Find_statement_builder_test, add_gruping_two_items_placeholder)
{
  *args.Add() << "type: V_SINT v_signed_int: 2";

  Find_statement_builder_impl::Grouping_list group;
  *group.Add() << "type: IDENT identifier { name: 'alpha' }";
  *group.Add() << "type: PLACEHOLDER position: 0";
  ASSERT_NO_THROW(builder.add_grouping(group));
  EXPECT_EQ(" GROUP BY `alpha`,2", query.get());
}


TEST_F(Find_statement_builder_test, add_gruping_criteria)
{
  Find_statement_builder_impl::Grouping_criteria criteria;
  criteria << "type: OPERATOR operator {name: '>'"
      "param {type: IDENT identifier {name: 'alpha'}}"
      "param {type: LITERAL literal {type: V_DOUBLE v_double: 1.0}}}";
  ASSERT_NO_THROW(builder.add_grouping_criteria(criteria));
  EXPECT_EQ(" HAVING (`alpha` > 1)", query.get());
}


TEST_F(Find_statement_builder_test, add_gruping_criteria_placeholder)
{
  *args.Add() << "type: V_DOUBLE v_double: 2.3";

  Find_statement_builder_impl::Grouping_criteria criteria;
  criteria << "type: OPERATOR operator {name: '>'"
      "param {type: IDENT identifier {name: 'alpha'}}"
      "param {type: PLACEHOLDER position: 0}}";
  ASSERT_NO_THROW(builder.add_grouping_criteria(criteria));
  EXPECT_EQ(" HAVING (`alpha` > 2.3)", query.get());
}


TEST_F(Find_statement_builder_test, build_table)
{
  msg <<
      "collection {name: 'xtable' schema: 'xschema'}"
      "data_model: TABLE "
      "projection {source {type: IDENT identifier {name: 'alpha'}}"
      "            alias: 'zeta'} "
      "criteria {type: OPERATOR "
      "          operator {name: '>' "
      "                    param {type: IDENT identifier {name: 'delta'}}"
      "                    param {type: LITERAL literal "
      "                                                 {type: V_DOUBLE"
      "                                                  v_double: 1.0}}}}"
      "order {expr {type: IDENT identifier {name: 'gamma'}}"
      "       direction: DESC} "
      "grouping {type: IDENT identifier {name: 'beta'}}"
      "grouping_criteria {type: OPERATOR "
      "          operator {name: '<' "
      "                    param {type: IDENT identifier {name: 'lambda'}}"
      "                    param {type: LITERAL literal"
      "                                                {type: V_DOUBLE"
      "                                             v_double: 2.0}}}}";
  ASSERT_NO_THROW(builder.build(msg));
  EXPECT_EQ(
      "SELECT `alpha` AS `zeta` "
      "FROM `xschema`.`xtable` "
      "WHERE (`delta` > 1) "
      "GROUP BY `beta` "
      "HAVING (`lambda` < 2) "
      "ORDER BY `gamma` DESC", query.get());
}


TEST_F(Find_statement_builder_test, build_document_no_grouping)
{
  msg <<
      "collection {name: 'xtable' schema: 'xschema'}"
      "data_model: DOCUMENT "
      "projection {source {type: IDENT identifier {document_path {type: MEMBER "
      "                                                             value: 'alpha'}}}"
      "            alias: 'zeta'} "
      "criteria {type: OPERATOR "
      "          operator {name: '>' "
      "                    param {type: IDENT identifier {document_path {type: MEMBER "
      "                                                                  value: 'delta'}}}"
      "                    param {type: LITERAL literal"
      "                                                {type: V_DOUBLE"
      "                                             v_double: 1.0}}}}"
      "order {expr {type: IDENT identifier {document_path {type: MEMBER "
      "                                                     value: 'gamma'}}}"
      "       direction: DESC}";
  ASSERT_NO_THROW(builder.build(msg));
  EXPECT_EQ(
      "SELECT JSON_OBJECT('zeta', JSON_EXTRACT(doc,'$.alpha')) AS doc "
      "FROM `xschema`.`xtable` "
      "WHERE (JSON_EXTRACT(doc,'$.delta') > 1) "
      "ORDER BY JSON_EXTRACT(doc,'$.gamma') DESC",
      query.get());
}


TEST_F(Find_statement_builder_test, build_document_with_grouping_and_criteria)
{
  msg <<
      "collection {name: 'xtable' schema: 'xschema'}"
      "data_model: DOCUMENT "
      "projection {source {type: IDENT identifier {document_path {type: MEMBER "
      "                                                             value: 'alpha'}}}"
      "            alias: 'zeta'} "
      "criteria {type: OPERATOR "
      "          operator {name: '>' "
      "                    param {type: IDENT identifier {document_path {type: MEMBER "
      "                                                                  value: 'delta'}}}"
      "                    param {type: LITERAL literal"
      "                                                {type: V_DOUBLE"
      "                                             v_double: 1.0}}}}"
      "order {expr {type: IDENT identifier {document_path {type: MEMBER "
      "                                                     value: 'beta'}}}"
      "       direction: DESC} "
      "grouping {type: IDENT identifier {document_path {type: MEMBER "
      "                                                  value: 'alpha'}}}"
      "grouping_criteria {type: OPERATOR "
      "          operator {name: '<' "
      "                    param {type: IDENT identifier {document_path {type: MEMBER "
      "                                                                  value: 'lambda'}}}"
      "                    param {type: LITERAL literal"
      "                                                {type: V_DOUBLE"
      "                                                v_double: 2.0}}}}";
  ASSERT_NO_THROW(builder.build(msg));
  EXPECT_STREQ(
      "SELECT JSON_OBJECT('zeta', `_DERIVED_TABLE_`.`zeta`) AS doc FROM ("
      "SELECT JSON_EXTRACT(doc,'$.alpha') AS `zeta` "
      "FROM `xschema`.`xtable` "
      "WHERE (JSON_EXTRACT(doc,'$.delta') > 1) "
      "GROUP BY JSON_EXTRACT(doc,'$.alpha') "
      "HAVING (JSON_EXTRACT(doc,'$.lambda') < 2) "
      "ORDER BY JSON_EXTRACT(doc,'$.beta') DESC"
      ") AS `_DERIVED_TABLE_`",
      query.get().c_str());
}


TEST_F(Find_statement_builder_test, build_document_with_grouping)
{
  msg <<
      "collection {name: 'xtable' schema: 'xschema'}"
      "data_model: DOCUMENT "
      "projection {source {type: IDENT identifier {document_path {type: MEMBER "
      "                                                             value: 'alpha'}}}"
      "            alias: 'zeta'} "
      "projection {source {type: IDENT identifier {document_path {type: MEMBER "
      "                                                             value: 'gama'}}}"
      "            alias: 'ksi'} "
      "grouping {type: IDENT identifier {document_path {type: MEMBER "
      "                                                  value: 'alpha'}}}"
      "grouping {type: IDENT identifier {document_path {type: MEMBER "
      "                                                  value: 'gama'}}}";
  ASSERT_NO_THROW(builder.build(msg));
  EXPECT_EQ(
      "SELECT JSON_OBJECT('zeta', `_DERIVED_TABLE_`.`zeta`,'ksi', `_DERIVED_TABLE_`.`ksi`) AS doc FROM ("
      "SELECT JSON_EXTRACT(doc,'$.alpha') AS `zeta`,JSON_EXTRACT(doc,'$.gama') AS `ksi` "
      "FROM `xschema`.`xtable` "
      "GROUP BY JSON_EXTRACT(doc,'$.alpha'),JSON_EXTRACT(doc,'$.gama')"
      ") AS `_DERIVED_TABLE_`",
      query.get());
}


TEST_F(Find_statement_builder_test, build_document_with_grouping_no_projection)
{
  msg <<
      "collection {name: 'xtable' schema: 'xschema'}"
      "data_model: DOCUMENT "
      "grouping {type: IDENT identifier {document_path {type: MEMBER "
      "                                                  value: 'beta'}}}";
  EXPECT_THROW(builder.build(msg), ngs::Error_code);
}

} // namespace test
} // namespace xpl
