"""Assertions independent of engine-reported summary arithmetic."""
from decimal import Decimal
import importlib.metadata
import unittest

from experiment import CASES, fixture, run_case


def semantic(value):
    """Exclude randomly allocated IDs, not prices, timing, status or metrics."""
    if isinstance(value, dict):
        return {k: semantic(v) for k, v in value.items()
                if k not in ("id", "order_id", "receipt")}
    if isinstance(value, list):
        return [semantic(v) for v in value]
    return value


class ConfigurationExperiment(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.data = fixture()
        cls.cases = {n: run_case(n, o, cls.data) for n, o in CASES.items()}

    def test_pinned_version(self):
        self.assertEqual(importlib.metadata.version("akquant"), "0.3.75")

    def test_synthetic_fixture(self):
        self.assertEqual(len(self.data), 8)
        self.assertTrue(self.data.date.is_monotonic_increasing)
        self.assertEqual(self.data.date.nunique(), 8)
        self.assertEqual(self.data.symbol.unique().tolist(), ["SYNTH"])
        self.assertTrue((self.data.high >= self.data.close).all())
        self.assertTrue((self.data.low <= self.data.open).all())

    def test_fill_prices_times_and_quantities(self):
        for name, case in self.cases.items():
            t1 = CASES[name].get("t_plus_one", False)
            expected_bars = [1, 5] if t1 else [1, 3]
            self.assertEqual([e["bar_index"] for e in case["executions"]], expected_bars)
            self.assertEqual([e["side"] for e in case["executions"]], ["buy", "sell"])
            for execution, i in zip(case["executions"], expected_bars):
                self.assertEqual(execution["quantity"], 100)
                self.assertEqual(execution["price"], self.data.iloc[i].open)
                self.assertEqual(execution["timestamp_iso"],
                                 self.data.iloc[i].date.tz_convert("UTC").strftime("%Y-%m-%dT%H:%M:%SZ"))
                order = next(o for o in case["orders"] if o["id"] == execution["order_id"])
                self.assertEqual(order["created_at_iso"],
                                 self.data.iloc[i-1].date.tz_convert("UTC").strftime("%Y-%m-%dT%H:%M:%SZ"))

    def test_t1_rejection_and_explicit_resubmission(self):
        for name, case in self.cases.items():
            if CASES[name].get("t_plus_one", False):
                self.assertEqual([o["status"] for o in case["orders"]], ["filled", "rejected", "filled"])
                rejected = case["orders"][1]
                self.assertEqual(rejected["reject_reason"],
                    "Risk: Insufficient available position for SYNTH at execution. Available: 0, Required: 100")
                self.assertEqual(rejected["filled_quantity"], 0)
                self.assertEqual(rejected["commission"], 0)
                self.assertEqual(case["observations"][4]["action"], "sell_100")
                self.assertEqual(case["observations"][4]["position_before"], 100)
                self.assertNotEqual(rejected["id"], case["orders"][2]["id"])
            else:
                self.assertEqual([o["status"] for o in case["orders"]], ["filled", "filled"])
                self.assertEqual(case["observations"][4]["action"], "none")

    def test_independent_decimal_cash_reconciliation(self):
        expected = {"A_default": "10100", "B_fees_only": "10097.9",
                    "C_t1_only": "9950", "D_fees_t1": "9948.05"}
        for name, case in self.cases.items():
            cash, fees = Decimal("10000"), Decimal("0")
            rate = Decimal(str(CASES[name].get("commission_rate", 0)))
            for execution in case["executions"]:
                notional = Decimal(str(execution["price"])) * Decimal(str(execution["quantity"]))
                fee = notional * rate
                self.assertEqual(Decimal(str(execution["commission"])), fee)
                fees += fee
                cash += notional * (-1 if execution["side"] == "buy" else 1) - fee
            metrics = {m["name"]: m["value"] for m in case["metrics"]}
            self.assertEqual(cash, Decimal(expected[name]))
            self.assertEqual(Decimal(str(metrics["end_market_value"])), cash)
            self.assertEqual(Decimal(str(metrics["total_commission"])), fees)
            self.assertEqual(Decimal(str(metrics["total_pnl"])), cash - 10000)

    def test_no_open_position_at_end(self):
        for case in self.cases.values():
            self.assertEqual(case["observations"][-1]["position_before"], 0)
            self.assertEqual(next(m["value"] for m in case["metrics"] if m["name"] == "open_position_count"), 0)

    def test_repeatability_ignoring_only_random_ids(self):
        for name, options in CASES.items():
            self.assertEqual(semantic(self.cases[name]), semantic(run_case(name, options, self.data)))


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