# -*- coding: utf-8 -*-
import subprocess
import tempfile
import shutil
import errno
import os

import pexpect
from . import PexpectTestCase

import pytest


class TestCaseWhich(PexpectTestCase.PexpectTestCase):
    " Tests for pexpect.which(). "

    def test_which_finds_ls(self):
        " which() can find ls(1). "
        exercise = pexpect.which("ls")
        assert exercise is not None
        assert exercise.startswith('/')

    def test_os_defpath_which(self):
        " which() finds an executable in $PATH and returns its abspath. "

        bin_dir = tempfile.mkdtemp()
        temp_obj = tempfile.NamedTemporaryFile(
            suffix=u'.sh', prefix=u'ǝpoɔıun-',
            dir=bin_dir, delete=False)
        bin_path = temp_obj.name
        fname = os.path.basename(temp_obj.name)
        save_path = os.environ['PATH']
        save_defpath = os.defpath

        try:
            # setup
            os.environ['PATH'] = ''
            os.defpath = bin_dir
            with open(bin_path, 'w') as fp:
                pass

            # given non-executable,
            os.chmod(bin_path, 0o400)

            # exercise absolute and relative,
            assert pexpect.which(bin_path) is None
            assert pexpect.which(fname) is None

            # given executable,
            os.chmod(bin_path, 0o700)

            # exercise absolute and relative,
            assert pexpect.which(bin_path) == bin_path
            assert pexpect.which(fname) == bin_path

        finally:
            # restore,
            os.environ['PATH'] = save_path
            os.defpath = save_defpath

            # destroy scratch files and folders,
            if os.path.exists(bin_path):
                os.unlink(bin_path)
            if os.path.exists(bin_dir):
                os.rmdir(bin_dir)

    def test_path_search_which(self):
        " which() finds an executable in $PATH and returns its abspath. "
        fname = 'gcc'
        bin_dir = tempfile.mkdtemp()
        bin_path = os.path.join(bin_dir, fname)
        save_path = os.environ['PATH']
        try:
            # setup
            os.environ['PATH'] = bin_dir
            with open(bin_path, 'w') as fp:
                pass

            # given non-executable,
            os.chmod(bin_path, 0o400)

            # exercise absolute and relative,
            assert pexpect.which(bin_path) is None
            assert pexpect.which(fname) is None

            # given executable,
            os.chmod(bin_path, 0o700)

            # exercise absolute and relative,
            assert pexpect.which(bin_path) == bin_path
            assert pexpect.which(fname) == bin_path

        finally:
            # restore,
            os.environ['PATH'] = save_path

            # destroy scratch files and folders,
            if os.path.exists(bin_path):
                os.unlink(bin_path)
            if os.path.exists(bin_dir):
                os.rmdir(bin_dir)

    def test_which_follows_symlink(self):
        " which() follows symlinks and returns its path. "
        fname = 'original'
        symname = 'extra-crispy'
        bin_dir = tempfile.mkdtemp()
        bin_path = os.path.join(bin_dir, fname)
        sym_path = os.path.join(bin_dir, symname)
        save_path = os.environ['PATH']
        try:
            # setup
            os.environ['PATH'] = bin_dir
            with open(bin_path, 'w') as fp:
                pass
            os.chmod(bin_path, 0o400)
            os.symlink(bin_path, sym_path)

            # should not be found because symlink points to non-executable
            assert pexpect.which(symname) is None

            # but now it should -- because it is executable
            os.chmod(bin_path, 0o700)
            assert pexpect.which(symname) == sym_path

        finally:
            # restore,
            os.environ['PATH'] = save_path

            # destroy scratch files, symlinks, and folders,
            if os.path.exists(sym_path):
                os.unlink(sym_path)
            if os.path.exists(bin_path):
                os.unlink(bin_path)
            if os.path.exists(bin_dir):
                os.rmdir(bin_dir)

    def test_which_should_not_match_folders(self):
        " Which does not match folders, even though they are executable. "
        # make up a path and insert a folder that is 'executable', a naive
        # implementation might match (previously pexpect versions 3.2 and
        # sh versions 1.0.8, reported by @lcm337.)
        fname = 'g++'
        bin_dir = tempfile.mkdtemp()
        bin_dir2 = os.path.join(bin_dir, fname)
        save_path = os.environ['PATH']
        try:
            os.environ['PATH'] = bin_dir
            os.mkdir(bin_dir2, 0o755)
            # should not be found because it is not executable *file*,
            # but rather, has the executable bit set, as a good folder
            # should -- it should not be returned because it fails isdir()
            exercise = pexpect.which(fname)
            assert exercise is None

        finally:
            # restore,
            os.environ['PATH'] = save_path
            # destroy scratch folders,
            for _dir in (bin_dir2, bin_dir,):
                if os.path.exists(_dir):
                    os.rmdir(_dir)

    def test_which_should_match_other_group_user(self):
        " which() returns executables by other, group, and user ownership. "
        # create an executable and test that it is found using which() for
        # each of the 'other', 'group', and 'user' permission bits.
        fname = 'g77'
        bin_dir = tempfile.mkdtemp()
        bin_path = os.path.join(bin_dir, fname)
        save_path = os.environ['PATH']
        try:
            # setup
            os.environ['PATH'] = bin_dir

            # an interpreted script requires the ability to read,
            # whereas a binary program requires only to be executable.
            #
            # to gain access to a binary program, we make a copy of
            # the existing system program echo(1).
            bin_echo = None
            for pth in ('/bin/echo', '/usr/bin/echo'):
                if os.path.exists(pth):
                    bin_echo = pth
                    break
            bin_which = None
            for pth in ('/bin/which', '/usr/bin/which'):
                if os.path.exists(pth):
                    bin_which = pth
                    break
            if not bin_echo or not bin_which:
                pytest.skip('needs `echo` and `which` binaries')
            shutil.copy(bin_echo, bin_path)
            isroot = os.getuid() == 0
            for should_match, mode in (
                # note that although the file may have matching 'group' or
                # 'other' executable permissions, it is *not* executable
                # because the current uid is the owner of the file -- which
                # takes precedence
                (False,  0o000),   # ----------, no
                (isroot, 0o001),   # ---------x, no
                (isroot, 0o010),   # ------x---, no
                (True,   0o100),   # ---x------, yes
                (False,  0o002),   # --------w-, no
                (False,  0o020),   # -----w----, no
                (False,  0o200),   # --w-------, no
                (isroot, 0o003),   # --------wx, no
                (isroot, 0o030),   # -----wx---, no
                (True,   0o300),   # --wx------, yes
                (False,  0o004),   # -------r--, no
                (False,  0o040),   # ----r-----, no
                (False,  0o400),   # -r--------, no
                (isroot, 0o005),   # -------r-x, no
                (isroot, 0o050),   # ----r-x---, no
                (True,   0o500),   # -r-x------, yes
                (False,  0o006),   # -------rw-, no
                (False,  0o060),   # ----rw----, no
                (False,  0o600),   # -rw-------, no
                (isroot, 0o007),   # -------rwx, no
                (isroot, 0o070),   # ----rwx---, no
                (True,   0o700),   # -rwx------, yes
                (isroot, 0o4001),  # ---S-----x, no
                (isroot, 0o4010),  # ---S--x---, no
                (True,   0o4100),  # ---s------, yes
                (isroot, 0o4003),  # ---S----wx, no
                (isroot, 0o4030),  # ---S-wx---, no
                (True,   0o4300),  # --ws------, yes
                (isroot, 0o2001),  # ------S--x, no
                (isroot, 0o2010),  # ------s---, no
                (True,   0o2100),  # ---x--S---, yes

            ):
                mode_str = '{0:0>4o}'.format(mode)

                # given file mode,
                os.chmod(bin_path, mode)

                # exercise whether we may execute
                can_execute = True
                try:
                    subprocess.Popen(fname).wait() == 0
                except OSError as err:
                    if err.errno != errno.EACCES:
                        raise
                    # permission denied
                    can_execute = False

                assert should_match == can_execute, (
                    should_match, can_execute, mode_str)

                # exercise whether which(1) would match
                proc = subprocess.Popen((bin_which, fname),
                                        env={'PATH': bin_dir},
                                        stdout=subprocess.PIPE)
                bin_which_match = bool(not proc.wait())
                assert should_match == bin_which_match, (
                    should_match, bin_which_match, mode_str)

                # finally, exercise pexpect's which(1) matches
                # the same.
                pexpect_match = bool(pexpect.which(fname))

                assert should_match == pexpect_match == bin_which_match, (
                    should_match, pexpect_match, bin_which_match, mode_str)

        finally:
            # restore,
            os.environ['PATH'] = save_path

            # destroy scratch files and folders,
            if os.path.exists(bin_path):
                os.unlink(bin_path)
            if os.path.exists(bin_dir):
                os.rmdir(bin_dir)
