# Copyright 2020 Canonical Ltd.
#
# 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

from unittest import mock, TestCase
from unittest.mock import Mock
from n2vc.juju_watcher import JujuModelWatcher, entity_ready, status
from n2vc.exceptions import EntityInvalidException
from .utils import FakeN2VC, AsyncMock, Deltas, FakeWatcher
from juju.application import Application
from juju.model import Model
from juju.annotation import Annotation
from juju.machine import Machine
from juju.action import Action


class JujuWatcherTest(asynctest.TestCase):
    def setUp(self):
        self.n2vc = FakeN2VC()
        self.model = Mock()
        self.loop = asyncio.new_event_loop()

    def test_get_status(self):
        tests = Deltas
        for test in tests:
            (status, message, vca_status) = JujuModelWatcher.get_status(test.delta)
            self.assertEqual(status, test.entity_status.status)
            self.assertEqual(message, test.entity_status.message)
            self.assertEqual(vca_status, test.entity_status.vca_status)

    @mock.patch("n2vc.juju_watcher.client.AllWatcherFacade.from_connection")
    def test_model_watcher(self, allwatcher):
        tests = Deltas
        allwatcher.return_value = FakeWatcher()
        n2vc = AsyncMock()
        for test in tests:
            with self.assertRaises(asyncio.TimeoutError):
                allwatcher.return_value.delta_to_return = [test.delta]
                self.loop.run_until_complete(
                    JujuModelWatcher.model_watcher(
                        self.model,
                        test.filter.entity_id,
                        test.filter.entity_type,
                        timeout=0,
                        db_dict={"something"},
                        n2vc=n2vc,
                        vca_id=None,
                    )
                )

            n2vc.write_app_status_to_db.assert_called()

    @mock.patch("n2vc.juju_watcher.asyncio.wait")
    def test_wait_for(self, wait):
        wait.return_value = asyncio.Future()
        wait.return_value.set_result(None)

        machine = AsyncMock()
        self.loop.run_until_complete(JujuModelWatcher.wait_for(self.model, machine))

    @mock.patch("n2vc.juju_watcher.asyncio.wait")
    def test_wait_for_exception(self, wait):
        wait.return_value = asyncio.Future()
        wait.return_value.set_result(None)
        wait.side_effect = Exception("error")

        machine = AsyncMock()
        with self.assertRaises(Exception):
            self.loop.run_until_complete(JujuModelWatcher.wait_for(self.model, machine))

    def test_wait_for_invalid_entity_exception(self):
        with self.assertRaises(EntityInvalidException):
            self.loop.run_until_complete(
                JujuModelWatcher.wait_for(
                    self.model,
                    Annotation(0, self.model),
                    total_timeout=None,
                    progress_timeout=None,
                )
            )


class EntityReadyTest(TestCase):
    @mock.patch("juju.application.Application.units")
    def setUp(self, mock_units):
        self.model = Model()
        self.model._connector = mock.MagicMock()

    def test_invalid_entity(self):
        with self.assertRaises(EntityInvalidException):
            entity_ready(Annotation(0, self.model))

    @mock.patch("juju.machine.Machine.agent_status")
    def test_machine_entity(self, mock_machine_agent_status):
        entity = Machine(0, self.model)
        self.assertEqual(entity.entity_type, "machine")
        self.assertTrue(isinstance(entity_ready(entity), bool))

    @mock.patch("juju.action.Action.status")
    def test_action_entity(self, mock_action_status):
        entity = Action(0, self.model)
        self.assertEqual(entity.entity_type, "action")
        self.assertTrue(isinstance(entity_ready(entity), bool))

    @mock.patch("juju.application.Application.status")
    def test_application_entity(self, mock_application_status):
        entity = Application(0, self.model)
        self.assertEqual(entity.entity_type, "application")
        self.assertTrue(isinstance(entity_ready(entity), bool))


class StatusTest(TestCase):
    def setUp(self):
        self.model = Model()
        self.model._connector = mock.MagicMock()

    @mock.patch("n2vc.juju_watcher.derive_status")
    def test_invalid_entity(self, mock_derive_status):
        application = mock.MagicMock()
        mock_derive_status.return_value = "active"

        class FakeUnit:
            @property
            def workload_status(self):
                return "active"

        application.units = [FakeUnit()]
        value = status(application)
        mock_derive_status.assert_called_once()
        self.assertTrue(isinstance(value, str))


@asynctest.mock.patch("asyncio.sleep")
class WaitForModelTest(asynctest.TestCase):
    @asynctest.mock.patch("juju.client.connector.Connector.connect")
    def setUp(self, mock_connect=None):
        self.loop = asyncio.new_event_loop()
        self.model = Model()

    @asynctest.mock.patch("juju.model.Model.block_until")
    def test_wait_for_model(self, mock_block_until, mock_sleep):
        self.loop.run_until_complete(
            JujuModelWatcher.wait_for_model(self.model, timeout=None)
        )
        mock_block_until.assert_called()

    @asynctest.mock.patch("asyncio.ensure_future")
    @asynctest.mock.patch("asyncio.wait")
    def test_wait_for_model_exception(self, mock_wait, mock_ensure_future, mock_sleep):
        task = Mock()
        mock_ensure_future.return_value = task
        mock_wait.side_effect = Exception
        with self.assertRaises(Exception):
            self.loop.run_until_complete(
                JujuModelWatcher.wait_for_model(self.model, timeout=None)
            )
        task.cancel.assert_called()
