#!/usr/bin/env python

# Copyright (c) 2017 Orange and others.
#
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Apache License, Version 2.0
# which accompanies this distribution, and is available at
# http://www.apache.org/licenses/LICENSE-2.0

# pylint: disable=missing-docstring

import os

from io import BytesIO
import logging
import subprocess
import unittest

import mock

from xtesting.core import feature
from xtesting.core import testcase
from xtesting.utils import constants


class FakeTestCase(feature.Feature):

    def execute(self, **kwargs):
        pass


class AbstractFeatureTesting(unittest.TestCase):

    def test_run_unimplemented(self):
        # pylint: disable=abstract-class-instantiated
        with self.assertRaises(TypeError):
            feature.Feature(case_name="feature", project_name="xtesting")


class FeatureTestingBase(unittest.TestCase):

    _case_name = "foo"
    _project_name = "bar"
    _repo = "dir_repo_bar"
    _cmd = "run_bar_tests.py"
    _output_file = os.path.join(constants.RESULTS_DIR, 'foo/foo.log')
    _max_duration = 1
    feature = None

    @mock.patch('time.time', side_effect=[1, 2])
    def _test_run(self, status, mock_method=None):
        self.assertEqual(self.feature.run(cmd=self._cmd), status)
        if status == testcase.TestCase.EX_OK:
            self.assertEqual(self.feature.result, 100)
        else:
            self.assertEqual(self.feature.result, 0)
        mock_method.assert_has_calls([mock.call(), mock.call()])
        self.assertEqual(self.feature.start_time, 1)
        self.assertEqual(self.feature.stop_time, 2)

    @mock.patch('time.time', side_effect=[1, 2])
    def _test_run_console(self, console, status, mock_method=None):
        self.assertEqual(
            self.feature.run(cmd=self._cmd, console=console), status)
        self.assertEqual(self.feature.result, 100)
        mock_method.assert_has_calls([mock.call(), mock.call()])
        self.assertEqual(self.feature.start_time, 1)
        self.assertEqual(self.feature.stop_time, 2)

    @mock.patch('time.time', side_effect=[1, 2])
    def _test_run_max_duration(self, status, mock_method=None):
        self.assertEqual(
            self.feature.run(cmd=self._cmd, max_duration=self._max_duration),
            status)
        mock_method.assert_has_calls([mock.call(), mock.call()])
        self.assertEqual(self.feature.start_time, 1)
        self.assertEqual(self.feature.stop_time, 2)


class FeatureTesting(FeatureTestingBase):

    def setUp(self):
        # logging must be disabled else it calls time.time()
        # what will break these unit tests.
        logging.disable(logging.CRITICAL)
        with mock.patch('builtins.open'):
            self.feature = FakeTestCase(
                project_name=self._project_name, case_name=self._case_name)

    def test_run_exc(self):
        with mock.patch.object(
                self.feature, 'execute',
                side_effect=Exception) as mock_method:
            self._test_run(testcase.TestCase.EX_RUN_ERROR)
            mock_method.assert_called_once_with(cmd=self._cmd)

    def test_run(self):
        self._test_run(testcase.TestCase.EX_RUN_ERROR)


class BashFeatureTesting(FeatureTestingBase):

    def setUp(self):
        # logging must be disabled else it calls time.time()
        # what will break these unit tests.
        logging.disable(logging.CRITICAL)
        with mock.patch('builtins.open'):
            self.feature = feature.BashFeature(
                project_name=self._project_name, case_name=self._case_name)

    @mock.patch('subprocess.Popen')
    def test_run_no_cmd(self, mock_subproc):
        self.assertEqual(
            self.feature.run(), testcase.TestCase.EX_RUN_ERROR)
        mock_subproc.assert_not_called()

    @mock.patch('os.path.isdir', return_value=True)
    @mock.patch('subprocess.Popen',
                side_effect=subprocess.CalledProcessError(0, '', ''))
    def test_run_ko1(self, *args):
        with mock.patch('builtins.open', mock.mock_open()) as mopen:
            self._test_run(testcase.TestCase.EX_RUN_ERROR)
        mopen.assert_called_once_with(self._output_file, "w", encoding='utf-8')
        args[0].assert_called_once_with(
            self._cmd, shell=True, stderr=mock.ANY, stdout=mock.ANY)
        args[1].assert_called_once_with(self.feature.res_dir)

    @mock.patch('os.path.isdir', return_value=True)
    @mock.patch('subprocess.Popen')
    def test_run_ko2(self, *args):
        stream = BytesIO()
        stream.write(b"foo")
        stream.seek(0)
        attrs = {'return_value.__enter__.return_value.stdout': stream,
                 'return_value.__enter__.return_value.returncode': 1}
        args[0].configure_mock(**attrs)
        with mock.patch('builtins.open', mock.mock_open()) as mopen:
            self._test_run(testcase.TestCase.EX_RUN_ERROR)
        self.assertIn(
            mock.call(self._output_file, 'w', encoding='utf-8'),
            mopen.mock_calls)
        self.assertIn(
            mock.call(self._output_file, 'r', encoding='utf-8'),
            mopen.mock_calls)
        args[0].assert_called_once_with(
            self._cmd, shell=True, stderr=mock.ANY, stdout=mock.ANY)
        args[1].assert_called_once_with(self.feature.res_dir)

    @mock.patch('subprocess.Popen')
    @mock.patch('os.path.isdir', return_value=True)
    def test_run_ko3(self, *args):
        stream = BytesIO()
        stream.write(b"foo")
        stream.seek(0)
        wait = mock.MagicMock(side_effect=subprocess.TimeoutExpired(
            cmd=FeatureTestingBase._cmd,
            timeout=FeatureTestingBase._max_duration))
        kill = mock.MagicMock()
        attrs = {'return_value.__enter__.return_value.wait': wait,
                 'return_value.__enter__.return_value.kill': kill,
                 'return_value.__enter__.return_value.stdout': stream,
                 'return_value.__enter__.return_value.returncode': 0}
        args[1].configure_mock(**attrs)
        with mock.patch('builtins.open', mock.mock_open()) as mopen:
            self._test_run_max_duration(testcase.TestCase.EX_RUN_ERROR)
        self.assertIn(
            mock.call(self._output_file, 'w', encoding='utf-8'),
            mopen.mock_calls)
        self.assertNotIn(
            mock.call(self._output_file, 'r', encoding='utf-8'),
            mopen.mock_calls)
        args[1].assert_called_once_with(
            self._cmd, shell=True, stderr=mock.ANY, stdout=mock.ANY)
        wait.assert_called_once_with(timeout=FeatureTestingBase._max_duration)
        kill.assert_called_once()
        args[0].assert_called_once_with(self.feature.res_dir)

    @mock.patch('os.path.isdir', return_value=True)
    @mock.patch('subprocess.Popen')
    def test_run1(self, *args):
        stream = BytesIO()
        stream.write(b"foo")
        stream.seek(0)
        attrs = {'return_value.__enter__.return_value.stdout': stream,
                 'return_value.__enter__.return_value.returncode': 0}
        args[0].configure_mock(**attrs)
        with mock.patch('builtins.open', mock.mock_open()) as mopen:
            self._test_run(testcase.TestCase.EX_OK)
        self.assertIn(
            mock.call(self._output_file, 'w', encoding='utf-8'),
            mopen.mock_calls)
        self.assertIn(
            mock.call(self._output_file, 'r', encoding='utf-8'),
            mopen.mock_calls)
        args[0].assert_called_once_with(
            self._cmd, shell=True, stderr=mock.ANY, stdout=mock.ANY)
        args[1].assert_called_once_with(self.feature.res_dir)

    @mock.patch('os.path.isdir', return_value=True)
    @mock.patch('subprocess.Popen')
    def test_run2(self, *args):
        stream = BytesIO()
        stream.write(b"foo")
        stream.seek(0)
        attrs = {'return_value.__enter__.return_value.stdout': stream,
                 'return_value.__enter__.return_value.returncode': 0}
        args[0].configure_mock(**attrs)
        with mock.patch('builtins.open', mock.mock_open()) as mopen:
            self._test_run_console(True, testcase.TestCase.EX_OK)
        self.assertIn(
            mock.call(self._output_file, 'w', encoding='utf-8'),
            mopen.mock_calls)
        self.assertIn(
            mock.call(self._output_file, 'r', encoding='utf-8'),
            mopen.mock_calls)
        args[0].assert_called_once_with(
            self._cmd, shell=True, stderr=mock.ANY, stdout=mock.ANY)
        args[1].assert_called_once_with(self.feature.res_dir)

    @mock.patch('os.path.isdir', return_value=True)
    @mock.patch('subprocess.Popen')
    def test_run3(self, *args):
        stream = BytesIO()
        stream.write(b"foo")
        stream.seek(0)
        attrs = {'return_value.__enter__.return_value.stdout': stream,
                 'return_value.__enter__.return_value.returncode': 0}
        args[0].configure_mock(**attrs)
        with mock.patch('builtins.open', mock.mock_open()) as mopen:
            self._test_run_console(False, testcase.TestCase.EX_OK)
        self.assertIn(
            mock.call(self._output_file, 'w', encoding='utf-8'),
            mopen.mock_calls)
        self.assertIn(
            mock.call(self._output_file, 'r', encoding='utf-8'),
            mopen.mock_calls)
        args[0].assert_called_once_with(
            self._cmd, shell=True, stderr=mock.ANY, stdout=mock.ANY)
        args[1].assert_called_once_with(self.feature.res_dir)

    @mock.patch('os.makedirs')
    @mock.patch('os.path.isdir', return_value=False)
    @mock.patch('subprocess.Popen')
    def test_run4(self, *args):
        stream = BytesIO()
        stream.write(b"foo")
        stream.seek(0)
        attrs = {'return_value.__enter__.return_value.stdout': stream,
                 'return_value.__enter__.return_value.returncode': 0}
        args[0].configure_mock(**attrs)
        with mock.patch('builtins.open', mock.mock_open()) as mopen:
            self._test_run_console(False, testcase.TestCase.EX_OK)
        self.assertIn(
            mock.call(self._output_file, 'w', encoding='utf-8'),
            mopen.mock_calls)
        self.assertIn(
            mock.call(self._output_file, 'r', encoding='utf-8'),
            mopen.mock_calls)
        args[0].assert_called_once_with(
            self._cmd, shell=True, stderr=mock.ANY, stdout=mock.ANY)
        args[1].assert_called_once_with(self.feature.res_dir)
        args[2].assert_called_once_with(self.feature.res_dir)


if __name__ == "__main__":
    unittest.main(verbosity=2)
