/*
 * Copyright (c) 2015 Cryptonomex, Inc., and contributors.
 *
 * The MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
#pragma once

#include <graphene/app/application.hpp>
#include <graphene/chain/database.hpp>
#include <fc/io/json.hpp>

#include <graphene/chain/operation_history_object.hpp>

#include <boost/parameter.hpp>

#include <iostream>

using namespace graphene::db;

extern uint32_t GRAPHENE_TESTING_GENESIS_TIMESTAMP;

#define PUSH_TX \
   graphene::chain::test::_push_transaction

#define PUSH_BLOCK \
   graphene::chain::test::_push_block

// See below
#define REQUIRE_OP_VALIDATION_SUCCESS( op, field, value ) \
{ \
   const auto temp = op.field; \
   op.field = value; \
   op.validate(); \
   op.field = temp; \
}
#define REQUIRE_OP_EVALUATION_SUCCESS( op, field, value ) \
{ \
   const auto temp = op.field; \
   op.field = value; \
   trx.operations.back() = op; \
   op.field = temp; \
   db.push_transaction( trx, ~0 ); \
}

#define GRAPHENE_REQUIRE_THROW( expr, exc_type )          \
{                                                         \
   std::string req_throw_info = fc::json::to_string(      \
      fc::mutable_variant_object()                        \
      ("source_file", __FILE__)                           \
      ("source_lineno", __LINE__)                         \
      ("expr", #expr)                                     \
      ("exc_type", #exc_type)                             \
      );                                                  \
   if( fc::enable_record_assert_trip )                    \
      std::cout << "GRAPHENE_REQUIRE_THROW begin "        \
         << req_throw_info << std::endl;                  \
   BOOST_REQUIRE_THROW( expr, exc_type );                 \
   if( fc::enable_record_assert_trip )                    \
      std::cout << "GRAPHENE_REQUIRE_THROW end "          \
         << req_throw_info << std::endl;                  \
}

#define GRAPHENE_CHECK_THROW( expr, exc_type )            \
{                                                         \
   std::string req_throw_info = fc::json::to_string(      \
      fc::mutable_variant_object()                        \
      ("source_file", __FILE__)                           \
      ("source_lineno", __LINE__)                         \
      ("expr", #expr)                                     \
      ("exc_type", #exc_type)                             \
      );                                                  \
   if( fc::enable_record_assert_trip )                    \
      std::cout << "GRAPHENE_CHECK_THROW begin "          \
         << req_throw_info << std::endl;                  \
   BOOST_CHECK_THROW( expr, exc_type );                   \
   if( fc::enable_record_assert_trip )                    \
      std::cout << "GRAPHENE_CHECK_THROW end "            \
         << req_throw_info << std::endl;                  \
}

#define REQUIRE_OP_VALIDATION_FAILURE_2( op, field, value, exc_type ) \
{ \
   const auto temp = op.field; \
   op.field = value; \
   GRAPHENE_REQUIRE_THROW( op.validate(), exc_type ); \
   op.field = temp; \
}
#define REQUIRE_OP_VALIDATION_FAILURE( op, field, value ) \
   REQUIRE_OP_VALIDATION_FAILURE_2( op, field, value, fc::exception )

#define REQUIRE_THROW_WITH_VALUE_2(op, field, value, exc_type) \
{ \
   auto bak = op.field; \
   op.field = value; \
   trx.operations.back() = op; \
   op.field = bak; \
   GRAPHENE_REQUIRE_THROW(db.push_transaction(trx, ~0), exc_type); \
}

#define REQUIRE_THROW_WITH_VALUE( op, field, value ) \
   REQUIRE_THROW_WITH_VALUE_2( op, field, value, fc::exception )

///This simply resets v back to its default-constructed value. Requires v to have a working assingment operator and
/// default constructor.
#define RESET(v) v = decltype(v)()
///This allows me to build consecutive test cases. It's pretty ugly, but it works well enough for unit tests.
/// i.e. This allows a test on update_account to begin with the database at the end state of create_account.
#define INVOKE(test) ((struct test*)this)->test_method(); trx.clear()

#define PREP_ACTOR(name) \
   fc::ecc::private_key name ## _private_key = generate_private_key(BOOST_PP_STRINGIZE(name));   \
   public_key_type name ## _public_key = name ## _private_key.get_public_key(); \
   (void) name ## _private_key; \
   (void) name ## _public_key;

#define ACTOR(name) \
   PREP_ACTOR(name) \
   const auto& name = create_account(BOOST_PP_STRINGIZE(name), name ## _public_key); \
   account_id_type name ## _id = name.id; (void)name ## _id;

#define GET_ACTOR(name) \
   fc::ecc::private_key name ## _private_key = generate_private_key(BOOST_PP_STRINGIZE(name)); \
   const account_object& name = get_account(BOOST_PP_STRINGIZE(name)); \
   account_id_type name ## _id = name.id; \
   (void)name ##_id

#define ACTORS_IMPL(r, data, elem) ACTOR(elem)
#define ACTORS(names) BOOST_PP_SEQ_FOR_EACH(ACTORS_IMPL, ~, names)

namespace graphene { namespace chain {

   namespace keywords {
      BOOST_PARAMETER_NAME(event_id)
      BOOST_PARAMETER_NAME(event_group_id)
      BOOST_PARAMETER_NAME(name)
      BOOST_PARAMETER_NAME(season)
      BOOST_PARAMETER_NAME(status)
      BOOST_PARAMETER_NAME(force)
      BOOST_PARAMETER_NAME(betting_market_group_id)
      BOOST_PARAMETER_NAME(description)
      BOOST_PARAMETER_NAME(rules_id)
   }

struct database_fixture {
   // the reason we use an app is to exercise the indexes of built-in
   //   plugins
   graphene::app::application app;
   genesis_state_type genesis_state;
   chain::database &db;
   signed_transaction trx;
   public_key_type committee_key;
   account_id_type committee_account;
   fc::ecc::private_key private_key = fc::ecc::private_key::generate();
   fc::ecc::private_key init_account_priv_key = fc::ecc::private_key::regenerate(fc::sha256::hash(string("null_key")) );
   public_key_type init_account_pub_key;

   optional<fc::temp_directory> data_dir;
   bool skip_key_index_test = false;
   uint32_t anon_acct_count;

   database_fixture();
   ~database_fixture();

   static fc::ecc::private_key generate_private_key(string seed);
   string generate_anon_acct_name();
   static void verify_asset_supplies( const database& db );
   void verify_account_history_plugin_index( )const;
   void open_database();
   signed_block generate_block(uint32_t skip = ~0,
                               const fc::ecc::private_key& key = generate_private_key("null_key"),
                               int miss_blocks = 0);

   /**
    * @brief Generates block_count blocks
    * @param block_count number of blocks to generate
    */
   void generate_blocks(uint32_t block_count);

   /**
    * @brief Generates blocks until the head block time matches or exceeds timestamp
    * @param timestamp target time to generate blocks until
    */
   void generate_blocks(fc::time_point_sec timestamp, bool miss_intermediate_blocks = true, uint32_t skip = ~0);

   ///////////
   /// @brief Skip intermediate blocks, and generate a maintenance block
   /// @returns true on success
   ///////////
   bool generate_maintenance_block();

   account_create_operation make_account(
      const std::string& name = "nathan",
      public_key_type = public_key_type()
      );

   account_create_operation make_account(
      const std::string& name,
      const account_object& registrar,
      const account_object& referrer,
      uint8_t referrer_percent = 100,
      public_key_type key = public_key_type()
      );

   void force_global_settle(const asset_object& what, const price& p);
   operation_result force_settle(account_id_type who, asset what)
   { return force_settle(who(db), what); }
   operation_result force_settle(const account_object& who, asset what);
   void update_feed_producers(asset_id_type mia, flat_set<account_id_type> producers)
   { update_feed_producers(mia(db), producers); }
   void update_feed_producers(const asset_object& mia, flat_set<account_id_type> producers);
   void publish_feed(asset_id_type mia, account_id_type by, const price_feed& f)
   { publish_feed(mia(db), by(db), f); }
   void publish_feed(const asset_object& mia, const account_object& by, const price_feed& f);
   const call_order_object* borrow(account_id_type who, asset what, asset collateral)
   { return borrow(who(db), what, collateral); }
   const call_order_object* borrow(const account_object& who, asset what, asset collateral);
   void cover(account_id_type who, asset what, asset collateral_freed)
   { cover(who(db), what, collateral_freed); }
   void cover(const account_object& who, asset what, asset collateral_freed);

   const asset_object& get_asset( const string& symbol )const;
   const account_object& get_account( const string& name )const;
   const asset_object& create_bitasset(const string& name,
                                       account_id_type issuer = GRAPHENE_WITNESS_ACCOUNT,
                                       uint16_t market_fee_percent = 100 /*1%*/,
                                       uint16_t flags = charge_market_fee);
   const asset_object& create_prediction_market(const string& name,
                                       account_id_type issuer = GRAPHENE_WITNESS_ACCOUNT,
                                       uint16_t market_fee_percent = 100 /*1%*/,
                                       uint16_t flags = charge_market_fee);
   const asset_object& create_user_issued_asset( const string& name );
   const asset_object& create_user_issued_asset( const string& name,
                                                 const account_object& issuer,
                                                 uint16_t flags );
   void issue_uia( const account_object& recipient, asset amount );
   void issue_uia( account_id_type recipient_id, asset amount );

   const account_object& create_account(
      const string& name,
      const public_key_type& key = public_key_type()
      );

   const account_object& create_account(
      const string& name,
      const account_object& registrar,
      const account_object& referrer,
      uint8_t referrer_percent = 100,
      const public_key_type& key = public_key_type()
      );

   const account_object& create_account(
      const string& name,
      const private_key_type& key,
      const account_id_type& registrar_id = account_id_type(),
      const account_id_type& referrer_id = account_id_type(),
      uint8_t referrer_percent = 100
      );

   const committee_member_object& create_committee_member( const account_object& owner );
   const witness_object& create_witness(account_id_type owner,
                                        const fc::ecc::private_key& signing_private_key = generate_private_key("null_key"));
   const witness_object& create_witness(const account_object& owner,
                                        const fc::ecc::private_key& signing_private_key = generate_private_key("null_key"));
   uint64_t fund( const account_object& account, const asset& amount = asset(500000) );
   digest_type digest( const transaction& tx );
   void sign( signed_transaction& trx, const fc::ecc::private_key& key );
   const limit_order_object* create_sell_order( account_id_type user, const asset& amount, const asset& recv );
   const limit_order_object* create_sell_order( const account_object& user, const asset& amount, const asset& recv );
   asset cancel_limit_order( const limit_order_object& order );
   void transfer( account_id_type from, account_id_type to, const asset& amount, const asset& fee = asset() );
   void transfer( const account_object& from, const account_object& to, const asset& amount, const asset& fee = asset() );
   void fund_fee_pool( const account_object& from, const asset_object& asset_to_fund, const share_type amount );
   void enable_fees();
   void change_fees( const flat_set< fee_parameters >& new_params, uint32_t new_scale = 0 );
   void upgrade_to_lifetime_member( account_id_type account );
   void upgrade_to_lifetime_member( const account_object& account );
   void upgrade_to_annual_member( account_id_type account );
   void upgrade_to_annual_member( const account_object& account );
   void print_market( const string& syma, const string& symb )const;
   string pretty( const asset& a )const;
   void print_limit_order( const limit_order_object& cur )const;
   void print_call_orders( )const;
   void print_joint_market( const string& syma, const string& symb )const;
   int64_t get_balance( account_id_type account, asset_id_type a )const;
   int64_t get_balance( const account_object& account, const asset_object& a )const;
   int64_t get_dividend_pending_payout_balance(asset_id_type dividend_holder_asset_type,
                                               account_id_type dividend_holder_account_id,
                                               asset_id_type dividend_payout_asset_type) const;
   vector< operation_history_object > get_operation_history( account_id_type account_id )const;
   void  process_operation_by_witnesses(operation op);
   void  process_operation_by_committee(operation op);
   void  force_operation_by_witnesses(operation op);
   void  set_is_proposed_trx(operation op);
   const sport_object& create_sport(internationalized_string_type name);
   void  update_sport(sport_id_type sport_id, internationalized_string_type name);
   void  delete_sport(sport_id_type sport_id);
   const event_group_object& create_event_group(internationalized_string_type name, sport_id_type sport_id);
   void  update_event_group(event_group_id_type event_group_id,
                            fc::optional<object_id_type> sport_id,
                            fc::optional<internationalized_string_type> name);
   void  delete_event_group(event_group_id_type event_group_id);
   void  try_update_event_group(event_group_id_type event_group_id,
                                fc::optional<object_id_type> sport_id,
                                fc::optional<internationalized_string_type> name,
                                bool dont_set_is_proposed_trx = false);
   const event_object& create_event(internationalized_string_type name, internationalized_string_type season, event_group_id_type event_group_id);
   void update_event_impl(event_id_type event_id,
                          fc::optional<object_id_type> event_group_id,
                          fc::optional<internationalized_string_type> name,
                          fc::optional<internationalized_string_type> season,
                          fc::optional<event_status> status,
                          bool force);
   BOOST_PARAMETER_MEMBER_FUNCTION((void), update_event, keywords::tag,
                                   (required (event_id, (event_id_type)))
                                   (optional (event_group_id, (fc::optional<object_id_type>), fc::optional<object_id_type>())
                                             (name, (fc::optional<internationalized_string_type>), fc::optional<internationalized_string_type>())
                                             (season, (fc::optional<internationalized_string_type>), fc::optional<internationalized_string_type>())
                                             (status, (fc::optional<event_status>), fc::optional<event_status>())
                                             (force, (bool), false)))
   {
      update_event_impl(event_id, event_group_id, name, season, status, force);
   }

   const betting_market_rules_object& create_betting_market_rules(internationalized_string_type name, internationalized_string_type description);
   void update_betting_market_rules(betting_market_rules_id_type rules_id,
                                     fc::optional<internationalized_string_type> name,
                                     fc::optional<internationalized_string_type> description);
   const betting_market_group_object& create_betting_market_group(internationalized_string_type description,
                                                                  event_id_type event_id,
                                                                  betting_market_rules_id_type rules_id,
                                                                  asset_id_type asset_id,
                                                                  bool never_in_play,
                                                                  uint32_t delay_before_settling);
   void update_betting_market_group_impl(betting_market_group_id_type betting_market_group_id,
                                         fc::optional<internationalized_string_type> description,
                                         fc::optional<object_id_type> rules_id,
                                         fc::optional<betting_market_group_status> status,
                                         bool force);
   BOOST_PARAMETER_MEMBER_FUNCTION((void), update_betting_market_group, keywords::tag,
                                   (required (betting_market_group_id, (betting_market_group_id_type)))
                                   (optional (description, (fc::optional<internationalized_string_type>), fc::optional<internationalized_string_type>())
                                             (rules_id, (fc::optional<object_id_type>), fc::optional<object_id_type>())
                                             (status, (fc::optional<betting_market_group_status>), fc::optional<betting_market_group_status>())
                                             (force, (bool), false)))
   {
      update_betting_market_group_impl(betting_market_group_id, description, rules_id, status, force);
   }

   const betting_market_object& create_betting_market(betting_market_group_id_type group_id, internationalized_string_type payout_condition);
   void update_betting_market(betting_market_id_type betting_market_id,
                                                fc::optional<object_id_type> group_id,
                                                /*fc::optional<internationalized_string_type> description,*/
                                                fc::optional<internationalized_string_type> payout_condition);

   bet_id_type place_bet(account_id_type bettor_id, betting_market_id_type betting_market_id, bet_type back_or_lay, asset amount_to_bet, bet_multiplier_type backer_multiplier);
   void resolve_betting_market_group(betting_market_group_id_type betting_market_group_id, std::map<betting_market_id_type, betting_market_resolution_type> resolutions);
   void cancel_unmatched_bets(betting_market_group_id_type betting_market_group_id);

   proposal_id_type propose_operation(operation op);
   void process_proposal_by_witnesses(const std::vector<witness_id_type>& witnesses, proposal_id_type proposal_id, bool remove = false);
};

namespace test {
/// set a reasonable expiration time for the transaction
void set_expiration( const database& db, transaction& tx );

bool _push_block( database& db, const signed_block& b, uint32_t skip_flags = 0 );
processed_transaction _push_transaction( database& db, const signed_transaction& tx, uint32_t skip_flags = 0 );
}

} }
