import sys
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tests"))
import fake_mt5
sys.modules["MetaTrader5"] = fake_mt5

import pandas as pd
from sailar import config, data, mt5conn
from sailar.broker import Mt5Broker, make_broker
from sailar.engine import Engine
from sailar.journal import Journal
from sailar.risk import Risk

CFG = config.load(str(ROOT / "config.mt5.json"))
PARAMS = {**CFG["strategy"], "min_confidence": 20, "mtf_confirm": False, "risk_pct": 0.01}   # trade on almost any signal, for testing
DF = data.synthetic_candles(1500, start=1.10, seed=3)


def build(demo=True):
    mt5conn.reset()
    fake_mt5.reset(demo=demo)
    j = Journal(":memory:")
    v = j.new_version(PARAMS, 100000)
    return j, v


def drive(eng, j, v, start=200, stop=1400):
    """Feed candles until the engine opens a trade. Returns the index it opened at."""
    for i in range(start, stop):
        win = DF.iloc[i - 199: i + 1]
        fake_mt5.set_bid(float(win.close.iloc[-1]))
        eng.step(win, win.index[-1], price=float(win.close.iloc[-1]))
        if j.current_trade(v):
            return i
    raise AssertionError("engine never opened a trade")


class Mt5Tests(unittest.TestCase):
    def test_refuses_real_account_in_demo_mode(self):
        j, v = build(demo=False)
        with self.assertRaises(PermissionError):
            Mt5Broker(CFG, v, "demo")

    def test_live_is_locked(self):
        j, v = build()
        cfg = {**CFG, "mode": "live"}
        with self.assertRaises(PermissionError):
            make_broker(cfg, {"version": 1})

    def test_open_sets_server_side_stops_and_sizes_1_percent(self):
        j, v = build()
        b = Mt5Broker(CFG, v, "demo")
        eng = Engine(CFG, b, j, Risk(CFG), v, PARAMS)
        drive(eng, j, v)
        t = j.current_trade(v)
        pos = fake_mt5.positions_get(symbol="EURUSD")
        self.assertEqual(len(pos), 1)
        self.assertGreater(pos[0].sl, 0)          # the BROKER holds the stop loss
        self.assertGreater(pos[0].tp, 0)
        self.assertAlmostEqual(round(t["qty"], 2), t["qty"], places=8)   # multiple of the 0.01 lot step
        self.assertAlmostEqual(t["risk_amount"], 1000, delta=60)        # about 1% of 100k
        self.assertEqual(t["ticket"], str(pos[0].ticket))

    def test_broker_closes_on_stop_and_engine_records_it(self):
        j, v = build()
        b = Mt5Broker(CFG, v, "demo")
        eng = Engine(CFG, b, j, Risk(CFG), v, PARAMS)
        i = drive(eng, j, v)
        t = j.current_trade(v)
        fake_mt5.trigger_sl(int(t["ticket"]))
        eq_after = fake_mt5.account_info().equity
        win = DF.iloc[i - 198: i + 2]
        eng.step(win, win.index[-1], price=float(win.close.iloc[-1]))
        self.assertIsNone(j.current_trade(v))   # recorded as closed, no new trade on the same candle
        first = j.trades(v)[0]
        self.assertEqual(first["status"], "closed")
        self.assertEqual(first["exit_reason"], "SL")
        self.assertAlmostEqual(first["pnl"], eq_after - 100000, places=4)   # P&L comes from the broker
        self.assertAlmostEqual(first["pnl"], -1000, delta=60)

    def test_stop_failure_closes_position_for_safety(self):
        j, v = build()
        fake_mt5.S["reject_sltp"] = True
        b = Mt5Broker(CFG, v, "demo")
        eng = Engine(CFG, b, j, Risk(CFG), v, PARAMS)
        for i in range(200, 1400):
            win = DF.iloc[i - 199: i + 1]
            fake_mt5.set_bid(float(win.close.iloc[-1]))
            eng.step(win, win.index[-1], price=float(win.close.iloc[-1]))
            if j.trades(v):
                break
        first = j.trades(v)[0]
        self.assertEqual(first["exit_reason"], "PROTECT_FAIL")
        self.assertEqual(len(fake_mt5.positions_get(symbol="EURUSD")), 0)   # never left a position without a stop

    def test_rejected_order_is_a_warning_not_a_crash(self):
        j, v = build()
        fake_mt5.S["reject_deal"] = 10018   # market closed
        b = Mt5Broker(CFG, v, "demo")
        eng = Engine(CFG, b, j, Risk(CFG), v, PARAMS)
        for i in range(200, 600):
            win = DF.iloc[i - 199: i + 1]
            fake_mt5.set_bid(float(win.close.iloc[-1]))
            eng.step(win, win.index[-1], price=float(win.close.iloc[-1]))
        self.assertEqual(j.trades(v), [])
        self.assertEqual(j.count_events("CRASH"), 0)
        self.assertGreater(j.count_events("WARN"), 0)

    def test_candles_and_price(self):
        mt5conn.reset()
        fake_mt5.reset()
        self.assertAlmostEqual(data.get_price(CFG), 1.10005, places=5)
        self.assertEqual(Mt5Broker.normalize_qty(Mt5Broker(CFG, 1, "demo"), "EURUSD", 0.0049), 0.0)
        self.assertEqual(Mt5Broker.normalize_qty(Mt5Broker(CFG, 1, "demo"), "EURUSD", 1.6789), 1.67)


if __name__ == "__main__":
    unittest.main(verbosity=2)
