//+------------------------------------------------------------------+
//|                                         TestScatterAnalytics.mq5 |
//+------------------------------------------------------------------+

#include <ScatterDashboard/ScatterTypes.mqh>
#include <ScatterDashboard/RegressionCalculator.mqh>

#define ASSERT(condition, message) TestAssert((condition), (message))

int g_pass_count = 0;
int g_fail_count = 0;

//+------------------------------------------------------------------+
//| TestAssert                                                       |
//| Prints a pass or fail message for a single test condition and    |
//| tracks the running pass and fail counts.                         |
//+------------------------------------------------------------------+
void TestAssert(bool condition, string message)
  {
   if(condition)
     {
      g_pass_count++;
      ::Print("PASS: ", message);
     }
   else
     {
      g_fail_count++;
      ::Print("FAIL: ", message);
     }
  }

//+------------------------------------------------------------------+
//| OnStart                                                          |
//| Builds a synthetic set of 26 trades with profit declining as     |
//| duration increases, then checks the regression fit and the       |
//| best-performing historical bucket against hand-worked values,    |
//| including a case where the short bucket is empty.                |
//+------------------------------------------------------------------+
void OnStart(void)
  {
//--- durations in minutes and matching profits, 12 short, 8 medium, 6 long
   double d[26] = {10,15,20,25,30,35,40,45,50,55,12,18,
                   70,90,110,130,150,170,200,230,
                   250,300,350,400,450,500
                  };
   double p[26] = {15,20,10,-5,25,12,-8,18,22,9,-3,14,
                   10,-15,5,20,-10,8,-2,12,
                   -20,-35,-15,-40,-25,-30
                  };
   CTradePoint trades[26];
   for(int i = 0; i < 26; i++)
     {
      trades[i].ticket           = (ulong)(i + 1);
      trades[i].symbol           = "EURUSD";
      trades[i].duration_minutes = d[i];
      trades[i].profit           = p[i];
     }
//--- fit the regression line
   CRegressionCalculator calculator;
   CRegressionResult     regression;
   bool ok = calculator.Compute(trades, 26, regression);
//--- test 1: the regression should succeed with 26 distinct-duration trades
   ASSERT(ok, "regression computes successfully for 26 trades");
//--- test 2: the slope should be negative, since profit declines with duration
   ASSERT(regression.slope < 0.0,
          "slope is negative, since profit declines as duration increases");
//--- test 3: the slope matches the hand-worked value
   ASSERT(::MathAbs(regression.slope - (-0.097558)) < 0.0001,
          "slope computes to approximately -0.0976 per minute");
//--- test 4: the intercept matches the hand-worked value
   ASSERT(::MathAbs(regression.intercept - 13.781949) < 0.001,
          "intercept computes to approximately 13.78");
//--- test 5: R² matches the hand-worked value
   ASSERT(::MathAbs(regression.r_squared - 0.594973) < 0.001,
          "R^2 computes to approximately 0.595");
//--- find the best-performing historical bucket
   double best_avg = 0.0;
   string range = calculator.ComputeBestHistoricalBucket(trades, 26, best_avg);
//--- test 6: the short duration bucket should be identified as best-performing
   ASSERT(range == "0-60 min",
          "the 0-60 minute bucket is identified as the best-performing historical bucket");
//--- test 7: the short bucket's average profit matches the hand-worked value
   ASSERT(::MathAbs(best_avg - 10.75) < 0.001,
          "the best historical bucket's average profit computes to 10.75");
//--- test 8 and 9: an empty short bucket must never be selected by default.
//--- 2 medium trades (avg -10) and 2 long trades (avg -20), no short
//--- trades at all; the fixed bug would have returned "0-60 min" here
//--- since an empty bucket's average defaults to 0.0, which beats both
//--- real, negative bucket averages
   CTradePoint empty_bucket_trades[4];
   empty_bucket_trades[0].symbol           = "EURUSD";
   empty_bucket_trades[0].duration_minutes = 70.0;
   empty_bucket_trades[0].profit           = -5.0;
   empty_bucket_trades[1].symbol           = "EURUSD";
   empty_bucket_trades[1].duration_minutes = 90.0;
   empty_bucket_trades[1].profit           = -15.0;
   empty_bucket_trades[2].symbol           = "EURUSD";
   empty_bucket_trades[2].duration_minutes = 300.0;
   empty_bucket_trades[2].profit           = -15.0;
   empty_bucket_trades[3].symbol           = "EURUSD";
   empty_bucket_trades[3].duration_minutes = 400.0;
   empty_bucket_trades[3].profit           = -25.0;
   double empty_bucket_best_avg = 0.0;
   string empty_bucket_label = calculator.ComputeBestHistoricalBucket(empty_bucket_trades, 4, empty_bucket_best_avg);
   ASSERT(empty_bucket_label != "0-60 min",
          "an empty short bucket is never selected by default over real, negative bucket averages");
   ASSERT(empty_bucket_label == "60-240 min",
          "the least-negative non-empty bucket (60-240 min, avg -10.0) is correctly selected as best");
//--- print the final summary of pass and fail counts
   ::Print("TestScatterAnalytics: ", g_pass_count, " passed, ", g_fail_count, " failed");
  }
//+------------------------------------------------------------------+