"""Tests for the configurable alerting system (issue #121).

Covers rule parsing, state transitions, notification channels, and routing.
"""

from __future__ import annotations

import logging
import time
from unittest.mock import AsyncMock, patch

import httpx
import pytest

from llmhosts.health.alerting import (
    AlertEvaluator,
    AlertResult,
    AlertRule,
    AlertState,
    parse_condition,
)
from llmhosts.health.notifications import (
    ConsoleChannel,
    NotificationChannel,
    NotificationRouter,
    WebhookChannel,
)

# ---------------------------------------------------------------------------
# Condition parsing
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestParseCondition:
    """Tests for safe condition expression parsing."""

    def test_parse_greater_than(self) -> None:
        p = parse_condition("latency_ms > 500")
        assert p.metric == "latency_ms"
        assert p.op_str == ">"
        assert p.threshold == 500.0

    def test_parse_less_than(self) -> None:
        p = parse_condition("cache_hit_rate < 0.5")
        assert p.metric == "cache_hit_rate"
        assert p.op_str == "<"
        assert p.threshold == 0.5

    def test_parse_greater_equal(self) -> None:
        p = parse_condition("error_count >= 10")
        assert p.metric == "error_count"
        assert p.op_str == ">="
        assert p.threshold == 10.0

    def test_parse_less_equal(self) -> None:
        p = parse_condition("uptime_percent <= 99.9")
        assert p.metric == "uptime_percent"
        assert p.op_str == "<="
        assert p.threshold == 99.9

    def test_parse_equal(self) -> None:
        p = parse_condition("backends_healthy == 0")
        assert p.metric == "backends_healthy"
        assert p.op_str == "=="
        assert p.threshold == 0.0

    def test_parse_not_equal(self) -> None:
        p = parse_condition("status_code != 200")
        assert p.metric == "status_code"
        assert p.op_str == "!="
        assert p.threshold == 200.0

    def test_parse_negative_threshold(self) -> None:
        p = parse_condition("temperature > -10")
        assert p.threshold == -10.0

    def test_parse_no_spaces(self) -> None:
        p = parse_condition("metric>100")
        assert p.metric == "metric"
        assert p.op_str == ">"
        assert p.threshold == 100.0

    def test_rejects_empty_string(self) -> None:
        with pytest.raises(ValueError, match="Invalid condition"):
            parse_condition("")

    def test_rejects_missing_operator(self) -> None:
        with pytest.raises(ValueError, match="Invalid condition"):
            parse_condition("latency_ms 500")

    def test_rejects_invalid_metric_name(self) -> None:
        with pytest.raises(ValueError, match="Invalid condition"):
            parse_condition("123invalid > 10")

    def test_rejects_no_eval_injection(self) -> None:
        with pytest.raises(ValueError, match="Invalid condition"):
            parse_condition("__import__('os').system('rm -rf /') > 0")

    def test_rejects_missing_threshold(self) -> None:
        with pytest.raises(ValueError, match="Invalid condition"):
            parse_condition("metric >")


# ---------------------------------------------------------------------------
# AlertRule validation
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestAlertRule:
    """Tests for AlertRule dataclass validation."""

    def test_default_channels(self) -> None:
        rule = AlertRule(name="test", condition="x > 1")
        assert rule.channels == ["console"]

    def test_default_severity(self) -> None:
        rule = AlertRule(name="test", condition="x > 1")
        assert rule.severity == "warning"

    def test_invalid_severity_raises(self) -> None:
        with pytest.raises(ValueError, match="Invalid severity"):
            AlertRule(name="test", condition="x > 1", severity="low")


# ---------------------------------------------------------------------------
# AlertEvaluator -- basic evaluation
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestAlertEvaluatorBasic:
    """Tests for basic metric evaluation without duration."""

    def test_condition_met_fires_instantly(self) -> None:
        rules = [AlertRule(name="high-latency", condition="latency > 500")]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"latency": 600.0})
        assert len(results) == 1
        assert results[0].state == AlertState.FIRING
        assert results[0].value == 600.0

    def test_condition_not_met_stays_ok(self) -> None:
        rules = [AlertRule(name="high-latency", condition="latency > 500")]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"latency": 200.0})
        assert results[0].state == AlertState.OK

    def test_missing_metric_stays_ok(self) -> None:
        rules = [AlertRule(name="missing", condition="nonexistent > 0")]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"latency": 100.0})
        assert results[0].state == AlertState.OK
        assert results[0].value is None

    def test_less_than_condition(self) -> None:
        rules = [AlertRule(name="low-hit-rate", condition="cache_hit_rate < 0.5")]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"cache_hit_rate": 0.3})
        assert results[0].state == AlertState.FIRING

    def test_equal_condition(self) -> None:
        rules = [AlertRule(name="exact", condition="backends == 0")]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"backends": 0.0})
        assert results[0].state == AlertState.FIRING

    def test_not_equal_condition_met(self) -> None:
        rules = [AlertRule(name="not-ok", condition="status != 200")]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"status": 500.0})
        assert results[0].state == AlertState.FIRING

    def test_not_equal_condition_not_met(self) -> None:
        rules = [AlertRule(name="not-ok", condition="status != 200")]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"status": 200.0})
        assert results[0].state == AlertState.OK


# ---------------------------------------------------------------------------
# AlertEvaluator -- state transitions
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestAlertEvaluatorTransitions:
    """Tests for state machine transitions: OK -> PENDING -> FIRING -> RESOLVED."""

    def test_instant_ok_to_firing(self) -> None:
        rules = [AlertRule(name="r", condition="x > 10", duration_seconds=0)]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"x": 20.0})
        assert results[0].state == AlertState.FIRING
        assert results[0].previous_state == AlertState.OK
        assert results[0].state_changed is True

    def test_firing_to_resolved(self) -> None:
        rules = [AlertRule(name="r", condition="x > 10")]
        ev = AlertEvaluator(rules)
        ev.evaluate({"x": 20.0})  # -> FIRING
        results = ev.evaluate({"x": 5.0})  # -> RESOLVED
        assert results[0].state == AlertState.RESOLVED
        assert results[0].previous_state == AlertState.FIRING

    def test_resolved_to_ok(self) -> None:
        rules = [AlertRule(name="r", condition="x > 10")]
        ev = AlertEvaluator(rules)
        ev.evaluate({"x": 20.0})  # -> FIRING
        ev.evaluate({"x": 5.0})  # -> RESOLVED
        results = ev.evaluate({"x": 5.0})  # -> OK
        assert results[0].state == AlertState.OK
        assert results[0].previous_state == AlertState.RESOLVED

    def test_pending_with_duration(self) -> None:
        rules = [AlertRule(name="r", condition="x > 10", duration_seconds=60)]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"x": 20.0})
        # With duration > 0, first evaluation should be PENDING
        assert results[0].state == AlertState.PENDING

    def test_pending_stays_pending_before_duration(self) -> None:
        rules = [AlertRule(name="r", condition="x > 10", duration_seconds=60)]
        ev = AlertEvaluator(rules)
        ev.evaluate({"x": 20.0})  # -> PENDING
        results = ev.evaluate({"x": 20.0})  # still PENDING (not enough time)
        assert results[0].state == AlertState.PENDING

    def test_pending_to_firing_after_duration(self) -> None:
        rules = [AlertRule(name="r", condition="x > 10", duration_seconds=1)]
        ev = AlertEvaluator(rules)
        ev.evaluate({"x": 20.0})  # -> PENDING

        # Simulate time passing by manipulating the tracker's condition_met_since
        tracker = ev._trackers[0]
        tracker.condition_met_since = time.monotonic() - 2.0  # 2s ago

        results = ev.evaluate({"x": 20.0})
        assert results[0].state == AlertState.FIRING

    def test_pending_clears_to_ok_when_condition_clears(self) -> None:
        rules = [AlertRule(name="r", condition="x > 10", duration_seconds=60)]
        ev = AlertEvaluator(rules)
        ev.evaluate({"x": 20.0})  # -> PENDING
        results = ev.evaluate({"x": 5.0})  # condition cleared -> OK
        assert results[0].state == AlertState.OK
        assert results[0].previous_state == AlertState.PENDING

    def test_firing_stays_firing(self) -> None:
        rules = [AlertRule(name="r", condition="x > 10")]
        ev = AlertEvaluator(rules)
        ev.evaluate({"x": 20.0})  # -> FIRING
        results = ev.evaluate({"x": 15.0})  # still above threshold -> FIRING
        assert results[0].state == AlertState.FIRING
        assert results[0].state_changed is False


# ---------------------------------------------------------------------------
# AlertEvaluator -- multiple rules
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestAlertEvaluatorMultipleRules:
    """Tests for evaluating multiple rules independently."""

    def test_multiple_rules_evaluate_independently(self) -> None:
        rules = [
            AlertRule(name="latency", condition="latency > 500"),
            AlertRule(name="errors", condition="error_rate > 0.1"),
            AlertRule(name="cache", condition="cache_hit_rate < 0.5"),
        ]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"latency": 600.0, "error_rate": 0.05, "cache_hit_rate": 0.8})

        assert results[0].state == AlertState.FIRING  # latency > 500
        assert results[1].state == AlertState.OK  # error_rate not > 0.1
        assert results[2].state == AlertState.OK  # cache_hit_rate not < 0.5

    def test_get_active_alerts_filters_correctly(self) -> None:
        rules = [
            AlertRule(name="a", condition="x > 10"),
            AlertRule(name="b", condition="y > 10"),
            AlertRule(name="c", condition="z > 10"),
        ]
        ev = AlertEvaluator(rules)
        ev.evaluate({"x": 20.0, "y": 5.0, "z": 20.0})

        active = ev.get_active_alerts()
        names = {a.rule_name for a in active}
        assert names == {"a", "c"}

    def test_get_active_alerts_empty_when_all_ok(self) -> None:
        rules = [AlertRule(name="a", condition="x > 10")]
        ev = AlertEvaluator(rules)
        ev.evaluate({"x": 5.0})
        assert ev.get_active_alerts() == []

    def test_get_active_alerts_includes_pending(self) -> None:
        rules = [AlertRule(name="a", condition="x > 10", duration_seconds=60)]
        ev = AlertEvaluator(rules)
        ev.evaluate({"x": 20.0})
        active = ev.get_active_alerts()
        assert len(active) == 1
        assert active[0].state == AlertState.PENDING


# ---------------------------------------------------------------------------
# AlertResult
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestAlertResult:
    """Tests for AlertResult dataclass."""

    def test_state_changed_true(self) -> None:
        r = AlertResult(
            rule_name="r",
            state=AlertState.FIRING,
            previous_state=AlertState.OK,
            changed_at=0.0,
        )
        assert r.state_changed is True

    def test_state_changed_false(self) -> None:
        r = AlertResult(
            rule_name="r",
            state=AlertState.OK,
            previous_state=AlertState.OK,
            changed_at=0.0,
        )
        assert r.state_changed is False

    def test_message_field(self) -> None:
        r = AlertResult(
            rule_name="r",
            state=AlertState.FIRING,
            previous_state=AlertState.OK,
            changed_at=0.0,
            message="test message",
        )
        assert r.message == "test message"


# ---------------------------------------------------------------------------
# ConsoleChannel
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestConsoleChannel:
    """Tests for console/logger notification channel."""

    async def test_logs_firing_alert(self, caplog: pytest.LogCaptureFixture) -> None:
        channel = ConsoleChannel()
        alert = AlertResult(
            rule_name="test",
            state=AlertState.FIRING,
            previous_state=AlertState.OK,
            changed_at=0.0,
            value=600.0,
            message="[WARNING] test: latency > 500",
        )
        with caplog.at_level(logging.WARNING, logger="llmhosts.health.notifications"):
            result = await channel.send(alert)
        assert result is True
        assert "ALERT FIRING" in caplog.text

    async def test_logs_resolved_alert(self, caplog: pytest.LogCaptureFixture) -> None:
        channel = ConsoleChannel()
        alert = AlertResult(
            rule_name="test",
            state=AlertState.RESOLVED,
            previous_state=AlertState.FIRING,
            changed_at=0.0,
            message="[RESOLVED] test: condition cleared",
        )
        with caplog.at_level(logging.INFO, logger="llmhosts.health.notifications"):
            result = await channel.send(alert)
        assert result is True
        assert "ALERT RESOLVED" in caplog.text

    async def test_logs_critical_firing(self, caplog: pytest.LogCaptureFixture) -> None:
        channel = ConsoleChannel()
        alert = AlertResult(
            rule_name="test",
            state=AlertState.FIRING,
            previous_state=AlertState.OK,
            changed_at=0.0,
            value=0.5,
            message="[CRITICAL] test: error_rate > 0.1",
        )
        with caplog.at_level(logging.CRITICAL, logger="llmhosts.health.notifications"):
            result = await channel.send(alert)
        assert result is True
        assert "ALERT FIRING" in caplog.text

    async def test_logs_pending_at_debug(self, caplog: pytest.LogCaptureFixture) -> None:
        channel = ConsoleChannel()
        alert = AlertResult(
            rule_name="test",
            state=AlertState.PENDING,
            previous_state=AlertState.OK,
            changed_at=0.0,
            message="[PENDING] test: waiting",
        )
        with caplog.at_level(logging.DEBUG, logger="llmhosts.health.notifications"):
            result = await channel.send(alert)
        assert result is True
        assert "ALERT PENDING" in caplog.text


# ---------------------------------------------------------------------------
# WebhookChannel
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestWebhookChannel:
    """Tests for HTTP webhook notification channel."""

    async def test_sends_post_request(self) -> None:
        channel = WebhookChannel(url="https://hooks.example.com/test")
        alert = AlertResult(
            rule_name="test",
            state=AlertState.FIRING,
            previous_state=AlertState.OK,
            changed_at=0.0,
            value=600.0,
            message="[WARNING] test: latency > 500",
        )

        mock_response = httpx.Response(200, request=httpx.Request("POST", "https://hooks.example.com/test"))

        with patch("llmhosts.health.notifications.httpx.AsyncClient") as mock_client_cls:
            mock_client = AsyncMock()
            mock_client.post = AsyncMock(return_value=mock_response)
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=False)
            mock_client_cls.return_value = mock_client

            result = await channel.send(alert)
            assert result is True
            mock_client.post.assert_called_once()
            call_kwargs = mock_client.post.call_args
            payload = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json")
            assert payload["rule_name"] == "test"
            assert payload["state"] == "firing"
            assert payload["text"] == alert.message  # Slack-compatible
            assert payload["content"] == alert.message  # Discord-compatible

    async def test_returns_false_on_server_error(self) -> None:
        channel = WebhookChannel(url="https://hooks.example.com/test")
        alert = AlertResult(
            rule_name="test",
            state=AlertState.FIRING,
            previous_state=AlertState.OK,
            changed_at=0.0,
            message="test",
        )

        mock_response = httpx.Response(500, request=httpx.Request("POST", "https://hooks.example.com/test"))

        with patch("llmhosts.health.notifications.httpx.AsyncClient") as mock_client_cls:
            mock_client = AsyncMock()
            mock_client.post = AsyncMock(return_value=mock_response)
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=False)
            mock_client_cls.return_value = mock_client

            result = await channel.send(alert)
            assert result is False

    async def test_returns_false_on_connection_error(self) -> None:
        channel = WebhookChannel(url="https://hooks.example.com/test")
        alert = AlertResult(
            rule_name="test",
            state=AlertState.FIRING,
            previous_state=AlertState.OK,
            changed_at=0.0,
            message="test",
        )

        with patch("llmhosts.health.notifications.httpx.AsyncClient") as mock_client_cls:
            mock_client = AsyncMock()
            mock_client.post = AsyncMock(side_effect=httpx.ConnectError("Connection refused"))
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=False)
            mock_client_cls.return_value = mock_client

            result = await channel.send(alert)
            assert result is False

    async def test_custom_headers(self) -> None:
        channel = WebhookChannel(
            url="https://hooks.example.com/test",
            headers={"Authorization": "Bearer token123"},
        )
        alert = AlertResult(
            rule_name="test",
            state=AlertState.FIRING,
            previous_state=AlertState.OK,
            changed_at=0.0,
            message="test",
        )

        mock_response = httpx.Response(200, request=httpx.Request("POST", "https://hooks.example.com/test"))

        with patch("llmhosts.health.notifications.httpx.AsyncClient") as mock_client_cls:
            mock_client = AsyncMock()
            mock_client.post = AsyncMock(return_value=mock_response)
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=False)
            mock_client_cls.return_value = mock_client

            await channel.send(alert)
            call_kwargs = mock_client.post.call_args
            headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers")
            assert headers["Authorization"] == "Bearer token123"


# ---------------------------------------------------------------------------
# NotificationRouter
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestNotificationRouter:
    """Tests for notification routing to channels."""

    async def test_dispatches_state_changed_alerts(self) -> None:
        mock_channel = AsyncMock(spec=NotificationChannel)
        mock_channel.send = AsyncMock(return_value=True)
        router = NotificationRouter(channels={"console": mock_channel})

        alerts = [
            AlertResult(
                rule_name="test",
                state=AlertState.FIRING,
                previous_state=AlertState.OK,
                changed_at=0.0,
                message="firing",
            ),
        ]
        await router.notify(alerts)
        mock_channel.send.assert_called_once()

    async def test_skips_unchanged_alerts(self) -> None:
        mock_channel = AsyncMock(spec=NotificationChannel)
        mock_channel.send = AsyncMock(return_value=True)
        router = NotificationRouter(channels={"console": mock_channel})

        alerts = [
            AlertResult(
                rule_name="test",
                state=AlertState.OK,
                previous_state=AlertState.OK,
                changed_at=0.0,
                message="",
            ),
        ]
        await router.notify(alerts)
        mock_channel.send.assert_not_called()

    async def test_skips_ok_state_even_if_changed(self) -> None:
        """RESOLVED -> OK is a state change but we don't notify for OK."""
        mock_channel = AsyncMock(spec=NotificationChannel)
        mock_channel.send = AsyncMock(return_value=True)
        router = NotificationRouter(channels={"console": mock_channel})

        alerts = [
            AlertResult(
                rule_name="test",
                state=AlertState.OK,
                previous_state=AlertState.RESOLVED,
                changed_at=0.0,
                message="",
            ),
        ]
        await router.notify(alerts)
        mock_channel.send.assert_not_called()

    async def test_notify_with_channels_routes_correctly(self) -> None:
        mock_console = AsyncMock(spec=NotificationChannel)
        mock_console.send = AsyncMock(return_value=True)
        mock_slack = AsyncMock(spec=NotificationChannel)
        mock_slack.send = AsyncMock(return_value=True)

        router = NotificationRouter(channels={"console": mock_console, "slack": mock_slack})

        alerts = [
            AlertResult(
                rule_name="latency",
                state=AlertState.FIRING,
                previous_state=AlertState.OK,
                changed_at=0.0,
                message="firing",
            ),
        ]
        # Route "latency" only to console
        await router.notify_with_channels(alerts, rule_channels={"latency": ["console"]})

        mock_console.send.assert_called_once()
        mock_slack.send.assert_not_called()

    async def test_handles_unknown_channel_gracefully(self, caplog: pytest.LogCaptureFixture) -> None:
        mock_console = AsyncMock(spec=NotificationChannel)
        mock_console.send = AsyncMock(return_value=True)
        router = NotificationRouter(channels={"console": mock_console})

        alerts = [
            AlertResult(
                rule_name="test",
                state=AlertState.FIRING,
                previous_state=AlertState.OK,
                changed_at=0.0,
                message="firing",
            ),
        ]

        with caplog.at_level(logging.WARNING, logger="llmhosts.health.notifications"):
            await router.notify_with_channels(alerts, rule_channels={"test": ["nonexistent"]})

        assert "Unknown notification channel" in caplog.text
        mock_console.send.assert_not_called()

    async def test_handles_channel_send_exception(self, caplog: pytest.LogCaptureFixture) -> None:
        mock_channel = AsyncMock(spec=NotificationChannel)
        mock_channel.send = AsyncMock(side_effect=RuntimeError("channel broken"))
        router = NotificationRouter(channels={"console": mock_channel})

        alerts = [
            AlertResult(
                rule_name="test",
                state=AlertState.FIRING,
                previous_state=AlertState.OK,
                changed_at=0.0,
                message="firing",
            ),
        ]

        with caplog.at_level(logging.ERROR, logger="llmhosts.health.notifications"):
            await router.notify(alerts)

        assert "Failed to send alert" in caplog.text


# ---------------------------------------------------------------------------
# Config integration
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestAlertingConfig:
    """Tests for alerting configuration models."""

    def test_alerting_config_defaults(self) -> None:
        from llmhosts.config import AlertingConfig

        cfg = AlertingConfig()
        assert cfg.enabled is False
        assert cfg.check_interval_seconds == 30
        assert cfg.rules == []
        assert cfg.webhooks == {}

    def test_alert_rule_config_from_dict(self) -> None:
        from llmhosts.config import AlertRuleConfig

        data = {
            "name": "high-latency",
            "condition": "latency > 500",
            "duration_seconds": 60,
            "severity": "critical",
            "channels": ["console", "slack"],
        }
        cfg = AlertRuleConfig.model_validate(data)
        assert cfg.name == "high-latency"
        assert cfg.severity == "critical"
        assert cfg.channels == ["console", "slack"]

    def test_llmhosts_config_includes_alerting(self) -> None:
        from llmhosts.config import LLMHostsConfig

        cfg = LLMHostsConfig()
        assert cfg.alerting is not None
        assert cfg.alerting.enabled is False

    def test_alerting_config_with_rules(self) -> None:
        from llmhosts.config import AlertingConfig, AlertRuleConfig

        cfg = AlertingConfig(
            enabled=True,
            rules=[
                AlertRuleConfig(name="r1", condition="x > 10"),
                AlertRuleConfig(name="r2", condition="y < 5"),
            ],
            webhooks={"slack": "https://hooks.slack.com/test"},
        )
        assert len(cfg.rules) == 2
        assert cfg.webhooks["slack"] == "https://hooks.slack.com/test"


# ---------------------------------------------------------------------------
# Message formatting
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestAlertMessageFormatting:
    """Tests for alert message generation."""

    def test_firing_message_includes_severity_and_condition(self) -> None:
        rules = [AlertRule(name="test", condition="x > 10", severity="warning")]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"x": 20.0})
        assert "[WARNING]" in results[0].message
        assert "test" in results[0].message
        assert "x" in results[0].message

    def test_resolved_message(self) -> None:
        rules = [AlertRule(name="test", condition="x > 10")]
        ev = AlertEvaluator(rules)
        ev.evaluate({"x": 20.0})
        results = ev.evaluate({"x": 5.0})
        assert "[RESOLVED]" in results[0].message
        assert "cleared" in results[0].message

    def test_pending_message_includes_duration(self) -> None:
        rules = [AlertRule(name="test", condition="x > 10", duration_seconds=30)]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"x": 20.0})
        assert "[PENDING]" in results[0].message
        assert "30s" in results[0].message

    def test_ok_message_is_empty(self) -> None:
        rules = [AlertRule(name="test", condition="x > 10")]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"x": 5.0})
        assert results[0].message == ""

    def test_critical_severity_in_message(self) -> None:
        rules = [AlertRule(name="test", condition="x > 10", severity="critical")]
        ev = AlertEvaluator(rules)
        results = ev.evaluate({"x": 20.0})
        assert "[CRITICAL]" in results[0].message
