import time
from typing import Optional, Iterable

from clarabot_nano.indexer.commit_tracker import DocumentCommitTracker


class _TestingDocumentCommitTracker(DocumentCommitTracker):
    def assert_store(self, upstream: Iterable[str], downstream: Iterable[str], added: Iterable[str]):
        upstream = set(upstream)
        downstream = set(downstream)
        added = set(added)
        self_upstream = set(self._affects_upstream)
        self_downstream = set(self._affects_downstream)
        self_added = set(self._added_directories)
        assert self_upstream == upstream and self_downstream == downstream and self_added == added, (
            f"Mismatch:"
            f"\nUpstream  :\n  {self_upstream.difference(upstream)}\n  {upstream.difference(self_upstream)}"
            f"\nDownstream:\n  {self_downstream.difference(downstream)}\n  {downstream.difference(self_downstream)}"
            f"\nAdded Dirs:\n  {self_added.difference(added)}\n  {added.difference(self_added)}"
        )

    def assert_time(self, new_event: bool, until: Optional[float]):
        assert self.new_time.is_set() is new_event
        if until is None:
            assert self.get_time_until_commit() is None
        else:
            assert abs(until - self.get_time_until_commit()) < 0.05


def test_indexer_scheduler_commit_tracker_0():
    """
    Paths matching tests for upstream only.
    """
    sc = 100.0
    dct = _TestingDocumentCommitTracker(sc)

    dct.assert_time(False, None)
    dct.assert_store([], [], [])
    for p, c in {
        "/a/": False,
        "/a//a": False,
        "/a//ba": False,
        "/a//b": False,
        "/a//b/a": False,
        "/a//b/ca": False,
        "/a//b/c": False,
        "/a//b/c/f": False,
        "/a//b/c/g/h": False,
        "/a//b/cz": False,
        "/a//b/d": False,
        "/a//bz": False,
        "/a//e": False,
        "/b/": False,
        "/b//w": False,
        "/": False,
        "//a": False,
        "//a/b": False,
    }.items():
        assert dct.check(p) is c, f"{p} {c}"

    # test directory added store
    dct.add_directory("/a//b")
    dct.assert_time(False, None)
    dct.assert_store([], [], ["/a//b"])
    assert dct.is_directory_added("/a/") is False
    assert dct.is_directory_added("/a//b") is True
    assert dct.is_directory_added("/a//b/c") is False
    dct.add_directory("/a/")
    dct.assert_time(False, None)
    dct.assert_store([], [], ["/a/", "/a//b"])
    assert dct.is_directory_added("/a/") is True
    assert dct.is_directory_added("/a//b") is True
    assert dct.is_directory_added("/a//b/c") is False
    dct.add_directory("/a//b/c")
    dct.assert_time(False, None)
    dct.assert_store([], [], ["/a/", "/a//b", "/a//b/c"])
    assert dct.is_directory_added("/a/") is True
    assert dct.is_directory_added("/a//b") is True
    assert dct.is_directory_added("/a//b/c") is True
    dct.clear()
    dct.assert_time(False, None)
    dct.assert_store([], [], [])

    for affects_downstream in [False, True]:
        # check track root /a
        dct.track("/a/", affects_downstream=affects_downstream)
        dct.assert_time(True, sc)
        dct.assert_store(x := ["/a/"], x if affects_downstream else [], [])
        for p, c in {
            "/a/": True,
            "/a//a": affects_downstream,
            "/a//ba": affects_downstream,
            "/a//b": affects_downstream,
            "/a//b/a": affects_downstream,
            "/a//b/ca": affects_downstream,
            "/a//b/c": affects_downstream,
            "/a//b/c/f": affects_downstream,
            "/a//b/c/g/h": affects_downstream,
            "/a//b/cz": affects_downstream,
            "/a//b/d": affects_downstream,
            "/a//bz": affects_downstream,
            "/a//e": affects_downstream,
            "/b/": False,
            "/b//w": False,
            "/": False,
            "//a": False,
            "//a/b": False,
        }.items():
            assert dct.check(p) is c, f"{p} {c}"
        dct.clear()
        dct.assert_time(False, None)
        dct.assert_store([], [], [])

        # check track root /
        dct.track("/", affects_downstream=affects_downstream)
        dct.assert_time(True, sc)
        dct.assert_store(x := ["/"], x if affects_downstream else [], [])
        for p, c in {
            "/a/": False,
            "/a//a": False,
            "/a//ba": False,
            "/a//b": False,
            "/a//b/a": False,
            "/a//b/ca": False,
            "/a//b/c": False,
            "/a//b/c/f": False,
            "/a//b/c/g/h": False,
            "/a//b/cz": False,
            "/a//b/d": False,
            "/a//bz": False,
            "/a//e": False,
            "/b/": False,
            "/b//w": False,
            "/": True,
            "//a": affects_downstream,
            "//a/b": affects_downstream,
        }.items():
            assert dct.check(p) is c, f"{p} {c}"
        dct.clear()
        dct.assert_time(False, None)
        dct.assert_store([], [], [])

        # check track root /b
        dct.track("/b/", affects_downstream=affects_downstream)
        dct.assert_time(True, sc)
        dct.assert_store(x := ["/b/"], x if affects_downstream else [], [])
        for p, c in {
            "/a/": False,
            "/a//a": False,
            "/a//ba": False,
            "/a//b": False,
            "/a//b/a": False,
            "/a//b/ca": False,
            "/a//b/c": False,
            "/a//b/c/f": False,
            "/a//b/c/g/h": False,
            "/a//b/cz": False,
            "/a//b/d": False,
            "/a//bz": False,
            "/a//e": False,
            "/b/": True,
            "/b//w": affects_downstream,
            "/": False,
            "//a": False,
            "//a/b": False,
        }.items():
            assert dct.check(p) is c, f"{p} {c}"
        dct.clear()
        dct.assert_time(False, None)
        dct.assert_store([], [], [])

        # check track path /a//b
        dct.track("/a//b", affects_downstream=affects_downstream)
        dct.assert_time(True, sc)
        dct.assert_store(x := ["/a//b"], x if affects_downstream else [], [])
        for p, c in {
            "/a/": True,
            "/a//a": False,
            "/a//ba": False,
            "/a//b": True,
            "/a//b/a": affects_downstream,
            "/a//b/ca": affects_downstream,
            "/a//b/c": affects_downstream,
            "/a//b/c/f": affects_downstream,
            "/a//b/c/g/h": affects_downstream,
            "/a//b/cz": affects_downstream,
            "/a//b/d": affects_downstream,
            "/a//bz": False,
            "/a//e": False,
            "/b/": False,
            "/b//w": False,
            "/": False,
            "//a": False,
            "//a/b": False,
        }.items():
            assert dct.check(p) is c, f"{p} {c}"
        dct.clear()
        dct.assert_time(False, None)
        dct.assert_store([], [], [])

        # check track path /a//b/c
        dct.track("/a//b/c", affects_downstream=affects_downstream)
        dct.assert_time(True, sc)
        dct.assert_store(x := ["/a//b/c"], x if affects_downstream else [], [])
        for p, c in {
            "/a/": True,
            "/a//a": False,
            "/a//ba": False,
            "/a//b": True,
            "/a//b/a": False,
            "/a//b/ca": False,
            "/a//b/c": True,
            "/a//b/c/f": affects_downstream,
            "/a//b/c/g/h": affects_downstream,
            "/a//b/cz": False,
            "/a//b/d": False,
            "/a//bz": False,
            "/a//e": False,
            "/b/": False,
            "/b//w": False,
            "/": False,
            "//a": False,
            "//a/b": False,
        }.items():
            assert dct.check(p) is c, f"{p} {c}"
        dct.clear()
        dct.assert_time(False, None)
        dct.assert_store([], [], [])

        # check track path //a
        dct.track("//a", affects_downstream=affects_downstream)
        dct.assert_time(True, sc)
        dct.assert_store(x := ["//a"], x if affects_downstream else [], [])
        for p, c in {
            "/a/": False,
            "/a//a": False,
            "/a//ba": False,
            "/a//b": False,
            "/a//b/a": False,
            "/a//b/ca": False,
            "/a//b/c": False,
            "/a//b/c/f": False,
            "/a//b/c/g/h": False,
            "/a//b/cz": False,
            "/a//b/d": False,
            "/a//bz": False,
            "/a//e": False,
            "/b/": False,
            "/b//w": False,
            "/": True,
            "//a": True,
            "//a/b": affects_downstream,
        }.items():
            assert dct.check(p) is c, f"{p} {c}"
        dct.clear()
        dct.assert_time(False, None)
        dct.assert_store([], [], [])

        # check track multiple upstream
        dct.track("/a//b/a", affects_downstream=affects_downstream)
        dct.track("/a//b/ca", affects_downstream=affects_downstream)
        dct.track("/a//b/cz", affects_downstream=affects_downstream)
        dct.track("/a//b/d", affects_downstream=affects_downstream)
        dct.track("/a//b/e", affects_downstream=affects_downstream)
        dct.assert_time(True, sc)
        dct.assert_store(x := ["/a//b/a", "/a//b/ca", "/a//b/cz", "/a//b/d", "/a//b/e"], x if affects_downstream else [], [])
        for p, c in {
            "/a/": True,
            "/a//a": False,
            "/a//ba": False,
            "/a//b": True,
            "/a//b/a": True,
            "/a//b/ca": True,
            "/a//b/c": False,
            "/a//b/c/f": False,
            "/a//b/c/g/h": False,
            "/a//b/cz": True,
            "/a//b/cz/wq": affects_downstream,
            "/a//b/d": True,
            "/a//b/e": True,
            "/a//bz": False,
            "/a//e": False,
            "/b/": False,
            "/b//w": False,
            "/": False,
            "//a": False,
            "//a/b": False,
        }.items():
            assert dct.check(p) is c, f"{p} {c}"
        dct.clear()
        dct.assert_time(False, None)
        dct.assert_store([], [], [])


def test_indexer_scheduler_commit_tracker_1():
    """
    Commit-within.
    """
    sc = 1.0
    dct = _TestingDocumentCommitTracker(sc)

    dct.track("/xy//a")
    dct.assert_store(["/xy//a"], [], [])
    dct.add_commit_time(0.5)
    dct.assert_store(["/xy//a"], [], [])
    dct.assert_time(True, 0.5)
    time.sleep(0.1)
    assert dct.check("/z/") is False
    dct.assert_time(False, 0.5 - 0.1)
    dct.track("/xy//b")
    dct.assert_store(["/xy//a", "/xy//b"], [], [])
    dct.add_commit_time(0.45)
    dct.assert_time(False, 0.5 - 0.1)
    dct.add_commit_time(0.2)
    dct.assert_time(True, 0.2)
    assert dct.check("/z/") is False
    time.sleep(0.3)
    assert dct.check("/z/") is True
    dct.assert_time(False, 0.2 - 0.3)
    dct.clear()
    dct.assert_store([], [], [])
    dct.assert_time(False, None)
