import logging
import os
import unittest
from contextlib import contextmanager
from shutil import copyfile
from tempfile import TemporaryDirectory

import cherrypy
from cmapi_server import helpers
from cmapi_server.constants import CMAPI_CONF_PATH
from cmapi_server.controllers.dispatcher import dispatcher, jsonify_error
from cmapi_server.logging_management import config_cmapi_server_logging
from cmapi_server.managers.application import (
    AppStatefulConfig, StatefulConfigModel, StatefulFlagsModel
)
from cmapi_server.managers.process import MCSProcessManager
from cmapi_server.managers.certificate import CertificateManager


TEST_API_KEY = 'somekey123'
MCS_CONFIG_FILEPATH = '/etc/columnstore/Columnstore.xml'
# Use absolute paths relative to this test package so tests work regardless of CWD
_TEST_DIR = os.path.dirname(__file__)
COPY_MCS_CONFIG_FILEPATH = os.path.join(_TEST_DIR, 'original_Columnstore.xml')
TEST_MCS_CONFIG_FILEPATH = os.path.join(_TEST_DIR, 'CS-config-test.xml')
# TODO:
#       - rename after fix in all places
#       - fix path to abs
mcs_config_filename = os.path.join(_TEST_DIR, 'CS-config-test.xml')
tmp_mcs_config_filename = os.path.join(_TEST_DIR, 'tmp.xml')
cmapi_config_filename = os.path.join(os.path.dirname(_TEST_DIR), 'cmapi_server.conf')
tmp_cmapi_config_filename = os.path.join(_TEST_DIR, 'tmp.conf')
single_node_xml = os.path.join(_TEST_DIR, '..', 'SingleNode.xml')
# constants for process dispatchers
DDL_SERVICE = 'mcs-ddlproc'
CONTROLLERNODE_SERVICE = 'mcs-controllernode.service'
UNKNOWN_SERVICE = 'unknown_service'
SYSTEMCTL = 'systemctl'


logging.basicConfig(level=logging.DEBUG)


def run_detect_processes():
    cfg_parser = helpers.get_config_parser(CMAPI_CONF_PATH)
    d_name, d_path = helpers.get_dispatcher_name_and_path(cfg_parser)
    MCSProcessManager.detect(d_name, d_path)


@contextmanager
def run_server():
    CertificateManager.create_self_signed_certificate_if_not_exist()

    cherrypy.engine.start()
    cherrypy.engine.wait(cherrypy.engine.states.STARTED)
    run_detect_processes()  #TODO: Move cause slow down each test for 5s
    yield

    cherrypy.engine.exit()
    cherrypy.engine.block()


class BaseServerTestCase(unittest.TestCase):
    HEADERS = {'x-api-key': TEST_API_KEY}
    NO_AUTH_HEADERS = {'x-api-key': None}
    TEST_PARAMS = (
        ('auth ok', HEADERS, 200),
        ('no auth', NO_AUTH_HEADERS, 401)
    )

    def run(self, result=None):
        with TemporaryDirectory() as tmp_dir:
            self.tmp_dir = tmp_dir
            self.cmapi_config_filename = os.path.join(
                tmp_dir, 'tmp_cmapi_config.conf'
            )
            self.mcs_config_filename = os.path.join(
                tmp_dir, 'tmp_mcs_config.xml'
            )
            copyfile(cmapi_config_filename, self.cmapi_config_filename)
            copyfile(TEST_MCS_CONFIG_FILEPATH, self.mcs_config_filename)
            config_cmapi_server_logging()
            self.app = cherrypy.tree.mount(
                root=None, config=self.cmapi_config_filename
            )
            self.app.config.update({
                '/': {
                    'request.dispatch': dispatcher,
                    'error_page.default': jsonify_error,
                },
                'config': {
                    'path': self.cmapi_config_filename,
                },
                'Authentication' : self.HEADERS
            })
            cherrypy.config.update(self.cmapi_config_filename)

            with run_server():
                return super().run(result=result)


class BaseNodeManipTestCase(unittest.TestCase):
    NEW_NODE_NAME = 'node.hostname'

    def setUp(self):
        self.tmp_files = []
        copyfile(TEST_MCS_CONFIG_FILEPATH, tmp_mcs_config_filename)

    def tearDown(self):
        for tmp_file in self.tmp_files:
            if os.path.exists(tmp_file):
                os.remove(tmp_file)
        if os.path.exists(tmp_mcs_config_filename):
            os.remove(tmp_mcs_config_filename)

    def _set_shared_storage(self, target_value: bool) -> bool:
        """Set shared_storage_on flag to target_value, return original value.

        If the current value already equals target_value, no update is applied.
        """
        current_cfg = AppStatefulConfig.get_config_copy()
        original_value = current_cfg.flags.shared_storage_on
        if original_value != target_value:
            new_cfg = StatefulConfigModel(
                version=current_cfg.version.next_seq(),
                flags=StatefulFlagsModel(shared_storage_on=target_value),
            )
            AppStatefulConfig.apply_update(new_cfg)
        return original_value


class BaseProcessDispatcherCase(unittest.TestCase):
    node_started = None

    @classmethod
    def setUpClass(cls) -> None:
        run_detect_processes()
        cls.node_started = MCSProcessManager.get_running_mcs_procs() != 0
        return super().setUpClass()

    @classmethod
    def tearDownClass(cls) -> None:
        if (MCSProcessManager.get_running_mcs_procs() !=0) == cls.node_started:
            return super().tearDownClass()
        if cls.node_started:
            MCSProcessManager.start_node(is_primary=True, use_sudo=False)
        else:
            MCSProcessManager.stop_node(is_primary=True, use_sudo=False)
        return super().tearDownClass()

    def setUp(self) -> None:
        if MCSProcessManager.process_dispatcher.is_service_running(
            CONTROLLERNODE_SERVICE
        ):
            self.controller_node_cmd = 'start'
        else:
            self.controller_node_cmd = 'stop'
        # prevent to get 'start-limit-hit' systemd error, see MCOL-5186
        os.system(f'{SYSTEMCTL} reset-failed')
        return super().setUp()

    def tearDown(self) -> None:
        os.system(
            f'{SYSTEMCTL} {self.controller_node_cmd} {CONTROLLERNODE_SERVICE}'
        )
        return super().tearDown()
