""" Direct tests of fixes to wheels """

import os
import sys
from os.path import (join as pjoin, basename, realpath, abspath, exists, isdir)
import stat
from glob import glob
import shutil
from subprocess import check_call

from ..delocating import (DelocationError, delocate_wheel, patch_wheel,
                          DLC_PREFIX)
from ..tools import (get_install_names, set_install_name, zip2dir,
                     dir2zip, back_tick, get_install_id, get_archs)
from ..wheeltools import InWheel

from ..tmpdirs import InTemporaryDirectory, InGivenDirectory

from .pytest_tools import (assert_true, assert_false, assert_raises,
                           assert_equal)

from .test_install_names import DATA_PATH, EXT_LIBS
from .test_tools import (ARCH_32, ARCH_BOTH)


def _collect_wheel(globber):
    glob_path = pjoin(DATA_PATH, globber)
    wheels = glob(glob_path)
    if len(wheels) == 0:
        raise ValueError("No wheels for glob {}".format(glob_path))
    elif len(wheels) > 1:
        raise ValueError("Too many wheels for glob {} ({})".format(
            glob_path, '; '.join(wheels)))
    return wheels[0]


PLAT_WHEEL = _collect_wheel('fakepkg1-1.0-cp*.whl')
PURE_WHEEL = _collect_wheel('fakepkg2-1.0-py*.whl')
RPATH_WHEEL = _collect_wheel('fakepkg_rpath-1.0-cp*.whl')
STRAY_LIB = pjoin(DATA_PATH, 'libextfunc.dylib')
# The install_name in the wheel for the stray library
with open(pjoin(DATA_PATH, 'wheel_build_path.txt'), 'rt') as fobj:
    _wheel_build_path = fobj.read().strip()
STRAY_LIB_DEP = _wheel_build_path + '/fakepkg1/libs/libextfunc.dylib'
WHEEL_PATCH = pjoin(DATA_PATH, 'fakepkg2.patch')
WHEEL_PATCH_BAD = pjoin(DATA_PATH, 'fakepkg2.bad_patch')


def test_fix_pure_python():
    # Test fixing a pure python package gives no change
    with InTemporaryDirectory():
        os.makedirs('wheels')
        shutil.copy2(PURE_WHEEL, 'wheels')
        wheel_name = pjoin('wheels', basename(PURE_WHEEL))
        assert_equal(delocate_wheel(wheel_name), {})
        zip2dir(wheel_name, 'pure_pkg')
        assert_true(exists(pjoin('pure_pkg', 'fakepkg2')))
        assert_false(exists(pjoin('pure_pkg', 'fakepkg2', '.dylibs')))


def _fixed_wheel(out_path):
    wheel_base = basename(PLAT_WHEEL)
    with InGivenDirectory(out_path):
        zip2dir(PLAT_WHEEL, '_plat_pkg')
        if not exists('_libs'):
            os.makedirs('_libs')
        shutil.copy2(STRAY_LIB, '_libs')
        stray_lib = pjoin(abspath(realpath('_libs')), basename(STRAY_LIB))
        requiring = pjoin('_plat_pkg', 'fakepkg1', 'subpkg', 'module2.so')
        old_lib = set(get_install_names(requiring)).difference(EXT_LIBS).pop()
        set_install_name(requiring, old_lib, stray_lib)
        dir2zip('_plat_pkg', wheel_base)
        shutil.rmtree('_plat_pkg')
    return pjoin(out_path, wheel_base), stray_lib


def _rename_module(in_wheel, mod_fname, out_wheel):
    # Rename module with library dependency in wheel
    with InWheel(in_wheel, out_wheel):
        mod_dir = pjoin('fakepkg1', 'subpkg')
        os.rename(pjoin(mod_dir, 'module2.so'), pjoin(mod_dir, mod_fname))
    return out_wheel


def test_fix_plat():
    # Can we fix a wheel with a stray library?
    # We have to make one that works first
    with InTemporaryDirectory() as tmpdir:
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        assert_true(exists(stray_lib))
        # Shortcut
        _rp = realpath
        # In-place fix
        dep_mod = pjoin('fakepkg1', 'subpkg', 'module2.so')
        assert_equal(delocate_wheel(fixed_wheel),
                     {_rp(stray_lib): {dep_mod: stray_lib}})
        zip2dir(fixed_wheel, 'plat_pkg')
        assert_true(exists(pjoin('plat_pkg', 'fakepkg1')))
        dylibs = pjoin('plat_pkg', 'fakepkg1', '.dylibs')
        assert_true(exists(dylibs))
        assert_equal(os.listdir(dylibs), ['libextfunc.dylib'])
        # New output name
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        assert_equal(delocate_wheel(fixed_wheel, 'fixed_wheel.ext'),
                     {_rp(stray_lib): {dep_mod: stray_lib}})
        zip2dir('fixed_wheel.ext', 'plat_pkg1')
        assert_true(exists(pjoin('plat_pkg1', 'fakepkg1')))
        dylibs = pjoin('plat_pkg1', 'fakepkg1', '.dylibs')
        assert_true(exists(dylibs))
        assert_equal(os.listdir(dylibs), ['libextfunc.dylib'])
        # Test another lib output directory
        assert_equal(delocate_wheel(fixed_wheel,
                                    'fixed_wheel2.ext',
                                    'dylibs_dir'),
                     {_rp(stray_lib): {dep_mod: stray_lib}})
        zip2dir('fixed_wheel2.ext', 'plat_pkg2')
        assert_true(exists(pjoin('plat_pkg2', 'fakepkg1')))
        dylibs = pjoin('plat_pkg2', 'fakepkg1', 'dylibs_dir')
        assert_true(exists(dylibs))
        assert_equal(os.listdir(dylibs), ['libextfunc.dylib'])
        # Test check for existing output directory
        assert_raises(DelocationError,
                      delocate_wheel,
                      fixed_wheel,
                      'broken_wheel.ext',
                      'subpkg')
        # Test that `wheel unpack` works
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        assert_equal(delocate_wheel(fixed_wheel),
                     {_rp(stray_lib): {dep_mod: stray_lib}})
        back_tick([sys.executable, '-m', 'wheel', 'unpack', fixed_wheel])
        # Check that copied libraries have modified install_name_ids
        zip2dir(fixed_wheel, 'plat_pkg3')
        base_stray = basename(stray_lib)
        the_lib = pjoin('plat_pkg3', 'fakepkg1', '.dylibs', base_stray)
        inst_id = DLC_PREFIX + 'fakepkg1/' + base_stray
        assert_equal(get_install_id(the_lib), inst_id)


def test_script_permissions():
    with InTemporaryDirectory():
        os.makedirs('wheels')
        wheel_name, stray_lib = _fixed_wheel('wheels')
        whl_name = basename(wheel_name)
        wheel_name = pjoin('wheels', whl_name)
        script_name = pjoin('fakepkg1-1.0.data', 'scripts', 'fakescript.py')
        exe_name = pjoin('fakepkg1', 'ascript')
        lib_path = pjoin('fakepkg1', '.dylibs')
        mtimes = {}
        with InWheel(wheel_name):
            assert not isdir(lib_path)
            for path in (script_name, exe_name):
                st = os.stat(path)
                assert st.st_mode & stat.S_IXUSR
                assert st.st_mode & stat.S_IFREG
                mtimes[path] = st.st_mtime
        os.makedirs('fixed-wheels')
        out_whl = pjoin('fixed-wheels', whl_name)
        delocate_wheel(wheel_name, out_wheel=out_whl)
        with InWheel(out_whl):
            assert isdir(lib_path)
            for path in (script_name, exe_name):
                st = os.stat(path)
                assert st.st_mode & stat.S_IXUSR
                assert st.st_mode & stat.S_IFREG
                # Check modification time is the same as the original
                assert st.st_mtime == mtimes[path]


def test_fix_plat_dylibs():
    # Check default and non-default searches for dylibs
    with InTemporaryDirectory() as tmpdir:
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        _rename_module(fixed_wheel, 'module.other', 'test.whl')
        # With dylibs-only - only analyze files with exts '.dylib', '.so'
        assert_equal(delocate_wheel('test.whl', lib_filt_func='dylibs-only'),
                     {})
        # With func that doesn't find the module

        def func(fn):
            return fn.endswith('.so')

        assert_equal(delocate_wheel('test.whl', lib_filt_func=func), {})
        # Default - looks in every file
        shutil.copyfile('test.whl', 'test2.whl')  # for following test
        dep_mod = pjoin('fakepkg1', 'subpkg', 'module.other')
        assert_equal(delocate_wheel('test.whl'),
                     {realpath(stray_lib): {dep_mod: stray_lib}})
        # With func that does find the module

        def func(fn):
            return fn.endswith('.other')

        assert_equal(delocate_wheel('test2.whl', lib_filt_func=func),
                     {realpath(stray_lib): {dep_mod: stray_lib}})


def _thin_lib(stray_lib, arch):
    check_call(['lipo', '-thin', arch, stray_lib, '-output', stray_lib])


def _thin_mod(wheel, arch):
    with InWheel(wheel, wheel):
        mod_fname = pjoin('fakepkg1', 'subpkg', 'module2.so')
        check_call(['lipo', '-thin', arch, mod_fname, '-output', mod_fname])


def test__thinning():
    with InTemporaryDirectory() as tmpdir:
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        mod_fname = pjoin('fakepkg1', 'subpkg', 'module2.so')
        assert_equal(get_archs(stray_lib), ARCH_BOTH)
        with InWheel(fixed_wheel):
            assert_equal(get_archs(mod_fname), ARCH_BOTH)
        _thin_lib(stray_lib, 'i386')
        _thin_mod(fixed_wheel, 'i386')
        assert_equal(get_archs(stray_lib), ARCH_32)
        with InWheel(fixed_wheel):
            assert_equal(get_archs(mod_fname), ARCH_32)


def test_check_plat_archs():
    # Check flag to check architectures
    with InTemporaryDirectory() as tmpdir:
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        dep_mod = pjoin('fakepkg1', 'subpkg', 'module2.so')
        # No complaint for stored / fixed wheel
        assert_equal(delocate_wheel(fixed_wheel, require_archs=()),
                     {realpath(stray_lib): {dep_mod: stray_lib}})
        # Make a new copy and break it and fix it again

        def _fix_break(arch_):
            _fixed_wheel(tmpdir)
            _thin_lib(stray_lib, arch_)

        def _fix_break_fix(arch_):
            _fixed_wheel(tmpdir)
            _thin_lib(stray_lib, arch_)
            _thin_mod(fixed_wheel, arch_)

        for arch in ('x86_64', 'i386'):
            # OK unless we check
            _fix_break(arch)
            assert_equal(
                delocate_wheel(fixed_wheel, require_archs=None),
                {realpath(stray_lib): {dep_mod: stray_lib}})
            # Now we check, and error raised
            _fix_break(arch)
            assert_raises(DelocationError, delocate_wheel, fixed_wheel,
                          require_archs=())
            # We can fix again by thinning the module too
            _fix_break_fix(arch)
            assert_equal(
                delocate_wheel(fixed_wheel, require_archs=()),
                {realpath(stray_lib): {dep_mod: stray_lib}})
            # But if we require the arch we don't have, it breaks
            for req_arch in ('intel',
                             ARCH_BOTH,
                             ARCH_BOTH.difference([arch])):
                _fix_break_fix(arch)
                assert_raises(DelocationError, delocate_wheel, fixed_wheel,
                              require_archs=req_arch)
        # Can be verbose (we won't check output though)
        _fix_break('x86_64')
        assert_raises(DelocationError, delocate_wheel, fixed_wheel,
                      require_archs=(), check_verbose=True)


def test_patch_wheel():
    # Check patching of wheel
    with InTemporaryDirectory():
        # First wheel needs proper wheel filename for later unpack test
        out_fname = basename(PURE_WHEEL)
        patch_wheel(PURE_WHEEL, WHEEL_PATCH, out_fname)
        zip2dir(out_fname, 'wheel1')
        with open(pjoin('wheel1', 'fakepkg2', '__init__.py'), 'rt') as fobj:
            assert_equal(fobj.read(), 'print("Am in init")\n')
        # Check that wheel unpack works
        back_tick([sys.executable, '-m', 'wheel', 'unpack', out_fname])
        # Copy the original, check it doesn't have patch
        shutil.copyfile(PURE_WHEEL, 'copied.whl')
        zip2dir('copied.whl', 'wheel2')
        with open(pjoin('wheel2', 'fakepkg2', '__init__.py'), 'rt') as fobj:
            assert_equal(fobj.read(), '')
        # Overwrite input wheel (the default)
        patch_wheel('copied.whl', WHEEL_PATCH)
        # Patched
        zip2dir('copied.whl', 'wheel3')
        with open(pjoin('wheel3', 'fakepkg2', '__init__.py'), 'rt') as fobj:
            assert_equal(fobj.read(), 'print("Am in init")\n')
        # Check bad patch raises error
        assert_raises(RuntimeError,
                      patch_wheel, PURE_WHEEL, WHEEL_PATCH_BAD, 'out.whl')


def test_fix_rpath():
    # Test wheels which have an @rpath dependency
    # Also verifies the delocated libraries signature
    with InTemporaryDirectory():
        # The module was set to expect its dependency in the libs/ directory
        os.symlink(DATA_PATH, 'libs')

        stray_lib = realpath('libs/libextfunc_rpath.dylib')
        with InWheel(RPATH_WHEEL):
            # dep_mod can vary depending the Python version used to build
            # the test wheel
            dep_mod = 'fakepkg/subpkg/module2.so'
        dep_path = '@rpath/libextfunc_rpath.dylib'

        assert_equal(
            delocate_wheel(RPATH_WHEEL, 'tmp.whl'),
            {stray_lib: {dep_mod: dep_path}},
        )
        with InWheel('tmp.whl'):
            check_call(['codesign', '--verify',
                        'fakepkg/.dylibs/libextfunc_rpath.dylib'])