import os
import subprocess
import tarfile
from unittest import TestCase
from contextlib import contextmanager

from backports.tempfile import TemporaryDirectory
from celery.exceptions import Retry
from hypothesis import given
from hypothesis import settings as hypothesis_settings
from hypothesis.strategies import text, integers
from mock import patch, Mock, ANY
from pathlib2 import Path

from src.conf.iniconf import SettingsPatcher, settings
from src.model_execution_worker.storage_manager import MissingInputsException
from src.model_execution_worker.tasks import start_analysis, InvalidInputsException, \
    start_analysis_task, get_oasislmf_config_path


#from oasislmf.utils.status import OASIS_TASK_STATUS
OASIS_TASK_STATUS = {
    'pending': {'id': 'PENDING', 'desc': 'Pending'},
    'running': {'id': 'RUNNING', 'desc': 'Running'},
    'success': {'id': 'SUCCESS', 'desc': 'Success'},
    'failure': {'id': 'FAILURE', 'desc': 'Failure'}
}

# Override default deadline for all tests to 8s
hypothesis_settings.register_profile("ci", deadline=800.0)
hypothesis_settings.load_profile("ci")


class StartAnalysis(TestCase):
    def create_tar(self, target):
        with TemporaryDirectory() as media_root, tarfile.open(target, 'w') as tar:
            paths = [
                Path(media_root, 'events.bin'),
                Path(media_root, 'returnperiods.bin'),
                Path(media_root, 'occurrence.bin'),
                Path(media_root, 'periods.bin'),
            ]

            for path in paths:
                path.touch()
                tar.add(str(path), path.name)

    def test_input_tar_file_does_not_exist___exception_is_raised(self):
        with TemporaryDirectory() as media_root:
            with SettingsPatcher(MEDIA_ROOT=media_root):
                Path(media_root, 'analysis_settings.json').touch()
                with self.assertRaises(MissingInputsException):
                    start_analysis(
                        input_location=os.path.join(media_root, 'non-existant-location.tar'),
                        analysis_settings=os.path.join(media_root, 'analysis_settings.json')
                    )

    def test_settings_file_does_not_exist___exception_is_raised(self):
        with TemporaryDirectory() as media_root:
            with SettingsPatcher(MEDIA_ROOT=media_root):
                self.create_tar(str(Path(media_root, 'location.tar')))
                with self.assertRaises(MissingInputsException):
                    start_analysis(
                        input_location=os.path.join(media_root, 'location.tar'),
                        analysis_settings=os.path.join(media_root, 'analysis_settings.json')
                    )

    def test_input_location_is_not_a_tar___exception_is_raised(self):
        with TemporaryDirectory() as media_root:
            with SettingsPatcher(MEDIA_ROOT=media_root):
                Path(media_root, 'not-tar-file.tar').touch()
                Path(media_root, 'analysis_settings.json').touch()
                self.assertRaises(InvalidInputsException, start_analysis, 
                    os.path.join(media_root, 'analysis_settings.json'), 
                    os.path.join(media_root, 'not-tar-file.tar')
                )

    def test_custom_model_runner_does_not_exist___generate_losses_is_called_output_files_are_tared_up(self):
        with TemporaryDirectory() as media_root, \
                TemporaryDirectory() as model_data_dir, \
                TemporaryDirectory() as run_dir, \
                TemporaryDirectory() as work_dir:
            with SettingsPatcher(
                    MODEL_SUPPLIER_ID='supplier',
                    MODEL_ID='model',
                    MODEL_VERSION_ID='version',
                    MEDIA_ROOT=media_root,
                    MODEL_DATA_DIRECTORY=model_data_dir,
                    WORKING_DIRECTORY=work_dir,):
                self.create_tar(str(Path(media_root, 'location.tar')))
                Path(media_root, 'analysis_settings.json').touch()
                Path(run_dir, 'output').mkdir(parents=True)
                Path(model_data_dir, 'supplier', 'model', 'version').mkdir(parents=True)

                cmd_instance = Mock()
                cmd_instance.stdout = b'output'
                cmd_instance.stderr = b'errors'

                @contextmanager
                def fake_run_dir(*args, **kwargs):
                    yield run_dir

                with patch('src.model_execution_worker.tasks.subprocess.run', Mock(return_value=cmd_instance)) as cmd_mock, \
                        patch('src.model_execution_worker.tasks.get_worker_versions', Mock(return_value='')), \
                        patch('src.model_execution_worker.tasks.filestore.compress') as tarfile, \
                        patch('src.model_execution_worker.tasks.TemporaryDir', fake_run_dir):

                    output_location, log_location, error_location, returncode = start_analysis(
                        os.path.join(media_root, 'analysis_settings.json'),
                        os.path.join(media_root, 'location.tar'),
                    )
                    cmd_mock.assert_called_once_with(['oasislmf', 'model', 'generate-losses',
                        '--oasis-files-dir', os.path.join(run_dir, 'input'),
                        '--config', get_oasislmf_config_path(settings.get('worker', 'model_id')),
                        '--model-run-dir', run_dir,
                        '--analysis-settings-json', os.path.join(media_root, 'analysis_settings.json'),
                        '--ktools-fifo-relative',
                        '--ktools-num-processes', settings.get('worker', 'KTOOLS_NUM_PROCESSES'),
                        '--ktools-alloc-rule-gul', settings.get('worker', 'KTOOLS_ALLOC_RULE_GUL'),
                        '--ktools-alloc-rule-il', settings.get('worker', 'KTOOLS_ALLOC_RULE_IL'),
                        '--ktools-alloc-rule-ri', settings.get('worker', 'KTOOLS_ALLOC_RULE_RI')
                    ], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
                    tarfile.assert_called_once_with(output_location, os.path.join(run_dir, 'output'), 'output')


class StartAnalysisTask(TestCase):
    @given(pk=integers(), location=text(), analysis_settings_path=text())
    def test_lock_is_not_acquireable___retry_esception_is_raised(self, pk, location, analysis_settings_path):
        with patch('fasteners.InterProcessLock.acquire', Mock(return_value=False)), \
             patch('src.model_execution_worker.tasks.notify_api_status') as api_notify:
            with self.assertRaises(Retry):
                start_analysis_task(pk, location, analysis_settings_path)

    @given(pk=integers(), location=text(), analysis_settings_path=text())
    def test_lock_is_acquireable___start_analysis_is_ran(self, pk, location, analysis_settings_path):
        with patch('src.model_execution_worker.tasks.start_analysis', Mock(return_value=('', '', '', 0))) as start_analysis_mock, \
        patch('src.model_execution_worker.tasks.notify_api_status') as api_notify:
            start_analysis_task.update_state = Mock()
            start_analysis_task(pk, location, analysis_settings_path)

            api_notify.assert_called_once_with(pk, 'RUN_STARTED')
            start_analysis_task.update_state.assert_called_once_with(state=OASIS_TASK_STATUS["running"]["id"])
            start_analysis_mock.assert_called_once_with(
                analysis_settings_path,
                location,
                complex_data_files=None
            )