"""Integration tests for ExprTemplate — full bind flow and JSON round-trip."""

from __future__ import annotations

import datetime
import json

import pytest

from therismos import F
from therismos.expr._expr import TemplateParam
from therismos.expr.template import (
    ExprTemplate,
    MissingParamError,
    RuleSerializer,
    TemplateParamSpec,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def date_range_template() -> ExprTemplate:
    """'last 7 days' template: created{date} >= $start{date}; created{date} <= $end{date}"""
    field = F("created", datetime.date)
    p_start = TemplateParam("start", datetime.date)
    p_end = TemplateParam("end", datetime.date)
    expr = (field >= p_start) & (field <= p_end)

    rule_ser = RuleSerializer()
    return ExprTemplate(
        expr=expr,
        params={
            "start": TemplateParamSpec(description="Range start (inclusive)"),
            "end": TemplateParamSpec(description="Range end (inclusive)"),
        },
        rules={
            "start": rule_ser.deserialize("$now | extract_date | sub_time(7d)"),
            "end": rule_ser.deserialize("$now | extract_date"),
        },
    )


# ---------------------------------------------------------------------------
# ExprTemplate.bind — integration flow
# ---------------------------------------------------------------------------


class TestExprTemplateBind:
    def test_bind_produces_concrete_expr(self) -> None:
        tmpl = date_range_template()
        now = datetime.datetime(2026, 3, 18, 16, 30)
        bound = tmpl.bind({"now": now})
        from therismos.expr.template import collect_params

        assert collect_params(bound) == {}

    def test_bind_produces_correct_dates(self) -> None:
        tmpl = date_range_template()
        now = datetime.datetime(2026, 3, 18, 16, 30)
        bound = tmpl.bind({"now": now})

        # AllExpr(Ge, Le) — extract children
        from therismos import Ge, Le

        children = bound.exprs
        ge_expr = next(e for e in children if isinstance(e, Ge))
        le_expr = next(e for e in children if isinstance(e, Le))

        assert ge_expr.value == datetime.date(2026, 3, 11)
        assert le_expr.value == datetime.date(2026, 3, 18)

    def test_bind_override_takes_priority(self) -> None:
        tmpl = date_range_template()
        now = datetime.datetime(2026, 3, 18, 16, 30)
        explicit_start = datetime.date(2026, 1, 1)
        bound = tmpl.bind({"now": now}, start=explicit_start)

        from therismos import Ge

        children = bound.exprs
        ge_expr = next(e for e in children if isinstance(e, Ge))
        assert ge_expr.value == explicit_start

    def test_bind_without_rules_requires_overrides(self) -> None:
        field = F("score")
        p = TemplateParam("threshold")
        expr = field >= p
        tmpl = ExprTemplate(expr=expr)
        bound = tmpl.bind(threshold=90)
        assert bound.value == 90

    def test_bind_without_rules_missing_raises(self) -> None:
        field = F("score")
        p = TemplateParam("threshold")
        expr = field >= p
        tmpl = ExprTemplate(expr=expr)
        with pytest.raises(MissingParamError):
            tmpl.bind()

    def test_bind_with_default_fallback(self) -> None:
        field = F("score")
        p = TemplateParam("threshold")
        expr = field >= p
        tmpl = ExprTemplate(
            expr=expr,
            params={"threshold": TemplateParamSpec(default=50)},
        )
        bound = tmpl.bind()  # no context, no overrides — falls back to default
        assert bound.value == 50

    def test_collect_missing_none_missing(self) -> None:
        tmpl = date_range_template()
        missing = tmpl.collect_missing({"now": datetime.datetime(2026, 3, 18)})
        assert missing == set()

    def test_collect_missing_context_key_absent(self) -> None:
        tmpl = date_range_template()
        # Context does not have 'now' → both params are missing
        missing = tmpl.collect_missing({})
        assert missing == {"start", "end"}

    def test_collect_missing_with_defaults(self) -> None:
        field = F("x")
        p = TemplateParam("val")
        expr = field == p
        tmpl = ExprTemplate(
            expr=expr,
            params={"val": TemplateParamSpec(default=42)},
        )
        assert tmpl.collect_missing({}) == set()


# ---------------------------------------------------------------------------
# JSON round-trip
# ---------------------------------------------------------------------------


class TestExprTemplateJsonRoundTrip:
    def test_to_dict_has_required_keys(self) -> None:
        tmpl = date_range_template()
        d = tmpl.to_dict()
        assert d["version"] == "1"
        assert "expr" in d
        assert "params" in d
        assert "rules" in d

    def test_from_dict_roundtrip(self) -> None:
        tmpl = date_range_template()
        d = tmpl.to_dict()
        restored = ExprTemplate.from_dict(d)
        # Re-bind and check correctness
        now = datetime.datetime(2026, 3, 18, 16, 30)
        bound = restored.bind({"now": now})
        from therismos import Ge

        ge_expr = next(e for e in bound.exprs if isinstance(e, Ge))
        assert ge_expr.value == datetime.date(2026, 3, 11)

    def test_to_json_from_json_roundtrip(self) -> None:
        tmpl = date_range_template()
        json_str = tmpl.to_json()
        restored = ExprTemplate.from_json(json_str)
        now = datetime.datetime(2026, 3, 18, 16, 30)
        bound = restored.bind({"now": now})
        from therismos.expr.template import collect_params

        assert collect_params(bound) == {}

    def test_json_is_valid_json(self) -> None:
        tmpl = date_range_template()
        s = tmpl.to_json()
        parsed = json.loads(s)
        assert parsed["version"] == "1"

    def test_roundtrip_no_rules(self) -> None:
        field = F("score")
        p = TemplateParam("threshold")
        expr = field >= p
        tmpl = ExprTemplate(
            expr=expr,
            params={"threshold": TemplateParamSpec(description="Score cutoff")},
        )
        d = tmpl.to_dict()
        restored = ExprTemplate.from_dict(d)
        bound = restored.bind(threshold=75)
        assert bound.value == 75

    def test_roundtrip_with_description(self) -> None:
        field = F("score")
        p = TemplateParam("threshold")
        expr = field >= p
        tmpl = ExprTemplate(
            expr=expr,
            params={"threshold": TemplateParamSpec(description="Score cutoff")},
        )
        d = tmpl.to_dict()
        assert d["params"]["threshold"]["description"] == "Score cutoff"
        restored = ExprTemplate.from_dict(d)
        assert restored.params["threshold"].description == "Score cutoff"

    def test_unsupported_version_raises(self) -> None:
        d = {"version": "99", "expr": "true()", "params": {}, "rules": {}}
        with pytest.raises(ValueError, match="version"):
            ExprTemplate.from_dict(d)

    def test_from_dict_ignores_legacy_type_field(self) -> None:
        """Old JSON with params.type should be silently ignored."""
        d = {
            "version": "1",
            "expr": "score>=$threshold{int}",
            "params": {"threshold": {"type": "int", "description": "Score cutoff"}},
            "rules": {},
        }
        restored = ExprTemplate.from_dict(d)
        assert restored.params["threshold"].description == "Score cutoff"
        bound = restored.bind(threshold=75)
        assert bound.value == 75


# ---------------------------------------------------------------------------
# End-to-end example from the design spec
# ---------------------------------------------------------------------------


class TestDesignSpecExample:
    """Verify the canonical example from the design document."""

    def test_canonical_example(self) -> None:
        """
        Rules:
            $end   = $now | extract_date
            $start = $now | extract_date | sub_time(7d)

        Expression:
            created{date}>=$start{date}; created{date}<=$end{date}

        Bind with {"now": datetime(2026, 3, 18, 16, 30)}:
            → created{date}>=2026-03-11; created{date}<=2026-03-18
        """
        field = F("created", datetime.date)
        p_start = TemplateParam("start", datetime.date)
        p_end = TemplateParam("end", datetime.date)
        expr = (field >= p_start) & (field <= p_end)

        rule_ser = RuleSerializer()
        tmpl = ExprTemplate(
            expr=expr,
            rules={
                "end": rule_ser.deserialize("$now | extract_date"),
                "start": rule_ser.deserialize("$now | extract_date | sub_time(7d)"),
            },
        )

        now = datetime.datetime(2026, 3, 18, 16, 30)
        bound = tmpl.bind({"now": now})

        from therismos import Ge, Le

        ge_expr = next(e for e in bound.exprs if isinstance(e, Ge))
        le_expr = next(e for e in bound.exprs if isinstance(e, Le))

        assert ge_expr.value == datetime.date(2026, 3, 11)
        assert le_expr.value == datetime.date(2026, 3, 18)
