"""Tests of the sync-helper utilities"""
import logging
import os
import shutil
import time
from pathlib import Path
from urllib.parse import urlparse

import pytest

from enderchest.sync import file
from enderchest.sync import utils as sync_utils
from enderchest.sync.utils import Operation as Op


class TestPathFromURI:
    def test_roundtrip(self, tmpdir):
        original_path = Path(tmpdir) / "this has a space in it" / "(="
        assert (
            sync_utils.abspath_from_uri(urlparse(original_path.as_uri()))
            == original_path
        )


class TestURIToSSH:
    def test_simple_parse(self):
        address = sync_utils.uri_to_ssh(
            urlparse("rsync://openbagtwo@couchgaming:22/home/openbagtwo/minecraft")
        )
        assert address == "openbagtwo@couchgaming:22:/home/openbagtwo/minecraft"

    def test_no_username_parse(self):
        address = sync_utils.uri_to_ssh(
            urlparse("rsync://steamdeck/home/openbagtwo/minecraft")
        )
        assert address == "steamdeck:/home/openbagtwo/minecraft"

    def test_no_netloc_parse(self):
        address = sync_utils.uri_to_ssh(urlparse("rsync:///mnt/external/minecraft-bkp"))
        assert address == "localhost:/mnt/external/minecraft-bkp"

    def test_no_hostname_parse(self):
        """Can't believe this is a valid URI"""
        address = sync_utils.uri_to_ssh(urlparse("rsync://nugget@/home/nugget/"))
        assert address == "nugget@localhost:/home/nugget/"


class TestRenderRemote:
    @pytest.mark.parametrize("alias", ("matching", "not-matching"))
    def test_render_omits_alias_only_if_it_matches_hostname(self, alias):
        uri = "prtcl://youser@matching:101/some/directory"
        rendered = sync_utils.render_remote(alias, urlparse(uri))
        if alias == "matching":
            assert rendered == uri
        else:
            assert rendered == uri + " (not-matching)"


class TestIsIdentical:
    def test_copied_files_are_identical(self, tmp_path):
        one = tmp_path / "one"
        one.write_text("I'm the original\n")
        two = tmp_path / "two"
        shutil.copy2(one, two)
        assert sync_utils.is_identical(one.stat(), two.stat())

    def test_modified_files_are_not_identical(self, tmp_path):
        one = tmp_path / "one"
        one.write_text("I'm the original")
        two = tmp_path / "two"
        shutil.copy2(one, two)
        with two.open("a") as f:
            f.write("\n")
        assert not sync_utils.is_identical(one.stat(), two.stat())

    @pytest.mark.parametrize("target_type", ("file", "folder"))
    def test_symlink_is_not_identical_to_its_target(self, tmp_path, target_type):
        target = tmp_path / "bulleye"
        if target_type == "file":
            target.touch()
        else:
            target.mkdir()

        linky = tmp_path / "linky"
        linky.symlink_to(target, target_is_directory=target_type == "folder")

        assert not sync_utils.is_identical(target.lstat(), linky.lstat())

    def test_checks_modified_time(self, tmp_path):
        one = tmp_path / "one"
        one.write_text("I'm the original")
        two = tmp_path / "two"
        shutil.copy(one, two)
        time.sleep(1)  # ugh
        two.write_text("I'm the original")

        # meta-test that the file contents are identical
        assert one.read_bytes() == two.read_bytes()

        assert not sync_utils.is_identical(one.stat(), two.stat())

    def test_does_not_check_modified_time_of_directories(self, tmp_path):
        one = tmp_path / "one"
        one.mkdir(parents=True)
        two = tmp_path / "two"
        time.sleep(1)  # ugh
        two.mkdir()
        assert sync_utils.is_identical(one.stat(), two.stat())

    @pytest.mark.xfail(reason="Method does not compute hashes")
    def test_two_files_of_the_same_size_are_not_identical(self, tmp_path):
        one = tmp_path / "one"
        one.write_text("I'm the original")
        two = tmp_path / "two"
        two.write_text("I'm the octagonl")

        assert not sync_utils.is_identical(one.stat(), two.stat())


@pytest.fixture
def file_system(tmp_path):
    source_tree = tmp_path / "oak"
    (source_tree / "branch").mkdir(parents=True)
    (source_tree / "branch" / "twig").mkdir()
    (source_tree / "branch" / "leaf").write_text("green")
    (source_tree / "branch" / "acorn").touch()
    (source_tree / "branch2").mkdir()
    (source_tree / "branch2" / "acorn").touch()

    dst_tree = tmp_path / "birch"
    (dst_tree / "branch").mkdir(parents=True)
    (dst_tree / "branch" / "twig").mkdir()
    (dst_tree / "branch" / "leaf").write_text("yellow")
    shutil.copy2((source_tree / "branch" / "acorn"), (dst_tree / "branch" / "acorn"))
    (dst_tree / "beehive").touch()
    (dst_tree / "root").mkdir()
    (dst_tree / "root" / "cicada").touch()

    yield tmp_path


@pytest.fixture
def diff(file_system):
    yield sync_utils.diff(
        file.get_contents(file_system / "oak"),
        file.get_contents(file_system / "birch"),
    )


class TestDiff:
    def test_diff_omits_identical_contents(self, diff):
        assert {
            Path("branch") / "acorn",
            Path("branch") / "twig",
            Path("branch"),
        }.intersection((f for f, *_ in diff)) == set()

    def test_diff_captures_updates(self, diff):
        assert [Path("branch") / "leaf"] == [f for f, _, op in diff if op == Op.REPLACE]

    def test_diff_captures_creation_of_files_and_folders(self, diff):
        assert [
            Path("branch2"),
            Path("branch2") / "acorn",
        ] == [f for f, _, op in diff if op == Op.CREATE]

    def test_diff_captures_deletion_of_files_and_folders(self, diff):
        assert [
            Path("root") / "cicada",
            Path("beehive"),
            Path("root"),
        ] == [f for f, _, op in diff if op == Op.DELETE]


class TestGenerateSyncReport:
    def test_generate_sync_report(self, diff, caplog):
        expected = [
            "Deleting beehive",
            """Within branch...
  - Creating 0 files
  - Replacing 1 file
  - Deleting 0 files""",
            "Creating branch2",
            "Deleting root",
        ]

        sync_utils.generate_sync_report(diff)

        info_logs = [
            (record.msg % record.args).strip()
            for record in caplog.records
            if record.levelno == logging.INFO
        ]

        assert info_logs == expected


class TestFileIgnorePatternBuilder:
    def test_simple_match(self):
        assert file.ignore_patterns("hello")(
            "greetings", ("bonjour", "hello", "hellooooo")
        ) == {"hello"}

    def test_wildcard(self):
        assert file.ignore_patterns("hel*")(
            "responses", ("como sa va", "hello", "hell no", "help")
        ) == {"hello", "hell no", "help"}

    def test_multi_pattern_match(self):
        assert file.ignore_patterns("hel*", "bye")(
            "responses", ("hello", "goodbye", "hellooo", "bye")
        ) == {"hello", "hellooo", "bye"}

    def test_full_path_check(self):
        ignore = file.ignore_patterns(os.path.join("root", "branch"))
        assert (
            ignore("root", ("branch", "trunk")),
            ignore("trunk", ("branch", "leaf")),
        ) == ({"branch"}, set())

    def test_match_is_performed_on_the_end(self):
        ignore = file.ignore_patterns(os.path.join("root", "branch"), "leaf")
        assert (
            ignore(os.path.join("tree", "root"), ("branch", "trunk")),
            ignore("wind", ("leaf", "blows")),
        ) == ({"branch"}, {"leaf"})


class TestFileClean:
    def test_clean_cleans_recursively_while_respecting_ignore(self, tmp_path):
        root = tmp_path / "root"

        (root / "empty dir").mkdir(parents=True)
        (root / "will be empty").mkdir()
        (root / "will be empty" / "filey").touch()
        (root / "will not be empty").mkdir()
        (root / "will not be empty" / "file-o bread").touch()
        (root / "will not be empty" / "important.txt").write_text("leave me be\n")
        (root / "dont go inside").mkdir()
        (root / "dont go inside" / "keep me safe").touch()
        (root / "tricky linky").symlink_to(root / "will not be empty")
        (root / "trickier linkier").symlink_to(root / "dont go inside")

        file.clean(root, ignore=file.ignore_patterns("*.txt", "dont*"), dry_run=False)

        assert set(root.rglob("**/*")) == {
            root / "will not be empty",
            root / "will not be empty" / "important.txt",
            root / "dont go inside",
            root / "dont go inside" / "keep me safe",
        }
