"""
This file contains the tests for the periodic tasks API.

This tests api/periodictask.py
"""
import jwt
from contextlib import contextmanager
import mock
from dateutil.parser import parse as parse_timestamp

from privacyidea.lib.periodictask import TASK_MODULES
from privacyidea.models import NodeName, db
from tests.base import MyApiTestCase


class APIPeriodicTasksTestCase(MyApiTestCase):
    @contextmanager
    def mock_task_module(self):
        """
        Mock a UnitTest task module, use as ``with self.mock_task_module() as module:``
        """
        taskmodule = mock.MagicMock()

        def cls(config):
            return taskmodule
        with mock.patch.dict(TASK_MODULES, {"UnitTest": cls}, clear=True):
            yield taskmodule

    def simulate_request(self, *args, **kwargs):
        """
        Run test request, return a tuple (status code, decoded body)
        If 'Authorization' is not given in the request headers, add ``self.at``.
        """
        headers = kwargs.get('headers', {}).copy()
        if 'Authorization' not in headers:
            headers['Authorization'] = self.at
        kwargs['headers'] = headers
        with self.app.test_request_context(*args, **kwargs):
            res = self.app.full_dispatch_request()
            return res.status_code, res.json

    def test_01_crud(self):
        # no tasks yet
        status_code, data = self.simulate_request('/periodictask/', method='GET')
        self.assertEqual(status_code, 200)
        self.assertTrue(data['result']['status'])
        self.assertEqual(data['result']['value'], [])

        # need authorization
        fake_auth_token = jwt.encode({"role": "admin"}, key="313233343536", algorithm="HS256")
        status_code, data = self.simulate_request('/periodictask/', method='GET',
                                                  headers={'Authorization': fake_auth_token})
        self.assertEqual(status_code, 401)
        self.assertFalse(data['result']['status'])

        # create task
        with self.mock_task_module():
            task_dict1 = {
                'name': 'some task',
                'nodes': 'pinode1, pinode2',
                'active': False,
                'interval': '0 8 * * *',
                'taskmodule': 'UnitTest',
                'ordering': 5,
                'retry_if_failed': False,
                'options': '{"something": 123, "else": true}',
            }
            status_code, data = self.simulate_request('/periodictask/', method='POST', data=task_dict1)
            self.assertEqual(status_code, 200)
            self.assertEqual(data['result']['status'], True)
            ptask_id1 = data['result']['value']

        # some invalid tasks
        invalid_task_dicts = [
            # invalid ordering
            {
                'name': 'some other task',
                'active': False,
                'nodes': 'a, b',
                'interval': '0 8 * * *',
                'taskmodule': 'UnitTest',
                'ordering': '-3',
                'options': '{"something": "123", "else": true}',
            },
            # no nodes
            {
                'name': 'some other task',
                'active': False,
                'interval': '0 8 * * *',
                'taskmodule': 'UnitTest',
                'options': '{"something": "123", "else": true}',
            },
            # empty nodes
            {
                'name': 'some other task',
                'active': False,
                'interval': '0 8 * * *',
                'nodes': '    ',
                'taskmodule': 'UnitTest',
                'options': '{"something": "123", "else": true}',
            },
            # unknown taskmodule
            {
                'name': 'some other task',
                'nodes': 'pinode1, pinode2',
                'active': False,
                'interval': '0 8 * * *',
                'taskmodule': 'Unknown',
                'options': '{"something": "123"}',
            },
            # invalid interval
            {
                'name': 'some other task',
                'nodes': 'pinode1, pinode2',
                'active': False,
                'interval': 'every day',
                'taskmodule': 'UnitTest',
                'options': '{"something": "123"}',
            },
            # invalid options
            {
                'name': 'some task',
                'nodes': 'pinode1, pinode2',
                'active': False,
                'interval': '0 8 * * *',
                'taskmodule': 'UnitTest',
                'options': '[1, 2]',
            }
        ]
        # all result in ERR905
        with self.mock_task_module():
            for invalid_task_dict in invalid_task_dicts:
                status_code, data = self.simulate_request('/periodictask/', method='POST',
                                                          data=invalid_task_dict)
                self.assertEqual(status_code, 400)
                self.assertFalse(data['result']['status'])
                self.assertIn('ERR905', data['result']['error']['message'])

        # create another task
        with self.mock_task_module():
            task_dict2 = {
                'name': 'some other task',
                'nodes': 'pinode1',
                'active': False,
                'interval': '0 8 * * 0',
                'taskmodule': 'UnitTest',
                'ordering': 2,
            }
            status_code, data = self.simulate_request('/periodictask/', method='POST',
                                                      data=task_dict2)

            self.assertEqual(status_code, 200)
            self.assertTrue(data['result']['status'])
            ptask_id2 = data['result']['value']

        # can list the periodic tasks
        status_code, data = self.simulate_request('/periodictask/', method='GET')
        self.assertEqual(status_code, 200)
        self.assertTrue(data['result']['status'])
        self.assertEqual(len(data['result']['value']), 2)
        self.assertEqual([task['name'] for task in data['result']['value']],
                         ['some other task', 'some task'])

        # find first task
        result_dict = data['result']['value'][1]
        self.assertEqual(result_dict['id'], ptask_id1)
        self.assertEqual(result_dict['ordering'], 5)
        self.assertEqual(result_dict['name'], 'some task')
        self.assertEqual(result_dict['active'], False)
        self.assertEqual(result_dict['interval'], '0 8 * * *')
        self.assertEqual(result_dict['nodes'], ['pinode1', 'pinode2'])
        self.assertEqual(result_dict['last_runs'], {})
        last_update = parse_timestamp(result_dict['last_update'])
        self.assertEqual(result_dict['retry_if_failed'], False)
        self.assertIsNotNone(last_update)
        self.assertEqual(result_dict['options'], {'something': '123', 'else': 'True'})

        # get one
        status_code, data = self.simulate_request('/periodictask/{}'.format(ptask_id1), method='GET')
        self.assertEqual(status_code, 200)
        self.assertEqual(data['result']['value']['id'], ptask_id1)

        # unknown ID
        status_code, data = self.simulate_request('/periodictask/4242', method='GET')
        self.assertEqual(status_code, 404)
        self.assertFalse(data['result']['status'])

        # update existing task
        task_dict1['name'] = 'new name'
        task_dict1['options'] = '{"key": "value"}'
        task_dict1['id'] = ptask_id1
        task_dict1['ordering'] = '2'
        with self.mock_task_module():
            status_code, data = self.simulate_request('/periodictask/', method='POST',
                                                      data=task_dict1)
            self.assertEqual(status_code, 200)
            self.assertTrue(data['result']['status'])
            self.assertEqual(data['result']['value'], ptask_id1)

        # can list the periodic tasks in new order
        status_code, data = self.simulate_request('/periodictask/', method='GET')
        self.assertEqual(status_code, 200)
        self.assertTrue(data['result']['status'])
        self.assertEqual(len(data['result']['value']), 2)
        self.assertIn('new name', [task['name'] for task in data['result']['value']], data)
        self.assertIn('some other task', [task['name'] for task in data['result']['value']], data)

        # get updated task
        status_code, data = self.simulate_request('/periodictask/{}'.format(ptask_id1), method='GET')
        self.assertEqual(status_code, 200)
        self.assertEqual(data['result']['value']['id'], ptask_id1)
        self.assertEqual(data['result']['value']['ordering'], 2)
        self.assertEqual(data['result']['value']['name'], 'new name')
        self.assertEqual(data['result']['value']['options'], {'key': 'value'})
        self.assertGreater(parse_timestamp(data['result']['value']['last_update']),
                           last_update)
        last_update = parse_timestamp(data['result']['value']['last_update'])

        # enable
        status_code, data = self.simulate_request('/periodictask/enable/{}'.format(ptask_id1), method='POST')
        self.assertEqual(status_code, 200)

        # get updated task
        status_code, data = self.simulate_request('/periodictask/{}'.format(ptask_id1), method='GET')
        self.assertEqual(status_code, 200)
        self.assertEqual(data['result']['value']['name'], 'new name')
        self.assertEqual(data['result']['value']['active'], True)
        self.assertGreater(parse_timestamp(data['result']['value']['last_update']),
                           last_update)
        last_update = parse_timestamp(data['result']['value']['last_update'])

        # disable
        status_code, data = self.simulate_request('/periodictask/disable/{}'.format(ptask_id1), method='POST')
        self.assertEqual(status_code, 200)

        # get updated task
        status_code, data = self.simulate_request('/periodictask/{}'.format(ptask_id1), method='GET')
        self.assertEqual(status_code, 200)
        self.assertEqual(data['result']['value']['name'], 'new name')
        self.assertEqual(data['result']['value']['active'], False)
        self.assertGreater(parse_timestamp(data['result']['value']['last_update']),
                           last_update)

        # disable again without effect
        status_code, data = self.simulate_request('/periodictask/disable/{}'.format(ptask_id1), method='POST')
        self.assertEqual(status_code, 200)

        # get updated task
        status_code, data = self.simulate_request('/periodictask/{}'.format(ptask_id1), method='GET')
        self.assertEqual(status_code, 200)
        self.assertEqual(data['result']['value']['name'], 'new name')
        self.assertEqual(data['result']['value']['active'], False)

        # delete
        status_code, data = self.simulate_request('/periodictask/{}'.format(ptask_id1), method='DELETE')
        self.assertEqual(status_code, 200)
        self.assertEqual(data['result']['value'], ptask_id1)

        # get updated task impossible now
        status_code, data = self.simulate_request('/periodictask/{}'.format(ptask_id1), method='GET')
        self.assertEqual(status_code, 404)
        self.assertFalse(data['result']['status'], False)

        # only 1 task left
        status_code, data = self.simulate_request('/periodictask/', method='GET')
        self.assertEqual(status_code, 200)
        self.assertTrue(data['result']['status'])
        self.assertEqual(len(data['result']['value']), 1)

        # delete the second task as well
        status_code, data = self.simulate_request('/periodictask/{}'.format(ptask_id2), method='DELETE')
        self.assertEqual(status_code, 200)
        self.assertEqual(data['result']['value'], ptask_id2)

        # no tasks left
        status_code, data = self.simulate_request('/periodictask/', method='GET')
        self.assertEqual(status_code, 200)
        self.assertTrue(data['result']['status'])
        self.assertEqual(data['result']['value'], [])

    def test_02_taskmodules(self):
        with self.mock_task_module() as taskmodule:
            status_code, data = self.simulate_request('/periodictask/taskmodules/', method='GET')
            self.assertEqual(status_code, 200)
            self.assertTrue(data['result']['status'])
            self.assertEqual(data['result']['value'], ['UnitTest'])

            options = {'key1': {'type': 'str', 'desc': 'description'},
                       'key2': {'type': 'str', 'desc': 'other description'}}
            taskmodule.options = options

            status_code, data = self.simulate_request('/periodictask/options/UnitTest', method='GET')
            self.assertEqual(status_code, 200)
            self.assertTrue(data['result']['status'])
            self.assertEqual(data['result']['value'], options)

    def test_03_nodes(self):
        db.session.add(NodeName(id="8e4272a9-9037-40df-8aa3-976e4a04b5a9", name="Node1"))
        db.session.add(NodeName(id="d1d7fde6-330f-4c12-88f3-58a1752594bf", name="Node2"))
        db.session.commit()
        status_code, data = self.simulate_request('/periodictask/nodes/', method='GET')
        self.assertEqual(status_code, 200)
        self.assertTrue(data['result']['status'])
        self.assertEqual({'Node2', 'Node1'}, set(data['result']['value']), data)
