import os
import pwd
from unittest import mock

import pytest
from pkgcore import os_data
from pkgcore.sync import base, git, tar
from snakeoil.process import CommandNotFound

existing_user = pwd.getpwall()[0].pw_name
existing_uid = pwd.getpwnam(existing_user).pw_uid


class TestSyncer:

    @pytest.fixture(autouse=True)
    def _setup(self, tmp_path):
        self.repo_path = str(tmp_path / 'repo')

    def test_split_users(self):
        o = base.Syncer(self.repo_path, "http://dar")
        assert o.uid == os.getuid()
        assert o.uri == "http://dar"

        o = base.Syncer(self.repo_path, f"http://{existing_user}::@site")
        assert o.uid == existing_uid
        assert o.uri == "http://site"

        o = base.Syncer(self.repo_path, f"http://{existing_user}::foon@site")
        assert o.uid == existing_uid
        assert o.uri == "http://foon@site"

        o = base.Syncer(self.repo_path, f"{existing_user}::foon@site")
        assert o.uid == existing_uid
        assert o.uri == "foon@site"

        with pytest.raises(base.MissingLocalUser):
            base.Syncer(self.repo_path, f"foo_nonexistent_user::foon@site")

    @mock.patch('snakeoil.process.spawn.spawn')
    def test_usersync_disabled(self, spawn):
        o = base.Syncer(self.repo_path, f"http://foo/bar.git", usersync=False)
        o.uid == os_data.uid
        o.gid == os_data.gid

    @mock.patch('snakeoil.process.spawn.spawn')
    def test_usersync_portage_perms(self, spawn):
        # sync uses portage perms if repo dir doesn't exist
        o = base.Syncer(self.repo_path, f"http://foo/bar.git", usersync=True)
        o.uid == os_data.portage_uid
        o.gid == os_data.portage_gid

    @mock.patch('snakeoil.process.spawn.spawn')
    def test_usersync_repo_dir_perms(self, spawn):
        # and repo dir perms if it does exist
        with mock.patch('os.stat') as stat:
            stat.return_value = mock.Mock(st_uid=1234, st_gid=5678)
            o = base.Syncer(self.repo_path, f"http://foo/bar.git", usersync=True)
            stat.assert_called()
            assert o.uid == 1234
            assert o.gid == 5678


@mock.patch('snakeoil.process.find_binary')
class TestExternalSyncer:

    @pytest.fixture(autouse=True)
    def _setup(self, tmp_path):
        self.repo_path = str(tmp_path / 'repo')

    def test_missing_binary(self, find_binary):
        find_binary.side_effect = CommandNotFound('foo')
        with pytest.raises(base.MissingBinary):
            base.ExternalSyncer(self.repo_path, 'http://dar')

    def test_existing_binary(self, find_binary):
        # fake external syncer
        class FooSyncer(base.ExternalSyncer):
            binary = 'foo'

        # fake that the external binary exists
        find_binary.side_effect = lambda x: x

        o = FooSyncer(self.repo_path, 'http://dar')
        assert o.uri == 'http://dar'
        assert o.binary == 'foo'

    @mock.patch('snakeoil.process.spawn.spawn')
    def test_usersync(self, spawn, find_binary):
        # fake external syncer
        class FooSyncer(base.ExternalSyncer):
            binary = 'foo'

        # fake that the external binary exists
        find_binary.side_effect = lambda x: x

        o = FooSyncer(self.repo_path, 'http://dar')
        o.uid = 1234
        o.gid = 2345
        o._spawn('cmd', pipes={})
        assert spawn.call_args[1]['uid'] == o.uid
        assert spawn.call_args[1]['gid'] == o.gid


@mock.patch('snakeoil.process.find_binary', return_value='git')
@mock.patch('snakeoil.process.spawn.spawn')
class TestVcsSyncer:

    def test_basedir_perms_error(self, spawn, find_binary, tmp_path):
        syncer = git.git_syncer(str(tmp_path), 'git://blah.git')
        with pytest.raises(base.PathError):
            with mock.patch('os.stat') as stat:
                stat.side_effect = EnvironmentError('fake exception')
                syncer.sync()

    def test_basedir_is_file_error(self, spawn, find_binary, tmp_path):
        repo = tmp_path / "repo"
        repo.touch()
        syncer = git.git_syncer(str(repo), 'git://blah.git')

        # basedir gets '/' appended by default and stat errors out
        with pytest.raises(base.PathError) as excinfo:
            syncer.sync()

        # remove trailing slash from basedir and file check catches it instead
        syncer.basedir = str(repo)
        with pytest.raises(base.PathError) as excinfo:
            syncer.sync()
        assert "isn't a directory" in str(excinfo.value)

    def test_verbose_sync(self, spawn, find_binary, tmp_path):
        syncer = git.git_syncer(str(tmp_path), 'git://blah.git')
        syncer.sync(verbosity=1)
        assert '-v' == spawn.call_args[0][0][-1]
        syncer.sync(verbosity=2)
        assert '-vv' == spawn.call_args[0][0][-1]

    def test_quiet_sync(self, spawn, find_binary, tmp_path):
        syncer = git.git_syncer(str(tmp_path), 'git://blah.git')
        syncer.sync(verbosity=-1)
        assert '-q' == spawn.call_args[0][0][-1]


class TestGenericSyncer:

    def test_init(self):
        with pytest.raises(base.UriError):
            base.GenericSyncer('/', 'seriouslynotaprotocol://blah/')

        syncer = base.GenericSyncer('/', f'tar+https://blah.tar.gz')
        assert tar.tar_syncer is syncer.__class__


class TestDisabledSyncer:

    def test_init(self):
        syncer = base.DisabledSyncer('/foo/bar', f'https://blah.git')
        assert syncer.disabled
        # syncing should also be disabled
        assert not syncer.uri
        assert not syncer.sync()


class TestAutodetectSyncer:

    def test_no_syncer_detected(self, tmp_path):
        syncer = base.AutodetectSyncer(str(tmp_path))
        assert isinstance(syncer, base.DisabledSyncer)

    @mock.patch('snakeoil.process.find_binary', return_value='git')
    def test_syncer_detected(self, find_binary, tmp_path):
        d = tmp_path / '.git'
        d.mkdir()
        syncer = base.AutodetectSyncer(str(tmp_path))
        assert isinstance(syncer, git.git_syncer)
