#property strict

#include <Debugging_Profiling_Part2/TestLite.mqh>
#include <Debugging_Profiling_Part2/TradeMathCore.mqh>

//+------------------------------------------------------------------+
//| Test pip and point conversion                                    |
//+------------------------------------------------------------------+
void TestPipPointConversion(CTestLite &test)
  {
   test.BeginTest("pip and point conversion");
   //--- Five-digit symbols use fractional pips, so one pip is ten points.
   test.AssertNearDouble(100.0, CTradeMathCore::PipsToPoints(10.0, 5), 0.000001, "5-digit symbols use ten points per pip");
   test.AssertNearDouble(10.0, CTradeMathCore::PointsToPips(100.0, 5), 0.000001, "5-digit points convert back to pips");
   //--- Four- and two-digit symbols keep the point and pip units equal in this helper.
   test.AssertNearDouble(10.0, CTradeMathCore::PipsToPoints(10.0, 4), 0.000001, "4-digit symbols use one point per pip");
   test.AssertNearDouble(12.5, CTradeMathCore::PointsToPips(12.5, 2), 0.000001, "2-digit symbols keep point and pip units equal");
  }

//+------------------------------------------------------------------+
//| Test stop-distance validation                                    |
//+------------------------------------------------------------------+
void TestStopDistance(CTestLite &test)
  {
   test.BeginTest("stop distance validation");
   //--- Test both sides of the minimum so an off-by-one style change is visible.
   test.AssertTrue(CTradeMathCore::IsStopDistanceValid(1.10000, 1.09800, 150, 0.00001), "200 points is valid when minimum is 150");
   test.AssertFalse(CTradeMathCore::IsStopDistanceValid(1.10000, 1.09900, 150, 0.00001), "100 points is too close");
   //--- A two-digit quote checks that the formula is not tied to forex-style decimals.
   test.AssertTrue(CTradeMathCore::IsStopDistanceValid(1850.00, 1849.50, 50, 0.01), "gold-style two-digit quote can satisfy the minimum");
   test.AssertFalse(CTradeMathCore::IsStopDistanceValid(1.10000, 1.09900, 10, 0.0), "zero point size is invalid input");
  }

//+------------------------------------------------------------------+
//| Test volume normalization                                        |
//+------------------------------------------------------------------+
void TestVolumeNormalization(CTestLite &test)
  {
   test.BeginTest("volume normalization");
   //--- These cases protect the three main rules: clamp low, align to step, clamp high.
   test.AssertNearDouble(0.01, CTradeMathCore::NormalizeVolume(0.001, 0.01, 5.0, 0.01), 0.0000001, "volume below minimum clamps up");
   test.AssertNearDouble(0.02, CTradeMathCore::NormalizeVolume(0.029, 0.01, 5.0, 0.01), 0.0000001, "volume rounds down to the step");
   test.AssertNearDouble(5.00, CTradeMathCore::NormalizeVolume(8.0, 0.01, 5.0, 0.01), 0.0000001, "volume above maximum clamps down");
   //--- Step alignment starts from the broker minimum, not from zero.
   test.AssertNearDouble(0.30, CTradeMathCore::NormalizeVolume(0.37, 0.10, 2.0, 0.10), 0.0000001, "step alignment starts at the minimum");
   test.AssertNearDouble(0.0, CTradeMathCore::NormalizeVolume(1.0, 0.0, 2.0, 0.1), 0.0000001, "invalid symbol settings return zero");
  }

//+------------------------------------------------------------------+
//| Test crossover classification                                    |
//+------------------------------------------------------------------+
void TestCrossovers(CTestLite &test)
  {
   test.BeginTest("moving average crossover classification");
   //--- The classifier receives older values first and newer values second.
   test.AssertEqualsInt(TM_SIGNAL_BUY, CTradeMathCore::ClassifyMaCross(1.0, 1.1, 1.2, 1.1), "fast line moved from below to above");
   test.AssertEqualsInt(TM_SIGNAL_SELL, CTradeMathCore::ClassifyMaCross(1.2, 1.1, 1.0, 1.1), "fast line moved from above to below");
   test.AssertEqualsInt(TM_SIGNAL_NONE, CTradeMathCore::ClassifyMaCross(1.2, 1.1, 1.3, 1.2), "both bars stayed above");
   //--- Not enough bars is different from a valid no-signal result.
   test.AssertEqualsInt(TM_SIGNAL_INVALID, CTradeMathCore::ClassifyMaCross(1.0, 1.0, 1.0, 1.0, 1), "insufficient bars are reported as invalid");
   test.AssertStringEquals("BUY", CTradeMathCore::SignalToString(TM_SIGNAL_BUY), "signal string conversion stays stable");
   test.AssertStringEquals("INVALID", CTradeMathCore::SignalToString(TM_SIGNAL_INVALID), "invalid signal has a clear label");
  }

//+------------------------------------------------------------------+
//| Test floating-point tolerance                                    |
//+------------------------------------------------------------------+
void TestFloatingTolerance(CTestLite &test)
  {
   test.BeginTest("floating point tolerance checks");
   //--- 0.1 + 0.2 is a familiar example where binary floating-point can surprise equality checks.
   double value = 0.1 + 0.2;
   test.AssertNearDouble(0.3, value, 0.0000001, "decimal addition should be compared with tolerance");
   test.AssertFalse(MathAbs(0.3 - value) > 0.0000001, "the same tolerance can be checked with AssertFalse");
  }

//+------------------------------------------------------------------+
//| Script start function                                            |
//+------------------------------------------------------------------+
void OnStart()
  {
   CTestLite test("TradeMathCore deterministic tests");

   //--- Each group protects one small trading rule that should not depend on market state.
   TestPipPointConversion(test);
   TestStopDistance(test);
   TestVolumeNormalization(test);
   TestCrossovers(test);
   TestFloatingTolerance(test);

   string report_name = "TradeMathCore_TestReport.txt";
   //--- The report is written to MQL5\Files so it is easy to open after the script runs.
   bool report_written = test.WriteReport(report_name, false);

   if(report_written)
      PrintFormat("UnitTestRunner: %s. Report saved: %s", test.Summary(), report_name);
   else
      PrintFormat("UnitTestRunner: %s. Report could not be written: %s", test.Summary(), report_name);
  }
