import asyncio
import os
import shutil
from functools import partial
from pathlib import Path

import pytest

from clarabot_nano_tests.indexer.scheduler.utility import SchedulerTesterBase, PathStructureBuilder


@pytest.mark.asyncio
async def test_indexer_scheduler_root_sync_0(tmpdir, indexer_scheduler_tester: SchedulerTesterBase):
    """
    Roots come and go by filesystem conditions. (losing a root, no immediate removal)
    """
    rs = indexer_scheduler_tester.generate_tmp_roots_change_config(tmpdir, 3)
    psb = PathStructureBuilder(rs)

    # Initial indexing of content
    psb.create_files_in_dirs(rs[0], {"": 8, "a": 8})
    psb.create_files_in_dirs(rs[1], {"": 8, "b": 8})
    psb.create_files_in_dirs(rs[2], {"": 8, "c": 8})
    await indexer_scheduler_tester.run_check_repeated_polls(psb)

    # Test losing a root while one goes and comes back each time
    repeat = indexer_scheduler_tester.ec.indexer_remove_lost_count
    assert repeat > 1
    with psb.keep_paths_from_lost(rs[1], []):
        with psb.keep_paths_from_lost(rs[2], []):
            shutil.move(rs[2], rs[2] + "X")
            r1k, r1e = rs[1], rs[1] + "X"
            for _ in range(repeat):
                if os.path.exists(r1e):
                    shutil.move(r1e, r1k)
                else:
                    shutil.move(r1k, r1e)
                await indexer_scheduler_tester.run_check_repeated_polls(psb, repeat=1)
            if os.path.exists(r1e):
                shutil.move(r1e, r1k)
    await indexer_scheduler_tester.run_check_repeated_polls(psb)

    # check usage of soft commits (triggered by simulated interrupts)
    sc = [i for i in indexer_scheduler_tester.soft_commits if i > 1]
    assert sc, f"{sc}"


@pytest.mark.asyncio
async def test_indexer_scheduler_root_sync_1(tmpdir, indexer_scheduler_tester: SchedulerTesterBase):
    """
    Roots come and go by config change. (immediate removal)
    """
    rs = indexer_scheduler_tester.generate_tmp_roots_change_config(tmpdir, 3)
    rs_config = [{"path": r} for r in rs]
    psb_full = PathStructureBuilder(rs)

    psb_full.create_files_in_dirs(rs[0], {"": 8, "a": 8})
    psb_full.create_files_in_dirs(rs[1], {"": 8, "b": 8})
    psb_full.create_files_in_dirs(rs[2], {"": 8, "c": 8})

    # Initial indexing of content
    with indexer_scheduler_tester.change_config() as config:
        config["remote"]["drives"] = rs_config[:2]
    psb = PathStructureBuilder(rs[:2])
    ps = await indexer_scheduler_tester.run_check_repeated_polls(psb)
    assert ps.get(rs[0], {}).get("file") is False, str(ps.get(rs[0]))  # root gets doc by direct children
    assert all(p.startswith(rs[0]) or p.startswith(rs[1]) for p in ps.keys())

    # Change roots
    with indexer_scheduler_tester.change_config() as config:
        config["remote"]["drives"] = rs_config[1:]
    psb = PathStructureBuilder(rs[1:])
    ps = await indexer_scheduler_tester.run_check_repeated_polls(psb)
    assert ps.get(rs[0]) is None, str(ps.get(rs[0]))  # root doc by direct children is properly removed
    assert all(p.startswith(rs[1]) or p.startswith(rs[2]) for p in ps.keys())

    # check usage of soft commits (triggered by simulated interrupts)
    sc = [i for i in indexer_scheduler_tester.soft_commits if i > 1]
    assert sc, f"{sc}"


@pytest.mark.asyncio
async def test_indexer_scheduler_root_sync_2(tmpdir, indexer_scheduler_tester: SchedulerTesterBase):
    """
    Roots come and go by config change and filesystem conditions. (failed root blocking immediate removal)
    """
    rs = indexer_scheduler_tester.generate_tmp_roots_change_config(tmpdir, 3)
    rs_config = [{"path": r} for r in rs]
    psb_full = PathStructureBuilder(rs)

    psb_full.create_files_in_dirs(rs[0], {"": 8, "a": 8})
    psb_full.create_files_in_dirs(rs[1], {"": 8, "b": 8})
    psb_full.create_files_in_dirs(rs[2], {"": 8, "c": 8})

    # Initial indexing of content
    with indexer_scheduler_tester.change_config() as config:
        config["remote"]["drives"] = rs_config[:2]
    psb = PathStructureBuilder(rs[:2])
    ps = await indexer_scheduler_tester.run_check_repeated_polls(psb)
    assert all(p.startswith(rs[0]) or p.startswith(rs[1]) for p in ps.keys())

    # Change roots
    with psb.keep_paths_from_lost(rs[0], []):
        with psb.keep_paths_from_lost(rs[1], []):
            psb.roots.remove(rs[0])
            psb.roots.add(rs[2])
            shutil.move(rs[1], rs[1] + "X")
            with indexer_scheduler_tester.change_config() as config:
                config["remote"]["drives"] = rs_config[1:]
            with indexer_scheduler_tester.old_roots(rs[0]):
                ps = await indexer_scheduler_tester.run_check_repeated_polls(
                    psb, repeat=indexer_scheduler_tester.ec.indexer_remove_lost_count
                )
            assert all(p.startswith(rs[0]) or p.startswith(rs[1]) or p.startswith(rs[2]) for p in ps.keys())
            assert any(p.startswith(rs[0]) for p in ps.keys())
            assert any(p.startswith(rs[1]) for p in ps.keys())
            assert any(p.startswith(rs[2]) for p in ps.keys())
    ps = await indexer_scheduler_tester.run_check_repeated_polls(psb)
    assert all(p.startswith(rs[2]) for p in ps.keys())

    # check usage of soft commits (triggered by simulated interrupts)
    sc = [i for i in indexer_scheduler_tester.soft_commits if i > 1]
    assert sc, f"{sc}"


@pytest.mark.asyncio
async def test_indexer_scheduler_root_sync_3(tmpdir, indexer_scheduler_tester: SchedulerTesterBase):
    """
    Root consistency changes during sync. (failed becomes available and available fails)
    """
    rs = indexer_scheduler_tester.generate_tmp_roots_change_config(tmpdir, 4)
    psb = PathStructureBuilder(rs)
    psb.create_files_in_dirs(rs[0], {"": 48, "a": 48, "b": 48, "c": 48, "d": 48})
    psb.create_files_in_dirs(rs[1], {"": 48, "j": 48, "k": 48, "l": 48, "m": 48})
    Path(rs[2]).rmdir()
    psb.create_files_in_dirs(rs[3], {"": 5, "j": 5, "k": 5, "l": 5, "m": 5})

    # 4th root is denied by restriction
    psb.roots.remove(rs[3])
    with indexer_scheduler_tester.change_config() as config:
        config["local"]["drive_restriction_paths"] = [str(r) for r in rs[:-1]]

    e = indexer_scheduler_tester.new_scheduler_engine()

    def _lose_r(r_):
        shutil.move(r_, r_ + "X")

    def _acquire_r(r_):
        shutil.move(r_ + "X", r_)

    def _symlink_r():
        Path(rs[2]).symlink_to(Path(rs[1]), target_is_directory=True)

    _lose_r(rs[1])
    assert await e.index_state_db.get_roots() == set()
    await e.run_single_poll_cycle(simulate_interruptions=False, custom_sync_callbacks={2: partial(_lose_r, rs[0])})
    # r1 never indexed, r0 has been interrupted
    assert await e.index_state_db.get_roots() == {rs[0]}
    await asyncio.sleep(e.ec.indexer_delay)
    _acquire_r(rs[1])
    assert await e.index_state_db.get_roots() == {rs[0]}
    await e.run_single_poll_cycle(
        simulate_interruptions=False, custom_sync_callbacks={2: partial(_acquire_r, rs[0]), 3: _symlink_r}
    )
    # r1 is stable and indexed for the first time, r0 is re-acquired, r2 becomes a symlink to r1 (as alternate)
    assert await e.index_state_db.get_roots() == {rs[0], rs[1]}
    ps = psb.generate_paths_structure()
    await indexer_scheduler_tester.assert_from_paths(ps)
    assert await e.index_state_db.get_roots() == {rs[0], rs[1]}

    # lift path restrictions & assert indexing the no-longer failed drive
    psb.roots.add(rs[3])
    with indexer_scheduler_tester.change_config() as config:
        config["local"]["drive_restriction_paths"] = []

    e = indexer_scheduler_tester.new_scheduler_engine()

    assert await e.index_state_db.get_roots() == {rs[0], rs[1]}
    await e.run_single_poll_cycle(simulate_interruptions=False)
    assert await e.index_state_db.get_roots() == {rs[0], rs[1], rs[3]}
    ps = psb.generate_paths_structure()
    await indexer_scheduler_tester.assert_from_paths(ps)
    assert await e.index_state_db.get_roots() == {rs[0], rs[1], rs[3]}
