"""Tests for ScheduledRule model + tick task + cron helper (task 3.6.5).

Coverage:
- ``compute_next_run`` — pure cron helper
- ``ScheduledRule`` model validation
- ``tick_scheduled_rules`` — DB-level dispatch + next_run_at advancement
"""

from __future__ import annotations

import uuid
from datetime import datetime, timezone as dt_timezone

import pytest

from simorgh.apps.automation.tasks import compute_next_run
from simorgh.apps.automation.registry import (
    ActionSpec,
    ActionContext,
    register_action,
    reset_registry_for_tests,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def clean_registry():
    reset_registry_for_tests()
    yield
    reset_registry_for_tests()


@pytest.fixture
def noop_action():
    """Register a no-op action so rules can be executed without errors."""
    def _handler(ctx: ActionContext) -> None:
        pass

    spec = ActionSpec(key="test.noop", label="Noop", module="test", handler=_handler)
    register_action(spec)
    return spec


@pytest.fixture
def tenant_and_node(db):
    from simorgh.apps.organizations.services import create_node
    from simorgh.apps.tenants.models import Tenant

    tenant = Tenant.objects.create(slug=f"t-{uuid.uuid4().hex[:8]}", name="Test")
    node = create_node(tenant_id=tenant.pk, name="Root")
    return tenant, node


@pytest.fixture
def make_rule(tenant_and_node):
    tenant, node = tenant_and_node

    def _factory(*, name: str = "Sched Rule", actions=None, is_active: bool = True):
        from simorgh.apps.automation.models import AutomationRule, TriggerType

        return AutomationRule.objects.create(
            tenant=tenant,
            organization_node=node,
            name=name,
            trigger_type=TriggerType.SCHEDULE,
            conditions=[],
            actions=actions or [{"action": "test.noop", "params": {}}],
            is_active=is_active,
        )

    return _factory


@pytest.fixture
def make_scheduled(tenant_and_node, make_rule):
    tenant, node = tenant_and_node

    def _factory(
        *,
        rule=None,
        cron_expression: str = "*/5 * * * *",
        tz: str = "UTC",
        next_run_at: datetime | None = None,
        is_active: bool = True,
    ):
        from simorgh.apps.automation.models import ScheduledRule

        if rule is None:
            rule = make_rule()
        if next_run_at is None:
            # Default: already past-due (1 hour ago)
            from django.utils import timezone
            next_run_at = timezone.now() - __import__("datetime").timedelta(hours=1)

        return ScheduledRule.objects.create(
            tenant=tenant,
            organization_node=node,
            rule=rule,
            cron_expression=cron_expression,
            timezone=tz,
            next_run_at=next_run_at,
            is_active=is_active,
        )

    return _factory


# ---------------------------------------------------------------------------
# compute_next_run tests (3.6.4)
# ---------------------------------------------------------------------------

class TestComputeNextRun:
    def test_every_5_min_basic(self):
        """'*/5 * * * *' returns a datetime ~5 min ahead of base."""
        base = datetime(2026, 1, 1, 9, 0, 0, tzinfo=dt_timezone.utc)
        result = compute_next_run("*/5 * * * *", base=base)

        assert result.tzinfo is not None
        delta = (result - base).total_seconds()
        assert 0 < delta <= 600  # at most 10 min ahead

    def test_returns_utc(self):
        """Result is always UTC-aware regardless of timezone param."""
        base = datetime(2026, 6, 1, 8, 0, 0, tzinfo=dt_timezone.utc)
        result = compute_next_run("0 9 * * *", "Asia/Tehran", base=base)

        assert result.tzinfo is not None
        # Asia/Tehran is UTC+3:30; 09:00 local = 05:30 UTC → next day if already past
        assert result.utcoffset().total_seconds() == 0

    def test_weekly_monday_9am(self):
        """'0 9 * * 1' triggers on the next Monday at 09:00 local time."""
        # 2026-06-01 is a Monday; base is before 09:00 so next run is same day
        base = datetime(2026, 6, 1, 6, 0, 0, tzinfo=dt_timezone.utc)
        result = compute_next_run("0 9 * * 1", "UTC", base=base)

        assert result.weekday() == 0  # Monday
        assert result.hour == 9
        assert result.minute == 0

    def test_invalid_cron_raises(self):
        import pytest
        with pytest.raises(ValueError, match="Invalid cron"):
            compute_next_run("not a cron")

    def test_invalid_timezone_raises(self):
        import pytest
        with pytest.raises(ValueError, match="Unknown timezone"):
            compute_next_run("*/5 * * * *", "Mars/Olympus_Mons")

    def test_naive_base_treated_as_utc(self):
        """Naive base datetime is treated as UTC internally."""
        naive_base = datetime(2026, 1, 1, 12, 0, 0)  # no tzinfo
        result = compute_next_run("0 * * * *", "UTC", base=naive_base)
        assert result.tzinfo is not None
        assert result > naive_base.replace(tzinfo=dt_timezone.utc)

    def test_consecutive_calls_advance(self):
        """Calling twice with consecutive base values returns increasing datetimes."""
        base1 = datetime(2026, 1, 1, 9, 0, 0, tzinfo=dt_timezone.utc)
        r1 = compute_next_run("*/5 * * * *", base=base1)
        r2 = compute_next_run("*/5 * * * *", base=r1)
        assert r2 > r1


# ---------------------------------------------------------------------------
# ScheduledRule model tests (3.6.2)
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestScheduledRuleModel:
    def test_create(self, make_scheduled):
        sr = make_scheduled()
        assert sr.pk is not None
        assert sr.cron_expression == "*/5 * * * *"
        assert sr.timezone == "UTC"
        assert sr.is_active is True

    def test_str(self, make_scheduled):
        sr = make_scheduled()
        assert str(sr)  # just non-empty

    def test_last_run_at_null_by_default(self, make_scheduled):
        sr = make_scheduled()
        assert sr.last_run_at is None

    def test_deactivation(self, make_scheduled):
        sr = make_scheduled()
        from simorgh.apps.automation.models import ScheduledRule

        ScheduledRule.objects.filter(pk=sr.pk).update(is_active=False)
        sr.refresh_from_db()
        assert sr.is_active is False


# ---------------------------------------------------------------------------
# tick_scheduled_rules tests (3.6.3)
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestTickScheduledRules:
    def test_due_rule_dispatched(self, make_scheduled, noop_action):
        """A past-due ScheduledRule advances next_run_at after dispatch."""
        from unittest.mock import patch
        from simorgh.apps.automation.tasks import tick_scheduled_rules, execute_automation_rule_task

        sr = make_scheduled(cron_expression="*/5 * * * *")
        original_next = sr.next_run_at

        with patch.object(execute_automation_rule_task, "delay") as mock_delay:
            tick_scheduled_rules()

        mock_delay.assert_called_once()
        call_args = mock_delay.call_args
        assert call_args.args[0] == sr.rule.pk  # rule_id
        assert call_args.kwargs["trigger_event"] == "schedule"

        sr.refresh_from_db()
        assert sr.next_run_at > original_next
        assert sr.last_run_at is not None

    def test_future_rule_not_dispatched(self, make_scheduled, noop_action):
        """A ScheduledRule with next_run_at in the future is not dispatched."""
        from unittest.mock import patch
        from django.utils import timezone
        import datetime

        future = timezone.now() + datetime.timedelta(hours=1)
        sr = make_scheduled(next_run_at=future)

        from simorgh.apps.automation.tasks import tick_scheduled_rules, execute_automation_rule_task

        with patch.object(execute_automation_rule_task, "delay") as mock_delay:
            tick_scheduled_rules()

        mock_delay.assert_not_called()
        sr.refresh_from_db()
        assert sr.last_run_at is None

    def test_inactive_scheduled_rule_skipped(self, make_scheduled, noop_action):
        """An inactive ScheduledRule is never dispatched."""
        from unittest.mock import patch

        sr = make_scheduled(is_active=False)

        from simorgh.apps.automation.tasks import tick_scheduled_rules, execute_automation_rule_task

        with patch.object(execute_automation_rule_task, "delay") as mock_delay:
            tick_scheduled_rules()

        mock_delay.assert_not_called()
        sr.refresh_from_db()
        assert sr.last_run_at is None

    def test_inactive_parent_rule_skipped(self, make_scheduled, make_rule, noop_action):
        """ScheduledRule linked to an inactive AutomationRule is skipped."""
        from unittest.mock import patch

        inactive_rule = make_rule(is_active=False)
        sr = make_scheduled(rule=inactive_rule)

        from simorgh.apps.automation.tasks import tick_scheduled_rules, execute_automation_rule_task

        with patch.object(execute_automation_rule_task, "delay") as mock_delay:
            tick_scheduled_rules()

        mock_delay.assert_not_called()
        sr.refresh_from_db()
        assert sr.last_run_at is None

    def test_bad_cron_deactivates_row(self, tenant_and_node, make_rule):
        """A ScheduledRule with an unparseable cron is deactivated by the tick."""
        from unittest.mock import patch
        from django.utils import timezone
        from simorgh.apps.automation.models import ScheduledRule

        tenant, node = tenant_and_node
        rule = make_rule()

        sr = ScheduledRule.objects.create(
            tenant=tenant,
            organization_node=node,
            rule=rule,
            cron_expression="NOT_A_CRON",
            timezone="UTC",
            next_run_at=timezone.now() - __import__("datetime").timedelta(minutes=1),
            is_active=True,
        )

        from simorgh.apps.automation.tasks import tick_scheduled_rules, execute_automation_rule_task

        # Mock dispatch so we reach the cron-parsing step
        with patch.object(execute_automation_rule_task, "delay"):
            tick_scheduled_rules()

        sr.refresh_from_db()
        assert sr.is_active is False

    def test_multiple_due_rules(self, tenant_and_node, make_rule, noop_action):
        """Multiple past-due rules are all dispatched in one tick."""
        from unittest.mock import patch, call
        from django.utils import timezone
        from simorgh.apps.automation.models import ScheduledRule
        import datetime

        tenant, node = tenant_and_node

        rules = [make_rule(name=f"Rule {i}") for i in range(3)]
        scheduled = []
        for r in rules:
            sr = ScheduledRule.objects.create(
                tenant=tenant,
                organization_node=node,
                rule=r,
                cron_expression="*/5 * * * *",
                timezone="UTC",
                next_run_at=timezone.now() - datetime.timedelta(hours=1),
                is_active=True,
            )
            scheduled.append(sr)

        from simorgh.apps.automation.tasks import tick_scheduled_rules, execute_automation_rule_task

        with patch.object(execute_automation_rule_task, "delay") as mock_delay:
            tick_scheduled_rules()

        assert mock_delay.call_count == 3
        dispatched_rule_ids = {c.args[0] for c in mock_delay.call_args_list}
        assert dispatched_rule_ids == {r.pk for r in rules}

        for sr in scheduled:
            sr.refresh_from_db()
            assert sr.last_run_at is not None

    def test_next_run_advances_by_cron_interval(self, make_scheduled, noop_action):
        """next_run_at advances to the next cron slot after now, not just +5min."""
        from unittest.mock import patch
        from django.utils import timezone

        sr = make_scheduled(cron_expression="0 * * * *")  # top of every hour

        from simorgh.apps.automation.tasks import tick_scheduled_rules, execute_automation_rule_task

        with patch.object(execute_automation_rule_task, "delay"):
            tick_scheduled_rules()

        sr.refresh_from_db()
        # next_run_at should be top-of-the-hour in the future
        assert sr.next_run_at > timezone.now()
        assert sr.next_run_at.minute == 0
        assert sr.next_run_at.second == 0
