#######################################################################################
# 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 asyncio
import unittest.mock as mock

from juju.application import Application
from juju.controller import Controller
from juju.model import Model
from juju.errors import JujuError
from juju.unit import Unit
from n2vc.temporal_libjuju import ConnectionInfo
from osm_common.dataclasses.temporal_dataclasses import (
    CharmInfo,
    CheckCharmStatusInput,
    ModelInfo,
    VduInstantiateInput,
)
from osm_lcm.temporal.juju_paas_activities import JujuPaasConnector
from parameterized import parameterized
from temporalio.testing import ActivityEnvironment
from unittest.mock import ANY, AsyncMock, Mock

vim_id = "some-vim-uuid"
namespace = "some-namespace"
model_info = ModelInfo(vim_id, namespace)
connection_info = ConnectionInfo(
    "1.2.3.4:17070", "user", "password", "cacert", "cloud_name", "cloud_credentials"
)
vim_content = {
    "_id": "82258772-0145-47cf-9a56-98a83aab38cc",
    "name": "juju-with-key",
    "vim_type": "paas",
    "description": "",
    "vim_url": "10.152.183.83:17070",
    "vim_user": "admin",
    "vim_password": "********",
    "vim_tenant_name": "null",
    "config": {
        "paas_provider": "juju",
        "cloud": "microk8s",
        "cloud_credentials": "microk8s",
        "authorized_keys": "1yc2EAAAADAQABAAABAQDM3js",
        "ca_cert_content": "BEGIN CERTIFICATE\nMIIEEj",
    },
}


class TestJujuPaasConnector(asynctest.TestCase):
    def setUp(self):
        self.db = Mock()
        self.env = ActivityEnvironment()
        self.juju_paas_connector = JujuPaasConnector(self.db)
        self.controller = AsyncMock(spec=Controller)

        async def get_controller(_: str):
            return self.controller

        self.juju_paas_connector._get_controller = get_controller
        self._api_endpoints = []
        self.controller.api_endpoints = self.api_endpoints

    @property
    async def api_endpoints(self):
        return self._api_endpoints

    async def test_create_model_nominal_case(self):
        self.db.get_one.side_effect = [vim_content]
        self.juju_paas_connector._decrypt_password = Mock()
        self.juju_paas_connector._decrypt_password.side_effect = ["password"]
        await self.env.run(self.juju_paas_connector.create_model, model_info)
        self.controller.add_model.assert_called_once_with(
            model_info.model_name,
            config=ANY,
            cloud_name=vim_content["config"]["cloud"],
            credential_name=vim_content["config"]["cloud_credentials"],
        )

    async def test_create_model_already_exists(self):
        self.controller.list_models.return_value = [model_info.model_name]
        await self.env.run(self.juju_paas_connector.create_model, model_info)
        self.controller.add_model.assert_not_called()


class TestJujuPaasActivitiesBase(asynctest.TestCase):
    def setUp(self) -> None:
        self.db = Mock()
        self.env = ActivityEnvironment()
        self.controller = AsyncMock()
        self.model = Mock(spec=Model)
        self.model.applications = {}
        self.controller.get_model.return_value = self.model

        async def get_controller(_: str):
            return self.controller

        self.juju_paas = JujuPaasConnector(self.db)
        self.juju_paas._get_controller = get_controller

    def add_application(self, application_name) -> None:
        self.application_name = application_name
        self.application = Mock(spec=Application)
        self.application.name = self.application_name
        self.model.applications = {self.application_name: self.application}


class TestCheckCharmStatus(TestJujuPaasActivitiesBase):
    def setUp(self) -> None:
        super().setUp()
        self.env.on_heartbeat = self.on_heartbeat
        self.heartbeat_count = 0
        self.heartbeat_maximum = 5
        self.add_application("application")

    def on_heartbeat(self, *args, **kwargs):
        self.heartbeat_count += 1
        if self.heartbeat_count > self.heartbeat_maximum:
            self.env.cancel()

    @parameterized.expand(
        [
            ("App active, Unit active", ["active"], ["active"], 1),
            ("App blocked, Unit active", ["blocked"], ["active"], 1),
            ("App active, Unit blocked", ["active"], ["blocked"], 1),
            ("App blocked, Unit blocked", ["blocked"], ["blocked"], 1),
            (
                "App maint, then active, Unit active",
                ["maintenance", "active"],
                ["active"],
                2,
            ),
            (
                "App active, unit maint then active",
                ["active", "active"],
                ["maintenance", "active"],
                2,
            ),
        ]
    )
    async def test_check_charm_status_application_with_one_unit(
        self, _, app_events, unit_events, heartbeat_maximum
    ):
        arg = CheckCharmStatusInput(
            application_name=self.application_name,
            model_name="model",
            vim_uuid="vim-uuid",
            poll_interval=0,
        )

        self.heartbeat_maximum = heartbeat_maximum
        units = [Mock(spec=Unit)]
        self.application.units = units

        type(self.application).status = mock.PropertyMock(side_effect=app_events)

        type(units[0]).workload_status = mock.PropertyMock(side_effect=unit_events)

        await self.env.run(self.juju_paas.check_charm_status, arg)

    async def test_check_charm_status_cancel(self):
        arg = CheckCharmStatusInput(
            application_name=self.application_name,
            model_name="model",
            vim_uuid="vim-uuid",
            poll_interval=0,
        )

        self.heartbeat_maximum = 3
        units = [Mock(spec=Unit)]
        self.application.units = units

        type(self.application).status = mock.PropertyMock(
            side_effect=["maintenance", "maintenance", "maintenance", "maintenance"]
        )

        type(units[0]).workload_status = mock.PropertyMock(side_effect=[])

        with self.assertRaises(asyncio.exceptions.CancelledError):
            await self.env.run(self.juju_paas.check_charm_status, arg)

    async def test_check_charm_status_multiple_units(self):
        arg = CheckCharmStatusInput(
            application_name=self.application_name,
            model_name="model",
            vim_uuid="vim-uuid",
            poll_interval=0,
        )

        self.heartbeat_maximum = 4

        units = [Mock(spec=Unit), Mock(spec=Unit)]
        self.application.units = units

        type(self.application).status = mock.PropertyMock(
            side_effect=["active", "active", "active", "active"]
        )

        type(units[0]).workload_status = mock.PropertyMock(
            side_effect=["maintenance", "maintenance", "active", "active"]
        )

        type(units[1]).workload_status = mock.PropertyMock(
            side_effect=["maintenance", "active", "maintenance", "active"]
        )

        await self.env.run(self.juju_paas.check_charm_status, arg)


class TestDeployCharm(TestJujuPaasActivitiesBase):
    app_name = "my_app_name"
    channel = "latest"
    entity_url = "ch:my-charm"
    charm_info = CharmInfo(app_name, channel, entity_url)
    vdu_instantiate_input = VduInstantiateInput(vim_id, namespace, charm_info)

    async def test_deploy_charm_nominal_case(self):
        await self.env.run(self.juju_paas.deploy_charm, self.vdu_instantiate_input)
        self.model.deploy.assert_called_once_with(
            entity_url=self.entity_url,
            application_name=self.app_name,
            channel=self.channel,
        )

    async def test_deploy_charm_app_already_exists(self):
        self.add_application(self.app_name)
        with self.assertRaises(Exception) as err:
            await self.env.run(self.juju_paas.deploy_charm, self.vdu_instantiate_input)
        self.model.deploy.assert_not_called()
        self.assertEqual(
            str(err.exception.args[0]),
            "Application {} already exists".format(self.app_name),
        )

    async def test_deploy_charm_raises_exception(self):
        self.controller.get_model.side_effect = JujuError()
        with self.assertRaises(JujuError):
            await self.env.run(self.juju_paas.deploy_charm, self.vdu_instantiate_input)
        self.model.deploy.assert_not_called()
