import os
import platform

import pytest


pytest_plugins = 'pytester'

# could not make some of the tests work on PyPy, patches are welcome!
skip_pypy = pytest.mark.skipif(platform.python_implementation() == 'PyPy',
                               reason='could not make work on pypy')


class UnixFS(object):
    """
    Wrapper to os functions to simulate a Unix file system, used for testing
    the mock fixture.
    """

    @classmethod
    def rm(cls, filename):
        os.remove(filename)

    @classmethod
    def ls(cls, path):
        return os.listdir(path)


@pytest.fixture
def check_unix_fs_mocked(tmpdir, mocker):
    """
    performs a standard test in a UnixFS, assuming that both `os.remove` and
    `os.listdir` have been mocked previously.
    """

    def check(mocked_rm, mocked_ls):
        assert mocked_rm is os.remove
        assert mocked_ls is os.listdir

        file_name = tmpdir / 'foo.txt'
        file_name.ensure()

        UnixFS.rm(str(file_name))
        mocked_rm.assert_called_once_with(str(file_name))
        assert os.path.isfile(str(file_name))

        mocked_ls.return_value = ['bar.txt']
        assert UnixFS.ls(str(tmpdir)) == ['bar.txt']
        mocked_ls.assert_called_once_with(str(tmpdir))

        mocker.stopall()

        assert UnixFS.ls(str(tmpdir)) == ['foo.txt']
        UnixFS.rm(str(file_name))
        assert not os.path.isfile(str(file_name))

    return check


def mock_using_patch_object(mocker):
    return mocker.patch.object(os, 'remove'), mocker.patch.object(os, 'listdir')


def mock_using_patch(mocker):
    return mocker.patch('os.remove'), mocker.patch('os.listdir')


def mock_using_patch_multiple(mocker):
    from pytest_mock import mock_module

    r = mocker.patch.multiple('os', remove=mock_module.DEFAULT,
                              listdir=mock_module.DEFAULT)
    return r['remove'], r['listdir']


@pytest.mark.parametrize('mock_fs', [mock_using_patch_object, mock_using_patch,
                                     mock_using_patch_multiple],
)
def test_mock_patches(mock_fs, mocker, check_unix_fs_mocked):
    """
    Installs mocks into `os` functions and performs a standard testing of
    mock functionality. We parametrize different mock methods to ensure
    all (intended, at least) mock API is covered.
    """
    # mock it twice on purpose to ensure we unmock it correctly later
    mock_fs(mocker)
    mocked_rm, mocked_ls = mock_fs(mocker)
    check_unix_fs_mocked(mocked_rm, mocked_ls)


def test_mock_patch_dict(mocker):
    """
    Testing
    :param mock:
    """
    x = {'original': 1}
    mocker.patch.dict(x, values=[('new', 10)], clear=True)
    assert x == {'new': 10}
    mocker.stopall()
    assert x == {'original': 1}


def test_mock_fixture_is_deprecated(testdir):
    """
    Test that a warning emitted when using deprecated "mock" fixture.
    """
    testdir.makepyfile('''
        import warnings
        import os
        warnings.simplefilter('always')

        def test_foo(mock, tmpdir):
            mock.patch('os.listdir', return_value=['mocked'])
            assert os.listdir(str(tmpdir)) == ['mocked']
            mock.stopall()
            assert os.listdir(str(tmpdir)) == []
    ''')
    result = testdir.runpytest('-s')
    result.stderr.fnmatch_lines(['*"mock" fixture has been deprecated*'])


def test_deprecated_mock(mock, tmpdir):
    """
    Use backward-compatibility-only mock fixture to ensure complete coverage.
    """
    mock.patch('os.listdir', return_value=['mocked'])
    assert os.listdir(str(tmpdir)) == ['mocked']
    mock.stopall()
    assert os.listdir(str(tmpdir)) == []


def test_mocker_has_magic_mock_class_as_attribute_for_instantiation():
    from pytest_mock import mock_module, MockFixture

    mocker = MockFixture()
    assert isinstance(mocker.MagicMock(), mock_module.MagicMock)


def test_mocker_has_mock_class_as_attribute_for_instantiation():
    from pytest_mock import mock_module, MockFixture

    mocker = MockFixture()
    assert isinstance(mocker.Mock(), mock_module.Mock)


def test_mocker_stub(mocker):
    def foo(on_something):
        on_something('foo', 'bar')

    stub = mocker.stub()

    foo(stub)
    stub.assert_called_once_with('foo', 'bar')


def test_instance_method_spy(mocker):
    class Foo(object):

        def bar(self, arg):
            return arg * 2

    foo = Foo()
    other = Foo()
    spy = mocker.spy(foo, 'bar')
    assert foo.bar(arg=10) == 20
    assert other.bar(arg=10) == 20
    foo.bar.assert_called_once_with(arg=10)
    spy.assert_called_once_with(arg=10)


@skip_pypy
def test_instance_method_by_class_spy(mocker):
    from pytest_mock import mock_module

    class Foo(object):

        def bar(self, arg):
            return arg * 2

    spy = mocker.spy(Foo, 'bar')
    foo = Foo()
    other = Foo()
    assert foo.bar(arg=10) == 20
    assert other.bar(arg=10) == 20
    calls = [mock_module.call(foo, arg=10), mock_module.call(other, arg=10)]
    assert spy.call_args_list == calls


@skip_pypy
def test_class_method_spy(mocker):
    class Foo(object):

        @classmethod
        def bar(cls, arg):
            return arg * 2

    spy = mocker.spy(Foo, 'bar')
    assert Foo.bar(arg=10) == 20
    Foo.bar.assert_called_once_with(arg=10)
    spy.assert_called_once_with(arg=10)


@skip_pypy
def test_class_method_with_metaclass_spy(mocker):
    class MetaFoo(type):
        pass

    class Foo(object):

        __metaclass__ = MetaFoo

        @classmethod
        def bar(cls, arg):
            return arg * 2

    spy = mocker.spy(Foo, 'bar')
    assert Foo.bar(arg=10) == 20
    Foo.bar.assert_called_once_with(arg=10)
    spy.assert_called_once_with(arg=10)


@skip_pypy
def test_static_method_spy(mocker):
    class Foo(object):

        @staticmethod
        def bar(arg):
            return arg * 2

    spy = mocker.spy(Foo, 'bar')
    assert Foo.bar(arg=10) == 20
    Foo.bar.assert_called_once_with(arg=10)
    spy.assert_called_once_with(arg=10)
