from unittest.mock import MagicMock, patch

import pytest
from nornir.core.inventory import Host
from nornir.core.task import Result, Task

from nornflow.builtins.hooks import IfHook
from nornflow.hooks.exceptions import HookValidationError


class TestIfHook:
    """Test suite for IfHook conditional execution."""

    def test_hook_name_registration(self):
        """Test that hook has correct name for registration."""
        assert IfHook.hook_name == "if"

    def test_run_once_per_task_flag(self):
        """Test that hook evaluates per host, not once per task."""
        assert IfHook.run_once_per_task is False

    def test_init_with_filter_value(self):
        """Test hook initialization with filter configuration."""
        hook = IfHook({"platform": "ios"})
        assert hook.value == {"platform": "ios"}

    def test_init_with_jinja_value(self):
        """Test hook initialization with Jinja2 expression."""
        hook = IfHook("{{ host.platform == 'ios' }}")
        assert hook.value == "{{ host.platform == 'ios' }}"

    def test_init_without_value(self):
        """Test hook initialization without a value."""
        hook = IfHook()
        assert hook.value is None

    def test_execute_hook_validations_valid_filter_dict(self):
        """Test validation passes for valid filter dictionary."""
        hook = IfHook({"platform": "ios"})
        mock_task_model = MagicMock()

        # Should not raise
        hook.execute_hook_validations(mock_task_model)

    def test_execute_hook_validations_valid_jinja_string(self):
        """Test validation passes for valid Jinja2 expression."""
        hook = IfHook("{{ host.platform == 'ios' }}")
        mock_task_model = MagicMock()

        # Should not raise
        hook.execute_hook_validations(mock_task_model)

    def test_execute_hook_validations_invalid_multiple_filters(self):
        """Test validation fails for dict with multiple filter keys."""
        hook = IfHook({"platform": "ios", "site": "dc1"})
        mock_task_model = MagicMock()

        with pytest.raises(HookValidationError, match="if must specify exactly one filter"):
            hook.execute_hook_validations(mock_task_model)

    def test_execute_hook_validations_invalid_empty_jinja(self):
        """Test validation fails for empty Jinja2 expression."""
        hook = IfHook("")
        mock_task_model = MagicMock()

        with pytest.raises(HookValidationError, match="if expression cannot be empty"):
            hook.execute_hook_validations(mock_task_model)

    def test_execute_hook_validations_invalid_no_jinja_markers(self):
        """Test validation fails for string without Jinja2 markers."""
        hook = IfHook("host.platform == 'ios'")
        mock_task_model = MagicMock()

        with pytest.raises(HookValidationError, match="if expression must be a valid Jinja2 template"):
            hook.execute_hook_validations(mock_task_model)

    def test_execute_hook_validations_invalid_type(self):
        """Test validation fails for invalid value type."""
        hook = IfHook(123)
        mock_task_model = MagicMock()

        with pytest.raises(HookValidationError, match="if value must be a dict \\(filter\\) or string \\(expression\\)"):
            hook.execute_hook_validations(mock_task_model)

    def test_task_started_applies_decorator(self):
        """Test that task_started applies the skip decorator to the task function."""
        hook = IfHook("{{ true }}")
        mock_task = MagicMock(spec=Task)
        mock_task.name = "test_task"
        original_func = MagicMock()
        mock_task.task = original_func

        hook.task_started(mock_task)

        # The task function should be wrapped
        assert mock_task.task != original_func
        # The wrapper should be the skip_if_condition_flagged decorated version
        assert hasattr(mock_task.task, '__wrapped__')

    def test_task_started_no_value_does_nothing(self):
        """Test that task_started does nothing when value is None."""
        hook = IfHook(None)
        mock_task = MagicMock(spec=Task)
        mock_task.name = "test_task"
        original_func = MagicMock()
        mock_task.task = original_func

        hook.task_started(mock_task)

        # Task function should remain unchanged
        assert mock_task.task == original_func

    @patch('nornflow.builtins.hooks.if_hook.logger')
    def test_task_instance_started_filter_condition_skip(self, mock_logger):
        """Test task_instance_started sets skip flag when filter condition fails."""
        hook = IfHook({"platform": "ios"})
        
        mock_task = MagicMock(spec=Task)
        mock_host = MagicMock(spec=Host)
        mock_host.name = "router1"
        mock_host.data = {}
        
        # Mock filter that returns False (should skip)
        mock_filter_func = MagicMock(return_value=False)
        filters_catalog = {"platform": (mock_filter_func, ["value"])}
        
        hook._current_context = {"filters_catalog": filters_catalog}

        hook.task_instance_started(mock_task, mock_host)

        assert mock_host.data['nornflow_skip_flag'] is True
        mock_filter_func.assert_called_once_with(mock_host, value="ios")

    @patch('nornflow.builtins.hooks.if_hook.logger')
    def test_task_instance_started_filter_condition_continue(self, mock_logger):
        """Test task_instance_started doesn't set skip flag when filter condition passes."""
        hook = IfHook({"platform": "ios"})
        
        mock_task = MagicMock(spec=Task)
        mock_host = MagicMock(spec=Host)
        mock_host.name = "router1"
        mock_host.data = {}
        
        # Mock filter that returns True (should continue)
        mock_filter_func = MagicMock(return_value=True)
        filters_catalog = {"platform": (mock_filter_func, ["value"])}
        
        hook._current_context = {"filters_catalog": filters_catalog}

        hook.task_instance_started(mock_task, mock_host)

        assert 'nornflow_skip_flag' not in mock_host.data
        mock_filter_func.assert_called_once_with(mock_host, value="ios")

    def test_task_instance_started_filter_missing_catalog_raises_error(self):
        """Test task_instance_started raises error when filters_catalog is missing."""
        hook = IfHook({"platform": "ios"})
        
        mock_task = MagicMock(spec=Task)
        mock_host = MagicMock(spec=Host)
        mock_host.name = "router1"
        
        hook._current_context = {}  # No filters_catalog

        with pytest.raises(HookValidationError, match="Failed to evaluate condition"):
            hook.task_instance_started(mock_task, mock_host)

    def test_task_instance_started_filter_unknown_filter_raises_error(self):
        """Test task_instance_started raises error when filter is not in catalog."""
        hook = IfHook({"unknown_filter": "value"})
        
        mock_task = MagicMock(spec=Task)
        mock_host = MagicMock(spec=Host)
        mock_host.name = "router1"
        
        filters_catalog = {"platform": (MagicMock(), ["value"])}
        hook._current_context = {"filters_catalog": filters_catalog}

        with pytest.raises(HookValidationError, match="Filter 'unknown_filter' not found"):
            hook.task_instance_started(mock_task, mock_host)

    @patch('nornflow.builtins.hooks.if_hook.logger')
    def test_task_instance_started_jinja_condition_skip(self, mock_logger):
        """Test task_instance_started sets skip flag when Jinja2 condition evaluates to False."""
        hook = IfHook("{{ host.platform == 'ios' }}")
        
        mock_task = MagicMock(spec=Task)
        mock_host = MagicMock(spec=Host)
        mock_host.name = "router1"
        mock_host.data = {}
        
        # Mock vars_manager that resolves to "False"
        mock_vars_manager = MagicMock()
        mock_vars_manager.resolve_string.return_value = "False"
        
        hook._current_context = {"vars_manager": mock_vars_manager}

        hook.task_instance_started(mock_task, mock_host)

        assert mock_host.data['nornflow_skip_flag'] is True
        mock_vars_manager.resolve_string.assert_called_once_with("{{ host.platform == 'ios' }}", "router1")

    @patch('nornflow.builtins.hooks.if_hook.logger')
    def test_task_instance_started_jinja_condition_continue(self, mock_logger):
        """Test task_instance_started doesn't set skip flag when Jinja2 condition evaluates to True."""
        hook = IfHook("{{ host.platform == 'ios' }}")
        
        mock_task = MagicMock(spec=Task)
        mock_host = MagicMock(spec=Host)
        mock_host.name = "router1"
        mock_host.data = {}
        
        # Mock vars_manager that resolves to "True"
        mock_vars_manager = MagicMock()
        mock_vars_manager.resolve_string.return_value = "True"
        
        hook._current_context = {"vars_manager": mock_vars_manager}

        hook.task_instance_started(mock_task, mock_host)

        assert 'nornflow_skip_flag' not in mock_host.data
        mock_vars_manager.resolve_string.assert_called_once_with("{{ host.platform == 'ios' }}", "router1")

    def test_task_instance_started_jinja_missing_vars_manager_raises_error(self):
        """Test task_instance_started raises error when vars_manager is missing."""
        hook = IfHook("{{ true }}")
        
        mock_task = MagicMock(spec=Task)
        mock_host = MagicMock(spec=Host)
        mock_host.name = "router1"
        
        hook._current_context = {}  # No vars_manager

        with pytest.raises(HookValidationError, match="vars_manager not available"):
            hook.task_instance_started(mock_task, mock_host)

    def test_task_instance_started_jinja_invalid_expression_raises_error(self):
        """Test task_instance_started raises error when Jinja2 expression doesn't evaluate to boolean."""
        hook = IfHook("{{ not_boolean }}")
        
        mock_task = MagicMock(spec=Task)
        mock_host = MagicMock(spec=Host)
        mock_host.name = "router1"
        
        mock_vars_manager = MagicMock()
        mock_vars_manager.resolve_string.return_value = "not_boolean"
        
        hook._current_context = {"vars_manager": mock_vars_manager}

        with pytest.raises(HookValidationError, match="Failed to evaluate condition"):
            hook.task_instance_started(mock_task, mock_host)

    def test_skip_if_condition_flagged_decorator_skip(self):
        """Test skip_if_condition_flagged decorator returns skipped result when flag is set."""
        from nornflow.builtins.hooks.if_hook import skip_if_condition_flagged
        
        mock_task = MagicMock(spec=Task)
        mock_host = MagicMock(spec=Host)
        mock_task.host = mock_host
        mock_host.data = {'nornflow_skip_flag': True}
        
        @skip_if_condition_flagged
        def dummy_task(task):
            return Result(host=task.host, result="should not run")
        
        result = dummy_task(mock_task)
        
        assert result.skipped is True
        assert result.result is None
        assert result.changed is False
        assert result.failed is False
        # Flag should be cleaned up
        assert 'nornflow_skip_flag' not in mock_host.data

    def test_skip_if_condition_flagged_decorator_continue(self):
        """Test skip_if_condition_flagged decorator executes task when flag is not set."""
        from nornflow.builtins.hooks.if_hook import skip_if_condition_flagged
        
        mock_task = MagicMock(spec=Task)
        mock_host = MagicMock(spec=Host)
        mock_task.host = mock_host
        mock_host.data = {}  # No skip flag
        
        @skip_if_condition_flagged
        def dummy_task(task):
            return Result(host=task.host, result="executed")
        
        result = dummy_task(mock_task)
        
        assert result.result == "executed"

    def test_should_execute_always_returns_true(self):
        """Test that hook executes for every host (run_once_per_task=False)."""
        hook = IfHook("{{ true }}")
        mock_task = MagicMock()

        # Should always return True since run_once_per_task is False
        assert hook.should_execute(mock_task) is True
        assert hook.should_execute(mock_task) is True

    def test_context_property_returns_empty_when_no_context(self):
        """Test context property returns empty dict when no context is set."""
        hook = IfHook("{{ true }}")
        
        context = hook.context
        assert context == {}

    def test_other_processor_methods_do_nothing(self):
        """Test that other processor methods have no implementation."""
        hook = IfHook("{{ true }}")
        mock_task = MagicMock()
        mock_host = MagicMock()
        mock_result = MagicMock()

        # These should not raise any exceptions
        hook.task_completed(mock_task, mock_result)
        hook.task_instance_completed(mock_task, mock_host, mock_result)
        hook.subtask_instance_started(mock_task, mock_host)
        hook.subtask_instance_completed(mock_task, mock_host, mock_result)