import os
import shutil
import subprocess
import sys
import time
import uuid
from pathlib import Path

import pytest
import safe_radix32

from clarabot_nano.utility import parse_environment_flag
from clarabot_nano_tests.gateway.inter.common import BackupTestingObserver
from clarabot_nano_tests.gateway.inter.utility import run_nano_setup_drive, check_fs_recursive
from clarabot_nano_tests.testing.client.manager import GraphQLClientManager
from clarabot_nano_tests.testing.core.common import clear_nano_slots
from clarabot_nano_tests.testing.file import temp_path
from clarabot_nano_tests.testing.server.manager import DevServerManager
from clarabot_nano_tests.testing.utility import clear_nano_app_config_data_dir


@pytest.mark.skipif(parse_environment_flag("CBNANO_TEST_WITHOUT_SERVER"), reason="server-dependant tests disabled")
@pytest.mark.asyncio
async def test_gateway_backup(
    tmpdir,
    reset_user_app_dirs,
    reset_user_app_dirs_override2,
    dev_nano_root_dir,
    testing_server_manager: DevServerManager,
    testing_server_user0,
    testing_client_manager: GraphQLClientManager,
    testing_server_reset_data,
):
    u0_id_sr32 = safe_radix32.encode_fw(testing_server_user0["account_id"])
    u0_kr = testing_server_user0["_acc_kr"]
    await clear_nano_slots(testing_server_manager, [testing_server_user0["account_id"]])

    u1_data = testing_server_reset_data["user_accounts"][1]
    u2_data = testing_server_reset_data["user_accounts"][2]  # NOTE: has nano license as well
    u3_data = testing_server_reset_data["user_accounts"][3]

    u0_rooms = [testing_server_user0["rooms"][i] for i in range(4)]
    u2_room = u2_data["rooms"][0]

    invalid_room_id = safe_radix32.encode_fw(u0_rooms[-1]["id"])  # NOTE: not actually invalid, but unused
    session = await testing_server_manager.session()  # debug client session

    u0_drive0, u0_drive1 = [str(temp_path(tmpdir)) for _ in range(2)]
    u2_drive0 = str(temp_path(tmpdir))
    for dp in [u0_drive0, u0_drive1, u2_drive0]:  # TODO: assert u0_drive1 has no reflink support or build ad-hoc dev
        if sys.platform.startswith("linux") and dp == u0_drive0:
            reflink_supported_path = Path("/tmp/._test_cb_nano_reflink_test_")
            if reflink_supported_path.is_dir():
                tmpa = reflink_supported_path / str(uuid.uuid4())
                tmpa.write_bytes(os.urandom(8))
                if (
                    subprocess.call(
                        ["cp", "--preserve=timestamps", "--reflink=always", tmpa.as_posix(), tmpa.as_posix() + "_2"]
                    )
                    == 0
                ):
                    u0_drive0 = reflink_supported_path.as_posix()
            if u0_drive0 != reflink_supported_path.as_posix():
                _ln = os.getlogin()
                assert False, (
                    "Backup testing requires a reflink supported drive. Do something like:"
                    "\n  mkdir /tmp/._test_cb_nano_reflink_test_"
                    "\n  dd bs=1MiB if=/dev/zero of=/tmp/._test_xfs_cb_nano_mp_test_ count=1000"
                    "\n  mkfs.xfs /tmp/._test_xfs_cb_nano_mp_test_"
                    "\n  sudo mount /tmp/._test_xfs_cb_nano_mp_test_ /tmp/._test_cb_nano_reflink_test_"
                    f"\n  sudo chown {_ln}:{_ln} /tmp/._test_cb_nano_reflink_test_"
                )
            for item in reflink_supported_path.iterdir():
                if item.is_dir():
                    shutil.rmtree(item)
                else:
                    item.unlink()
        else:
            os.makedirs(dp)

    async with run_nano_setup_drive(
        testing_server_manager,
        testing_client_manager,
        reset_user_app_dirs,
        dev_nano_root_dir,
        testing_server_user0,
        u0_drive0,
        u0_rooms[0],
        extra_env={"CBNANO_TEST_MOCK_INTER_NANO_ERRORS": "1"},
    ) as (u0wsc0, u0a_sess0, u0mut_name0, u0nano_id0):
        # First round: from user2 drive to user0 drive0
        #   - user0 drive0 is reflink capable
        async with run_nano_setup_drive(
            testing_server_manager,
            testing_client_manager,
            reset_user_app_dirs_override2[1:],
            dev_nano_root_dir,
            u2_data,
            u2_drive0,
            u2_room,
            extra_env={
                "CBNANO_USER_DIR_OVERRIDE": reset_user_app_dirs_override2[0],
                "CBNANO_TEST_MOCK_INTER_NANO_ERRORS": "1",
            },
        ) as (u2wsc, u2a_sess, u2mut_name, u2nano_id):
            u2room_id = safe_radix32.encode_fw(u2_room["id"])

            bto = BackupTestingObserver(
                testing_client_manager,
                u0mut_name0,
                u0a_sess0,
                u0nano_id0,
                u0wsc0,
                u0_kr,
                u2_drive0,
                u0_drive0,
                u2room_id,
                invalid_room_id,
            )

            backup_const_paths = []
            const_data = os.urandom(16)

            Path(u2_drive0, "f0_const").write_bytes(const_data)
            Path(u2_drive0, "f0_b0").write_bytes(b"")
            Path(u2_drive0, "f0_b8").write_bytes(os.urandom(8))
            Path(u2_drive0, "f0_b2mib").write_bytes(os.urandom(4096) * 512)
            Path(u2_drive0, "f0_b50mib").write_bytes(
                os.urandom(4096) + b"".join([bytes([i] * 1024) * 1024 + os.urandom(4096) for i in range(50)])
            )

            await bto.wait_finish(room_id=u2room_id)
            backup_const_paths.append(bto.path.joinpath(bto.finished_folder, "f0_const"))

            Path(u2_drive0, "d0", "d01").mkdir(parents=True)
            Path(u2_drive0, "d0", "d01", "f01_b0").write_bytes(b"")
            Path(u2_drive0, "d0", "d01", "f01_b8").write_bytes(os.urandom(8))
            Path(u2_drive0, "d1", "d11").mkdir(parents=True)
            Path(u2_drive0, "d1", "d12").mkdir(parents=True)
            Path(u2_drive0, "d1", "f0_bx").write_bytes(os.urandom(4500) * 377)
            Path(u2_drive0, "d2").mkdir(parents=True)
            Path(u2_drive0, "d2", "f1_b0").write_bytes(b"")
            Path(u2_drive0, "d3").mkdir(parents=True)

            await bto.wait_finish()
            backup_const_paths.append(bto.path.joinpath(bto.finished_folder, "f0_const"))

            Path(u2_drive0, "f0_b0").write_bytes(os.urandom(8))
            Path(u2_drive0, "f0_b8").write_bytes(os.urandom(8))
            Path(u2_drive0, "f0_b2mib").write_bytes(os.urandom(8) + Path(u2_drive0, "f0_b2mib").read_bytes())
            Path(u2_drive0, "d0", "d01", "f01_b0").write_bytes(os.urandom(8))
            Path(u2_drive0, "d1", "d11", "f11_b8").write_bytes(os.urandom(8))
            Path(u2_drive0, "d1", "d12").rmdir()
            Path(u2_drive0, "d2", "f1_b0").unlink()
            Path(u2_drive0, "d2").rmdir()
            Path(u2_drive0, "d3", "f31_b8").write_bytes(os.urandom(8))

            await bto.wait_finish()
            backup_const_paths.append(bto.path.joinpath(bto.finished_folder, "f0_const"))

            # create unused temporary files
            (bto.path / "Temporary" / "xy_test").mkdir()
            (bto.path / "Temporary" / "xy_test" / "wat").write_bytes(os.urandom(8))
            (bto.path / "Temporary" / "xy_wat").write_bytes(os.urandom(8))
            (bto.path / "Temporary" / "xy_wat2").write_bytes(os.urandom(8))
            os.utime((bto.path / "Temporary" / "xy_wat2"), (time.time(), time.time() - 60 * 60 * 4))
            # create conflicts
            next_folder = bto.next_folder
            next_dir = bto.path / next_folder
            (next_dir / "d3").mkdir(parents=True)
            conflict0_data = os.urandom(10)
            Path(next_dir, "d3", "f31_b8").write_bytes(conflict0_data)
            conflict1_data = os.urandom(11)
            Path(next_dir, "d0").write_bytes(conflict1_data)
            Path(next_dir, "f0_b0").mkdir()

            await bto.wait_finish(check=False, current_folder=next_folder)
            backup_const_paths.append(bto.path.joinpath(bto.finished_folder, "f0_const"))
            assert next_folder == bto.finished_folder
            assert not (bto.path / "Temporary" / "xy_test").exists()
            assert (bto.path / "Temporary" / "xy_wat").exists()
            assert not (bto.path / "Temporary" / "xy_wat2").exists()
            for item in next_dir.iterdir():
                assert not item.name.startswith(".~CbN.")
                if item.is_dir() and item.name != "f0_b0" and item.name.find("f0_b0") >= 0:
                    item.rmdir()  # conflicting dir with file was moved
                elif item.is_file() and item.name != "d0" and item.name.find("d0") >= 0:
                    assert item.read_bytes() == conflict1_data
                    item.unlink()  # conflicting file with dir was moved
            for item in (next_dir / "d3").iterdir():
                if item.is_file() and item.name != "f31_b8" and item.name.find("f31_b8") >= 0:
                    assert item.read_bytes() == conflict0_data
                    item.unlink()  # conflicting file with file was moved
            check_fs_recursive(bto.remote_path, next_dir)  # check matching without the conflicts

            # snapshots are okay
            for p in backup_const_paths:
                assert p.read_bytes() == const_data

        # Second round: from user0 drive0 to user0 drive1
        #   - user0 drive1 does not support reflink
        clear_nano_app_config_data_dir(*reset_user_app_dirs_override2[1:])
        async with run_nano_setup_drive(
            testing_server_manager,
            testing_client_manager,
            reset_user_app_dirs_override2[1:],
            dev_nano_root_dir,
            testing_server_user0,
            u0_drive1,
            u0_rooms[1],
            extra_env={"CBNANO_USER_DIR_OVERRIDE": reset_user_app_dirs_override2[0]},
            salt="x2",
        ) as (u0wsc1, u0a_sess1, u0mut_name1, u0nano_id1):
            u0room_id0 = safe_radix32.encode_fw(u0_rooms[0]["id"])

            bto = BackupTestingObserver(
                testing_client_manager,
                u0mut_name1,
                u0a_sess1,
                u0nano_id1,
                u0wsc1,
                u0_kr,
                u0_drive0,
                u0_drive1,
                u0room_id0,
                invalid_room_id,
            )

            const_data = os.urandom(16)
            first_8 = os.urandom(8)
            Path(u0_drive0, "f0_const").write_bytes(const_data)
            Path(u0_drive0, "f0_b0").write_bytes(b"")
            Path(u0_drive0, "f0_b8").write_bytes(first_8)
            Path(u0_drive0, "f0_b2mib").write_bytes(os.urandom(4096) * 512)

            await bto.wait_finish(room_id=u0room_id0, timeout=30.0)
            first_bup = bto.path / bto.finished_folder

            second_8 = os.urandom(10)
            Path(u0_drive0, "f0_b8").write_bytes(second_8)

            await bto.wait_finish()
            second_bup = bto.path / bto.finished_folder

            assert not (first_bup / "f0_const").exists()
            assert (first_bup / "f0_b8").read_bytes() == first_8
            assert (second_bup / "f0_const").read_bytes() == const_data
            assert (second_bup / "f0_b8").read_bytes() == second_8

            assert sorted(item.name for item in first_bup.iterdir() if item.is_file()) == ["f0_b8"]
