//+------------------------------------------------------------------+
//|                                        TestExecutionGateway.mq5  |
//|                        Verification script: synthetic unit tests |
//|                        for the normalizer, validator, and        |
//|                        gateway retcode classification logic      |
//+------------------------------------------------------------------+

#property script_show_inputs

#include <ExecutionGateway/LotNormalizer.mqh>
#include <ExecutionGateway/SlTpValidator.mqh>
#include <ExecutionGateway/ExecutionGateway.mqh>

//--- test bookkeeping
int g_tests_run    = 0;
int g_tests_passed = 0;

//+------------------------------------------------------------------+
//| ASSERT                                                           |
//| Records a single test outcome and prints a pass/fail line. Does  |
//| not halt the script on failure so every test still runs.         |
//+------------------------------------------------------------------+
void ASSERT(const bool condition,const string test_name)
  {
   g_tests_run++;

   if(condition)
     {
      g_tests_passed++;
      ::PrintFormat("PASS: %s",test_name);
     }
   else
     {
      ::PrintFormat("FAIL: %s",test_name);
     }
  }

//+------------------------------------------------------------------+
//| ASSERT_DOUBLE_EQ                                                 |
//| Compares two doubles within a small tolerance, since exact       |
//| equality is unreliable for floating point results.               |
//+------------------------------------------------------------------+
void ASSERT_DOUBLE_EQ(const double actual,const double expected,const string test_name)
  {
   bool ok = (::MathAbs(actual - expected) < 0.0000001);
   g_tests_run++;

   if(ok)
     {
      g_tests_passed++;
      ::PrintFormat("PASS: %s (expected %.6f, got %.6f)",test_name,expected,actual);
     }
   else
     {
      ::PrintFormat("FAIL: %s (expected %.6f, got %.6f)",test_name,expected,actual);
     }
  }

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart(void)
  {
   ::Print("=== TestExecutionGateway starting ===");

   TestLotNormalizer();
   TestSlTpValidator();
   TestRetryableRetcodes();

   ::PrintFormat("=== TestExecutionGateway finished: %d/%d passed ===",g_tests_passed,g_tests_run);

   if(g_tests_passed == g_tests_run)
      ::Print("ALL TESTS PASSED");
   else
      ::Print("SOME TESTS FAILED - see log above");
  }

//+------------------------------------------------------------------+
//| TestLotNormalizer                                                |
//| Uses the current chart symbol's real broker constraints so the   |
//| test reflects actual SYMBOL_VOLUME_STEP/MIN/MAX behavior rather  |
//| than a mocked value.                                             |
//+------------------------------------------------------------------+
void TestLotNormalizer(void)
  {
   ::Print("--- CLotNormalizer tests ---");

   CLotNormalizer normalizer;
   string symbol = ::Symbol();

   double step = 0.0, vol_min = 0.0, vol_max = 0.0;
   normalizer.GetConstraints(symbol,step,vol_min,vol_max);

   ::PrintFormat("Using symbol %s constraints: step=%.5f min=%.5f max=%.5f",symbol,step,vol_min,vol_max);

//--- a lot far below the minimum must clamp up to the minimum
   double below_min = normalizer.Normalize(symbol,vol_min * 0.1);
   ASSERT_DOUBLE_EQ(below_min,vol_min,"lot below minimum clamps to SYMBOL_VOLUME_MIN");

//--- a lot far above the maximum must clamp down to the maximum
   double above_max = normalizer.Normalize(symbol,vol_max * 2.0);
   ASSERT_DOUBLE_EQ(above_max,vol_max,"lot above maximum clamps to SYMBOL_VOLUME_MAX");

//--- a lot already on a valid step and within range must be returned unchanged
   double valid_lot         = vol_min + step * 3.0;
   double normalized_valid  = normalizer.Normalize(symbol,valid_lot);
   ASSERT_DOUBLE_EQ(normalized_valid,valid_lot,"already-valid lot on a step boundary is unchanged");

//--- a lot slightly off a step boundary must round onto the nearest step
   double off_boundary       = vol_min + step * 3.3;
   double normalized_rounded = normalizer.Normalize(symbol,off_boundary);
   double expected_rounded   = ::MathRound(off_boundary / step) * step;
   ASSERT_DOUBLE_EQ(normalized_rounded,expected_rounded,"off-boundary lot rounds to the nearest step");

//--- zero must clamp up to the minimum
   double zero_lot = normalizer.Normalize(symbol,0.0);
   ASSERT_DOUBLE_EQ(zero_lot,vol_min,"zero lot clamps to SYMBOL_VOLUME_MIN");
  }

//+------------------------------------------------------------------+
//| TestSlTpValidator                                                |
//| Uses the current chart symbol's real SYMBOL_TRADE_STOPS_LEVEL    |
//| and current Bid/Ask so the test reflects live broker behavior.   |
//+------------------------------------------------------------------+
void TestSlTpValidator(void)
  {
   ::Print("--- CSlTpValidator tests ---");

   CSlTpValidator validator;
   string symbol = ::Symbol();

   double min_distance = validator.GetMinDistance(symbol);
   double point_size   = ::SymbolInfoDouble(symbol,SYMBOL_POINT);
   double ask          = ::SymbolInfoDouble(symbol,SYMBOL_ASK);
   double bid          = ::SymbolInfoDouble(symbol,SYMBOL_BID);

   ::PrintFormat("Using symbol %s min stop distance: %.5f (in price terms)",symbol,min_distance);

//--- BUY with an SL far too close must be widened
   double sl_close  = ask - point_size;              // one point away, certainly inside the minimum
   double tp_far    = ask + min_distance * 5.0;      // comfortably far, should not be touched
   bool sl_adj      = false, tp_adj = false;

   validator.Validate(symbol,ORDER_TYPE_BUY,ask,sl_close,tp_far,sl_adj,tp_adj);

   ASSERT(sl_adj == true,"BUY: too-close SL is flagged as adjusted");
   ASSERT(tp_adj == false,"BUY: sufficiently far TP is left untouched");
   ASSERT((ask - sl_close) >= min_distance - 0.0000001,"BUY: adjusted SL now meets the minimum distance");

//--- BUY with an SL already far enough must be left unchanged
   double sl_ok        = ask - min_distance * 5.0;
   double tp_ok        = ask + min_distance * 5.0;
   bool sl_adj2        = false, tp_adj2 = false;
   double sl_ok_before = sl_ok;

   validator.Validate(symbol,ORDER_TYPE_BUY,ask,sl_ok,tp_ok,sl_adj2,tp_adj2);

   ASSERT(sl_adj2 == false,"BUY: sufficiently far SL is not adjusted");
   ASSERT_DOUBLE_EQ(sl_ok,sl_ok_before,"BUY: unadjusted SL value is unchanged");

//--- SELL with a TP far too close must be widened
   double sl_far_sell   = bid + min_distance * 5.0;
   double tp_close_sell = bid - point_size;
   bool sl_adj3         = false, tp_adj3 = false;

   validator.Validate(symbol,ORDER_TYPE_SELL,bid,sl_far_sell,tp_close_sell,sl_adj3,tp_adj3);

   ASSERT(tp_adj3 == true,"SELL: too-close TP is flagged as adjusted");
   ASSERT((bid - tp_close_sell) >= min_distance - 0.0000001,"SELL: adjusted TP now meets the minimum distance");

//--- a zero SL/TP (no stop requested) must never be touched
   double sl_zero = 0.0;
   double tp_zero  = 0.0;
   bool sl_adj4 = false, tp_adj4 = false;

   validator.Validate(symbol,ORDER_TYPE_BUY,ask,sl_zero,tp_zero,sl_adj4,tp_adj4);

   ASSERT(sl_adj4 == false && tp_adj4 == false,"zero SL/TP (no stop requested) is never adjusted");
  }

//+------------------------------------------------------------------+
//| TestRetryableRetcodes                                            |
//| Confirms the three constants the gateway treats as retryable     |
//| match the documented MQL5 ENUM_TRADE_RETCODE values, and that a  |
//| terminal retcode such as TRADE_RETCODE_INVALID_STOPS is not      |
//| misclassified as retryable.                                      |
//+------------------------------------------------------------------+
void TestRetryableRetcodes(void)
  {
   ::Print("--- Retryable retcode classification tests ---");

   ASSERT((uint)TRADE_RETCODE_REQUOTE == 10004,"TRADE_RETCODE_REQUOTE equals 10004");
   ASSERT((uint)TRADE_RETCODE_TIMEOUT == 10012,"TRADE_RETCODE_TIMEOUT equals 10012");
   ASSERT((uint)TRADE_RETCODE_PRICE_CHANGED == 10020,"TRADE_RETCODE_PRICE_CHANGED equals 10020");

//--- a permanent rejection such as invalid stops must never be treated as retryable
   ASSERT((uint)TRADE_RETCODE_INVALID_STOPS != (uint)TRADE_RETCODE_REQUOTE &&
          (uint)TRADE_RETCODE_INVALID_STOPS != (uint)TRADE_RETCODE_TIMEOUT &&
          (uint)TRADE_RETCODE_INVALID_STOPS != (uint)TRADE_RETCODE_PRICE_CHANGED,
          "TRADE_RETCODE_INVALID_STOPS is not classified as retryable");
  }
//+------------------------------------------------------------------+