//+------------------------------------------------------------------+
//|                                     TestRollingProfitFactor.mq5  |
//|        Independent verification script for rolling calculations  |
//+------------------------------------------------------------------+
#property script_show_inputs

#include <RollingProfitFactor/RollingPFTypes.mqh>
#include <RollingProfitFactor/ITradeSource.mqh>
#include <RollingProfitFactor/TradeHistoryReader.mqh>
#include <RollingProfitFactor/RollingWindowCalculator.mqh>

int g_assert_count = 0;
int g_fail_count   = 0;

//+------------------------------------------------------------------+
//| ExpectTrue                                                       |
//| Assert a boolean condition and report the result                 |
//+------------------------------------------------------------------+
void ExpectTrue(const bool condition, const string test_name)
  {
   g_assert_count++;
   if(!condition)
     {
      g_fail_count++;
      Print("FAILED: ", test_name);
     }
   else
      Print("PASSED: ", test_name);
  }

//+------------------------------------------------------------------+
//| ExpectNear                                                       |
//| Assert that two doubles are equal within a tolerance             |
//+------------------------------------------------------------------+
void ExpectNear(const double actual, const double expected, const double tolerance, const string test_name)
  {
   g_assert_count++;
   if(MathAbs(actual - expected) > tolerance)
     {
      g_fail_count++;
      Print("FAILED: ", test_name, " expected=", DoubleToString(expected, 5),
            " actual=", DoubleToString(actual, 5));
     }
   else
      Print("PASSED: ", test_name);
  }

//+------------------------------------------------------------------+
//| CSyntheticTradeSource                                            |
//| Minimal synthetic trade source used to drive controlled tests.   |
//| Implements ITradeSource directly, the same interface             |
//| CRollingWindowCalculator consumes from CTradeHistoryReader, so   |
//| every test below exercises the real Compute() path rather than   |
//| a duplicate hand-written formula.                                |
//+------------------------------------------------------------------+
class CSyntheticTradeSource : public ITradeSource
  {
private:
   STradeRecord      m_trades[];

public:
   void              Add(const double net_profit)
     {
      int size = ArraySize(m_trades);
      ArrayResize(m_trades, size + 1);
      m_trades[size].net_profit = net_profit;
      m_trades[size].close_time = (datetime)(1000000 + size * 3600);
     }

   virtual int       TradeCount(void) const { return(ArraySize(m_trades)); }

   virtual bool      GetTrade(const int index, STradeRecord &trade) const
     {
      if(index < 0 || index >= ArraySize(m_trades))
         return(false);
      trade = m_trades[index];
      return(true);
     }
  };

//+------------------------------------------------------------------+
//| NaiveProfitFactor                                                |
//| Naive reference computation used to validate the optimized path  |
//| independently of CRollingWindowCalculator itself                 |
//+------------------------------------------------------------------+
double NaiveProfitFactor(CSyntheticTradeSource &source, const int start_index, const int window_size)
  {
   double gross_profit = 0.0, gross_loss = 0.0;
   STradeRecord trade;

   for(int i = start_index; i < start_index + window_size; i++)
     {
      source.GetTrade(i, trade);
      if(trade.net_profit > 0.0)
         gross_profit += trade.net_profit;
      else
         if(trade.net_profit < 0.0)
            gross_loss += MathAbs(trade.net_profit);
     }

   if(gross_loss <= 0.0000001)
     {
      if(gross_profit <= 0.0000001)
         return(ROLLING_PF_UNDEFINED);
      return(ROLLING_PF_INFINITE);
     }

   return(gross_profit / gross_loss);
  }

//+------------------------------------------------------------------+
//| TestBasicRollingCalculation                                      |
//| Validate the rolling calculation against the naive reference by  |
//| calling the real CRollingWindowCalculator, not by re-deriving    |
//| Profit Factor a second time by hand                              |
//+------------------------------------------------------------------+
void TestBasicRollingCalculation(void)
  {
   CSyntheticTradeSource source;
   source.Add(100.0);
   source.Add(-40.0);
   source.Add(60.0);
   source.Add(-20.0);
   source.Add(30.0);
   source.Add(-50.0);
   source.Add(80.0);

   int window_size = 4;
   int total       = source.TradeCount();

   CRollingWindowCalculator calculator;
   ExpectTrue(calculator.Compute(source, window_size), "Compute succeeds with sufficient history");
   ExpectTrue(calculator.SeriesCount() == total - window_size + 1,
              "Series length matches the expected number of rolling windows");

   for(int start = 0; start <= total - window_size; start++)
     {
      double expected = NaiveProfitFactor(source, start, window_size);

      SRollingPFRecord record;
      calculator.GetRecord(start, record);

      ExpectNear(record.profit_factor, expected, 0.0001,
                 StringFormat("Compute() window at %d matches naive reference", start));
      ExpectTrue(record.end_index == start + window_size - 1,
                 StringFormat("Window at %d reports the correct end_index", start));
     }
  }

//+------------------------------------------------------------------+
//| TestSlidingWindowCounts                                          |
//| Validate that win/loss counts track correctly across a slide,    |
//| not only the resulting Profit Factor ratio                       |
//+------------------------------------------------------------------+
void TestSlidingWindowCounts(void)
  {
   CSyntheticTradeSource source;
   source.Add(10.0);
   source.Add(-5.0);
   source.Add(10.0);
   source.Add(10.0);
   source.Add(-5.0);

   CRollingWindowCalculator calculator;
   ExpectTrue(calculator.Compute(source, 3), "Compute succeeds for the sliding window count test");

   SRollingPFRecord first, second, third;
   calculator.GetRecord(0, first);
   calculator.GetRecord(1, second);
   calculator.GetRecord(2, third);

   ExpectTrue(first.win_count == 2 && first.loss_count == 1, "First window win/loss counts are correct");
   ExpectTrue(second.win_count == 2 && second.loss_count == 1, "Second window win/loss counts are correct after a slide");
   ExpectTrue(third.win_count == 2 && third.loss_count == 1, "Third window win/loss counts are correct after a second slide");
  }

//+------------------------------------------------------------------+
//| TestAllWinningWindow                                             |
//| Validate the all-winning window edge case through Compute()      |
//+------------------------------------------------------------------+
void TestAllWinningWindow(void)
  {
   CSyntheticTradeSource source;
   source.Add(100.0);
   source.Add(50.0);
   source.Add(25.0);

   CRollingWindowCalculator calculator;
   ExpectTrue(calculator.Compute(source, 3), "Compute succeeds for the all-winning window test");

   SRollingPFRecord record;
   calculator.GetRecord(0, record);

   ExpectNear(record.profit_factor, ROLLING_PF_INFINITE, 0.0001,
              "All-winning window returns the sentinel infinite value");
  }

//+------------------------------------------------------------------+
//| TestAllLosingWindow                                              |
//| Validate the all-losing window edge case through Compute()       |
//+------------------------------------------------------------------+
void TestAllLosingWindow(void)
  {
   CSyntheticTradeSource source;
   source.Add(-30.0);
   source.Add(-45.0);

   CRollingWindowCalculator calculator;
   ExpectTrue(calculator.Compute(source, 2), "Compute succeeds for the all-losing window test");

   SRollingPFRecord record;
   calculator.GetRecord(0, record);

   ExpectNear(record.profit_factor, 0.0, 0.0001, "All-losing window returns a Profit Factor of zero");
  }

//+------------------------------------------------------------------+
//| TestFlatWindow                                                   |
//| Validate the flat, all-zero-profit window edge case through      |
//| Compute(). This now returns ROLLING_PF_UNDEFINED rather than a   |
//| neutral 1.0, since a window with no realized result either way   |
//| is mathematically undefined, not a genuine breakeven.            |
//+------------------------------------------------------------------+
void TestFlatWindow(void)
  {
   CSyntheticTradeSource source;
   source.Add(0.0);
   source.Add(0.0);

   CRollingWindowCalculator calculator;
   ExpectTrue(calculator.Compute(source, 2), "Compute succeeds for the flat window test");

   SRollingPFRecord record;
   calculator.GetRecord(0, record);

   ExpectNear(record.profit_factor, ROLLING_PF_UNDEFINED, 0.0001,
              "Window with only zero-profit trades returns the undefined sentinel, not a neutral breakeven");
  }

//+------------------------------------------------------------------+
//| TestInsufficientHistory                                          |
//| Validate graceful failure when history is shorter than the       |
//| requested window size                                            |
//+------------------------------------------------------------------+
void TestInsufficientHistory(void)
  {
   CTradeHistoryReader reader;
   CRollingWindowCalculator calculator;

   bool result = calculator.Compute(reader, 30);

   ExpectTrue(result == false, "Calculator reports failure when trade count is below the window size");
  }

//+------------------------------------------------------------------+
//| Script entry point                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
   Print("Running Rolling Profit Factor verification suite...");

   TestBasicRollingCalculation();
   TestSlidingWindowCounts();
   TestAllWinningWindow();
   TestAllLosingWindow();
   TestFlatWindow();
   TestInsufficientHistory();

   Print("----------------------------------------------------------");
   Print("Verification complete. ", g_assert_count, " assertions executed, ", g_fail_count, " failed.");
   Print("----------------------------------------------------------");
  }
//+------------------------------------------------------------------+