"""
validation_lab.py
Reproduces locally the test matrix of the Market automatic validation and
summarizes the tester journals.

  make   writes one tester .ini per scenario plus run_all.bat
  scan   reads the tester agent logs and prints the PT_SUMMARY lines and
         the error patterns that the validator reports

Examples:
  python validation_lab.py make --expert "PortableTrade\\PortableBreakoutEA" ^
         --terminal "C:\\Program Files\\MetaTrader 5\\terminal64.exe" --out lab
  python validation_lab.py scan --logs "%APPDATA%\\MetaQuotes\\Tester\\<id>\\Agent-127.0.0.1-3000\\logs"

The scenario list is the one shown in validation reports during 2026
(EURUSD H1, XAUUSD D1, GBPUSD M30, EURUSD M1, hedging). MetaQuotes may change
it at any time; edit SCENARIOS if your report shows other combinations.
"""
import argparse
import re
import sys
from pathlib import Path

SCENARIOS = [("EURUSD", "H1"), ("XAUUSD", "D1"), ("GBPUSD", "M30"), ("EURUSD", "M1")]

INI_TEMPLATE = """[Tester]
Expert={expert}
Symbol={symbol}
Period={period}
Model={model}
ExecutionMode=0
Optimization=0
FromDate={date_from}
ToDate={date_to}
ForwardMode=0
Deposit={deposit}
Currency={currency}
Leverage=1:{leverage}
Report={report}
ReplaceReport=1
ShutdownTerminal=1
"""

# what the validator turns into a failed test, as it appears in the journal
ERROR_PATTERNS = {
    "invalid_stops": re.compile(r"invalid stops", re.I),
    "invalid_volume": re.compile(r"invalid volume", re.I),
    "no_money": re.compile(r"not enough money|no money", re.I),
    "invalid_price": re.compile(r"invalid price", re.I),
    "market_closed": re.compile(r"market (is )?closed", re.I),
    "array_range": re.compile(r"array out of range", re.I),
    "zero_divide": re.compile(r"zero divide", re.I),
    "failed_request": re.compile(r"\bfailed (market|modify|instant|request|buy|sell)", re.I),
}
SUMMARY_RE = re.compile(r"PT_SUMMARY\s+(.*)$")


def cmd_make(args):
    out = Path(args.out)
    out.mkdir(parents=True, exist_ok=True)
    bat = ["@echo off", "rem Close every MetaTrader 5 window before running this file.",
           f'set TERMINAL="{args.terminal}"']
    for deposit in args.deposit:
        for symbol, period in SCENARIOS:
            name = f"val_{symbol}_{period}_{deposit}"
            ini = INI_TEMPLATE.format(expert=args.expert, symbol=symbol + args.suffix,
                                      period=period, model=args.model,
                                      date_from=args.date_from, date_to=args.date_to,
                                      deposit=deposit, currency=args.currency,
                                      leverage=args.leverage, report=f"reports\\{name}")
            (out / f"{name}.ini").write_text(ini, encoding="ascii")
            bat.append(f'echo Running {name}')
            bat.append(f'start "" /wait %TERMINAL% /config:"%~dp0{name}.ini"')
    bat.append("echo Done. Now run: python validation_lab.py scan --logs <agent logs folder>")
    (out / "run_all.bat").write_text("\r\n".join(bat) + "\r\n", encoding="ascii")
    print(f"{len(SCENARIOS) * len(args.deposit)} scenario(s) written to {out.resolve()}")
    return 0


def read_log(path: Path) -> str:
    raw = path.read_bytes()
    # terminal and agent journals are UTF-16 LE
    if raw.startswith(b"\xff\xfe") or (len(raw) > 1 and raw[1] == 0):
        return raw.decode("utf-16-le", errors="replace").lstrip("\ufeff")
    return raw.decode("utf-8", errors="replace")


def cmd_scan(args):
    logs = sorted(Path(args.logs).glob("*.log"), key=lambda p: p.stat().st_mtime)
    if not logs:
        print(f"No .log files in {args.logs}")
        return 2
    logs = logs[-args.last:]
    counts = {k: 0 for k in ERROR_PATTERNS}
    summaries = []
    for log in logs:
        for line in read_log(log).splitlines():
            m = SUMMARY_RE.search(line)
            if m:
                summaries.append(dict(kv.split("=", 1) for kv in m.group(1).split() if "=" in kv))
                continue
            for key, rx in ERROR_PATTERNS.items():
                if rx.search(line):
                    counts[key] += 1
    failed = False
    print(f"Logs read: {', '.join(p.name for p in logs)}\n")
    if summaries:
        cols = ["symbol", "period", "signals", "spread_skips", "sent", "done", "closed",
                "risk", "margin", "stops", "freeze", "check", "send"]
        print(" ".join(f"{c:>12}" for c in cols))
        for s in summaries:
            print(" ".join(f"{s.get(c, '-'):>12}" for c in cols))
            if s.get("done", "0") == "0":
                failed = True
                print(f"{'':>12} ^ no trades: the validator reports 'there are no trading operations'")
    else:
        print("No PT_SUMMARY line found (was the EA built with TradeGate?).")
    print("\nJournal patterns:")
    for key, value in counts.items():
        mark = "FAIL" if value else " ok "
        print(f"  [{mark}] {key:<16} {value}")
        failed = failed or value > 0
    return 1 if failed else 0


def main():
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = p.add_subparsers(dest="cmd", required=True)
    mk = sub.add_parser("make", help="write tester .ini files and run_all.bat")
    mk.add_argument("--expert", required=True, help=r"path relative to MQL5\Experts, without extension")
    mk.add_argument("--terminal", default=r"C:\Program Files\MetaTrader 5\terminal64.exe")
    mk.add_argument("--out", default="lab")
    mk.add_argument("--from", dest="date_from", default="2025.01.01")
    mk.add_argument("--to", dest="date_to", default="2025.12.31")
    mk.add_argument("--deposit", nargs="+", type=int, default=[10000, 100])
    mk.add_argument("--currency", default="USD")
    mk.add_argument("--leverage", type=int, default=100)
    mk.add_argument("--model", type=int, default=0, help="0 every tick, 1 OHLC M1, 4 real ticks")
    mk.add_argument("--suffix", default="", help="broker symbol suffix, for example .m")
    sc = sub.add_parser("scan", help="summarize tester agent logs")
    sc.add_argument("--logs", required=True)
    sc.add_argument("--last", type=int, default=1, help="number of most recent log files to read")
    args = p.parse_args()
    return cmd_make(args) if args.cmd == "make" else cmd_scan(args)


if __name__ == "__main__":
    sys.exit(main())
