"""Pure-Python tests for the Expr DSL serialization.

These tests verify that the Python DSL produces JSON matching the Rust
bt_expr::Expr serde (externally-tagged) format.  No compiled Rust
extension needed.
"""
from manifoldbt.expr import Expr, col, lit, param, when


def test_column_serializes():
    assert col("close").to_json() == {"Column": "close"}


def test_literal_float():
    assert lit(1.0).to_json() == {"Literal": {"Float64": 1.0}}


def test_literal_int():
    assert lit(42).to_json() == {"Literal": {"Int64": 42}}


def test_literal_bool():
    assert lit(True).to_json() == {"Literal": {"Bool": True}}


def test_literal_null():
    assert lit(None).to_json() == {"Literal": "Null"}


def test_parameter():
    assert param("size", default=1.0).to_json() == {"Parameter": "size"}


def test_add():
    expr = col("close") + lit(1.0)
    assert expr.to_json() == {
        "Add": [{"Column": "close"}, {"Literal": {"Float64": 1.0}}]
    }


def test_sub():
    expr = col("close") - col("open")
    assert expr.to_json() == {
        "Sub": [{"Column": "close"}, {"Column": "open"}]
    }


def test_mul_with_raw_float():
    expr = col("signal") * 0.5
    assert expr.to_json() == {
        "Mul": [{"Column": "signal"}, {"Literal": {"Float64": 0.5}}]
    }


def test_rmul():
    expr = 2.0 * col("signal")
    assert expr.to_json() == {
        "Mul": [{"Literal": {"Float64": 2.0}}, {"Column": "signal"}]
    }


def test_neg():
    expr = -col("x")
    assert expr.to_json() == {
        "Mul": [{"Literal": {"Float64": -1.0}}, {"Column": "x"}]
    }


def test_div():
    expr = col("a") / col("b")
    assert expr.to_json() == {
        "Div": [{"Column": "a"}, {"Column": "b"}]
    }


def test_gt():
    expr = col("close") > lit(100.0)
    assert expr.to_json() == {
        "Gt": [{"Column": "close"}, {"Literal": {"Float64": 100.0}}]
    }


def test_lt():
    expr = col("close") < 50.0
    assert expr.to_json() == {
        "Lt": [{"Column": "close"}, {"Literal": {"Float64": 50.0}}]
    }


def test_eq():
    expr = col("side") == lit(1)
    assert expr.to_json() == {
        "Eq": [{"Column": "side"}, {"Literal": {"Int64": 1}}]
    }


def test_and_or():
    a = col("x") > lit(0.0)
    b = col("y") < lit(1.0)
    expr = a & b
    assert expr.to_json()["And"][0] == {"Gt": [{"Column": "x"}, {"Literal": {"Float64": 0.0}}]}
    expr2 = a | b
    assert "Or" in expr2.to_json()


def test_not():
    expr = ~(col("flag") == lit(True))
    assert expr.to_json()["Not"]["Eq"][0] == {"Column": "flag"}


def test_rolling_mean():
    expr = col("close").rolling_mean(20)
    assert expr.to_json() == {"RollingMean": [{"Column": "close"}, 20]}


def test_rolling_std():
    expr = col("close").rolling_std(30)
    assert expr.to_json() == {"RollingStd": [{"Column": "close"}, 30]}


def test_lag():
    expr = col("close").lag(5)
    assert expr.to_json() == {"Lag": [{"Column": "close"}, 5]}


def test_diff():
    expr = col("close").diff()
    assert expr.to_json() == {"Diff": [{"Column": "close"}, 1]}


def test_pct_change():
    expr = col("close").pct_change(3)
    assert expr.to_json() == {"PctChange": [{"Column": "close"}, 3]}


def test_ewm_mean():
    expr = col("close").ewm_mean(10.0)
    assert expr.to_json() == {"EwmMean": [{"Column": "close"}, 10.0]}


def test_zscore():
    expr = col("close").zscore(20)
    assert expr.to_json() == {"ZScore": [{"Column": "close"}, 20]}


def test_cumsum():
    expr = col("volume").cumsum()
    assert expr.to_json() == {"CumSum": {"Column": "volume"}}


def test_cumprod():
    expr = col("returns").cumprod()
    assert expr.to_json() == {"CumProd": {"Column": "returns"}}


def test_rank():
    expr = col("score").rank()
    assert expr.to_json() == {"Rank": {"Column": "score"}}


def test_if_else():
    cond = col("x") > lit(0.0)
    expr = when(cond, lit(1.0), lit(-1.0))
    expected = {
        "IfElse": [
            {"Gt": [{"Column": "x"}, {"Literal": {"Float64": 0.0}}]},
            {"Literal": {"Float64": 1.0}},
            {"Literal": {"Float64": -1.0}},
        ]
    }
    assert expr.to_json() == expected


def test_complex_sma_cross():
    """SMA crossover — the canonical DSL example."""
    close = col("close")
    sma_fast = close.rolling_mean(20)
    sma_slow = close.rolling_mean(60)
    signal = when(sma_fast > sma_slow, lit(1.0), lit(-1.0))

    result = signal.to_json()
    assert result["IfElse"][0]["Gt"][0] == {"RollingMean": [{"Column": "close"}, 20]}
    assert result["IfElse"][0]["Gt"][1] == {"RollingMean": [{"Column": "close"}, 60]}
    assert result["IfElse"][1] == {"Literal": {"Float64": 1.0}}
    assert result["IfElse"][2] == {"Literal": {"Float64": -1.0}}


def test_param_with_meta():
    p = param("size", default=1.0, range=(0.5, 2.0), description="position size")
    assert p.to_json() == {"Parameter": "size"}
    meta = p._param_meta
    assert meta["name"] == "size"
    assert meta["default"] == 1.0
    assert meta["range"] == (0.5, 2.0)
    assert meta["description"] == "position size"


# -- Datetime extraction tests -----------------------------------------------


def test_hour():
    expr = col("timestamp").hour()
    assert expr.to_json() == {"Hour": {"Column": "timestamp"}}


def test_minute():
    expr = col("timestamp").minute()
    assert expr.to_json() == {"Minute": {"Column": "timestamp"}}


def test_day_of_week():
    expr = col("timestamp").day_of_week()
    assert expr.to_json() == {"DayOfWeek": {"Column": "timestamp"}}


def test_month():
    expr = col("timestamp").month()
    assert expr.to_json() == {"Month": {"Column": "timestamp"}}


def test_day_of_month():
    expr = col("timestamp").day_of_month()
    assert expr.to_json() == {"DayOfMonth": {"Column": "timestamp"}}


def test_datetime_in_filter_expression():
    """Datetime functions compose with arithmetic and boolean ops."""
    ts = col("timestamp")
    # US market hours filter: hour >= 14 AND hour < 21
    in_us = (ts.hour() > lit(13.5)) & (ts.hour() < lit(21.0))
    result = in_us.to_json()
    assert "And" in result
    assert "Gt" in result["And"][0]
    assert result["And"][0]["Gt"][0] == {"Hour": {"Column": "timestamp"}}


def test_datetime_indicators_module():
    """indicators.hour() etc. default to timestamp column."""
    from manifoldbt.indicators import hour, day_of_week, month

    assert hour().to_json() == {"Hour": {"Column": "timestamp"}}
    assert day_of_week().to_json() == {"DayOfWeek": {"Column": "timestamp"}}
    assert month().to_json() == {"Month": {"Column": "timestamp"}}


# -- Scan (stateful fold) tests ----------------------------------------------


def test_scan_prev_json():
    from manifoldbt.expr import s

    assert s.prev("x").to_json() == {"ScanPrev": "x"}


def test_scan_var_json():
    from manifoldbt.expr import s

    assert s.var("k").to_json() == {"ScanVar": "k"}


def test_scan_cumsum_json():
    from manifoldbt.expr import s, scan

    cumsum = scan(
        state={"total": lit(0.0)},
        update={"total": s.prev("total") + col("value")},
        output="total",
    )
    result = cumsum.to_json()
    assert "Scan" in result
    data = result["Scan"]
    assert data["state_names"] == ["total"]
    assert data["update_names"] == ["total"]
    assert data["output"] == "total"
    # init_exprs should be [Literal(Float64(0.0))]
    assert data["init_exprs"] == [{"Literal": {"Float64": 0.0}}]
    # update_exprs should be [Add(ScanPrev("total"), Column("value"))]
    assert data["update_exprs"] == [
        {"Add": [{"ScanPrev": "total"}, {"Column": "value"}]}
    ]


def test_scan_kalman_json():
    from manifoldbt.expr import s, scan

    kalman = scan(
        state={"x": col("close"), "p": lit(1.0)},
        update={
            "p_pred": s.prev("p") + param("q"),
            "k": s.var("p_pred") / (s.var("p_pred") + param("r")),
            "x": s.prev("x") + s.var("k") * (col("close") - s.prev("x")),
            "p": (lit(1.0) - s.var("k")) * s.var("p_pred"),
        },
        output="x",
    )
    result = kalman.to_json()
    assert "Scan" in result
    data = result["Scan"]
    assert data["state_names"] == ["x", "p"]
    assert data["update_names"] == ["p_pred", "k", "x", "p"]
    assert data["output"] == "x"
    assert len(data["init_exprs"]) == 2
    assert len(data["update_exprs"]) == 4


def test_kalman_indicator():
    from manifoldbt.indicators import kalman

    result = kalman().to_json()
    assert "Scan" in result
    data = result["Scan"]
    assert data["state_names"] == ["x", "p"]
    assert data["output"] == "x"


def test_garch_indicator():
    from manifoldbt.indicators import garch

    result = garch().to_json()
    assert "Scan" in result
    data = result["Scan"]
    assert "sigma2" in data["state_names"]
    assert data["output"] == "sigma"


def test_scan_exports():
    """Verify scan and s are accessible from the top-level package."""
    import manifoldbt as bt

    assert hasattr(bt, "scan")
    assert hasattr(bt, "s")
    assert bt.s.prev("x").to_json() == {"ScanPrev": "x"}
