//+------------------------------------------------------------------+
//|                                                     TestLite.mqh |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                                                     www.mql5.com |
//+------------------------------------------------------------------+
#property strict
//+------------------------------------------------------------------+
//| Structure: STestLiteFailure                                      |
//| Stores one failed assertion row for the text report.             |
//+------------------------------------------------------------------+
struct STestLiteFailure
  {
   string test_name;  // Current test group
   string assertion;  // Assertion method name
   string expected;   // Expected value rendered as text
   string actual;     // Actual value rendered as text
   string message;    // Human-readable failure message
  };
//+------------------------------------------------------------------+
//| Class: CTestLite                                                 |
//| Minimal assertion helper for deterministic MQL5 script tests.    |
//+------------------------------------------------------------------+
class CTestLite
  {
private:
   string m_suite_name;
   string m_current_test;
   int m_total_assertions;
   int m_passed_assertions;
   int m_failed_assertions;
   STestLiteFailure m_failures[];
//+------------------------------------------------------------------+
//| Sanitize values before writing report rows                       |
//+------------------------------------------------------------------+
   string EscapeValue(const string value) const
     {
      string result = value;
      StringReplace(result, "\r", " ");
      StringReplace(result, "\n", " ");
      StringReplace(result, ",", ";");
      return result;
     }
//+------------------------------------------------------------------+
//| Store one failed assertion                                       |
//+------------------------------------------------------------------+
   void RecordFailure(const string assertion,
                                   const string expected,
                                   const string actual,
                                   const string message)
     {
      int size = ArraySize(m_failures);
      ArrayResize(m_failures, size + 1);
      m_failures[size].test_name = m_current_test;
      m_failures[size].assertion = assertion;
      m_failures[size].expected = expected;
      m_failures[size].actual = actual;
      m_failures[size].message = message;
     }
//+------------------------------------------------------------------+
//| Update assertion counters and failure details                    |
//+------------------------------------------------------------------+
   bool RecordResult(const bool condition,
                     const string assertion,
                     const string expected,
                     const string actual,
                     const string message)
     {
      m_total_assertions++;
      if(condition)
        {
         m_passed_assertions++;
         return true;
        }

      //--- Only failing assertions need row-level detail in the report.
      m_failed_assertions++;
      RecordFailure(assertion, expected, actual, message);
      return false;
     }

public:
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
   CTestLite(const string suite_name = "TestLiteSuite")
     {
      Reset(suite_name);
     }
//+------------------------------------------------------------------+
//| Reset suite counters and failure rows                            |
//+------------------------------------------------------------------+
   void Reset(const string suite_name)
     {
      m_suite_name = suite_name;
      m_current_test = "";
      m_total_assertions = 0;
      m_passed_assertions = 0;
      m_failed_assertions = 0;
      ArrayResize(m_failures, 0);
     }
//+------------------------------------------------------------------+
//| Begin a named test group                                         |
//+------------------------------------------------------------------+
   void BeginTest(const string test_name)
     {
      m_current_test = test_name;
     }
//+------------------------------------------------------------------+
//| Assert that a condition is true                                  |
//+------------------------------------------------------------------+
   bool AssertTrue(const bool actual, const string message = "")
     {
      return RecordResult(actual, "AssertTrue", "true", (actual ? "true" : "false"), message);
     }
//+------------------------------------------------------------------+
//| Assert that a condition is false                                 |
//+------------------------------------------------------------------+
   bool AssertFalse(const bool actual, const string message = "")
     {
      return RecordResult(!actual, "AssertFalse", "false", (actual ? "true" : "false"), message);
     }
//+------------------------------------------------------------------+
//| Assert integer equality                                          |
//+------------------------------------------------------------------+
   bool AssertEqualsInt(const long expected, const long actual, const string message = "")
     {
      return RecordResult(expected == actual,
                          "AssertEqualsInt",
                          IntegerToString(expected),
                          IntegerToString(actual),
                          message);
     }
//+------------------------------------------------------------------+
//| Assert double equality within tolerance                          |
//+------------------------------------------------------------------+
   bool AssertNearDouble(const double expected,
                                      const double actual,
                                      const double tolerance,
                                      const string message = "")
     {
      bool close_enough = (MathAbs(expected - actual) <= tolerance);
      return RecordResult(close_enough,
                          "AssertNearDouble",
                          DoubleToString(expected, 8) + " +/- " + DoubleToString(tolerance, 8),
                          DoubleToString(actual, 8),
                          message);
     }
//+------------------------------------------------------------------+
//| Assert string equality                                           |
//+------------------------------------------------------------------+
   bool AssertStringEquals(const string expected, const string actual, const string message = "")
     {
      return RecordResult(expected == actual,
                          "AssertStringEquals",
                          expected,
                          actual,
                          message);
     }
//+------------------------------------------------------------------+
//| Return suite name                                                |
//+------------------------------------------------------------------+
   string SuiteName(void) const
     {
      return m_suite_name;
     }
//+------------------------------------------------------------------+
//| Return total assertion count                                     |
//+------------------------------------------------------------------+
   int TotalAssertions(void) const
     {
      return m_total_assertions;
     }
//+------------------------------------------------------------------+
//| Return passed assertion count                                    |
//+------------------------------------------------------------------+
   int Passed(void) const
     {
      return m_passed_assertions;
     }
//+------------------------------------------------------------------+
//| Return failed assertion count                                    |
//+------------------------------------------------------------------+
   int Failed(void) const
     {
      return m_failed_assertions;
     }
//+------------------------------------------------------------------+
//| Return PASS or FAIL status                                       |
//+------------------------------------------------------------------+
   string Status(void) const
     {
      return (m_failed_assertions == 0 ? "PASS" : "FAIL");
     }
//+------------------------------------------------------------------+
//| Build a one-line suite summary                                   |
//+------------------------------------------------------------------+
   string Summary(void) const
     {
      return StringFormat("suite=%s, assertions=%d, passed=%d, failed=%d, status=%s",
                          m_suite_name,
                          m_total_assertions,
                          m_passed_assertions,
                          m_failed_assertions,
                          Status());
     }
//+------------------------------------------------------------------+
//| Write deterministic test report                                  |
//+------------------------------------------------------------------+
   bool WriteReport(const string file_name, const bool common_file = false)
     {
      int flags = FILE_WRITE | FILE_TXT | FILE_ANSI;
      if(common_file)
         flags |= FILE_COMMON;

      int handle = FileOpen(file_name, flags);
      if(handle == INVALID_HANDLE)
        {
         PrintFormat("CTestLite: cannot open report '%s', error=%d", file_name, GetLastError());
         return false;
        }

      //--- The first key-value lines are intentionally easy for the gate to parse.
      FileWriteString(handle, "suite=" + m_suite_name + "\r\n");
      FileWriteString(handle, "total_assertions=" + IntegerToString(m_total_assertions) + "\r\n");
      FileWriteString(handle, "passed=" + IntegerToString(m_passed_assertions) + "\r\n");
      FileWriteString(handle, "failed=" + IntegerToString(m_failed_assertions) + "\r\n");
      FileWriteString(handle, "status=" + Status() + "\r\n");
      FileWriteString(handle, "failure_count=" + IntegerToString(ArraySize(m_failures)) + "\r\n");
      FileWriteString(handle, "failures_begin\r\n");
      FileWriteString(handle, "test_name,assertion,expected,actual,message\r\n");

      for(int i = 0; i < ArraySize(m_failures); i++)
        {
         //--- Failure text is sanitized so commas and newlines do not break CSV rows.
         FileWriteString(handle,
                         EscapeValue(m_failures[i].test_name) + "," +
                         EscapeValue(m_failures[i].assertion) + "," +
                         EscapeValue(m_failures[i].expected) + "," +
                         EscapeValue(m_failures[i].actual) + "," +
                         EscapeValue(m_failures[i].message) + "\r\n");
        }

      FileWriteString(handle, "failures_end\r\n");
      FileFlush(handle);
      FileClose(handle);
      return true;
     }
  };
