import datetime
import os
from unittest import mock

import pytest
from pkgcore.sync import base, rsync
from pkgcore.sync.tar import tar_syncer
from snakeoil.process import CommandNotFound


def fake_ips(num):
    """Generate simple IPv4 addresses given the amount to create."""
    return [(None, None, None, None, (".".join(str(x) * 4), 0)) for x in range(num)]


@mock.patch("socket.getaddrinfo", return_value=fake_ips(3))
@mock.patch("snakeoil.process.spawn.spawn")
class TestRsyncSyncer:
    _syncer_class = rsync.rsync_syncer

    @pytest.fixture(autouse=True)
    def _setup(self, tmp_path):
        self.repo_path = str(tmp_path / "repo")
        with mock.patch("snakeoil.process.find_binary", return_value="rsync"):
            self.syncer = self._syncer_class(
                self.repo_path, "rsync://rsync.gentoo.org/gentoo-portage"
            )

    @mock.patch("snakeoil.process.find_binary")
    def test_uri_parse_rsync_missing(self, find_binary, spawn, getaddrinfo):
        find_binary.side_effect = CommandNotFound("rsync")
        with pytest.raises(base.SyncError):
            self._syncer_class(self.repo_path, "rsync://foon.com/dar")

    @mock.patch("snakeoil.process.find_binary")
    def test_uri_parse(self, find_binary, spawn, getaddrinfo):
        find_binary.side_effect = lambda x: x
        o = self._syncer_class(self.repo_path, "rsync://dar/module")
        assert o.uri == "rsync://dar/module/"
        assert o.rsh == None

        o = self._syncer_class(self.repo_path, "rsync+/bin/sh://dar/module")
        assert o.uri == "rsync://dar/module/"
        assert o.rsh == "/bin/sh"

    def test_successful_sync(self, spawn, getaddrinfo):
        spawn.return_value = 0
        assert self.syncer.sync()
        spawn.assert_called_once()

    def test_bad_syntax_sync(self, spawn, getaddrinfo):
        spawn.return_value = 1
        with pytest.raises(base.SyncError) as excinfo:
            assert self.syncer.sync()
        assert str(excinfo.value).startswith("rsync command syntax error:")
        spawn.assert_called_once()

    def test_failed_disk_space_sync(self, spawn, getaddrinfo):
        spawn.return_value = 11
        with pytest.raises(base.SyncError) as excinfo:
            assert self.syncer.sync()
        assert str(excinfo.value) == "rsync ran out of disk space"
        spawn.assert_called_once()

    def test_retried_sync(self, spawn, getaddrinfo):
        spawn.return_value = 99
        with pytest.raises(base.SyncError) as excinfo:
            assert self.syncer.sync()
        assert str(excinfo.value) == "all attempts failed"
        # rsync should retry every resolved IP related to the sync URI
        assert len(spawn.mock_calls) == 3

    def test_retried_sync_max_retries(self, spawn, getaddrinfo):
        spawn.return_value = 99
        # generate more IPs than retries
        getaddrinfo.return_value = fake_ips(self.syncer.retries + 1)
        with pytest.raises(base.SyncError) as excinfo:
            assert self.syncer.sync()
        assert str(excinfo.value) == "all attempts failed"
        assert len(spawn.mock_calls) == self.syncer.retries

    def test_failed_dns_sync(self, spawn, getaddrinfo):
        getaddrinfo.side_effect = OSError()
        with pytest.raises(base.SyncError) as excinfo:
            assert self.syncer.sync()
        assert str(excinfo.value).startswith("DNS resolution failed")
        spawn.assert_not_called()


class TestRsyncTimestampSyncer(TestRsyncSyncer):
    _syncer_class = rsync.rsync_timestamp_syncer


@pytest.mark_network
class TestRsyncSyncerReal:
    def test_sync(self, tmp_path):
        # perform a tarball sync for initial week-old base
        path = tmp_path / "repo"
        week_old = datetime.datetime.now() - datetime.timedelta(days=7)
        date_str = week_old.strftime("%Y%m%d")
        syncer = tar_syncer(
            str(path),
            f"http://distfiles.gentoo.org/snapshots/portage-{date_str}.tar.xz",
        )
        assert syncer.sync()
        timestamp = os.path.join(path, "metadata", "timestamp.chk")
        assert os.path.exists(timestamp)
        stat = os.stat(timestamp)

        # run rsync over the unpacked repo tarball to update to the latest tree
        syncer = rsync.rsync_timestamp_syncer(
            str(path), "rsync://rsync.gentoo.org/gentoo-portage"
        )
        assert syncer.sync()
        assert stat != os.stat(timestamp)
