import json
import sys
import time
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import pandas as pd
from sailar import config, data, indicators as ind
from sailar.backtest import run_backtest, split_backtest
from sailar.broker import SimBroker, live_allowed
from sailar.evolve import autopsy, check_failure
from sailar.gate import evaluate
from sailar.journal import Journal
from sailar.risk import Risk
from sailar.signals import analyze_mtf

CFG = config.load()
DF = data.synthetic_candles(2500)


class Core(unittest.TestCase):
    def test_indicators(self):
        r = ind.rsi(DF.close)
        self.assertTrue(((r >= 0) & (r <= 100)).all())
        d, line = ind.supertrend(DF)
        self.assertTrue(set(d.unique()) <= {1, -1})
        self.assertFalse(line.isna().any())

    def test_signal_shape(self):
        s = analyze_mtf(DF.iloc[:400], CFG["strategy"])
        self.assertIn(s.side, ("BUY", "SELL", "NEUTRAL"))
        self.assertTrue(0 <= s.confidence <= 100)   # weighted 8-factor confidence is any value 0-100
        json.dumps(s.to_dict())  # must be JSON safe for the dashboard

    def test_sim_broker_netting(self):
        b = SimBroker(10000, fee_rate=0, slippage_bps=0)
        b.set_price(100)
        b.market_order("X", "buy", 10)
        b.set_price(110)
        self.assertAlmostEqual(b.equity(), 10100)
        b.market_order("X", "sell", 10, reduce_only=True)
        self.assertAlmostEqual(b.cash, 10100)
        self.assertEqual(b.pos, 0)
        b.set_price(100)
        b.market_order("X", "sell", 5)  # short
        b.set_price(90)
        self.assertAlmostEqual(b.equity(), 10150)

    def test_risk_sizing_is_one_percent(self):
        qty, unit_loss = Risk(CFG).size(10000, 60000, 600, CFG["strategy"])
        self.assertAlmostEqual(qty * unit_loss, 100.0, places=4)  # 1% of 10k
        self.assertAlmostEqual(qty, 100 / 600, places=6)

    def test_backtest_books_balance(self):
        res, trades = run_backtest(DF, CFG["strategy"], CFG)
        self.assertGreater(res["trades"], 5)
        closed = [t for t in trades if t["status"] == "closed"]
        self.assertAlmostEqual(CFG["sim_start_balance"] + sum(t["pnl"] for t in closed), res["final_equity"], places=1)
        for t in closed:  # every loss that hit SL must be about 1R (risk is capped at 1% + fees/slippage)
            if t["exit_reason"] == "SL":
                self.assertLess(abs(t["pnl"] / t["risk_amount"]), 1.6)

    def test_split_backtest(self):
        res, _ = split_backtest(DF, CFG["strategy"], CFG)
        self.assertIn("unseen_test", res)

    def test_autopsy_always_changes_something(self):
        res, trades = run_backtest(DF, CFG["strategy"], CFG)
        notes, new = autopsy(trades, CFG["strategy"])
        self.assertNotEqual(new, CFG["strategy"])
        self.assertTrue(notes)

    def test_failure_detection(self):
        self.assertIsNotNone(check_failure(CFG, [], 12.0))
        self.assertIsNone(check_failure(CFG, [], 2.0))

    def test_gate_blocks_new_version(self):
        j = Journal(":memory:")
        v = j.new_version(CFG["strategy"], 10000)
        r = evaluate(j, v, CFG)
        self.assertFalse(r["passed"])

    def test_live_is_locked_by_default(self):
        ok, why = live_allowed(CFG, 1)
        self.assertFalse(ok)

    def test_kill_switch_blocks_entries(self):
        from sailar.risk import KILL_FILE
        KILL_FILE.write_text("x")
        try:
            ok, why = Risk(CFG).allowed(10000, 10000, pd.Timestamp.now(tz="UTC"))
            self.assertFalse(ok)
        finally:
            KILL_FILE.unlink(missing_ok=True)

    def test_daily_loss_limit(self):
        ok, why = Risk(CFG).allowed(9600, 10000, pd.Timestamp.now(tz="UTC"))
        self.assertFalse(ok)


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