//+------------------------------------------------------------------+
//|                                          TestBasketManager.mq5   |
//| Verification script: tests basket ID extraction, CBasketInfo     |
//| population, aggregate P&L with synthetic data, stop threshold    |
//| logic, and the CloseBasket execution path.                       |
//+------------------------------------------------------------------+
#property script_show_inputs

#include <BasketManager/BasketManager.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 %.4f, got %.4f)", test_name, expected, actual);
     }
   else
      PrintFormat("FAIL: %s (expected %.4f, got %.4f)", test_name, expected, actual);
  }

//+------------------------------------------------------------------+
//| OnStart                                                          |
//+------------------------------------------------------------------+
void OnStart(void)
  {
   Print("=== TestBasketManager starting ===");

   TestBasketIdExtraction();
   TestAggregatePnl();
   TestVolumeWeightedPips();
   TestStopThresholdLogic();
   TestRegistryRegisterDeregister();
   TestBasketInfoFields();

   PrintFormat("=== TestBasketManager 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");
  }

//+------------------------------------------------------------------+
//| TestBasketIdExtraction                                           |
//| Verifies the prefix-based basket ID extraction logic directly.   |
//+------------------------------------------------------------------+
void TestBasketIdExtraction(void)
  {
   Print("--- Basket ID extraction tests ---");

//--- replicate the extraction logic from CBasketScanner
   string prefix = "BASKET:";

//--- helper lambda equivalent: extract ID from comment
#define EXTRACT(comment) (::StringFind(comment, prefix) >= 0 ? \
      ::StringSubstr(comment, ::StringFind(comment, prefix) + ::StringLen(prefix)) : "")

   string id1 = EXTRACT("BASKET:USD_LONG");
   ASSERT(id1 == "USD_LONG", "extracts 'USD_LONG' from 'BASKET:USD_LONG'");

   string id2 = EXTRACT("BASKET:GOLD_HEDGE");
   ASSERT(id2 == "GOLD_HEDGE", "extracts 'GOLD_HEDGE' from 'BASKET:GOLD_HEDGE'");

   string id3 = EXTRACT("no basket comment");
   ASSERT(id3 == "", "returns empty string for comment without prefix");

   string id4 = EXTRACT("BASKET:");
   ASSERT(id4 == "", "returns empty string for 'BASKET:' with no ID");

   string id5 = EXTRACT("entry BASKET:EUR_SHORT trailing");
   ASSERT(::StringFind(id5, "EUR_SHORT") == 0,
          "extracts ID when prefix is not at string start");

#undef EXTRACT
  }

//+------------------------------------------------------------------+
//| TestAggregatePnl                                                 |
//| Verifies that summing three synthetic P&L values produces the    |
//| correct basket total.                                            |
//+------------------------------------------------------------------+
void TestAggregatePnl(void)
  {
   Print("--- Aggregate P&L tests ---");

   double pnl_values[] = {85.20, -12.40, 69.70};
   double total = 0.0;
   for(int i = 0; i < 3; i++)
      total += pnl_values[i];

   ASSERT_DOUBLE_CLOSE(total, 142.50, 0.001,
                       "three legs 85.20 + (-12.40) + 69.70 = 142.50");

//--- verify that a net-negative basket is also computed correctly
   double pnl_neg[] = {-80.00, -50.00, -35.00};
   double total_neg = 0.0;
   for(int i = 0; i < 3; i++)
      total_neg += pnl_neg[i];

   ASSERT_DOUBLE_CLOSE(total_neg, -165.00, 0.001,
                       "three losing legs sum to -165.00");
  }

//+------------------------------------------------------------------+
//| TestVolumeWeightedPips                                           |
//| Verifies the volume-weighted average pip P&L formula.            |
//+------------------------------------------------------------------+
void TestVolumeWeightedPips(void)
  {
   Print("--- Volume-weighted pip P&L tests ---");

   double vol1 = 0.10, pips1 = 25.0;
   double vol2 = 0.20, pips2 = -8.0;
   double total_vol = vol1 + vol2;
   double vw = (vol1 * pips1 + vol2 * pips2) / total_vol;

//--- (0.10*25 + 0.20*(-8)) / 0.30 = (2.50 - 1.60) / 0.30 = 0.90 / 0.30 = 3.00
   ASSERT_DOUBLE_CLOSE(vw, 3.00, 0.001,
                       "vol-weighted: (0.10*25 + 0.20*(-8)) / 0.30 = 3.00 pips");

   ASSERT(vw > 0.0,
          "vol-weighted result is positive (long leg dominates by volume)");

//--- equal volumes: average should be the arithmetic mean
   double vw_eq = (0.10 * 10.0 + 0.10 * (-10.0)) / 0.20;
   ASSERT_DOUBLE_CLOSE(vw_eq, 0.0, 0.001,
                       "equal volumes, equal but opposite pips -> 0.0 weighted result");
  }

//+------------------------------------------------------------------+
//| TestStopThresholdLogic                                           |
//| Verifies the breached() check: aggregate P&L < threshold.        |
//+------------------------------------------------------------------+
void TestStopThresholdLogic(void)
  {
   Print("--- Stop threshold logic tests ---");

   double threshold = -150.00;

   double pnl_breach  = -165.00;
   double pnl_safe    = -80.00;
   double pnl_exactly = -150.00;

   ASSERT(pnl_breach < threshold,
          "P&L -165.00 < threshold -150.00 -> breached");

   ASSERT(!(pnl_safe < threshold),
          "P&L -80.00 not < threshold -150.00 -> not breached");

//--- strictly less than: exactly at threshold does not trigger
   ASSERT(!(pnl_exactly < threshold),
          "P&L exactly at threshold (-150.00) -> not breached (strict <)");

//--- positive P&L is never a breach
   ASSERT(!(50.00 < threshold),
          "positive P&L is never a breach");
  }

//+------------------------------------------------------------------+
//| TestRegistryRegisterDeregister                                   |
//| Confirms that the stop registry stores, retrieves, and removes   |
//| thresholds correctly.                                            |
//+------------------------------------------------------------------+
void TestRegistryRegisterDeregister(void)
  {
   Print("--- CBasketStopRegistry register/deregister tests ---");

   CBasketStopRegistry registry;

   ASSERT(registry.Count() == 0, "registry count is 0 before registration");

   registry.Register("USD_LONG",   -200.00);
   registry.Register("GOLD_HEDGE", -150.00);

   ASSERT(registry.Count() == 2, "registry count is 2 after two registrations");

   double t1 = 0.0;
   bool   ok1 = registry.GetThreshold("USD_LONG", t1);
   ASSERT(ok1, "GetThreshold returns true for registered basket");
   ASSERT_DOUBLE_CLOSE(t1, -200.00, 0.001, "USD_LONG threshold is -200.00");

//--- re-registering updates the threshold
   registry.Register("USD_LONG", -180.00);
   double t1b = 0.0;
   registry.GetThreshold("USD_LONG", t1b);
   ASSERT_DOUBLE_CLOSE(t1b, -180.00, 0.001, "re-registration updates threshold to -180.00");
   ASSERT(registry.Count() == 2, "count unchanged after re-registration");

//--- deregister
   bool dereg = registry.Deregister("GOLD_HEDGE");
   ASSERT(dereg, "Deregister() returns true for existing basket");

   double t2 = 0.0;
   bool ok2 = registry.GetThreshold("GOLD_HEDGE", t2);
   ASSERT(!ok2, "GetThreshold returns false after deregistration");

//--- deregistering non-existent basket
   bool dereg_miss = registry.Deregister("NONEXISTENT");
   ASSERT(!dereg_miss, "Deregister() returns false for non-existent basket");
  }

//+------------------------------------------------------------------+
//| TestBasketInfoFields                                             |
//| Verifies that a manually assembled CBasketInfo has the correct   |
//| field values and that distance_to_stop is computed correctly.    |
//+------------------------------------------------------------------+
void TestBasketInfoFields(void)
  {
   Print("--- CBasketInfo field tests ---");

   CBasketInfo info;
   info.basket_id          = "USD_LONG";
   info.legs               = 3;
   info.total_long_volume  = 0.30;
   info.total_short_volume = 0.0;
   info.aggregate_pnl      = 142.50;
   info.stop_threshold     = -200.00;
   info.distance_to_stop   = info.aggregate_pnl - info.stop_threshold;

   ASSERT(info.basket_id == "USD_LONG", "basket_id field is set correctly");
   ASSERT(info.legs == 3,               "legs field is 3");
   ASSERT_DOUBLE_CLOSE(info.total_long_volume,  0.30, 0.0001, "long volume is 0.30");
   ASSERT_DOUBLE_CLOSE(info.total_short_volume, 0.00, 0.0001, "short volume is 0.00");
   ASSERT_DOUBLE_CLOSE(info.aggregate_pnl,    142.50, 0.001,  "aggregate P&L is 142.50");
   ASSERT_DOUBLE_CLOSE(info.distance_to_stop, 342.50, 0.001,
                       "distance_to_stop = 142.50 - (-200.00) = 342.50");

//--- negative P&L basket
   CBasketInfo info2;
   info2.aggregate_pnl    = -38.20;
   info2.stop_threshold   = -150.00;
   info2.distance_to_stop = info2.aggregate_pnl - info2.stop_threshold;

   ASSERT_DOUBLE_CLOSE(info2.distance_to_stop, 111.80, 0.001,
                       "distance_to_stop = -38.20 - (-150.00) = 111.80");

   ASSERT(info2.distance_to_stop > 0.0,
          "distance_to_stop is positive when basket has not breached its stop");
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
