# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import abc

from unittest import mock

from watcher import notifications
from watcher import objects
from watcher.applier.actions import base as abase
from watcher.applier.actions import factory
from watcher.applier.actions import nop
from watcher.applier.workflow_engine import default as tflow
from watcher.common import exception
from watcher.common import utils
from watcher.tests.unit.db import base
from watcher.tests.unit.objects import utils as obj_utils


class ExpectedException(Exception):
    pass


class FakeAction(abase.BaseAction, metaclass=abc.ABCMeta):
    def schema(self):
        pass

    def post_condition(self):
        pass

    def pre_condition(self):
        pass

    def revert(self):
        pass

    def execute(self):
        return False

    def get_description(self):
        return "fake action, just for test"


class TestDefaultWorkFlowEngine(base.DbTestCase):
    def setUp(self):
        super().setUp()
        self.engine = tflow.DefaultWorkFlowEngine(
            config=mock.Mock(),
            context=self.context,
            applier_manager=mock.MagicMock(),
        )
        self.engine.config.max_workers = 2

    @mock.patch.object(objects.Strategy, "get_by_id")
    @mock.patch.object(objects.ActionPlan, "get_by_id")
    @mock.patch('taskflow.engines.load')
    @mock.patch('taskflow.patterns.graph_flow.Flow.link')
    def test_execute(self, graph_flow, engines, m_actionplan, m_strategy):
        actions = mock.MagicMock()
        try:
            self.engine.execute(actions)
            self.assertTrue(engines.called)
        except Exception as exc:
            self.fail(exc)

    def create_action(
        self, action_type, parameters, parents=None, uuid=None, state=None
    ):
        action = {
            'uuid': uuid or utils.generate_uuid(),
            'action_plan_id': 0,
            'action_type': action_type,
            'input_parameters': parameters,
            'state': objects.action.State.PENDING,
            'parents': parents or [],
        }
        new_action = objects.Action(self.context, **action)
        with mock.patch.object(notifications.action, 'send_create'):
            new_action.create()
        return new_action

    def check_action_state(self, action, expected_state):
        to_check = objects.Action.get_by_uuid(self.context, action.uuid)
        self.assertEqual(expected_state, to_check.state)

    def check_actions_state(self, actions, expected_state):
        for a in actions:
            self.check_action_state(a, expected_state)

    def check_notifications_contains(
        self, notification_calls, action_state, old_state=None
    ):
        """Check that an action notification contains the expected info.

        notification_calls: list of notification calls arguments
        action_state: expected action state (dict)
        old_state: expected old action state (optional)
        """
        if old_state:
            action_state['old_state'] = old_state
        for call in notification_calls:
            data_dict = call.args[1].as_dict()
            if call.kwargs and 'old_state' in call.kwargs:
                data_dict['old_state'] = call.kwargs['old_state']
            try:
                self.assertLessEqual(action_state.items(), data_dict.items())
                return True
            except AssertionError:
                continue
        return False

    @mock.patch('taskflow.engines.load')
    @mock.patch('taskflow.patterns.graph_flow.Flow.link')
    def test_execute_with_no_actions(self, graph_flow, engines):
        actions = []
        try:
            self.engine.execute(actions)
            self.assertFalse(graph_flow.called)
            self.assertTrue(engines.called)
        except Exception as exc:
            self.fail(exc)

    @mock.patch.object(objects.Strategy, "get_by_id")
    @mock.patch.object(objects.ActionPlan, "get_by_id")
    @mock.patch.object(notifications.action, 'send_execution_notification')
    @mock.patch.object(notifications.action, 'send_update')
    def test_execute_with_one_action(
        self,
        mock_send_update,
        mock_execution_notification,
        m_get_actionplan,
        m_get_strategy,
    ):
        m_get_actionplan.return_value = obj_utils.get_test_action_plan(
            self.context, id=0
        )
        m_get_strategy.return_value = obj_utils.get_test_strategy(
            self.context, id=1
        )
        actions = [self.create_action("nop", {'message': 'test'})]
        try:
            self.engine.execute(actions)
            self.check_actions_state(actions, objects.action.State.SUCCEEDED)

        except Exception as exc:
            self.fail(exc)

    @mock.patch.object(objects.Strategy, "get_by_id")
    @mock.patch.object(objects.ActionPlan, "get_by_id")
    @mock.patch.object(notifications.action, 'send_execution_notification')
    @mock.patch.object(notifications.action, 'send_update')
    def test_execute_nop_sleep(
        self,
        mock_send_update,
        mock_execution_notification,
        m_get_actionplan,
        m_get_strategy,
    ):
        m_get_actionplan.return_value = obj_utils.get_test_action_plan(
            self.context, id=0
        )
        m_get_strategy.return_value = obj_utils.get_test_strategy(
            self.context, id=1
        )
        actions = []
        first_nop = self.create_action("nop", {'message': 'test'})
        second_nop = self.create_action("nop", {'message': 'second test'})
        sleep = self.create_action(
            "sleep",
            {'duration': 0.0},
            parents=[first_nop.uuid, second_nop.uuid],
        )
        actions.extend([first_nop, second_nop, sleep])

        try:
            self.engine.execute(actions)
            self.check_actions_state(actions, objects.action.State.SUCCEEDED)

        except Exception as exc:
            self.fail(exc)

    @mock.patch.object(objects.Strategy, "get_by_id")
    @mock.patch.object(objects.ActionPlan, "get_by_id")
    @mock.patch.object(notifications.action, 'send_execution_notification')
    @mock.patch.object(notifications.action, 'send_update')
    def test_execute_with_parents(
        self,
        mock_send_update,
        mock_execution_notification,
        m_get_actionplan,
        m_get_strategy,
    ):
        m_get_actionplan.return_value = obj_utils.get_test_action_plan(
            self.context, id=0
        )
        m_get_strategy.return_value = obj_utils.get_test_strategy(
            self.context, id=1
        )
        actions = []
        first_nop = self.create_action(
            "nop",
            {'message': 'test'},
            uuid='bc7eee5c-4fbe-4def-9744-b539be55aa19',
        )
        second_nop = self.create_action(
            "nop",
            {'message': 'second test'},
            uuid='0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23',
        )
        first_sleep = self.create_action(
            "sleep",
            {'duration': 0.0},
            parents=[first_nop.uuid, second_nop.uuid],
            uuid='be436531-0da3-4dad-a9c0-ea1d2aff6496',
        )
        second_sleep = self.create_action(
            "sleep",
            {'duration': 0.0},
            parents=[first_sleep.uuid],
            uuid='9eb51e14-936d-4d12-a500-6ba0f5e0bb1c',
        )
        actions.extend([first_nop, second_nop, first_sleep, second_sleep])

        expected_nodes = [
            {
                'uuid': 'bc7eee5c-4fbe-4def-9744-b539be55aa19',
                'input_parameters': {'message': 'test'},
                'action_plan_id': 0,
                'state': 'PENDING',
                'parents': [],
                'action_type': 'nop',
                'id': 1,
            },
            {
                'uuid': '0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23',
                'input_parameters': {'message': 'second test'},
                'action_plan_id': 0,
                'state': 'PENDING',
                'parents': [],
                'action_type': 'nop',
                'id': 2,
            },
            {
                'uuid': 'be436531-0da3-4dad-a9c0-ea1d2aff6496',
                'input_parameters': {'duration': 0.0},
                'action_plan_id': 0,
                'state': 'PENDING',
                'parents': [
                    'bc7eee5c-4fbe-4def-9744-b539be55aa19',
                    '0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23',
                ],
                'action_type': 'sleep',
                'id': 3,
            },
            {
                'uuid': '9eb51e14-936d-4d12-a500-6ba0f5e0bb1c',
                'input_parameters': {'duration': 0.0},
                'action_plan_id': 0,
                'state': 'PENDING',
                'parents': ['be436531-0da3-4dad-a9c0-ea1d2aff6496'],
                'action_type': 'sleep',
                'id': 4,
            },
        ]

        expected_edges = [
            (
                'action_type:nop uuid:0565bd5c-aa00-46e5-8d81-2cb5cc1ffa23',
                'action_type:sleep uuid:be436531-0da3-4dad-a9c0-ea1d2aff6496',
            ),
            (
                'action_type:nop uuid:bc7eee5c-4fbe-4def-9744-b539be55aa19',
                'action_type:sleep uuid:be436531-0da3-4dad-a9c0-ea1d2aff6496',
            ),
            (
                'action_type:sleep uuid:be436531-0da3-4dad-a9c0-ea1d2aff6496',
                'action_type:sleep uuid:9eb51e14-936d-4d12-a500-6ba0f5e0bb1c',
            ),
        ]

        try:
            flow = self.engine.execute(actions)
            actual_nodes = sorted(
                [x[0]._db_action.as_dict() for x in flow.iter_nodes()],
                key=lambda x: x['id'],
            )
            for expected, actual in zip(expected_nodes, actual_nodes):
                for key in expected.keys():
                    self.assertIn(expected[key], actual.values())
            actual_edges = [
                (u.name, v.name) for (u, v, _) in flow.iter_links()
            ]

            for edge in expected_edges:
                self.assertIn(edge, actual_edges)

            self.check_actions_state(actions, objects.action.State.SUCCEEDED)

        except Exception as exc:
            self.fail(exc)

    @mock.patch.object(objects.Strategy, "get_by_id")
    @mock.patch.object(objects.ActionPlan, "get_by_id")
    @mock.patch.object(notifications.action, 'send_execution_notification')
    @mock.patch.object(notifications.action, 'send_update')
    def test_execute_with_two_actions(
        self, m_send_update, m_execution, m_get_actionplan, m_get_strategy
    ):
        m_get_actionplan.return_value = obj_utils.get_test_action_plan(
            self.context, id=0
        )
        m_get_strategy.return_value = obj_utils.get_test_strategy(
            self.context, id=1
        )
        actions = []
        second = self.create_action("sleep", {'duration': 0.0})
        first = self.create_action("nop", {'message': 'test'})

        actions.append(first)
        actions.append(second)

        try:
            self.engine.execute(actions)
            self.check_actions_state(actions, objects.action.State.SUCCEEDED)

        except Exception as exc:
            self.fail(exc)

    @mock.patch.object(objects.Strategy, "get_by_id")
    @mock.patch.object(objects.ActionPlan, "get_by_id")
    @mock.patch.object(notifications.action, 'send_execution_notification')
    @mock.patch.object(notifications.action, 'send_update')
    def test_execute_with_three_actions(
        self, m_send_update, m_execution, m_get_actionplan, m_get_strategy
    ):
        m_get_actionplan.return_value = obj_utils.get_test_action_plan(
            self.context, id=0
        )
        m_get_strategy.return_value = obj_utils.get_test_strategy(
            self.context, id=1
        )
        actions = []
        third = self.create_action("nop", {'message': 'next'})
        second = self.create_action("sleep", {'duration': 0.0})
        first = self.create_action("nop", {'message': 'hello'})

        self.check_action_state(first, objects.action.State.PENDING)
        self.check_action_state(second, objects.action.State.PENDING)
        self.check_action_state(third, objects.action.State.PENDING)

        actions.append(first)
        actions.append(second)
        actions.append(third)

        try:
            self.engine.execute(actions)
            self.check_actions_state(actions, objects.action.State.SUCCEEDED)

        except Exception as exc:
            self.fail(exc)

    @mock.patch.object(objects.Strategy, "get_by_id")
    @mock.patch.object(objects.ActionPlan, "get_by_id")
    @mock.patch.object(notifications.action, 'send_execution_notification')
    @mock.patch.object(notifications.action, 'send_update')
    def test_execute_with_exception(
        self, m_send_update, m_execution, m_get_actionplan, m_get_strategy
    ):
        m_get_actionplan.return_value = obj_utils.get_test_action_plan(
            self.context, id=0
        )
        m_get_strategy.return_value = obj_utils.get_test_strategy(
            self.context, id=1
        )
        actions = []

        third = self.create_action("no_exist", {'message': 'next'})
        second = self.create_action("sleep", {'duration': 0.0})
        first = self.create_action("nop", {'message': 'hello'})

        self.check_action_state(first, objects.action.State.PENDING)
        self.check_action_state(second, objects.action.State.PENDING)
        self.check_action_state(third, objects.action.State.PENDING)

        actions.append(first)
        actions.append(second)
        actions.append(third)

        self.engine.execute(actions)

        self.check_action_state(first, objects.action.State.SUCCEEDED)
        self.check_action_state(second, objects.action.State.SUCCEEDED)
        self.check_action_state(third, objects.action.State.FAILED)

    @mock.patch.object(objects.Strategy, "get_by_id")
    @mock.patch.object(objects.ActionPlan, "get_by_id")
    @mock.patch.object(notifications.action, 'send_execution_notification')
    @mock.patch.object(notifications.action, 'send_update')
    @mock.patch.object(factory.ActionFactory, "make_action")
    def test_execute_with_action_failed(
        self,
        m_make_action,
        m_send_update,
        m_send_execution,
        m_get_actionplan,
        m_get_strategy,
    ):
        m_get_actionplan.return_value = obj_utils.get_test_action_plan(
            self.context, id=0
        )
        m_get_strategy.return_value = obj_utils.get_test_strategy(
            self.context, id=1
        )
        actions = [self.create_action("fake_action", {})]
        m_make_action.return_value = FakeAction(mock.Mock())

        self.engine.execute(actions)
        self.check_action_state(actions[0], objects.action.State.FAILED)
        self.assertTrue(
            self.check_notifications_contains(
                m_send_update.call_args_list,
                {
                    'state': objects.action.State.FAILED,
                    'uuid': actions[0].uuid,
                    'action_type': 'fake_action',
                    'status_message': (
                        f"Action failed in execute: The action "
                        f"{actions[0].uuid} execution failed."
                    ),
                },
            )
        )

    @mock.patch.object(objects.ActionPlan, "get_by_uuid")
    def test_execute_with_action_plan_cancel(self, m_get_actionplan):
        obj_utils.create_test_goal(self.context)
        strategy = obj_utils.create_test_strategy(self.context)
        audit = obj_utils.create_test_audit(
            self.context, strategy_id=strategy.id
        )
        action_plan = obj_utils.create_test_action_plan(
            self.context,
            audit_id=audit.id,
            strategy_id=strategy.id,
            state=objects.action_plan.State.CANCELLING,
        )
        action1 = obj_utils.create_test_action(
            self.context,
            action_plan_id=action_plan.id,
            action_type='nop',
            state=objects.action.State.SUCCEEDED,
            input_parameters={'message': 'hello World'},
        )
        action2 = obj_utils.create_test_action(
            self.context,
            action_plan_id=action_plan.id,
            action_type='nop',
            state=objects.action.State.ONGOING,
            uuid='9eb51e14-936d-4d12-a500-6ba0f5e0bb1c',
            input_parameters={'message': 'hello World'},
        )
        action3 = obj_utils.create_test_action(
            self.context,
            action_plan_id=action_plan.id,
            action_type='nop',
            state=objects.action.State.PENDING,
            uuid='bc7eee5c-4fbe-4def-9744-b539be55aa19',
            input_parameters={'message': 'hello World'},
        )
        m_get_actionplan.return_value = action_plan
        actions = []
        actions.append(action1)
        actions.append(action2)
        actions.append(action3)
        self.assertRaises(
            exception.ActionPlanCancelled, self.engine.execute, actions
        )
        try:
            self.check_action_state(action1, objects.action.State.SUCCEEDED)
            self.check_action_state(action2, objects.action.State.CANCELLED)
            self.check_action_state(action3, objects.action.State.CANCELLED)

        except Exception as exc:
            self.fail(exc)

    @mock.patch.object(objects.ActionPlan, "get_by_id")
    @mock.patch.object(notifications.action, 'send_execution_notification')
    @mock.patch.object(notifications.action, 'send_update')
    @mock.patch.object(nop.Nop, 'debug_message')
    def test_execute_with_automatic_skipped(
        self, m_nop_message, m_send_update, m_execution, m_get_actionplan
    ):
        obj_utils.create_test_goal(self.context)
        strategy = obj_utils.create_test_strategy(self.context)
        audit = obj_utils.create_test_audit(
            self.context, strategy_id=strategy.id
        )
        action_plan = obj_utils.create_test_action_plan(
            self.context,
            audit_id=audit.id,
            strategy_id=strategy.id,
            state=objects.action_plan.State.ONGOING,
            id=0,
        )
        m_get_actionplan.return_value = action_plan
        actions = []

        action = self.create_action(
            "nop", {'message': 'action2', 'skip_pre_condition': True}
        )

        self.check_action_state(action, objects.action.State.PENDING)

        actions.append(action)

        self.engine.execute(actions)

        # action skipped automatically in the pre_condition phase
        self.check_action_state(action, objects.action.State.SKIPPED)
        self.assertEqual(
            objects.Action.get_by_uuid(
                self.context, action.uuid
            ).status_message,
            "Action was skipped automatically: Skipped in pre_condition",
        )
        action_state_dict = {
            'state': objects.action.State.SKIPPED,
            'status_message': "Action was skipped automatically: "
            "Skipped in pre_condition",
            'uuid': action.uuid,
            'action_type': 'nop',
        }
        self.assertTrue(
            self.check_notifications_contains(
                m_send_update.call_args_list, action_state_dict
            )
        )
        self.assertTrue(
            self.check_notifications_contains(
                m_send_update.call_args_list,
                action_state_dict,
                old_state=objects.action.State.PENDING,
            )
        )

        m_nop_message.assert_not_called()

    @mock.patch.object(objects.ActionPlan, "get_by_id")
    @mock.patch.object(notifications.action, 'send_execution_notification')
    @mock.patch.object(notifications.action, 'send_update')
    @mock.patch.object(nop.Nop, 'debug_message')
    @mock.patch.object(nop.Nop, 'pre_condition')
    def test_execute_with_manually_skipped(
        self,
        m_nop_pre_condition,
        m_nop_message,
        m_send_update,
        m_execution,
        m_get_actionplan,
    ):
        obj_utils.create_test_goal(self.context)
        strategy = obj_utils.create_test_strategy(self.context)
        audit = obj_utils.create_test_audit(
            self.context, strategy_id=strategy.id
        )
        action_plan = obj_utils.create_test_action_plan(
            self.context,
            audit_id=audit.id,
            strategy_id=strategy.id,
            state=objects.action_plan.State.ONGOING,
            id=0,
        )
        m_get_actionplan.return_value = action_plan
        actions = []
        action1 = obj_utils.create_test_action(
            self.context,
            action_type='nop',
            state=objects.action.State.PENDING,
            input_parameters={'message': 'action1'},
        )
        action2 = obj_utils.create_test_action(
            self.context,
            action_type='nop',
            state=objects.action.State.SKIPPED,
            uuid='bc7eee5c-4fbe-4def-9744-b539be55aa19',
            input_parameters={'message': 'action2'},
        )
        self.check_action_state(action1, objects.action.State.PENDING)
        self.check_action_state(action2, objects.action.State.SKIPPED)
        actions.append(action1)
        actions.append(action2)
        self.engine.execute(actions)
        # action skipped automatically in the pre_condition phase
        self.check_action_state(action1, objects.action.State.SUCCEEDED)
        self.check_action_state(action2, objects.action.State.SKIPPED)
        # pre_condition and execute are only called for action1
        m_nop_pre_condition.assert_called_once_with()
        m_nop_message.assert_called_once_with('action1')

    @mock.patch.object(objects.ActionPlan, "get_by_id")
    @mock.patch.object(notifications.action, 'send_execution_notification')
    @mock.patch.object(notifications.action, 'send_update')
    @mock.patch.object(nop.Nop, 'debug_message')
    def test_execute_different_action_results(
        self, m_nop_message, m_send_update, m_execution, m_get_actionplan
    ):
        obj_utils.create_test_goal(self.context)
        strategy = obj_utils.create_test_strategy(self.context)
        audit = obj_utils.create_test_audit(
            self.context, strategy_id=strategy.id
        )
        action_plan = obj_utils.create_test_action_plan(
            self.context,
            audit_id=audit.id,
            strategy_id=strategy.id,
            state=objects.action_plan.State.ONGOING,
            id=0,
        )
        m_get_actionplan.return_value = action_plan
        actions = []

        action1 = self.create_action("nop", {'message': 'action1'})
        action2 = self.create_action(
            "nop", {'message': 'action2', 'skip_pre_condition': True}
        )
        action3 = self.create_action(
            "nop", {'message': 'action3', 'fail_pre_condition': True}
        )
        action4 = self.create_action(
            "nop", {'message': 'action4', 'fail_execute': True}
        )
        action5 = self.create_action(
            "nop", {'message': 'action5', 'fail_post_condition': True}
        )
        action6 = self.create_action("sleep", {'duration': 1.0})

        self.check_action_state(action1, objects.action.State.PENDING)
        self.check_action_state(action2, objects.action.State.PENDING)
        self.check_action_state(action3, objects.action.State.PENDING)
        self.check_action_state(action4, objects.action.State.PENDING)
        self.check_action_state(action5, objects.action.State.PENDING)
        self.check_action_state(action6, objects.action.State.PENDING)

        actions.append(action1)
        actions.append(action2)
        actions.append(action3)
        actions.append(action4)
        actions.append(action5)
        actions.append(action6)

        self.engine.execute(actions)

        # successful nop action
        self.check_action_state(action1, objects.action.State.SUCCEEDED)
        self.assertIsNone(
            objects.Action.get_by_uuid(
                self.context, action1.uuid
            ).status_message
        )
        # action skipped automatically in the pre_condition phase
        self.check_action_state(action2, objects.action.State.SKIPPED)
        self.assertEqual(
            objects.Action.get_by_uuid(
                self.context, action2.uuid
            ).status_message,
            "Action was skipped automatically: Skipped in pre_condition",
        )
        # action failed in the pre_condition phase
        self.check_action_state(action3, objects.action.State.FAILED)
        self.assertEqual(
            objects.Action.get_by_uuid(
                self.context, action3.uuid
            ).status_message,
            "Action failed in pre_condition: Failed in pre_condition",
        )
        # action failed in the execute phase
        self.check_action_state(action4, objects.action.State.FAILED)
        self.assertEqual(
            objects.Action.get_by_uuid(
                self.context, action4.uuid
            ).status_message,
            f"Action failed in execute: The action {action4.uuid} "
            "execution failed.",
        )
        # action failed in the post_condition phase
        self.check_action_state(action5, objects.action.State.FAILED)
        self.assertEqual(
            objects.Action.get_by_uuid(
                self.context, action5.uuid
            ).status_message,
            "Action failed in post_condition: Failed in post_condition",
        )
        # successful sleep action
        self.check_action_state(action6, objects.action.State.SUCCEEDED)

        # execute method should not be called for actions that are skipped of
        # failed in the pre_condition phase
        expected_execute_calls = [
            mock.call('action1'),
            mock.call('action4'),
            mock.call('action5'),
        ]
        m_nop_message.assert_has_calls(expected_execute_calls, any_order=True)
        self.assertEqual(m_nop_message.call_count, 3)

    def test_decider(self):
        # execution_rule is ALWAYS
        self.engine.execution_rule = 'ALWAYS'
        history = {'action1': True}
        self.assertTrue(self.engine.decider(history))

        history = {'action1': False}
        self.assertTrue(self.engine.decider(history))

        # execution_rule is ANY
        self.engine.execution_rule = 'ANY'
        history = {'action1': True}
        self.assertFalse(self.engine.decider(history))

        history = {'action1': False}
        self.assertTrue(self.engine.decider(history))

    @mock.patch.object(objects.ActionPlan, "get_by_uuid")
    def test_notify_with_status_message(self, m_get_actionplan):
        """Test that notify method properly handles status_message."""
        obj_utils.create_test_goal(self.context)
        strategy = obj_utils.create_test_strategy(self.context)
        audit = obj_utils.create_test_audit(
            self.context, strategy_id=strategy.id
        )
        action_plan = obj_utils.create_test_action_plan(
            self.context,
            audit_id=audit.id,
            strategy_id=strategy.id,
            state=objects.action_plan.State.ONGOING,
        )
        action1 = obj_utils.create_test_action(
            self.context,
            action_plan_id=action_plan.id,
            action_type='nop',
            state=objects.action.State.ONGOING,
            input_parameters={'message': 'hello World'},
        )
        m_get_actionplan.return_value = action_plan
        actions = []
        actions.append(action1)

        # Test notify with status_message provided
        test_status_message = "Action completed successfully"
        result = self.engine.notify(
            action1,
            objects.action.State.FAILED,
            status_message=test_status_message,
        )

        # Verify the action state was updated
        self.assertEqual(result.state, objects.action.State.FAILED)

        # Verify the status_message was set
        self.assertEqual(result.status_message, test_status_message)

        # Verify the changes were persisted to the database
        persisted_action = objects.Action.get_by_uuid(
            self.context, action1.uuid
        )
        self.assertEqual(persisted_action.state, objects.action.State.FAILED)
        self.assertEqual(persisted_action.status_message, test_status_message)

    @mock.patch.object(objects.ActionPlan, "get_by_uuid")
    def test_notify_without_status_message(self, m_get_actionplan):
        """Test that notify method works without status_message parameter."""
        obj_utils.create_test_goal(self.context)
        strategy = obj_utils.create_test_strategy(self.context)
        audit = obj_utils.create_test_audit(
            self.context, strategy_id=strategy.id
        )
        action_plan = obj_utils.create_test_action_plan(
            self.context,
            audit_id=audit.id,
            strategy_id=strategy.id,
            state=objects.action_plan.State.ONGOING,
        )
        action1 = obj_utils.create_test_action(
            self.context,
            action_plan_id=action_plan.id,
            action_type='nop',
            state=objects.action.State.ONGOING,
            input_parameters={'message': 'hello World'},
        )
        m_get_actionplan.return_value = action_plan
        actions = []
        actions.append(action1)

        # Test notify without status_message
        result = self.engine.notify(action1, objects.action.State.SUCCEEDED)
        # Verify the action state was updated
        self.assertEqual(result.state, objects.action.State.SUCCEEDED)

        # Verify the status_message
        self.assertIsNone(result.status_message)
        # Verify the changes were persisted to the database
        persisted_action = objects.Action.get_by_uuid(
            self.context, action1.uuid
        )
        self.assertEqual(
            persisted_action.state, objects.action.State.SUCCEEDED
        )
        self.assertIsNone(persisted_action.status_message)
