"""Tests for TransformRegistry and built-in transforms."""

from __future__ import annotations

import datetime

import pytest

from therismos.expr.transforms import DEFAULT_TRANSFORM_REGISTRY, TransformRegistry


class TestTransformRegistry:
    def test_register_and_apply(self) -> None:
        reg = TransformRegistry()
        reg.register("double", lambda x: x * 2)
        assert reg.apply("double", 5) == 10

    def test_register_decorator(self) -> None:
        reg = TransformRegistry()

        @reg.register_decorator("triple")
        def triple(x):
            return x * 3

        assert reg.apply("triple", 4) == 12

    def test_names_returns_registered(self) -> None:
        reg = TransformRegistry()
        reg.register("foo", lambda x: x)
        reg.register("bar", lambda x: x)
        assert {"foo", "bar"}.issubset(reg.names())

    def test_unknown_transform_raises(self) -> None:
        reg = TransformRegistry()
        with pytest.raises(KeyError, match="not_a_transform"):
            reg.apply("not_a_transform", 1)

    def test_overwrite_registration(self) -> None:
        reg = TransformRegistry()
        reg.register("f", lambda x: x)
        reg.register("f", lambda x: x * 2)
        assert reg.apply("f", 5) == 10


# ---------------------------------------------------------------------------
# Built-in date/time extraction
# ---------------------------------------------------------------------------


class TestDateTimeExtraction:
    def setup_method(self) -> None:
        self.dt = datetime.datetime(2026, 3, 18, 16, 30, 0)
        self.d = datetime.date(2026, 3, 18)
        self.r = DEFAULT_TRANSFORM_REGISTRY

    def test_extract_date_from_datetime(self) -> None:
        assert self.r.apply("extract_date", self.dt) == self.d

    def test_extract_date_from_date(self) -> None:
        assert self.r.apply("extract_date", self.d) == self.d

    def test_extract_time(self) -> None:
        result = self.r.apply("extract_time", self.dt)
        assert result == datetime.time(16, 30, 0)

    def test_extract_year(self) -> None:
        assert self.r.apply("extract_year", self.dt) == 2026

    def test_extract_month(self) -> None:
        assert self.r.apply("extract_month", self.dt) == 3

    def test_extract_day(self) -> None:
        assert self.r.apply("extract_day", self.dt) == 18

    def test_extract_date_wrong_type(self) -> None:
        with pytest.raises(TypeError):
            self.r.apply("extract_date", "not-a-date")


# ---------------------------------------------------------------------------
# Built-in date/time rounding
# ---------------------------------------------------------------------------


class TestDateTimeRounding:
    def setup_method(self) -> None:
        self.dt = datetime.datetime(2026, 3, 18, 16, 30, 0)
        self.d = datetime.date(2026, 3, 18)  # Wednesday
        self.r = DEFAULT_TRANSFORM_REGISTRY

    def test_start_of_day(self) -> None:
        result = self.r.apply("start_of_day", self.dt)
        assert result == datetime.datetime(2026, 3, 18, 0, 0, 0)

    def test_end_of_day(self) -> None:
        result = self.r.apply("end_of_day", self.dt)
        assert result == datetime.datetime(2026, 3, 18, 23, 59, 59, 999999)

    def test_start_of_week_date(self) -> None:
        # Wednesday → Monday
        result = self.r.apply("start_of_week", self.d)
        assert result == datetime.date(2026, 3, 16)

    def test_end_of_week_date(self) -> None:
        # Wednesday → Sunday
        result = self.r.apply("end_of_week", self.d)
        assert result == datetime.date(2026, 3, 22)

    def test_start_of_month_date(self) -> None:
        result = self.r.apply("start_of_month", self.d)
        assert result == datetime.date(2026, 3, 1)

    def test_end_of_month_date(self) -> None:
        result = self.r.apply("end_of_month", self.d)
        assert result == datetime.date(2026, 3, 31)

    def test_start_of_month_datetime(self) -> None:
        result = self.r.apply("start_of_month", self.dt)
        assert result == datetime.datetime(2026, 3, 1, 0, 0, 0)


# ---------------------------------------------------------------------------
# add_time / sub_time
# ---------------------------------------------------------------------------


class TestTimeArithmetic:
    def setup_method(self) -> None:
        self.r = DEFAULT_TRANSFORM_REGISTRY

    def test_sub_time_7d(self) -> None:
        d = datetime.date(2026, 3, 18)
        result = self.r.apply("sub_time", d, datetime.timedelta(days=7))
        assert result == datetime.date(2026, 3, 11)

    def test_add_time_1h(self) -> None:
        dt = datetime.datetime(2026, 3, 18, 10, 0)
        result = self.r.apply("add_time", dt, datetime.timedelta(hours=1))
        assert result == datetime.datetime(2026, 3, 18, 11, 0)

    def test_sub_time_30m(self) -> None:
        dt = datetime.datetime(2026, 3, 18, 10, 0)
        result = self.r.apply("sub_time", dt, datetime.timedelta(minutes=30))
        assert result == datetime.datetime(2026, 3, 18, 9, 30)


# ---------------------------------------------------------------------------
# Type casting
# ---------------------------------------------------------------------------


class TestTypeCasting:
    def setup_method(self) -> None:
        self.r = DEFAULT_TRANSFORM_REGISTRY

    def test_as_int(self) -> None:
        assert self.r.apply("as_int", "42") == 42
        assert self.r.apply("as_int", 3.9) == 3

    def test_as_float(self) -> None:
        assert self.r.apply("as_float", "3.14") == pytest.approx(3.14)

    def test_as_str(self) -> None:
        assert self.r.apply("as_str", 42) == "42"

    def test_as_date_from_datetime(self) -> None:
        dt = datetime.datetime(2026, 3, 18, 10, 0)
        assert self.r.apply("as_date", dt) == datetime.date(2026, 3, 18)

    def test_as_date_from_string(self) -> None:
        assert self.r.apply("as_date", "2026-03-18") == datetime.date(2026, 3, 18)

    def test_as_datetime_from_date(self) -> None:
        d = datetime.date(2026, 3, 18)
        result = self.r.apply("as_datetime", d)
        assert result == datetime.datetime(2026, 3, 18, 0, 0, 0)

    def test_as_datetime_from_string(self) -> None:
        result = self.r.apply("as_datetime", "2026-03-18T10:00:00")
        assert result == datetime.datetime(2026, 3, 18, 10, 0, 0)


# ---------------------------------------------------------------------------
# String transforms
# ---------------------------------------------------------------------------


class TestStringTransforms:
    def setup_method(self) -> None:
        self.r = DEFAULT_TRANSFORM_REGISTRY

    def test_lower(self) -> None:
        assert self.r.apply("lower", "HELLO") == "hello"

    def test_upper(self) -> None:
        assert self.r.apply("upper", "hello") == "HELLO"

    def test_strip(self) -> None:
        assert self.r.apply("strip", "  hello  ") == "hello"


# ---------------------------------------------------------------------------
# Math transforms
# ---------------------------------------------------------------------------


class TestMathTransforms:
    def setup_method(self) -> None:
        self.r = DEFAULT_TRANSFORM_REGISTRY

    def test_add(self) -> None:
        assert self.r.apply("add", 10, 5) == 15

    def test_sub(self) -> None:
        assert self.r.apply("sub", 10, 3) == 7

    def test_mul(self) -> None:
        assert self.r.apply("mul", 4, 3) == 12

    def test_div(self) -> None:
        assert self.r.apply("div", 10, 4) == 2.5

    def test_abs(self) -> None:
        assert self.r.apply("abs", -5) == 5

    def test_round(self) -> None:
        assert self.r.apply("round", 3.567, 2) == 3.57


# ---------------------------------------------------------------------------
# RuleSerializer — duration parsing
# ---------------------------------------------------------------------------


class TestRuleSerializerDuration:
    def setup_method(self) -> None:
        from therismos.expr.template import _format_duration, _parse_duration

        self._parse = _parse_duration
        self._format = _format_duration

    def test_parse_days(self) -> None:
        assert self._parse("7d") == datetime.timedelta(days=7)

    def test_parse_hours(self) -> None:
        assert self._parse("1h") == datetime.timedelta(hours=1)

    def test_parse_minutes(self) -> None:
        assert self._parse("30m") == datetime.timedelta(minutes=30)

    def test_parse_seconds(self) -> None:
        assert self._parse("90s") == datetime.timedelta(seconds=90)

    def test_parse_milliseconds(self) -> None:
        assert self._parse("500ms") == datetime.timedelta(milliseconds=500)

    def test_parse_invalid(self) -> None:
        with pytest.raises(ValueError):
            self._parse("5x")

    def test_format_days(self) -> None:
        assert self._format(datetime.timedelta(days=7)) == "7d"

    def test_format_hours(self) -> None:
        assert self._format(datetime.timedelta(hours=2)) == "2h"

    def test_format_minutes(self) -> None:
        assert self._format(datetime.timedelta(minutes=30)) == "30m"


# ---------------------------------------------------------------------------
# RuleSerializer — parse / serialize round-trip
# ---------------------------------------------------------------------------


class TestRuleSerializerRoundTrip:
    def setup_method(self) -> None:
        from therismos.expr.template import RuleSerializer

        self.ser = RuleSerializer()

    def test_source_only(self) -> None:
        rule = self.ser.deserialize("$now")
        assert rule.source == "now"
        assert rule.steps == ()
        assert self.ser.serialize(rule) == "$now"

    def test_one_step(self) -> None:
        rule = self.ser.deserialize("$now | extract_date")
        assert rule.source == "now"
        assert len(rule.steps) == 1
        assert rule.steps[0].name == "extract_date"
        assert rule.steps[0].args == ()

    def test_multi_step(self) -> None:
        rule = self.ser.deserialize("$now | extract_date | sub_time(7d)")
        assert rule.source == "now"
        assert len(rule.steps) == 2
        assert rule.steps[1].name == "sub_time"
        assert rule.steps[1].args == (datetime.timedelta(days=7),)

    def test_step_with_int_arg(self) -> None:
        rule = self.ser.deserialize("$x | add(5)")
        assert rule.steps[0].args == (5,)

    def test_step_with_float_arg(self) -> None:
        rule = self.ser.deserialize("$x | round(2)")
        assert rule.steps[0].args == (2,)

    def test_serialize_round_trip(self) -> None:
        s = "$now | extract_date | sub_time(7d)"
        rule = self.ser.deserialize(s)
        serialized = self.ser.serialize(rule)
        assert serialized == s

    def test_missing_dollar_raises(self) -> None:
        with pytest.raises(ValueError, match="\\$"):
            self.ser.deserialize("now | extract_date")

    def test_custom_registry_transform(self) -> None:
        from therismos.expr.transforms import TransformRegistry

        reg = TransformRegistry()
        reg.register("double", lambda x: x * 2)
        result = reg.apply("double", 5)
        assert result == 10
