# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
""" Test scripts

If we appear to be running from the development directory, use the scripts in
the top-level folder ``scripts``.  Otherwise try and get the scripts from the
path
"""
from __future__ import division, print_function, absolute_import

import os
from os.path import (dirname, join as pjoin, isfile, abspath, realpath,
                     basename, exists, splitext)
import shutil

from ..tmpdirs import InTemporaryDirectory
from ..tools import back_tick, set_install_name, zip2dir, dir2zip
from ..wheeltools import InWheel
from .scriptrunner import ScriptRunner

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

from .test_install_names import EXT_LIBS
from .test_delocating import _make_libtree, _copy_to, _make_bare_depends
from .test_wheelies import (_fixed_wheel, PLAT_WHEEL, PURE_WHEEL,
                            STRAY_LIB_DEP, WHEEL_PATCH, WHEEL_PATCH_BAD,
                            _thin_lib, _thin_mod, _rename_module)
from .test_fuse import assert_same_tree
from .test_wheeltools import assert_winfo_similar


def _proc_lines(in_str):
    """ Decode `in_string` to str, split lines, strip whitespace

    Remove any empty lines.

    Parameters
    ----------
    in_str : bytes
        Input bytes for splitting, stripping

    Returns
    -------
    out_lines : list
        List of line ``str`` where each line has been stripped of leading and
        trailing whitespace and empty lines have been removed.
    """
    lines = in_str.decode('latin1').splitlines()
    return [line.strip() for line in lines if line.strip() != '']


lines_runner = ScriptRunner(output_processor=_proc_lines)
run_command = lines_runner.run_command
bytes_runner = ScriptRunner()


DATA_PATH = abspath(pjoin(dirname(__file__), 'data'))


def test_listdeps():
    # smokey tests of list dependencies command
    local_libs = set(['liba.dylib', 'libb.dylib', 'libc.dylib'])
    # single path, with libs
    code, stdout, stderr = run_command(['delocate-listdeps', DATA_PATH])
    assert_equal(set(stdout), local_libs)
    assert_equal(code, 0)
    # single path, no libs
    with InTemporaryDirectory():
        zip2dir(PURE_WHEEL, 'pure')
        code, stdout, stderr = run_command(['delocate-listdeps', 'pure'])
        assert_equal(set(stdout), set())
        assert_equal(code, 0)
        # Multiple paths one with libs
        zip2dir(PLAT_WHEEL, 'plat')
        code, stdout, stderr = run_command(
            ['delocate-listdeps', 'pure', 'plat'])
        assert_equal(stdout,
                     ['pure:', 'plat:', STRAY_LIB_DEP])
        assert_equal(code, 0)
        # With -d flag, get list of dependending modules
        code, stdout, stderr = run_command(
            ['delocate-listdeps', '-d', 'pure', 'plat'])
        assert_equal(stdout,
                     ['pure:', 'plat:', STRAY_LIB_DEP + ':',
                      pjoin('plat', 'fakepkg1', 'subpkg', 'module2.so')])
        assert_equal(code, 0)
    # With --all flag, get all dependencies
    code, stdout, stderr = run_command(
        ['delocate-listdeps', '--all', DATA_PATH])
    rp_ext_libs = set(realpath(L) for L in EXT_LIBS)
    assert_equal(set(stdout), local_libs | rp_ext_libs)
    assert_equal(code, 0)
    # Works on wheels as well
    code, stdout, stderr = run_command(
        ['delocate-listdeps', PURE_WHEEL])
    assert_equal(set(stdout), set())
    code, stdout, stderr = run_command(
        ['delocate-listdeps', PURE_WHEEL, PLAT_WHEEL])
    assert_equal(stdout,
                 [PURE_WHEEL + ':', PLAT_WHEEL + ':', STRAY_LIB_DEP])
    # -d flag (is also --dependency flag)
    m2 = pjoin('fakepkg1', 'subpkg', 'module2.so')
    code, stdout, stderr = run_command(
        ['delocate-listdeps', '--depending', PURE_WHEEL, PLAT_WHEEL])
    assert_equal(stdout,
                 [PURE_WHEEL + ':', PLAT_WHEEL + ':', STRAY_LIB_DEP + ':',
                  m2])
    # Can be used with --all
    code, stdout, stderr = run_command(
        ['delocate-listdeps', '--all', '--depending', PURE_WHEEL, PLAT_WHEEL])
    assert_equal(stdout,
                 [PURE_WHEEL + ':', PLAT_WHEEL + ':',
                  STRAY_LIB_DEP + ':', m2,
                  EXT_LIBS[1] + ':', m2])


def test_path():
    # Test path cleaning
    with InTemporaryDirectory():
        # Make a tree; use realpath for OSX /private/var - /var
        liba, _, _, test_lib, slibc, stest_lib = _make_libtree(
            realpath('subtree'))
        os.makedirs('fakelibs')
        # Make a fake external library to link to
        fake_lib = realpath(_copy_to(liba, 'fakelibs', 'libfake.dylib'))
        _, _, _, test_lib, slibc, stest_lib = _make_libtree(
            realpath('subtree2'))
        back_tick([test_lib])
        back_tick([stest_lib])
        set_install_name(slibc, EXT_LIBS[0], fake_lib)
        # Check it fixes up correctly
        code, stdout, stderr = run_command(
            ['delocate-path', 'subtree', 'subtree2', '-L', 'deplibs'])
        assert_equal(len(os.listdir(pjoin('subtree', 'deplibs'))), 0)
        # Check fake libary gets copied and delocated
        out_path = pjoin('subtree2', 'deplibs')
        assert_equal(os.listdir(out_path), ['libfake.dylib'])


def test_path_dylibs():
    # Test delocate-path with and without dylib extensions
    with InTemporaryDirectory():
        # With 'dylibs-only' - does not inspect non-dylib files
        liba, bare_b = _make_bare_depends()
        out_dypath = pjoin('subtree', 'deplibs')
        code, stdout, stderr = run_command(
            ['delocate-path', 'subtree', '-L', 'deplibs', '-d'])
        assert_equal(len(os.listdir(out_dypath)), 0)
        code, stdout, stderr = run_command(
            ['delocate-path', 'subtree', '-L', 'deplibs', '--dylibs-only'])
        assert_equal(len(os.listdir(pjoin('subtree', 'deplibs'))), 0)
        # Default - does inspect non-dylib files
        code, stdout, stderr = run_command(
            ['delocate-path', 'subtree', '-L', 'deplibs'])
        assert_equal(os.listdir(out_dypath), ['liba.dylib'])


def _check_wheel(wheel_fname, lib_sdir):
    wheel_fname = abspath(wheel_fname)
    with InTemporaryDirectory():
        zip2dir(wheel_fname, 'plat_pkg')
        dylibs = pjoin('plat_pkg', 'fakepkg1', lib_sdir)
        assert_true(exists(dylibs))
        assert_equal(os.listdir(dylibs), ['libextfunc.dylib'])


def test_wheel():
    # Some tests for wheel fixing
    with InTemporaryDirectory() as tmpdir:
        # Default in-place fix
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        code, stdout, stderr = run_command(
            ['delocate-wheel', fixed_wheel])
        _check_wheel(fixed_wheel, '.dylibs')
        # Make another copy to test another output directory
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        code, stdout, stderr = run_command(
            ['delocate-wheel', '-L', 'dynlibs_dir', fixed_wheel])
        _check_wheel(fixed_wheel, 'dynlibs_dir')
        # Another output directory
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        code, stdout, stderr = run_command(
            ['delocate-wheel', '-w', 'fixed', fixed_wheel])
        _check_wheel(pjoin('fixed', basename(fixed_wheel)), '.dylibs')
        # More than one wheel
        shutil.copy2(fixed_wheel, 'wheel_copy.ext')
        code, stdout, stderr = run_command(
            ['delocate-wheel', '-w', 'fixed2', fixed_wheel, 'wheel_copy.ext'])
        assert_equal(stdout,
                     ['Fixing: ' + name
                      for name in (fixed_wheel, 'wheel_copy.ext')])
        _check_wheel(pjoin('fixed2', basename(fixed_wheel)), '.dylibs')
        _check_wheel(pjoin('fixed2', 'wheel_copy.ext'), '.dylibs')
        # Verbose - single wheel
        code, stdout, stderr = run_command(
            ['delocate-wheel', '-w', 'fixed3', fixed_wheel, '-v'])
        _check_wheel(pjoin('fixed3', basename(fixed_wheel)), '.dylibs')
        wheel_lines1 = ['Fixing: ' + fixed_wheel,
                        'Copied to package .dylibs directory:',
                        stray_lib]
        assert_equal(stdout, wheel_lines1)
        code, stdout, stderr = run_command(
            ['delocate-wheel', '-v', '--wheel-dir', 'fixed4',
             fixed_wheel, 'wheel_copy.ext'])
        wheel_lines2 = ['Fixing: wheel_copy.ext',
                        'Copied to package .dylibs directory:',
                        stray_lib]
        assert_equal(stdout, wheel_lines1 + wheel_lines2)


def test_fix_wheel_dylibs():
    # Check default and non-default search for dynamic libraries
    with InTemporaryDirectory() as tmpdir:
        # Default in-place fix
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        _rename_module(fixed_wheel, 'module.other', 'test.whl')
        shutil.copyfile('test.whl', 'test2.whl')
        # Default is to look in all files and therefore fix
        code, stdout, stderr = run_command(
            ['delocate-wheel', 'test.whl'])
        _check_wheel('test.whl', '.dylibs')
        # Can turn this off to only look in dynamic lib exts
        code, stdout, stderr = run_command(
            ['delocate-wheel', 'test2.whl', '-d'])
        with InWheel('test2.whl'):  # No fix
            assert_false(exists(pjoin('fakepkg1', '.dylibs')))


def test_fix_wheel_archs():
    # Some tests for wheel fixing
    with InTemporaryDirectory() as tmpdir:
        # Test check of architectures
        fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
        # Fixed wheel, architectures are OK
        code, stdout, stderr = run_command(
            ['delocate-wheel', fixed_wheel, '-k'])
        _check_wheel(fixed_wheel, '.dylibs')
        # Broken with one architecture removed still OK without checking
        # But if we check, raise error
        fmt_str = 'Fixing: {0}\n{1} needs arch {2} missing from {3}'
        archs = set(('x86_64', 'i386'))

        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 archs:
            # Not checked
            _fix_break(arch)
            code, stdout, stderr = run_command(
                ['delocate-wheel', fixed_wheel])
            _check_wheel(fixed_wheel, '.dylibs')
            # Checked
            _fix_break(arch)
            code, stdout, stderr = bytes_runner.run_command(
                ['delocate-wheel', fixed_wheel, '--check-archs'],
                check_code=False)
            assert_false(code == 0)
            stderr = stderr.decode('latin1').strip()
            assert_true(stderr.startswith('Traceback'))
            assert_true(stderr.endswith(
                "Some missing architectures in wheel"))
            assert_equal(stdout.strip(), b'')
            # Checked, verbose
            _fix_break(arch)
            code, stdout, stderr = bytes_runner.run_command(
                ['delocate-wheel', fixed_wheel, '--check-archs', '-v'],
                check_code=False)
            assert_false(code == 0)
            stderr = stderr.decode('latin1').strip()
            assert_true(stderr.startswith('Traceback'))
            assert_true(stderr.endswith(
                "Some missing architectures in wheel"))
            stdout = stdout.decode('latin1').strip()
            assert_equal(stdout,
                         fmt_str.format(
                             fixed_wheel,
                             'fakepkg1/subpkg/module2.so',
                             archs.difference([arch]).pop(),
                             stray_lib))
            # Require particular architectures
        both_archs = 'i386,x86_64'
        for ok in ('intel', 'i386', 'x86_64', both_archs):
            _fixed_wheel(tmpdir)
            code, stdout, stderr = run_command(
                ['delocate-wheel', fixed_wheel, '--require-archs=' + ok])
        for arch in archs:
            other_arch = archs.difference([arch]).pop()
            for not_ok in ('intel', both_archs, other_arch):
                _fix_break_fix(arch)
                code, stdout, stderr = run_command(
                    ['delocate-wheel', fixed_wheel,
                     '--require-archs=' + not_ok],
                    check_code=False)
                assert_false(code == 0)


def test_fuse_wheels():
    # Some tests for wheel fusing
    with InTemporaryDirectory():
        zip2dir(PLAT_WHEEL, 'to_wheel')
        zip2dir(PLAT_WHEEL, 'from_wheel')
        dir2zip('to_wheel', 'to_wheel.whl')
        dir2zip('from_wheel', 'from_wheel.whl')
        code, stdout, stderr = run_command(
            ['delocate-fuse', 'to_wheel.whl', 'from_wheel.whl'])
        assert_equal(code, 0)
        zip2dir('to_wheel.whl', 'to_wheel_fused')
        assert_same_tree('to_wheel_fused', 'from_wheel')
        # Test output argument
        os.mkdir('wheels')
        code, stdout, stderr = run_command(
            ['delocate-fuse', 'to_wheel.whl', 'from_wheel.whl',
             '-w', 'wheels'])
        zip2dir(pjoin('wheels', 'to_wheel.whl'), 'to_wheel_refused')
        assert_same_tree('to_wheel_refused', 'from_wheel')


def test_patch_wheel():
    # Some tests for patching wheel
    with InTemporaryDirectory():
        shutil.copyfile(PURE_WHEEL, 'example.whl')
        # Default is to overwrite input
        code, stdout, stderr = run_command(
            ['delocate-patch', 'example.whl', WHEEL_PATCH])
        zip2dir('example.whl', 'wheel1')
        with open(pjoin('wheel1', 'fakepkg2', '__init__.py'), 'rt') as fobj:
            assert_equal(fobj.read(), 'print("Am in init")\n')
        # Pass output directory
        shutil.copyfile(PURE_WHEEL, 'example.whl')
        code, stdout, stderr = run_command(
            ['delocate-patch', 'example.whl', WHEEL_PATCH, '-w', 'wheels'])
        zip2dir(pjoin('wheels', 'example.whl'), 'wheel2')
        with open(pjoin('wheel2', 'fakepkg2', '__init__.py'), 'rt') as fobj:
            assert_equal(fobj.read(), 'print("Am in init")\n')
        # Bad patch fails
        shutil.copyfile(PURE_WHEEL, 'example.whl')
        assert_raises(RuntimeError,
                      run_command,
                      ['delocate-patch', 'example.whl', WHEEL_PATCH_BAD])


def test_add_platforms():
    # Check adding platform to wheel name and tag section
    exp_items = [('Generator', 'bdist_wheel {pip_version}'),
                 ('Root-Is-Purelib', 'false'),
                 ('Tag', '{pyver}-{abi}-macosx_10_6_intel'),
                 ('Wheel-Version', '1.0')]
    assert_winfo_similar(PLAT_WHEEL, exp_items, drop_version=False)
    with InTemporaryDirectory() as tmpdir:
        # First wheel needs proper wheel filename for later unpack test
        out_fname = basename(PURE_WHEEL)
        # Need to specify at least one platform
        assert_raises(RuntimeError, run_command,
                      ['delocate-addplat', PURE_WHEEL, '-w', tmpdir])
        plat_args = ['-p', 'macosx_10_9_intel',
                     '--plat-tag', 'macosx_10_9_x86_64']
        # Can't add platforms to a pure wheel
        assert_raises(RuntimeError, run_command,
                      ['delocate-addplat', PURE_WHEEL, '-w', tmpdir] +
                      plat_args)
        assert_false(exists(out_fname))
        # Error raised (as above) unless ``--skip-error`` flag set
        code, stdout, stderr = run_command(
            ['delocate-addplat', PURE_WHEEL, '-w', tmpdir, '-k'] + plat_args)
        # Still doesn't do anything though
        assert_false(exists(out_fname))
        # Works for plat_wheel
        out_fname = (splitext(basename(PLAT_WHEEL))[0] +
                     '.macosx_10_9_intel.macosx_10_9_x86_64.whl')
        code, stdout, stderr = run_command(
            ['delocate-addplat', PLAT_WHEEL, '-w', tmpdir] + plat_args)
        assert_true(isfile(out_fname))
        # Expected output minus wheel-version (that might change)
        extra_exp = [('Generator', 'bdist_wheel {pip_version}'),
                     ('Root-Is-Purelib', 'false'),
                     ('Tag', '{pyver}-{abi}-macosx_10_6_intel'),
                     ('Tag', '{pyver}-{abi}-macosx_10_9_intel'),
                     ('Tag', '{pyver}-{abi}-macosx_10_9_x86_64')]
        assert_winfo_similar(out_fname, extra_exp)
        # If wheel exists (as it does) then raise error
        assert_raises(RuntimeError, run_command,
                      ['delocate-addplat', PLAT_WHEEL, '-w', tmpdir] +
                      plat_args)
        # Unless clobber is set
        code, stdout, stderr = run_command(
            ['delocate-addplat', PLAT_WHEEL, '-c', '-w', tmpdir] + plat_args)
        # Can also specify platform tags via --osx-ver flags
        code, stdout, stderr = run_command(
            ['delocate-addplat', PLAT_WHEEL, '-c', '-w', tmpdir, '-x', '10_9'])
        assert_winfo_similar(out_fname, extra_exp)
        # Can mix plat_tag and osx_ver
        out_big_fname = (splitext(basename(PLAT_WHEEL))[0] +
                         '.macosx_10_9_intel.macosx_10_9_x86_64'
                         '.macosx_10_10_intel.macosx_10_10_x86_64.whl')
        extra_big_exp = [('Generator', 'bdist_wheel {pip_version}'),
                         ('Root-Is-Purelib', 'false'),
                         ('Tag', '{pyver}-{abi}-macosx_10_10_intel'),
                         ('Tag', '{pyver}-{abi}-macosx_10_10_x86_64'),
                         ('Tag', '{pyver}-{abi}-macosx_10_6_intel'),
                         ('Tag', '{pyver}-{abi}-macosx_10_9_intel'),
                         ('Tag', '{pyver}-{abi}-macosx_10_9_x86_64')]
        code, stdout, stderr = run_command(
            ['delocate-addplat', PLAT_WHEEL, '-w', tmpdir, '-x', '10_10']
            + plat_args)
        assert_winfo_similar(out_big_fname, extra_big_exp)
        # Default is to write into directory of wheel
        os.mkdir('wheels')
        shutil.copy2(PLAT_WHEEL, 'wheels')
        local_plat = pjoin('wheels', basename(PLAT_WHEEL))
        local_out = pjoin('wheels', out_fname)
        code, stdout, stderr = run_command(
            ['delocate-addplat', local_plat] + plat_args)
        assert_true(exists(local_out))
        # With rm_orig flag, delete original unmodified wheel
        os.unlink(local_out)
        code, stdout, stderr = run_command(
            ['delocate-addplat', '-r', local_plat] + plat_args)
        assert_false(exists(local_plat))
        assert_true(exists(local_out))
        # Copy original back again
        shutil.copy2(PLAT_WHEEL, 'wheels')
        # If platforms already present, don't write more
        res = sorted(os.listdir('wheels'))
        assert_winfo_similar(local_out, extra_exp)
        code, stdout, stderr = run_command(
            ['delocate-addplat', local_out, '--clobber'] + plat_args)
        assert_equal(sorted(os.listdir('wheels')), res)
        assert_winfo_similar(local_out, extra_exp)
        # The wheel doesn't get deleted output name same as input, as here
        code, stdout, stderr = run_command(
            ['delocate-addplat', local_out, '-r', '--clobber'] + plat_args)
        assert_equal(sorted(os.listdir('wheels')), res)
        # But adds WHEEL tags if missing, even if file name is OK
        shutil.copy2(local_plat, local_out)
        assert_raises(AssertionError,
                      assert_winfo_similar, local_out, extra_exp)
        code, stdout, stderr = run_command(
            ['delocate-addplat', local_out, '--clobber'] + plat_args)
        assert_equal(sorted(os.listdir('wheels')), res)
        assert_winfo_similar(local_out, extra_exp)