"""Tests that all visitors raise UnboundTemplateParamError for unbound TemplateParam."""

from __future__ import annotations

import pytest

from therismos import F
from therismos.expr._expr import TemplateParam, UnboundTemplateParamError


def _make_eq_param():
    return F("x") == TemplateParam("val")


def _make_ge_param():
    return F("x") >= TemplateParam("val")


def _make_between_param():
    return F("score").between(TemplateParam("lo"), TemplateParam("hi"))


def _make_in_param():
    return F("x").is_in(TemplateParam("a"), "literal")


# ---------------------------------------------------------------------------
# StringVisitor
# ---------------------------------------------------------------------------


class TestStringVisitorGuard:
    def setup_method(self) -> None:
        from therismos.expr.visitors._visitors import StringVisitor

        self.v = StringVisitor()

    def test_eq_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_eq_param().accept(self.v)

    def test_ge_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_ge_param().accept(self.v)

    def test_between_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_between_param().accept(self.v)

    def test_in_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_in_param().accept(self.v)


# ---------------------------------------------------------------------------
# MongoVisitor
# ---------------------------------------------------------------------------


class TestMongoVisitorGuard:
    def setup_method(self) -> None:
        from therismos.expr.visitors.mongo import MongoVisitor

        self.v = MongoVisitor()

    def test_eq_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_eq_param().accept(self.v)

    def test_ge_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_ge_param().accept(self.v)

    def test_between_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_between_param().accept(self.v)

    def test_in_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_in_param().accept(self.v)


# ---------------------------------------------------------------------------
# PandasExprVisitor
# ---------------------------------------------------------------------------


class TestPandasVisitorGuard:
    def setup_method(self) -> None:
        pytest.importorskip("pandas")
        from therismos.expr.visitors.pandas import PandasExprVisitor

        self.v = PandasExprVisitor()

    def test_eq_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_eq_param().accept(self.v)

    def test_ge_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_ge_param().accept(self.v)

    def test_between_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_between_param().accept(self.v)

    def test_in_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_in_param().accept(self.v)


# ---------------------------------------------------------------------------
# PolarsExprVisitor
# ---------------------------------------------------------------------------


class TestPolarsVisitorGuard:
    def setup_method(self) -> None:
        pytest.importorskip("polars")
        from therismos.expr.visitors.polars import PolarsExprVisitor

        self.v = PolarsExprVisitor()

    def test_eq_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_eq_param().accept(self.v)

    def test_ge_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_ge_param().accept(self.v)

    def test_between_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_between_param().accept(self.v)

    def test_in_raises(self) -> None:
        with pytest.raises(UnboundTemplateParamError):
            _make_in_param().accept(self.v)


# ---------------------------------------------------------------------------
# Serializer (_SerializerVisitor) — should NOT raise (serializes $param syntax)
# ---------------------------------------------------------------------------


class TestSerializerAllowsTemplateParam:
    """The serializer should output $param{type} rather than raising."""

    def test_eq_serializes(self) -> None:
        from therismos import Serializer

        ser = Serializer()
        s = ser.serialize(_make_eq_param())
        assert "$val" in s

    def test_ge_serializes(self) -> None:
        from therismos import Serializer

        ser = Serializer()
        s = ser.serialize(_make_ge_param())
        assert "$val" in s


# ---------------------------------------------------------------------------
# Optimizer guard
# ---------------------------------------------------------------------------


class TestOptimizerGuard:
    def test_optimize_raises_on_unbound_param(self) -> None:
        from therismos.expr.optimizer import optimize

        with pytest.raises(UnboundTemplateParamError):
            optimize(_make_eq_param())

    def test_optimize_succeeds_after_bind(self) -> None:
        from therismos.expr.optimizer import optimize
        from therismos.expr.template import bind

        expr = _make_eq_param()
        bound = bind(expr, {"val": 42})
        result, _ = optimize(bound)
        assert result is not None
