#######################################################################################
# Copyright ETSI Contributors and Others.
#
# 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 asynctest
import copy
from datetime import timedelta
from unittest.mock import Mock, patch

from osm_common.dataclasses.temporal_dataclasses import (
    GetNsRecordInput,
    GetNsRecordOutput,
    GetVnfDetailsInput,
    GetVnfDetailsOutput,
    LcmOperationState,
    ModelInfo,
    NsLcmOperationInput,
    NsState,
    UpdateLcmOperationStateInput,
    UpdateNsStateInput,
    VnfInstantiateInput,
)
from osm_common.temporal_constants import (
    ACTIVITY_CREATE_MODEL,
    ACTIVITY_GET_NS_RECORD,
    ACTIVITY_GET_VNF_DETAILS,
    ACTIVITY_UPDATE_LCM_OPERATION_STATE,
    ACTIVITY_UPDATE_NS_STATE,
    LCM_TASK_QUEUE,
    WORKFLOW_VNF_INSTANTIATE,
)
from osm_lcm.temporal.ns_workflows import NsInstantiateWorkflow
from temporalio import activity, workflow
from temporalio.client import WorkflowFailureError
from temporalio.common import RetryPolicy
from temporalio.exceptions import (
    ActivityError,
    ChildWorkflowError,
    RetryState,
    TimeoutError,
)
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import Worker


vnfr_uuid_1 = "828d91ee-fa04-43bb-8471-f66ea74597e7"
vnfr_uuid_2 = "c4bbeb41-df7e-4daa-863d-c8fd29fac96d"
vnf_member_index_ref_1 = "vnf-profile-id"
vnf_member_index_ref_2 = "vnf-profile-id-2"
vnf_details = [
    (vnfr_uuid_1, vnf_member_index_ref_1),
    (vnfr_uuid_2, vnf_member_index_ref_2),
]
ns_uuid = "9c96e9c1-aae2-4c61-a85f-2ec8acb448fc"
vim_uuid = "a64f7c6c-bc27-4ec8-b664-5500a3324eca"
vdu_id = "test-vdu"
model_name = "2ec8acb448fc-5500a3324eca"
vnf_config = {
    "member-vnf-index": vnf_member_index_ref_1,
    "vdu": [
        {
            "id": vdu_id,
            "configurable-properties": {
                "config::redirect-map": "https://osm.instantiation.params"
            },
        }
    ],
}
sample_nsr = {
    "_id": ns_uuid,
    "name": "sol006_juju23",
    "name-ref": "sol006_juju23",
    "short-name": "sol006_juju23",
    "admin-status": "ENABLED",
    "nsState": "NOT_INSTANTIATED",
    "instantiate_params": {
        "nsdId": ns_uuid,
        "nsName": "sol006_juju23",
        "nsDescription": "default description",
        "vimAccountId": vim_uuid,
        "vnf": [vnf_config],
    },
}
mock_get_namespace = Mock()
retry_policy = RetryPolicy(
    initial_interval=timedelta(seconds=1),
    backoff_coefficient=2.0,
    maximum_interval=None,
    maximum_attempts=1,
    non_retryable_error_types=None,
)
SANDBOXED = False
DEBUG_MODE = True
TASK_TIMEOUT = timedelta(seconds=0.5)
EXECUTION_TIMEOUT = timedelta(seconds=1)


class TestException(Exception):
    pass


@activity.defn(name=ACTIVITY_CREATE_MODEL)
async def mock_create_model(create_model_input: ModelInfo) -> None:
    pass


@activity.defn(name=ACTIVITY_CREATE_MODEL)
async def mock_create_model_raises(create_model_input: ModelInfo) -> None:
    raise TestException(f"{ACTIVITY_CREATE_MODEL} failed.")


@activity.defn(name=ACTIVITY_GET_VNF_DETAILS)
async def mock_get_vnf_details(
    get_vnf_details_input: GetVnfDetailsInput,
) -> GetVnfDetailsOutput:
    return GetVnfDetailsOutput(vnf_details=vnf_details)


@activity.defn(name=ACTIVITY_GET_VNF_DETAILS)
async def mock_get_vnf_details_raises(
    et_vnf_details_input: GetVnfDetailsInput,
) -> GetVnfDetailsOutput:
    raise TestException(f"{ACTIVITY_GET_VNF_DETAILS} failed.")


@activity.defn(name=ACTIVITY_GET_NS_RECORD)
async def mock_get_ns_record(
    get_ns_record_input: GetNsRecordInput,
) -> GetNsRecordOutput:
    return GetNsRecordOutput(nsr=sample_nsr)


@activity.defn(name=ACTIVITY_GET_NS_RECORD)
async def mock_get_ns_record_raise_exception(
    get_ns_record_input: GetNsRecordInput,
) -> GetNsRecordOutput:
    raise TestException(f"{ACTIVITY_GET_NS_RECORD} failed.")


@workflow.defn(name=WORKFLOW_VNF_INSTANTIATE, sandboxed=SANDBOXED)
class MockVnfInstantiateWorkflow:
    @workflow.run
    async def run(self, input: VnfInstantiateInput) -> None:
        pass


@workflow.defn(name=WORKFLOW_VNF_INSTANTIATE, sandboxed=SANDBOXED)
class MockVnfInstantiateWorkflowFailed:
    @workflow.run
    async def run(self, input: VnfInstantiateInput) -> None:
        raise ChildWorkflowError(
            message=f"{WORKFLOW_VNF_INSTANTIATE} child workflow failed.",
            namespace="default",
            workflow_id="123",
            run_id="1",
            workflow_type=WORKFLOW_VNF_INSTANTIATE,
            initiated_event_id=0,
            started_event_id=0,
            retry_state=RetryState.NON_RETRYABLE_FAILURE,
        )


@patch(
    "osm_lcm.temporal.ns_workflows.NsInstantiateWorkflow._get_namespace",
    new=mock_get_namespace,
)
class TestNsInstantiateWorkflow(asynctest.TestCase):
    input = NsLcmOperationInput(
        nslcmop={
            "_id": "1234",
            "nsInstanceId": ns_uuid,
            "operationParams": {"vimAccountId": vim_uuid},
        }
    )

    @activity.defn(name=ACTIVITY_UPDATE_LCM_OPERATION_STATE)
    async def mock_update_lcm_operation_state(
        self,
        data: UpdateLcmOperationStateInput,
    ) -> None:
        self.mock_update_lcm_operation_state_tracker(data)

    @activity.defn(name=ACTIVITY_UPDATE_NS_STATE)
    async def mock_update_ns_state(self, data: UpdateNsStateInput) -> None:
        self.mock_update_ns_state_tracker(data)

    async def setUp(self):
        self.env = await WorkflowEnvironment.start_time_skipping()
        self.client = self.env.client
        self.mock_update_lcm_operation_state_tracker = Mock()
        self.mock_update_ns_state_tracker = Mock()
        self.workflows = [NsInstantiateWorkflow, MockVnfInstantiateWorkflow]
        self.task_queue = LCM_TASK_QUEUE
        mock_get_namespace.return_value = model_name

    def get_worker(self, task_queue: str, workflows: list, activities: list) -> Worker:
        return Worker(
            self.client,
            task_queue=task_queue,
            workflows=workflows,
            activities=activities,
            debug_mode=DEBUG_MODE,
        )

    async def execute_workflow(self):
        return await self.client.execute_workflow(
            NsInstantiateWorkflow,
            arg=self.input,
            id=self.input.nslcmop["nsInstanceId"],
            task_queue=self.task_queue,
            task_timeout=TASK_TIMEOUT,
            execution_timeout=EXECUTION_TIMEOUT,
        )

    async def test_instantiate_workflow__successful__update_lcm_op_state(self):
        activities = [
            mock_create_model,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        async with self.env, self.get_worker(
            self.task_queue, self.workflows, activities
        ):
            await self.execute_workflow()
        self.assert_lcm_op_states(
            self.mock_update_lcm_operation_state_tracker.call_args_list,
            [LcmOperationState.PROCESSING, LcmOperationState.COMPLETED],
        )

    async def test_instantiate_workflow__successful__update_ns_state(self):
        activities = [
            mock_create_model,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        async with self.env, self.get_worker(
            self.task_queue, self.workflows, activities
        ):
            await self.execute_workflow()
        self.assert_ns_states(
            self.mock_update_ns_state_tracker.call_args_list, [NsState.INSTANTIATED]
        )

    async def test_instantiate_workflow__activity_failed__update_ns_state(
        self,
    ):
        activities = [
            mock_create_model_raises,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        with self.assertRaises(WorkflowFailureError):
            async with self.env, self.get_worker(
                self.task_queue, self.workflows, activities
            ):
                await self.execute_workflow()
        self.assert_ns_states(
            self.mock_update_ns_state_tracker.call_args_list, [NsState.INSTANTIATED]
        )

    async def test_instantiate_workflow__activity_failed__update_lcm_op_state(
        self,
    ):
        activities = [
            mock_create_model_raises,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        with self.assertRaises(WorkflowFailureError):
            async with self.env, self.get_worker(
                self.task_queue, self.workflows, activities
            ):
                await self.execute_workflow()
        self.assert_lcm_op_states(
            self.mock_update_lcm_operation_state_tracker.call_args_list,
            [LcmOperationState.PROCESSING, LcmOperationState.FAILED],
        )

    async def test_instantiate_workflow__create_model_activity_failed__raise_activity_error(
        self,
    ):
        activities = [
            mock_create_model_raises,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        with self.assertRaises(WorkflowFailureError) as err:
            async with self.env, self.get_worker(
                self.task_queue, self.workflows, activities
            ):
                await self.execute_workflow()
        self.assertTrue(isinstance(err.exception.cause, ActivityError))

    async def test_instantiate_workflow__create_model_activity_failed__error_details_expected(
        self,
    ):
        activities = [
            mock_create_model_raises,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        with self.assertRaises(WorkflowFailureError):
            async with self.env, self.get_worker(
                self.task_queue, self.workflows, activities
            ):
                await self.execute_workflow()
        self.assert_ns_error_details(
            self.mock_update_ns_state_tracker.call_args_list,
            ["TestException: create-model failed."],
        )

    async def test_instantiate_workflow__get_ns_record_activity_failed__error_details_expected(
        self,
    ):
        activities = [
            mock_create_model,
            mock_get_vnf_details,
            mock_get_ns_record_raise_exception,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        with self.assertRaises(WorkflowFailureError):
            async with self.env, self.get_worker(
                self.task_queue, self.workflows, activities
            ):
                await self.execute_workflow()
        self.assert_ns_error_details(
            self.mock_update_ns_state_tracker.call_args_list,
            ["TestException: get-ns-record failed."],
        )

    async def test_instantiate_workflow__get_ns_record_activity_failed__raise_activity_error(
        self,
    ):
        activities = [
            mock_create_model,
            mock_get_vnf_details,
            mock_get_ns_record_raise_exception,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        with self.assertRaises(WorkflowFailureError) as err:
            async with self.env, self.get_worker(
                self.task_queue, self.workflows, activities
            ):
                await self.execute_workflow()
        self.assertTrue(isinstance(err.exception.cause, ActivityError))

    async def test_instantiate_workflow__get_vnf_details_activity_failed__error_details_expected(
        self,
    ):
        activities = [
            mock_create_model,
            mock_get_vnf_details_raises,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        with self.assertRaises(WorkflowFailureError):
            async with self.env, self.get_worker(
                self.task_queue, self.workflows, activities
            ):
                await self.execute_workflow()
        self.assert_ns_error_details(
            self.mock_update_ns_state_tracker.call_args_list,
            ["TestException: get-vnf-details failed."],
        )

    async def test_instantiate_workflow__get_vnf_details_activity_failed__raise_activity_error(
        self,
    ):
        activities = [
            mock_create_model,
            mock_get_vnf_details_raises,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        with self.assertRaises(WorkflowFailureError) as err:
            async with self.env, self.get_worker(
                self.task_queue, self.workflows, activities
            ):
                await self.execute_workflow()
        self.assertTrue(isinstance(err.exception.cause, ActivityError))

    @patch("osm_lcm.temporal.ns_workflows.NsInstantiateWorkflow.get_vnf_config")
    async def test_instantiate_workflow__get_vnf_config_failed__workflow_times_out(
        self, mock_get_vnf_config
    ):
        # Because of workflow task timeout policy, workflow times out
        activities = [
            mock_create_model,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        mock_get_vnf_config.side_effect = TestException("get_vnf_config failed.")
        with self.assertRaises(WorkflowFailureError) as err:
            async with self.env, self.get_worker(
                self.task_queue, self.workflows, activities
            ):
                await self.execute_workflow()
        self.assertTrue(isinstance(err.exception.cause, TimeoutError))

    @patch("osm_lcm.temporal.ns_workflows.NsInstantiateWorkflow.get_vnf_config")
    async def test_instantiate_workflow__get_vnf_config_failed__update_ns_state(
        self, mock_get_vnf_config
    ):
        # Workflow task failure
        activities = [
            mock_create_model,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        mock_get_vnf_config.side_effect = TestException("get_vnf_config failed.")
        with self.assertRaises(WorkflowFailureError):
            async with self.env, self.get_worker(
                self.task_queue, self.workflows, activities
            ):
                await self.execute_workflow()
        self.assert_ns_states(
            self.mock_update_ns_state_tracker.call_args_list, [NsState.INSTANTIATED]
        )

    @patch("osm_lcm.temporal.ns_workflows.NsInstantiateWorkflow.get_vnf_config")
    async def test_instantiate_workflow__get_vnf_config_failed__update_lcm_op_state(
        self, mock_get_vnf_config
    ):
        # Workflow task failure
        activities = [
            mock_create_model,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        mock_get_vnf_config.side_effect = TestException("get_vnf_config failed.")
        with self.assertRaises(WorkflowFailureError):
            async with self.env, self.get_worker(
                self.task_queue, self.workflows, activities
            ):
                await self.execute_workflow()
        self.assert_lcm_op_states(
            self.mock_update_lcm_operation_state_tracker.call_args_list,
            [LcmOperationState.PROCESSING, LcmOperationState.FAILED],
        )

    async def test_instantiate_workflow__child_wf_failed__raise_child_workflow_error(
        self,
    ):
        activities = [
            mock_create_model,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        with self.assertRaises(WorkflowFailureError) as err:
            async with self.env, self.get_worker(
                self.task_queue,
                [NsInstantiateWorkflow, MockVnfInstantiateWorkflowFailed],
                activities,
            ):
                await self.execute_workflow()
        self.assertTrue(isinstance(err.exception.cause, ChildWorkflowError))

    async def test_instantiate_workflow__child_wf_failed__error_details_expected(
        self,
    ):
        activities = [
            mock_create_model,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        with self.assertRaises(WorkflowFailureError):
            async with self.env, self.get_worker(
                self.task_queue,
                [NsInstantiateWorkflow, MockVnfInstantiateWorkflowFailed],
                activities,
            ):
                await self.execute_workflow()
        self.assert_ns_error_details(
            self.mock_update_ns_state_tracker.call_args_list,
            ["vnf-instantiate child workflow failed."],
        )

    async def test_instantiate_workflow__child_wf_failed__update_ns_state(
        self,
    ):
        activities = [
            mock_create_model,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]
        with self.assertRaises(WorkflowFailureError):
            async with self.env, self.get_worker(
                self.task_queue,
                [NsInstantiateWorkflow, MockVnfInstantiateWorkflowFailed],
                activities,
            ):
                await self.execute_workflow()
        self.assert_ns_states(
            self.mock_update_ns_state_tracker.call_args_list, [NsState.INSTANTIATED]
        )

    async def test_instantiate_workflow__child_wf_failed__update_lcm_op_state(
        self,
    ):
        activities = [
            mock_create_model,
            mock_get_vnf_details,
            mock_get_ns_record,
            self.mock_update_ns_state,
            self.mock_update_lcm_operation_state,
        ]

        with self.assertRaises(WorkflowFailureError):
            async with self.env, self.get_worker(
                self.task_queue,
                [NsInstantiateWorkflow, MockVnfInstantiateWorkflowFailed],
                activities,
            ):
                await self.execute_workflow()
        self.assert_lcm_op_states(
            self.mock_update_lcm_operation_state_tracker.call_args_list,
            [LcmOperationState.PROCESSING, LcmOperationState.FAILED],
        )

    def assert_ns_states(self, call_args_list, expected_states):
        """Asserts that the NS state was set to each of the expected states (in order)."""
        self.assertEqual(
            [call.args[0].state for call in call_args_list], expected_states
        )

    def assert_ns_error_details(self, call_args_list, expected_error):
        """Asserts that the NS state was set to each of the expected states (in order)."""
        self.assertEqual(
            [call.args[0].message for call in call_args_list], expected_error
        )

    def assert_lcm_op_states(self, call_args_list, expected_states):
        """Asserts that the LCM operation was set to each of the expected states (in order)."""
        self.assertEqual(
            [call.args[0].op_state for call in call_args_list], expected_states
        )


class TestGetVnfConfig(asynctest.TestCase):
    def test_vnf_config__nsr_config_matches_with_vnf__return_expected_config(self):
        expected_config = {
            "member-vnf-index": "vnf-profile-id",
            "vdu": [
                {
                    "configurable-properties": {
                        "config::redirect-map": "https://osm.instantiation.params"
                    },
                    "id": "test-vdu",
                }
            ],
        }
        result = NsInstantiateWorkflow.get_vnf_config(
            vnf_member_index_ref_1, sample_nsr
        )
        self.assertEqual(result, expected_config)

    def test_vnf_config__nsr_does_not_have_config__expected_no_config(self):
        nsr = copy.deepcopy(sample_nsr)
        nsr["instantiate_params"] = {}
        result = NsInstantiateWorkflow.get_vnf_config(vnf_member_index_ref_1, nsr)
        self.assertEqual(result, {})

    def test_vnf_config__empty_vnf_member_index_ref__expected_no_config(self):
        result = NsInstantiateWorkflow.get_vnf_config("", sample_nsr)
        self.assertEqual(result, {})

    def test_vnf_config__nsr_config_does_not_match_with_vnf__expected_no_config(self):
        result = NsInstantiateWorkflow.get_vnf_config(
            vnf_member_index_ref_2, sample_nsr
        )
        self.assertEqual(result, {})
