from __future__ import annotations

import json
import sys
from textwrap import dedent

import pytest


def test_simple_run(tmpdir, runner):
    runner.write_with_general(
        dedent(
            """
    [pair my_pair]
    a = "my_a"
    b = "my_b"
    collections = null

    [storage my_a]
    type = "filesystem"
    path = "{0}/path_a/"
    fileext = ".txt"

    [storage my_b]
    type = "filesystem"
    path = "{0}/path_b/"
    fileext = ".txt"
    """
        ).format(str(tmpdir))
    )

    tmpdir.mkdir("path_a")
    tmpdir.mkdir("path_b")

    result = runner.invoke(["discover"])
    assert not result.exception

    result = runner.invoke(["sync"])
    assert not result.exception

    tmpdir.join("path_a/haha.txt").write("UID:haha")
    result = runner.invoke(["sync"])
    assert "Copying (uploading) item haha to my_b" in result.output
    assert tmpdir.join("path_b/haha.txt").read() == "UID:haha"


def test_sync_inexistant_pair(tmpdir, runner):
    runner.write_with_general("")

    result = runner.invoke(["sync", "foo"])
    assert result.exception
    assert "pair foo does not exist." in result.output.lower()


def test_empty_storage(tmpdir, runner):
    runner.write_with_general(
        dedent(
            """
    [pair my_pair]
    a = "my_a"
    b = "my_b"
    collections = null

    [storage my_a]
    type = "filesystem"
    path = "{0}/path_a/"
    fileext = ".txt"

    [storage my_b]
    type = "filesystem"
    path = "{0}/path_b/"
    fileext = ".txt"
    """
        ).format(str(tmpdir))
    )

    tmpdir.mkdir("path_a")
    tmpdir.mkdir("path_b")

    result = runner.invoke(["discover"])
    assert not result.exception

    result = runner.invoke(["sync"])
    assert not result.exception

    tmpdir.join("path_a/haha.txt").write("UID:haha")
    result = runner.invoke(["sync"])
    assert not result.exception
    tmpdir.join("path_b/haha.txt").remove()
    result = runner.invoke(["sync"])
    lines = result.output.splitlines()
    assert lines[0] == "Syncing my_pair"
    assert lines[1].startswith('error: my_pair: Storage "my_b" was completely emptied.')
    assert result.exception


def test_verbosity(tmpdir, runner):
    runner.write_with_general("")
    result = runner.invoke(["--verbosity=HAHA", "sync"])
    assert result.exception
    assert (
        'invalid value for "--verbosity"' in result.output.lower()
        or "invalid value for '--verbosity'" in result.output.lower()
    )


def test_collections_cache_invalidation(tmpdir, runner):
    foo = tmpdir.mkdir("foo")
    bar = tmpdir.mkdir("bar")
    for x in "abc":
        foo.mkdir(x)
        bar.mkdir(x)

    runner.write_with_general(
        dedent(
            """
    [storage foo]
    type = "filesystem"
    path = "{0}/foo/"
    fileext = ".txt"

    [storage bar]
    type = "filesystem"
    path = "{0}/bar/"
    fileext = ".txt"

    [pair foobar]
    a = "foo"
    b = "bar"
    collections = ["a", "b", "c"]
    """
        ).format(str(tmpdir))
    )

    foo.join("a/itemone.txt").write("UID:itemone")

    result = runner.invoke(["discover"])
    assert not result.exception

    result = runner.invoke(["sync"])
    assert not result.exception
    assert "detected change in config file" not in result.output.lower()

    rv = bar.join("a").listdir()
    assert len(rv) == 1
    assert rv[0].basename == "itemone.txt"

    runner.write_with_general(
        dedent(
            """
    [storage foo]
    type = "filesystem"
    path = "{0}/foo/"
    fileext = ".txt"

    [storage bar]
    type = "filesystem"
    path = "{0}/bar2/"
    fileext = ".txt"

    [pair foobar]
    a = "foo"
    b = "bar"
    collections = ["a", "b", "c"]
    """
        ).format(str(tmpdir))
    )

    for entry in tmpdir.join("status").listdir():
        if not str(entry).endswith(".collections"):
            entry.remove()
    bar2 = tmpdir.mkdir("bar2")
    for x in "abc":
        bar2.mkdir(x)
    result = runner.invoke(["sync"])
    assert "detected change in config file" in result.output.lower()
    assert result.exception

    result = runner.invoke(["discover"])
    assert not result.exception

    result = runner.invoke(["sync"])
    assert not result.exception

    rv = bar.join("a").listdir()
    rv2 = bar2.join("a").listdir()
    assert len(rv) == len(rv2) == 1
    assert rv[0].basename == rv2[0].basename == "itemone.txt"


def test_invalid_pairs_as_cli_arg(tmpdir, runner):
    runner.write_with_general(
        dedent(
            """
    [storage foo]
    type = "filesystem"
    path = "{0}/foo/"
    fileext = ".txt"

    [storage bar]
    type = "filesystem"
    path = "{0}/bar/"
    fileext = ".txt"

    [pair foobar]
    a = "foo"
    b = "bar"
    collections = ["a", "b", "c"]
    """
        ).format(str(tmpdir))
    )

    for base in ("foo", "bar"):
        base = tmpdir.mkdir(base)
        for c in "abc":
            base.mkdir(c)

    result = runner.invoke(["discover"])
    assert not result.exception

    result = runner.invoke(["sync", "foobar/d"])
    assert result.exception
    assert 'pair foobar: collection "d" not found' in result.output.lower()


def test_multiple_pairs(tmpdir, runner):
    def get_cfg():
        for name_a, name_b in ("foo", "bar"), ("bam", "baz"):
            yield dedent(
                """
            [pair {a}{b}]
            a = "{a}"
            b = "{b}"
            collections = null
            """
            ).format(a=name_a, b=name_b)

            for name in name_a, name_b:
                yield dedent(
                    """
                [storage {name}]
                type = "filesystem"
                path = "{path}"
                fileext = ".txt"
                """
                ).format(name=name, path=str(tmpdir.mkdir(name)))

    runner.write_with_general("".join(get_cfg()))

    result = runner.invoke(["discover"])
    assert not result.exception
    assert set(result.output.splitlines()) > {
        "Discovering collections for pair bambaz",
        "Discovering collections for pair foobar",
    }

    result = runner.invoke(["sync"])
    assert not result.exception
    assert set(result.output.splitlines()) == {
        "Syncing bambaz",
        "Syncing foobar",
    }


# XXX: https://github.com/pimutils/vdirsyncer/issues/617
@pytest.mark.skipif(sys.platform == "darwin", reason="This test inexplicably fails")
@pytest.mark.parametrize(
    "collections",
    [
        ("a", "A"),
        ("\ufffe",),
        ("Hello there!",),
        ("Österreich",),
        ("中国", "x1"),
        ("한글",),
        ("42a4ec99-b1c2-4859-b142-759112f2ca50",),
        ("فلسطين",),
    ],
)
def test_create_collections(collections, tmpdir, runner):
    runner.write_with_general(
        dedent(
            f"""
    [pair foobar]
    a = "foo"
    b = "bar"
    collections = {json.dumps(list(collections))}

    [storage foo]
    type = "filesystem"
    path = "{str(tmpdir)}/foo/"
    fileext = ".txt"

    [storage bar]
    type = "filesystem"
    path = "{str(tmpdir)}/bar/"
    fileext = ".txt"
    """
        )
    )

    result = runner.invoke(["discover"], input="y\n" * 2 * (len(collections) + 1))
    assert not result.exception, result.output

    result = runner.invoke(["sync"] + ["foobar/" + x for x in collections])
    assert not result.exception, result.output

    assert {x.basename for x in tmpdir.join("foo").listdir()} == {
        x.basename for x in tmpdir.join("bar").listdir()
    }


def test_ident_conflict(tmpdir, runner):
    runner.write_with_general(
        dedent(
            f"""
    [pair foobar]
    a = "foo"
    b = "bar"
    collections = null

    [storage foo]
    type = "filesystem"
    path = "{str(tmpdir)}/foo/"
    fileext = ".txt"

    [storage bar]
    type = "filesystem"
    path = "{str(tmpdir)}/bar/"
    fileext = ".txt"
    """
        )
    )

    foo = tmpdir.mkdir("foo")
    tmpdir.mkdir("bar")

    foo.join("one.txt").write("UID:1")
    foo.join("two.txt").write("UID:1")
    foo.join("three.txt").write("UID:1")

    result = runner.invoke(["discover"])
    assert not result.exception

    result = runner.invoke(["sync"])
    assert result.exception
    assert (
        'error: foobar: Storage "foo" contains multiple items with the '
        "same UID or even content"
    ) in result.output
    assert sorted(
        [
            "one.txt" in result.output,
            "two.txt" in result.output,
            "three.txt" in result.output,
        ]
    ) == [False, True, True]


@pytest.mark.parametrize(
    "existing,missing",
    [
        ("foo", "bar"),
        ("bar", "foo"),
    ],
)
def test_unknown_storage(tmpdir, runner, existing, missing):
    runner.write_with_general(
        dedent(
            f"""
    [pair foobar]
    a = "foo"
    b = "bar"
    collections = null

    [storage {existing}]
    type = "filesystem"
    path = "{str(tmpdir)}/{existing}/"
    fileext = ".txt"
    """
        )
    )

    tmpdir.mkdir(existing)

    result = runner.invoke(["discover"])
    assert result.exception

    assert (
        f"Storage '{missing}' not found. "
        f"These are the configured storages: ['{existing}']"
    ) in result.output


@pytest.mark.parametrize("cmd", ["sync", "metasync"])
def test_no_configured_pairs(tmpdir, runner, cmd):
    runner.write_with_general("")

    result = runner.invoke([cmd])
    assert result.output == ""
    assert not result.exception


@pytest.mark.parametrize(
    "resolution,expect_foo,expect_bar",
    [(["command", "cp"], "UID:lol\nfööcontent", "UID:lol\nfööcontent")],
)
def test_conflict_resolution(tmpdir, runner, resolution, expect_foo, expect_bar):
    runner.write_with_general(
        dedent(
            f"""
    [pair foobar]
    a = "foo"
    b = "bar"
    collections = null
    conflict_resolution = {json.dumps(resolution)}

    [storage foo]
    type = "filesystem"
    fileext = ".txt"
    path = "{str(tmpdir)}/foo"

    [storage bar]
    type = "filesystem"
    fileext = ".txt"
    path = "{str(tmpdir)}/bar"
    """
        )
    )

    foo = tmpdir.join("foo")
    bar = tmpdir.join("bar")
    fooitem = foo.join("lol.txt").ensure()
    fooitem.write("UID:lol\nfööcontent")
    baritem = bar.join("lol.txt").ensure()
    baritem.write("UID:lol\nbööcontent")

    r = runner.invoke(["discover"])
    assert not r.exception

    r = runner.invoke(["sync"])
    assert not r.exception

    assert fooitem.read() == expect_foo
    assert baritem.read() == expect_bar


@pytest.mark.parametrize("partial_sync", ["error", "ignore", "revert", None])
def test_partial_sync(tmpdir, runner, partial_sync):
    runner.write_with_general(
        dedent(
            """
    [pair foobar]
    a = "foo"
    b = "bar"
    collections = null
    {partial_sync}

    [storage foo]
    type = "filesystem"
    fileext = ".txt"
    path = "{base}/foo"

    [storage bar]
    type = "filesystem"
    read_only = true
    fileext = ".txt"
    path = "{base}/bar"
    """.format(
                partial_sync=(
                    f'partial_sync = "{partial_sync}"\n' if partial_sync else ""
                ),
                base=str(tmpdir),
            )
        )
    )

    foo = tmpdir.mkdir("foo")
    bar = tmpdir.mkdir("bar")

    foo.join("other.txt").write("UID:other")
    bar.join("other.txt").write("UID:other")

    baritem = bar.join("lol.txt")
    baritem.write("UID:lol")

    r = runner.invoke(["discover"])
    assert not r.exception

    r = runner.invoke(["sync"])
    assert not r.exception

    fooitem = foo.join("lol.txt")
    fooitem.remove()

    r = runner.invoke(["sync"])

    if partial_sync == "error":
        assert r.exception
        assert "Attempted change" in r.output
    elif partial_sync == "ignore":
        assert baritem.exists()
        r = runner.invoke(["sync"])
        assert not r.exception
        assert baritem.exists()
    else:
        assert baritem.exists()
        r = runner.invoke(["sync"])
        assert not r.exception
        assert baritem.exists()
        assert fooitem.exists()


def test_fetch_only_necessary_params(tmpdir, runner):
    fetched_file = tmpdir.join("fetched_flag")
    fetch_script = tmpdir.join("fetch_script")
    fetch_script.write(
        dedent(
            f"""
    set -e
    touch "{str(fetched_file)}"
    echo ".txt"
    """
        )
    )

    runner.write_with_general(
        dedent(
            """
    [pair foobar]
    a = "foo"
    b = "bar"
    collections = null

    [pair bambar]
    a = "bam"
    b = "bar"
    collections = null

    [storage foo]
    type = "filesystem"
    path = "{path}"
    fileext = ".txt"

    [storage bar]
    type = "filesystem"
    path = "{path}"
    fileext = ".txt"

    [storage bam]
    type = "filesystem"
    path = "{path}"
    fileext.fetch = ["command", "sh", "{script}"]
    """.format(path=str(tmpdir.mkdir("bogus")), script=str(fetch_script))
        )
    )

    def fetched():
        try:
            fetched_file.remove()
            return True
        except Exception:
            return False

    r = runner.invoke(["discover"])
    assert not r.exception
    assert fetched()

    r = runner.invoke(["sync", "foobar"])
    assert not r.exception
    assert not fetched()

    r = runner.invoke(["sync"])
    assert not r.exception
    assert fetched()

    r = runner.invoke(["sync", "bambar"])
    assert not r.exception
    assert fetched()
