# Copyright 2020 Red Hat, Inc. All rights reserved.
#
# 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.
from unittest import mock

from oslo_config import cfg
from oslo_config import fixture as oslo_fixture
import pecan

from octavia.api import config as pconfig
from octavia.api.healthcheck import healthcheck_plugins
from octavia.tests.functional.db import base as base_db_test


class TestHealthCheck(base_db_test.OctaviaDBTestBase):

    def setUp(self):
        super().setUp()

        # We need to define these early as they are late loaded in oslo
        # middleware and our configuration overrides would not apply.
        # Note: These must match exactly the option definitions in
        # oslo.middleware healthcheck! If not you will get duplicate option
        # errors.
        healthcheck_opts = [
            cfg.BoolOpt(
                'detailed', default=False,
                help='Show more detailed information as part of the response. '
                     'Security note: Enabling this option may expose '
                     'sensitive details about the service being monitored. '
                     'Be sure to verify that it will not violate your '
                     'security policies.'),
            cfg.ListOpt(
                'backends', default=[],
                help='Additional backends that can perform health checks and '
                     'report that information back as part of a request.'),
        ]
        cfg.CONF.register_opts(healthcheck_opts, group='healthcheck')

        self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
        # Mock log_opt_values, it prevents the dump of the configuration
        # with LOG.info for each test. It saves a lot of time when running
        # the functional tests.
        self.conf.conf.log_opt_values = mock.MagicMock()
        self.conf.config(group='healthcheck', backends=['octavia_db_check'])
        self.conf.config(group='api_settings', healthcheck_refresh_interval=5)
        self.UNAVAILABLE = (healthcheck_plugins.OctaviaDBHealthcheck.
                            UNAVAILABLE_REASON)

        def reset_pecan():
            pecan.set_config({}, overwrite=True)

        self.addCleanup(reset_pecan)

    def _make_app(self):
        # Note: we need to set argv=() to stop the wsgi setup_app from
        # pulling in the testing tool sys.argv
        return pecan.testing.load_test_app({'app': pconfig.app,
                                            'wsme': pconfig.wsme}, argv=())

    def _get_enabled_app(self):
        self.conf.config(group='api_settings', healthcheck_enabled=True)
        return self._make_app()

    def _get_disabled_app(self):
        self.conf.config(group='api_settings', healthcheck_enabled=False)
        return self._make_app()

    def _get(self, app, path, params=None, headers=None, status=200,
             expect_errors=False):
        response = app.get(path, params=params, headers=headers, status=status,
                           expect_errors=expect_errors)
        return response

    def _head(self, app, path, headers=None, status=204, expect_errors=False):
        response = app.head(path, headers=headers, status=status,
                            expect_errors=expect_errors)
        return response

    def _post(self, app, path, body, headers=None, status=201,
              expect_errors=False):
        response = app.post_json(path, params=body, headers=headers,
                                 status=status, expect_errors=expect_errors)
        return response

    def _put(self, app, path, body, headers=None, status=200,
             expect_errors=False):
        response = app.put_json(path, params=body, headers=headers,
                                status=status, expect_errors=expect_errors)
        return response

    def _delete(self, app, path, params=None, headers=None, status=204,
                expect_errors=False):
        response = app.delete(path, headers=headers, status=status,
                              expect_errors=expect_errors)
        return response

    def test_healthcheck_get_text(self):
        self.conf.config(group='healthcheck', detailed=False)
        response = self._get(self._get_enabled_app(), '/healthcheck')
        self.assertEqual(200, response.status_code)
        self.assertEqual('OK', response.text)

    # Note: For whatever reason, detailed=True text has no additonal info
    def test_healthcheck_get_text_detailed(self):
        self.conf.config(group='healthcheck', detailed=True)
        response = self._get(self._get_enabled_app(), '/healthcheck')
        self.assertEqual(200, response.status_code)
        self.assertEqual('OK', response.text)

    def test_healthcheck_get_text_plain(self):
        self.conf.config(group='healthcheck', detailed=False)
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             headers={'Accept': 'text/plain'})
        self.assertEqual(200, response.status_code)
        self.assertEqual('OK', response.text)

    def test_healthcheck_get_text_plain_detailed(self):
        self.conf.config(group='healthcheck', detailed=True)
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             headers={'Accept': 'text/plain'})
        self.assertEqual(200, response.status_code)
        self.assertEqual('OK', response.text)

    def test_healthcheck_get_json(self):
        self.conf.config(group='healthcheck', detailed=False)
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             headers={'Accept': 'application/json'})
        self.assertEqual(200, response.status_code)
        self.assertFalse(response.json['detailed'])
        self.assertEqual(['OK'], response.json['reasons'])

    def test_healthcheck_get_json_detailed(self):
        self.conf.config(group='healthcheck', detailed=True)
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             headers={'Accept': 'application/json'})
        self.assertEqual(200, response.status_code)
        self.assertTrue(response.json['detailed'])
        self.assertEqual('OK', response.json['reasons'][0]['reason'])
        self.assertTrue(response.json['gc'])

    def test_healthcheck_get_html(self):
        self.conf.config(group='healthcheck', detailed=False)
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             headers={'Accept': 'text/html'})
        self.assertEqual(200, response.status_code)
        self.assertIn('OK', response.text)

    def test_healthcheck_get_html_detailed(self):
        self.conf.config(group='healthcheck', detailed=True)
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             headers={'Accept': 'text/html'})
        self.assertEqual(200, response.status_code)
        self.assertIn('OK', response.text)
        self.assertIn('Garbage collector', response.text)

    def test_healthcheck_get_text_cached(self):
        self.conf.config(group='healthcheck', detailed=False)
        app = self._get_enabled_app()
        for i in range(10):
            response = self._get(app, '/healthcheck')
            self.assertEqual(200, response.status_code)
            self.assertEqual('OK', response.text)

    def test_healthcheck_disabled_get(self):
        self._get(self._get_disabled_app(), '/healthcheck', status=404)

    def test_healthcheck_head(self):
        response = self._head(self._get_enabled_app(), '/healthcheck')
        self.assertEqual(204, response.status_code)

    def test_healthcheck_disabled_head(self):
        self._head(self._get_disabled_app(), '/healthcheck', status=404)

    # These should be denied by the API
    def test_healthcheck_post(self):
        self._post(self._get_enabled_app(), '/healthcheck',
                   {'foo': 'bar'}, status=405)

    def test_healthcheck_put(self):
        self._put(self._get_enabled_app(), '/healthcheck',
                  {'foo': 'bar'}, status=405)

    def test_healthcheck_delete(self):
        self._delete(self._get_enabled_app(), '/healthcheck',
                     status=405)

    @mock.patch('octavia.db.api.get_session')
    def test_healthcheck_get_failed(self, mock_get_session):
        mock_session = mock.MagicMock()
        mock_session.execute.side_effect = [Exception('boom')]
        mock_get_session.return_value = mock_session
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             status=503)
        self.assertEqual(503, response.status_code)
        self.assertEqual(self.UNAVAILABLE, response.text)

    @mock.patch('octavia.db.api.get_session')
    def test_healthcheck_head_failed(self, mock_get_session):
        mock_session = mock.MagicMock()
        mock_session.execute.side_effect = [Exception('boom')]
        mock_get_session.return_value = mock_session
        response = self._head(self._get_enabled_app(), '/healthcheck',
                              status=503)
        self.assertEqual(503, response.status_code)

    @mock.patch('octavia.db.healthcheck.check_database_connection',
                side_effect=Exception('boom'))
    def test_healthcheck_get_failed_check(self, mock_db_check):
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             status=503)
        self.assertEqual(503, response.status_code)
        self.assertEqual(self.UNAVAILABLE, response.text)

    @mock.patch('octavia.db.api.get_session')
    def test_healthcheck_get_json_failed(self, mock_get_session):
        self.conf.config(group='healthcheck', detailed=False)
        mock_session = mock.MagicMock()
        mock_session.execute.side_effect = [Exception('boom')]
        mock_get_session.return_value = mock_session
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             headers={'Accept': 'application/json'},
                             status=503)
        self.assertEqual(503, response.status_code)
        self.assertFalse(response.json['detailed'])
        self.assertEqual([self.UNAVAILABLE],
                         response.json['reasons'])

    @mock.patch('octavia.db.api.get_session')
    def test_healthcheck_get_json_detailed_failed(self, mock_get_session):
        self.conf.config(group='healthcheck', detailed=True)
        mock_session = mock.MagicMock()
        mock_session.execute.side_effect = [Exception('boom')]
        mock_get_session.return_value = mock_session
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             headers={'Accept': 'application/json'},
                             status=503)
        self.assertEqual(503, response.status_code)
        self.assertTrue(response.json['detailed'])
        self.assertEqual(self.UNAVAILABLE,
                         response.json['reasons'][0]['reason'])
        self.assertIn('boom', response.json['reasons'][0]['details'])

    @mock.patch('octavia.db.api.get_session')
    def test_healthcheck_get_html_failed(self, mock_get_session):
        self.conf.config(group='healthcheck', detailed=False)
        mock_session = mock.MagicMock()
        mock_session.execute.side_effect = [Exception('boom')]
        mock_get_session.return_value = mock_session
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             headers={'Accept': 'text/html'}, status=503)
        self.assertEqual(503, response.status_code)
        self.assertIn(self.UNAVAILABLE, response.text)

    @mock.patch('octavia.db.api.get_session')
    def test_healthcheck_get_html_detailed_failed(self, mock_get_session):
        self.conf.config(group='healthcheck', detailed=True)
        mock_session = mock.MagicMock()
        mock_session.execute.side_effect = [Exception('boom')]
        mock_get_session.return_value = mock_session
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             headers={'Accept': 'text/html'}, status=503)
        self.assertEqual(503, response.status_code)
        self.assertIn(self.UNAVAILABLE, response.text)
        self.assertIn('boom', response.text)
        self.assertIn('Garbage collector', response.text)

    # Note: For whatever reason, detailed=True text has no additonal info
    @mock.patch('octavia.db.api.get_session')
    def test_healthcheck_get_text_detailed_failed(self, mock_get_session):
        self.conf.config(group='healthcheck', detailed=True)
        mock_session = mock.MagicMock()
        mock_session.execute.side_effect = [Exception('boom')]
        mock_get_session.return_value = mock_session
        response = self._get(self._get_enabled_app(), '/healthcheck',
                             status=503)
        self.assertEqual(503, response.status_code)
        self.assertEqual(self.UNAVAILABLE, response.text)
