"""Tests for AutomationEngine — rule evaluation in isolation."""

from datetime import datetime, timezone
from unittest.mock import MagicMock, patch

import pytest

from ledgrab.core.automations.automation_engine import AutomationEngine
from ledgrab.storage.automation import (
    ApplicationRule,
    Automation,
    DisplayStateRule,
    StartupRule,
    SystemIdleRule,
    TimeOfDayRule,
    WebhookRule,
)
from ledgrab.storage.automation_store import AutomationStore

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def mock_store(tmp_db) -> AutomationStore:
    return AutomationStore(tmp_db)


@pytest.fixture
def mock_manager():
    m = MagicMock()
    m.fire_event = MagicMock()
    return m


@pytest.fixture
def engine(mock_store, mock_manager) -> AutomationEngine:
    """Build an AutomationEngine with the PlatformDetector mocked out.

    PlatformDetector starts a Windows display-power listener thread that
    causes access violations in the test environment, so we replace it
    with a simple MagicMock.
    """
    with patch("ledgrab.core.automations.automation_engine.PlatformDetector"):
        eng = AutomationEngine(
            automation_store=mock_store,
            processor_manager=mock_manager,
            poll_interval=0.1,
        )
    return eng


# ---------------------------------------------------------------------------
# Rule evaluation (unit-level)
# ---------------------------------------------------------------------------


class TestRuleEvaluation:
    """Test _evaluate_rule for each rule type individually."""

    def _make_automation(self, rules):
        now = datetime.now(timezone.utc)
        return Automation(
            id="test_auto",
            name="Test",
            enabled=True,
            rule_logic="or",
            rules=rules,
            scene_preset_id=None,
            deactivation_mode="none",
            deactivation_scene_preset_id=None,
            created_at=now,
            updated_at=now,
        )

    def _eval(self, engine, rule, **kwargs):
        """Invoke the private _evaluate_rule method."""
        defaults = dict(
            running_procs=set(),
            topmost_proc=None,
            topmost_fullscreen=False,
            fullscreen_procs=set(),
            idle_seconds=None,
            display_state=None,
        )
        defaults.update(kwargs)
        return engine._evaluate_rule(
            rule,
            defaults["running_procs"],
            defaults["topmost_proc"],
            defaults["topmost_fullscreen"],
            defaults["fullscreen_procs"],
            defaults["idle_seconds"],
            defaults["display_state"],
        )

    def test_startup_true(self, engine):
        assert self._eval(engine, StartupRule()) is True

    def test_application_running_match(self, engine):
        rule = ApplicationRule(apps=["chrome.exe"], match_type="running")
        result = self._eval(
            engine,
            rule,
            running_procs={"chrome.exe", "explorer.exe"},
        )
        assert result is True

    def test_application_running_no_match(self, engine):
        rule = ApplicationRule(apps=["chrome.exe"], match_type="running")
        result = self._eval(
            engine,
            rule,
            running_procs={"explorer.exe"},
        )
        assert result is False

    def test_application_topmost_match(self, engine):
        rule = ApplicationRule(apps=["game.exe"], match_type="topmost")
        result = self._eval(
            engine,
            rule,
            topmost_proc="game.exe",
        )
        assert result is True

    def test_application_topmost_no_match(self, engine):
        rule = ApplicationRule(apps=["game.exe"], match_type="topmost")
        result = self._eval(
            engine,
            rule,
            topmost_proc="chrome.exe",
        )
        assert result is False

    def test_time_of_day_within_range(self, engine):
        rule = TimeOfDayRule(start_time="00:00", end_time="23:59")
        result = self._eval(engine, rule)
        assert result is True

    def test_system_idle_when_idle(self, engine):
        rule = SystemIdleRule(idle_minutes=5, when_idle=True)
        result = self._eval(engine, rule, idle_seconds=600.0)  # 10 minutes idle
        assert result is True

    def test_system_idle_not_idle(self, engine):
        rule = SystemIdleRule(idle_minutes=5, when_idle=True)
        result = self._eval(engine, rule, idle_seconds=60.0)  # 1 minute idle
        assert result is False

    def test_system_idle_when_not_idle(self, engine):
        """when_idle=False means active when user is NOT idle."""
        rule = SystemIdleRule(idle_minutes=5, when_idle=False)
        result = self._eval(engine, rule, idle_seconds=60.0)  # 1 min idle (not yet 5)
        assert result is True

    def test_display_state_match(self, engine):
        rule = DisplayStateRule(state="on")
        result = self._eval(engine, rule, display_state="on")
        assert result is True

    def test_display_state_no_match(self, engine):
        rule = DisplayStateRule(state="off")
        result = self._eval(engine, rule, display_state="on")
        assert result is False

    def test_webhook_active(self, engine):
        rule = WebhookRule(token="tok123")
        engine._webhook_states["tok123"] = True
        result = self._eval(engine, rule)
        assert result is True

    def test_webhook_inactive(self, engine):
        rule = WebhookRule(token="tok123")
        # Not in _webhook_states → False
        result = self._eval(engine, rule)
        assert result is False


# ---------------------------------------------------------------------------
# Rule logic (AND / OR)
# ---------------------------------------------------------------------------


class TestRuleLogic:
    def _make_automation(self, rules, logic="or"):
        now = datetime.now(timezone.utc)
        return Automation(
            id="logic_auto",
            name="Logic",
            enabled=True,
            rule_logic=logic,
            rules=rules,
            scene_preset_id=None,
            deactivation_mode="none",
            deactivation_scene_preset_id=None,
            created_at=now,
            updated_at=now,
        )

    def test_or_any_true(self, engine):
        auto = self._make_automation(
            [
                ApplicationRule(apps=["missing.exe"], match_type="running"),
                StartupRule(),
            ],
            logic="or",
        )
        result = engine._evaluate_rules(
            auto,
            running_procs=set(),
            topmost_proc=None,
            topmost_fullscreen=False,
            fullscreen_procs=set(),
            idle_seconds=None,
            display_state=None,
        )
        assert result is True

    def test_and_all_must_be_true(self, engine):
        auto = self._make_automation(
            [
                StartupRule(),
                ApplicationRule(apps=["missing.exe"], match_type="running"),
            ],
            logic="and",
        )
        result = engine._evaluate_rules(
            auto,
            running_procs=set(),
            topmost_proc=None,
            topmost_fullscreen=False,
            fullscreen_procs=set(),
            idle_seconds=None,
            display_state=None,
        )
        assert result is False


# ---------------------------------------------------------------------------
# Webhook state management
# ---------------------------------------------------------------------------


class TestWebhookState:
    @pytest.mark.asyncio
    async def test_set_webhook_state_activate(self, engine):
        await engine.set_webhook_state("tok_1", True)
        assert engine._webhook_states["tok_1"] is True

    @pytest.mark.asyncio
    async def test_set_webhook_state_deactivate(self, engine):
        engine._webhook_states["tok_1"] = True
        await engine.set_webhook_state("tok_1", False)
        assert engine._webhook_states["tok_1"] is False


# ---------------------------------------------------------------------------
# Start / Stop lifecycle
# ---------------------------------------------------------------------------


class TestEngineLifecycle:
    @pytest.mark.asyncio
    async def test_start_creates_task(self, engine):
        await engine.start()
        assert engine._task is not None
        await engine.stop()

    @pytest.mark.asyncio
    async def test_stop_cancels_task(self, engine):
        await engine.start()
        await engine.stop()
        assert engine._task is None

    @pytest.mark.asyncio
    async def test_double_start_is_safe(self, engine):
        await engine.start()
        await engine.start()  # no-op
        await engine.stop()

    @pytest.mark.asyncio
    async def test_stop_without_start_is_safe(self, engine):
        await engine.stop()  # no-op


# ---------------------------------------------------------------------------
# HTTP poll rule — operator helpers + integration with mocked value stream
# ---------------------------------------------------------------------------


class TestApplyOperator:
    """Unit tests for the rule-comparison operator dispatch."""

    def test_operator_equals(self):
        from ledgrab.core.automations.automation_engine import _apply_operator

        assert _apply_operator("equals", "ok", "ok") is True
        assert _apply_operator("equals", 42, "42") is True  # str coercion
        assert _apply_operator("equals", "ok", "fail") is False

    def test_operator_contains(self):
        from ledgrab.core.automations.automation_engine import _apply_operator

        assert _apply_operator("contains", "hello world", "world") is True
        assert _apply_operator("contains", "hello", "xyz") is False

    def test_operator_regex(self):
        from ledgrab.core.automations.automation_engine import _apply_operator

        assert _apply_operator("regex", "abc123", r"\d+") is True
        assert _apply_operator("regex", "abc", r"\d+") is False
        assert _apply_operator("regex", "abc", "[") is False  # invalid regex

    def test_operator_gt_lt(self):
        from ledgrab.core.automations.automation_engine import _apply_operator

        assert _apply_operator("gt", 5, "3") is True
        assert _apply_operator("gt", 2, "3") is False
        assert _apply_operator("lt", 2, "3") is True
        assert _apply_operator("gt", "not-a-number", "3") is False

    def test_operator_not_equals(self):
        from ledgrab.core.automations.automation_engine import _apply_operator

        assert _apply_operator("not_equals", "a", "b") is True
        assert _apply_operator("not_equals", "a", "a") is False


class TestHTTPPollRuleEvaluation:
    """_evaluate_http_poll behavior with a mocked ValueStreamManager.

    Rule now points at an HTTPValueSource via ``value_source_id``; the
    engine reads the cached raw value from the corresponding ValueStream
    and compares with ``_apply_operator``.
    """

    def _engine_with_vsm(self, mock_store, mock_manager, vsm):
        with patch("ledgrab.core.automations.automation_engine.PlatformDetector"):
            return AutomationEngine(
                automation_store=mock_store,
                processor_manager=mock_manager,
                poll_interval=0.1,
                value_stream_manager=vsm,
            )

    def _vsm_with_stream(self, vs_id: str, raw_value):
        """Build a mock ValueStreamManager whose ``peek(vs_id)`` returns a
        stream whose ``get_raw_value()`` yields *raw_value*."""
        vsm = MagicMock()
        stream = MagicMock()
        stream.get_raw_value.return_value = raw_value
        vsm.peek.side_effect = lambda q: stream if q == vs_id else None
        return vsm

    def test_no_manager_returns_false(self, mock_store, mock_manager):
        from ledgrab.storage.automation import HTTPPollRule

        with patch("ledgrab.core.automations.automation_engine.PlatformDetector"):
            eng = AutomationEngine(
                automation_store=mock_store,
                processor_manager=mock_manager,
                poll_interval=0.1,
            )
        rule = HTTPPollRule(value_source_id="vs_x", operator="equals", value="ok")
        assert eng._evaluate_http_poll(rule) is False

    def test_no_value_source_id_returns_false(self, mock_store, mock_manager):
        from ledgrab.storage.automation import HTTPPollRule

        vsm = MagicMock()
        vsm.peek.return_value = None
        eng = self._engine_with_vsm(mock_store, mock_manager, vsm)
        rule = HTTPPollRule(value_source_id="", operator="equals", value="ok")
        assert eng._evaluate_http_poll(rule) is False

    def test_stream_missing_returns_false(self, mock_store, mock_manager):
        """If the value stream hasn't been acquired yet, the rule is False."""
        from ledgrab.storage.automation import HTTPPollRule

        vsm = MagicMock()
        vsm.peek.return_value = None  # nothing acquired
        eng = self._engine_with_vsm(mock_store, mock_manager, vsm)
        rule = HTTPPollRule(value_source_id="vs_x", operator="equals", value="ok")
        assert eng._evaluate_http_poll(rule) is False

    def test_raw_value_none_returns_false(self, mock_store, mock_manager):
        from ledgrab.storage.automation import HTTPPollRule

        vsm = self._vsm_with_stream("vs_x", None)
        eng = self._engine_with_vsm(mock_store, mock_manager, vsm)
        rule = HTTPPollRule(value_source_id="vs_x", operator="equals", value="ok")
        assert eng._evaluate_http_poll(rule) is False

    def test_equals_match(self, mock_store, mock_manager):
        from ledgrab.storage.automation import HTTPPollRule

        vsm = self._vsm_with_stream("vs_x", "playing")
        eng = self._engine_with_vsm(mock_store, mock_manager, vsm)
        rule = HTTPPollRule(value_source_id="vs_x", operator="equals", value="playing")
        assert eng._evaluate_http_poll(rule) is True

    def test_exists_when_value_present(self, mock_store, mock_manager):
        from ledgrab.storage.automation import HTTPPollRule

        vsm = self._vsm_with_stream("vs_x", 1)
        eng = self._engine_with_vsm(mock_store, mock_manager, vsm)
        rule = HTTPPollRule(value_source_id="vs_x", operator="exists")
        assert eng._evaluate_http_poll(rule) is True

    def test_exists_when_value_none(self, mock_store, mock_manager):
        from ledgrab.storage.automation import HTTPPollRule

        vsm = self._vsm_with_stream("vs_x", None)
        eng = self._engine_with_vsm(mock_store, mock_manager, vsm)
        rule = HTTPPollRule(value_source_id="vs_x", operator="exists")
        assert eng._evaluate_http_poll(rule) is False

    def test_gt_numeric(self, mock_store, mock_manager):
        from ledgrab.storage.automation import HTTPPollRule

        vsm = self._vsm_with_stream("vs_x", 72.5)
        eng = self._engine_with_vsm(mock_store, mock_manager, vsm)
        rule = HTTPPollRule(value_source_id="vs_x", operator="gt", value="70")
        assert eng._evaluate_http_poll(rule) is True

    def test_stream_without_raw_value_attr_returns_false(self, mock_store, mock_manager):
        """A non-HTTP stream type that has no ``get_raw_value`` short-circuits."""
        from ledgrab.storage.automation import HTTPPollRule

        plain_stream = object()  # no get_raw_value attribute
        vsm = MagicMock()
        vsm.peek.return_value = plain_stream
        eng = self._engine_with_vsm(mock_store, mock_manager, vsm)
        rule = HTTPPollRule(value_source_id="vs_x", operator="equals", value="ok")
        assert eng._evaluate_http_poll(rule) is False


class TestSyncValueStreamRefs:
    """Engine acquire/release diffing against the ValueStreamManager."""

    def test_no_manager_is_noop(self, mock_store, mock_manager):
        with patch("ledgrab.core.automations.automation_engine.PlatformDetector"):
            eng = AutomationEngine(
                automation_store=mock_store,
                processor_manager=mock_manager,
                poll_interval=0.1,
            )
        eng._sync_value_stream_refs()  # should not raise

    def test_acquires_referenced_sources(self, mock_store, mock_manager):
        from ledgrab.storage.automation import HTTPPollRule

        vsm = MagicMock()

        with patch("ledgrab.core.automations.automation_engine.PlatformDetector"):
            eng = AutomationEngine(
                automation_store=mock_store,
                processor_manager=mock_manager,
                poll_interval=0.1,
                value_stream_manager=vsm,
            )

        mock_store.create_automation(
            name="poll1",
            enabled=True,
            rule_logic="or",
            rules=[
                HTTPPollRule(
                    value_source_id="vs_abc",
                    operator="equals",
                    value="ok",
                ),
            ],
            scene_preset_id=None,
            deactivation_mode="none",
            deactivation_scene_preset_id=None,
        )

        eng._sync_value_stream_refs()
        vsm.acquire.assert_called_once_with("vs_abc")
        assert eng._value_sources_acquired == {"vs_abc"}

    def test_releases_unreferenced_sources(self, mock_store, mock_manager):
        """When a rule is removed, the engine releases its formerly-held source."""
        vsm = MagicMock()

        with patch("ledgrab.core.automations.automation_engine.PlatformDetector"):
            eng = AutomationEngine(
                automation_store=mock_store,
                processor_manager=mock_manager,
                poll_interval=0.1,
                value_stream_manager=vsm,
            )

        eng._value_sources_acquired = {"vs_old"}
        eng._sync_value_stream_refs()  # no rules reference it
        vsm.release.assert_called_once_with("vs_old")
        assert eng._value_sources_acquired == set()

    def test_disabled_automation_ignored(self, mock_store, mock_manager):
        from ledgrab.storage.automation import HTTPPollRule

        vsm = MagicMock()

        with patch("ledgrab.core.automations.automation_engine.PlatformDetector"):
            eng = AutomationEngine(
                automation_store=mock_store,
                processor_manager=mock_manager,
                poll_interval=0.1,
                value_stream_manager=vsm,
            )

        mock_store.create_automation(
            name="off",
            enabled=False,
            rule_logic="or",
            rules=[
                HTTPPollRule(
                    value_source_id="vs_xyz",
                    operator="equals",
                    value="",
                ),
            ],
            scene_preset_id=None,
            deactivation_mode="none",
            deactivation_scene_preset_id=None,
        )

        eng._sync_value_stream_refs()
        vsm.acquire.assert_not_called()


class TestHTTPValueStreamExtraction:
    """Unit tests for the dot-path extractor used by HTTPValueStream."""

    def test_empty_path_returns_body_text(self):
        from ledgrab.core.processing.value_stream import _extract_simple_path

        assert _extract_simple_path(None, "", "raw body") == "raw body"

    def test_simple_key(self):
        from ledgrab.core.processing.value_stream import _extract_simple_path

        assert _extract_simple_path({"status": "ok"}, "status", "") == "ok"

    def test_nested_with_array_index(self):
        from ledgrab.core.processing.value_stream import _extract_simple_path

        body = {"MediaContainer": {"size": 2, "Metadata": [{"title": "Show"}]}}
        assert _extract_simple_path(body, "MediaContainer.size", "") == 2
        assert _extract_simple_path(body, "MediaContainer.Metadata[0].title", "") == "Show"

    def test_chained_indices(self):
        from ledgrab.core.processing.value_stream import _extract_simple_path

        body = {"matrix": [[1, 2, 3], [4, 5, 6]]}
        assert _extract_simple_path(body, "matrix[0][1]", "") == 2
        assert _extract_simple_path(body, "matrix[1][2]", "") == 6

    def test_missing_returns_none(self):
        from ledgrab.core.processing.value_stream import _extract_simple_path

        assert _extract_simple_path({"a": 1}, "b", "") is None
        assert _extract_simple_path({"a": [1]}, "a[5]", "") is None
