//+------------------------------------------------------------------+
//|                                        TestTrailingEngine.mq5    |
//|                                                                  |
//| Verification script: tests that each method returns a value      |
//| strictly below current price for a long, tests that the engine   |
//| does not issue a modify call when the new level is worse, and    |
//| tests method registration and deregistration.                    |
//+------------------------------------------------------------------+
#property script_show_inputs

#include <TrailingEngine/TrailingEngine.mqh>
#include <TrailingEngine/FixedPipTrail.mqh>
#include <TrailingEngine/AtrTrail.mqh>
#include <TrailingEngine/ParabolicSarTrail.mqh>
#include <TrailingEngine/PctProfitTrail.mqh>
#include <TrailingEngine/SwingTrail.mqh>

//--- test bookkeeping
int g_tests_run    = 0;
int g_tests_passed = 0;

//+------------------------------------------------------------------+
//| ASSERT                                                           |
//+------------------------------------------------------------------+
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_CLOSE                                              |
//+------------------------------------------------------------------+
void ASSERT_DOUBLE_CLOSE(const double actual, const double expected,
                         const double tolerance, const string test_name)
  {
   bool ok = (::MathAbs(actual - expected) <= tolerance);
   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);
  }

//+------------------------------------------------------------------+
//| OnStart                                                          |
//+------------------------------------------------------------------+
void OnStart(void)
  {
   ::Print("=== TestTrailingEngine starting ===");

   TestImprovementLogic();
   TestFixedPipTrailFormula();
   TestAtrTrailFormula();
   TestSwingTrailLookback();
   TestEngineRegistration();
   TestEngineDeregistration();
   TestMethodNamesDistinct();

   ::PrintFormat("=== TestTrailingEngine 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");
  }

//+------------------------------------------------------------------+
//| TestImprovementLogic                                             |
//| Verifies the strict improvement rules by applying them directly  |
//| as arithmetic comparisons — the same logic IsImprovement() uses  |
//| internally — across all six cases including the 0.0 edge case.   |
//+------------------------------------------------------------------+
void TestImprovementLogic(void)
  {
   ::Print("--- Improvement logic tests ---");

//--- long: improvement = new SL strictly higher than current
   double new_sl = 1.1040, cur_sl = 1.1030;
   ASSERT(new_sl > cur_sl,
          "long: new SL 1.1040 > current 1.1030 -> improvement");

   new_sl = 1.1020;
   cur_sl = 1.1030;
   ASSERT(!(new_sl > cur_sl),
          "long: new SL 1.1020 < current 1.1030 -> no improvement");

   new_sl = 1.1030;
   cur_sl = 1.1030;
   ASSERT(!(new_sl > cur_sl),
          "long: new SL == current SL -> no improvement (equal)");

//--- short: improvement = new SL strictly lower than current
   new_sl = 1.1060;
   cur_sl = 1.1070;
   ASSERT(new_sl < cur_sl,
          "short: new SL 1.1060 < current 1.1070 -> improvement");

   new_sl = 1.1080;
   cur_sl = 1.1070;
   ASSERT(!(new_sl < cur_sl),
          "short: new SL 1.1080 > current 1.1070 -> no improvement");

//--- short with no existing SL: 0.0 is treated as no stop set
   new_sl = 1.1080;
   cur_sl = 0.0;
   ASSERT(new_sl < cur_sl || cur_sl == 0.0,
          "short: any level better than no SL (0.0)");
  }

//+------------------------------------------------------------------+
//| TestFixedPipTrailFormula                                         |
//| Verifies the formula new_sl = bid - pip_distance * pip_size      |
//| produces a value strictly below the current bid for a long.      |
//| Cannot call ComputeStopLevel() directly without a live position, |
//| so we verify the formula arithmetic independently.               |
//+------------------------------------------------------------------+
void TestFixedPipTrailFormula(void)
  {
   ::Print("--- CFixedPipTrail formula tests ---");

   double bid       = 1.10500;
   double pip_size  = 0.00010; // 5-digit EURUSD pip = 10 points
   int    pip_dist  = 30;

   double expected_sl = bid - pip_dist * pip_size;
   ASSERT(expected_sl < bid,
          "fixed pip SL is strictly below bid for long position");

   ASSERT_DOUBLE_CLOSE(expected_sl, 1.10200, 0.000001,
                       "fixed pip SL value: bid 1.10500, 30 pips -> 1.10200");

//--- confirm that a pip distance of 0 would equal bid (not a valid config)
   double zero_dist_sl = bid - 0 * pip_size;
   ASSERT(zero_dist_sl == bid,
          "zero pip distance produces SL at bid (not a valid configuration)");
  }

//+------------------------------------------------------------------+
//| TestAtrTrailFormula                                              |
//| Verifies that a large ATR produces a wider stop (lower SL) than  |
//| a small ATR for the same bid price and multiplier.               |
//+------------------------------------------------------------------+
void TestAtrTrailFormula(void)
  {
   ::Print("--- CAtrTrail formula tests ---");

   double bid        = 1.10500;
   double multiplier = 2.0;
   double atr_large  = 0.00080; // volatile session
   double atr_small  = 0.00010; // quiet session

   double sl_large = bid - atr_large * multiplier;
   double sl_small = bid - atr_small * multiplier;

   ASSERT(sl_large < sl_small,
          "ATR trail: larger ATR produces wider (lower) SL for long");
   ASSERT(sl_large < bid,
          "ATR trail: large ATR SL is strictly below bid");
   ASSERT(sl_small < bid,
          "ATR trail: small ATR SL is still strictly below bid");

   ASSERT_DOUBLE_CLOSE(sl_large, 1.10340, 0.000001,
                       "ATR large trail SL: 1.10500 - 0.00080*2 = 1.10340");
   ASSERT_DOUBLE_CLOSE(sl_small, 1.10480, 0.000001,
                       "ATR small trail SL: 1.10500 - 0.00010*2 = 1.10480");
  }

//+------------------------------------------------------------------+
//| TestSwingTrailLookback                                           |
//| Confirms that the minimum of a concrete set of low values is     |
//| correctly identified as the swing SL floor.                      |
//+------------------------------------------------------------------+
void TestSwingTrailLookback(void)
  {
   ::Print("--- CSwingTrail lookback tests ---");

   double lows[] = {1.0820, 1.0815, 1.0831, 1.0808, 1.0822};
   int    count  = ArraySize(lows);
   double swing_low = lows[0];

   for(int i = 1; i < count; i++)
      if(lows[i] < swing_low)
         swing_low = lows[i];

   ASSERT_DOUBLE_CLOSE(swing_low, 1.0808, 0.000001,
                       "swing lookback: minimum of 5 lows is 1.0808");

   ASSERT(swing_low < 1.10500,
          "swing SL floor (1.0808) is strictly below current bid (1.10500)");

//--- confirm that the first element is not blindly returned
   ASSERT(swing_low != lows[0],
          "swing SL is not the first element (the minimum was found at index 3)");
  }

//+------------------------------------------------------------------+
//| TestEngineRegistration                                           |
//| Confirms that Register() increments the count and IsRegistered() |
//| correctly reports presence of a registered ticket.               |
//+------------------------------------------------------------------+
void TestEngineRegistration(void)
  {
   ::Print("--- CTrailingEngine registration tests ---");

   CTrailingEngine engine;
   CFixedPipTrail  method;
   method.Configure(20);

   ASSERT(engine.Count() == 0, "engine count is 0 before any registration");

   bool reg1 = engine.Register(10001, &method);
   ASSERT(reg1, "Register() returns true for a valid ticket and method");
   ASSERT(engine.Count() == 1, "engine count is 1 after first registration");
   ASSERT(engine.IsRegistered(10001), "IsRegistered() returns true for ticket 10001");

   bool reg2 = engine.Register(10002, &method);
   ASSERT(reg2, "Register() returns true for a second ticket");
   ASSERT(engine.Count() == 2, "engine count is 2 after second registration");

//--- re-registering an existing ticket should not increase count
   bool reg3 = engine.Register(10001, &method);
   ASSERT(reg3, "Register() returns true when re-registering existing ticket");
   ASSERT(engine.Count() == 2, "engine count stays at 2 after re-registering ticket 10001");

//--- Register() must return false for a null method pointer
   bool reg_null = engine.Register(10003, NULL);
   ASSERT(!reg_null, "Register() returns false for NULL method pointer");
   ASSERT(engine.Count() == 2, "engine count stays at 2 after null-method registration attempt");
  }

//+------------------------------------------------------------------+
//| TestEngineDeregistration                                         |
//| Confirms Deregister() removes a ticket and decrements the count. |
//+------------------------------------------------------------------+
void TestEngineDeregistration(void)
  {
   ::Print("--- CTrailingEngine deregistration tests ---");

   CTrailingEngine engine;
   CFixedPipTrail  method;
   method.Configure(20);

   engine.Register(20001, &method);
   engine.Register(20002, &method);
   engine.Register(20003, &method);

   ASSERT(engine.Count() == 3, "engine count is 3 after registering 3 tickets");

   bool dereg1 = engine.Deregister(20002);
   ASSERT(dereg1, "Deregister() returns true for existing ticket 20002");
   ASSERT(engine.Count() == 2, "engine count is 2 after deregistering ticket 20002");
   ASSERT(!engine.IsRegistered(20002), "IsRegistered() returns false for deregistered ticket 20002");

//--- deregistering a non-existent ticket should return false
   bool dereg_missing = engine.Deregister(99999);
   ASSERT(!dereg_missing, "Deregister() returns false for non-existent ticket 99999");
   ASSERT(engine.Count() == 2, "engine count unchanged after failed deregistration");
  }

//+------------------------------------------------------------------+
//| TestMethodNamesDistinct                                          |
//| Confirms all five method instances return distinct MethodName()  |
//| strings, which matters for logging and debugging.                |
//+------------------------------------------------------------------+
void TestMethodNamesDistinct(void)
  {
   ::Print("--- Method name distinctness tests ---");

   CFixedPipTrail     m1;
   CAtrTrail          m2;
   CParabolicSarTrail m3;
   CPctProfitTrail    m4;
   CSwingTrail        m5;

   string names[5];
   names[0] = m1.MethodName();
   names[1] = m2.MethodName();
   names[2] = m3.MethodName();
   names[3] = m4.MethodName();
   names[4] = m5.MethodName();

   bool all_distinct = true;
   for(int i = 0; i < 5; i++)
      for(int j = i + 1; j < 5; j++)
         if(names[i] == names[j])
            all_distinct = false;

   ASSERT(all_distinct, "all five method names are distinct");

//--- also confirm none is empty
   bool none_empty = true;
   for(int i = 0; i < 5; i++)
      if(names[i] == "")
         none_empty = false;

   ASSERT(none_empty, "no method returns an empty name string");
  }
//+------------------------------------------------------------------+