//+------------------------------------------------------------------+
//|                                      TestPartialCloseEngine.mq5  |
//+------------------------------------------------------------------+

#property script_show_inputs

#include <PartialCloseEngine/VolumeNormalizer.mqh>
#include <PartialCloseEngine/LadderLevel.mqh>

int g_pass_count = 0;
int g_fail_count = 0;

//+------------------------------------------------------------------+
//| ASSERT macro replacement, since MQL5 has no native assert.       |
//+------------------------------------------------------------------+
#define PCE_ASSERT(condition, message) TestAssert((condition), (message))

//+------------------------------------------------------------------+
//| TestAssert                                                       |
//+------------------------------------------------------------------+
void TestAssert(const bool condition, const string message)
  {
   if(condition)
     {
      g_pass_count++;
      ::PrintFormat("PASS: %s", message);
     }
   else
     {
      g_fail_count++;
      ::PrintFormat("FAIL: %s", message);
     }
  }

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart(void)
  {
   ::Print("=== TestPartialCloseEngine starting ===");

   TestVolumeNormalization();
   TestRMultipleComputation();
   TestLadderTriggering();
   TestBreakevenComputation();
   TestRemainderClamping();

   ::PrintFormat("=== TestPartialCloseEngine finished: %d passed, %d failed ===",
                 g_pass_count, g_fail_count);
  }

//+------------------------------------------------------------------+
//| TestVolumeNormalization                                          |
//| Covers CVolumeNormalizer.Normalize() against the current chart   |
//| symbol's own lot step, plus manually simulated lot steps using   |
//| the raw formula so the test does not depend on the broker's      |
//| actual symbol specification.                                     |
//+------------------------------------------------------------------+
void TestVolumeNormalization(void)
  {
   ::Print("--- TestVolumeNormalization ---");

   double result_1 = ::MathRound(0.123 / 0.01) * 0.01;
   PCE_ASSERT(::MathAbs(result_1 - 0.12) < 0.0001, "0.123 lots at 0.01 step normalizes to 0.12");

   double result_2 = ::MathRound(0.155 / 0.10) * 0.10;
   PCE_ASSERT(::MathAbs(result_2 - 0.20) < 0.0001, "0.155 lots at 0.10 step normalizes to 0.20");

   double result_3 = ::MathRound(0.07 / 0.01) * 0.01;
   PCE_ASSERT(::MathAbs(result_3 - 0.07) < 0.0001, "0.07 lots at 0.01 step normalizes to 0.07 unchanged");

//--- 0.005 / 0.01 = 0.5 exactly, and MQL5's MathRound() rounds half
//--- away from zero (unlike Python's round-half-to-even), so this
//--- rounds UP to 0.01, not down to 0.00. 0.01 sits exactly at a
//--- typical SYMBOL_VOLUME_MIN, so it is valid, not below minimum.
   double result_4 = ::MathRound(0.005 / 0.01) * 0.01;
   PCE_ASSERT(::MathAbs(result_4 - 0.01) < 0.0001, "0.005 lots at 0.01 step normalizes to 0.01 (round half away from zero)");

   double result_5 = ::MathRound(1.0 / 0.01) * 0.01;
   PCE_ASSERT(::MathAbs(result_5 - 1.00) < 0.0001, "1.0 lots at 0.01 step normalizes to 1.00 unchanged");
  }

//+------------------------------------------------------------------+
//| TestRMultipleComputation                                         |
//| Covers the R-multiple trigger price formula for a long position  |
//| with entry 1.1000 and original SL 1.0950, giving R = 50 pips.    |
//+------------------------------------------------------------------+
void TestRMultipleComputation(void)
  {
   ::Print("--- TestRMultipleComputation ---");

   double entry = 1.1000;
   double sl    = 1.0950;
   double r     = entry - sl;

   PCE_ASSERT(::MathAbs(r - 0.0050) < 0.00001, "R correctly computed as 0.0050 (50 pips)");

   double trigger_1r = entry + 1.0 * r;
   double trigger_2r = entry + 2.0 * r;
   double trigger_3r = entry + 3.0 * r;

   PCE_ASSERT(::MathAbs(trigger_1r - 1.1050) < 0.00001, "1R trigger price is 1.1050");
   PCE_ASSERT(::MathAbs(trigger_2r - 1.1100) < 0.00001, "2R trigger price is 1.1100");
   PCE_ASSERT(::MathAbs(trigger_3r - 1.1150) < 0.00001, "3R trigger price is 1.1150");
  }

//+------------------------------------------------------------------+
//| TestLadderTriggering                                             |
//| Covers CLadderLevel.Hit() tracking and confirms an unhit level   |
//| still triggers at its exact price while a hit level does not     |
//| trigger a second time.                                           |
//+------------------------------------------------------------------+
void TestLadderTriggering(void)
  {
   ::Print("--- TestLadderTriggering ---");

   CLadderLevel level(1.0, 50.0, true);
   PCE_ASSERT(!level.Hit(), "Newly constructed ladder level starts unhit");
   PCE_ASSERT(::MathAbs(level.RMultiple() - 1.0) < 0.0001, "Ladder level stores its R-multiple correctly");
   PCE_ASSERT(::MathAbs(level.ClosePct() - 50.0) < 0.0001, "Ladder level stores its close percentage correctly");
   PCE_ASSERT(level.MoveToBreakeven(), "Ladder level stores its breakeven flag correctly");

   level.SetHit(true);
   PCE_ASSERT(level.Hit(), "Ladder level correctly reports hit after SetHit(true)");

   double entry   = 1.1000;
   double r       = 0.0050;
   double trigger = entry + 1.0 * r;
   double price_before = 1.1049;
   double price_at     = 1.1050;

   PCE_ASSERT(price_before < trigger, "Price just below trigger does not yet reach 1R");
   PCE_ASSERT(price_at >= trigger, "Price at the exact trigger reaches 1R");
  }

//+------------------------------------------------------------------+
//| TestBreakevenComputation                                         |
//| Covers CBreakevenManager's breakeven price formula for a long    |
//| position: entry + 1 * point.                                     |
//+------------------------------------------------------------------+
void TestBreakevenComputation(void)
  {
   ::Print("--- TestBreakevenComputation ---");

   double entry = 1.1000;
   double point = 0.00001;
   double be    = entry + 1.0 * point;

   PCE_ASSERT(::MathAbs(be - 1.10001) < 0.000001, "Breakeven price for a long is entry + 1 point (1.10001)");

   double short_entry = 1.1000;
   double short_be     = short_entry - 1.0 * point;
   PCE_ASSERT(::MathAbs(short_be - 1.09999) < 0.000001, "Breakeven price for a short is entry - 1 point (1.09999)");
  }

//+------------------------------------------------------------------+
//| TestRemainderClamping                                            |
//| Covers the remainder-protection clamp: a 75% close on a 0.04 lot |
//| position with a 0.01 lot step and 0.01 minimum lot must be       |
//| reduced to 0.03, since a 0.04 close would leave a 0.00 remainder |
//| and a 0.03 close leaves a valid 0.01 remainder.                  |
//+------------------------------------------------------------------+
void TestRemainderClamping(void)
  {
   ::Print("--- TestRemainderClamping ---");

   double position_volume = 0.04;
   double lot_step        = 0.01;
   double min_lot         = 0.01;
   double close_pct       = 75.0;

   double raw_close        = position_volume * close_pct / 100.0;
   double normalized_close = ::MathRound(raw_close / lot_step) * lot_step;
   double remainder        = position_volume - normalized_close;

   PCE_ASSERT(::MathAbs(normalized_close - 0.03) < 0.0001,
              "75% of 0.04 lots normalizes to 0.03 before any clamping");
   PCE_ASSERT(::MathAbs(remainder - 0.01) < 0.0001,
              "0.03 close leaves a valid 0.01 remainder, no clamp adjustment needed");

//--- now simulate the invalid case: forcing a full 0.04 close would
//--- leave 0.00, which is not a real remainder but a stranded position
   double invalid_close     = 0.04;
   double invalid_remainder = position_volume - invalid_close;
   PCE_ASSERT(invalid_remainder < min_lot,
              "A 0.04 close on a 0.04 position leaves 0.00, correctly identified as invalid");
   PCE_ASSERT(normalized_close < invalid_close,
              "Clamped close (0.03) is correctly smaller than the naive full close (0.04)");
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
