import asyncio
import base64
import os
import random
from datetime import datetime
from pathlib import Path
from typing import Optional

import pytest
import safe_radix32
from clarabot_crypto.group_config.block import GroupConfigBlockFlags, GroupConfigBlockMemberFlags
from clarabot_nano_common.common.config.constants import NanoServeAPI
from clarabot_nano_common.common.crypto.keyring import NanoSelfAccountKeyring
from clarabot_nano_common.common.protocol.request import NanoRequestContext
from clarabot_nano_common.utility.path.common import normalize_absolute_path
from clarabot_nano_common.utility.serialize import msgpack_dumps

from clarabot_nano.server.document.proxy import DocumentWsSession
from clarabot_nano.utility import parse_environment_flag
from clarabot_nano_tests.testing.client.manager import GraphQLClientManager
from clarabot_nano_tests.testing.client.wopi import WopiApiTester
from clarabot_nano_tests.testing.config import room_blocks_from_room_data
from clarabot_nano_tests.testing.core.common import clear_nano_slots
from clarabot_nano_tests.testing.core.instance import ConfigTestingNanoInstanceHandler
from clarabot_nano_tests.testing.core.manager import DevCoreProcessManager
from clarabot_nano_tests.testing.document import EchoDocumentWsSessionTester
from clarabot_nano_tests.testing.file import temp_path
from clarabot_nano_tests.testing.gateway import do_gateway_login
from clarabot_nano_tests.testing.server.manager import DevServerManager
from clarabot_nano_tests.testing.wsd import WsdTestingServerMock


@pytest.fixture
async def wsd_mock_server():
    async with WsdTestingServerMock(12152).run() as wsd_srv:
        yield wsd_srv


@pytest.mark.skipif(parse_environment_flag("CBNANO_TEST_WITHOUT_SERVER"), reason="server-dependant tests disabled")
@pytest.mark.asyncio
async def test_server_document_mock(
    tmpdir,
    testing_server_manager: DevServerManager,
    reset_user_app_dirs,
    dev_nano_root_dir,
    testing_client_manager: GraphQLClientManager,
    testing_server_user0,
    testing_server_reset_data,
    wsd_mock_server: WsdTestingServerMock,
):
    u0_id = testing_server_user0["account_id"]
    u0_id_sr32 = safe_radix32.encode_fw(u0_id)
    room0 = testing_server_user0["rooms"][0]
    room0_id = room0["id"]
    room0_sr32 = safe_radix32.encode_fw(room0_id)
    room0_kr = room0["_room_kr"]
    await testing_server_manager.call_nano_clear_sessions([room0_sr32])
    await clear_nano_slots(testing_server_manager, [u0_id])

    u1_data = testing_server_reset_data["user_accounts"][1]
    u2_data = testing_server_reset_data["user_accounts"][2]
    u3_data = testing_server_reset_data["user_accounts"][3]
    u4_data = testing_server_reset_data["user_accounts"][4]
    u5_data = testing_server_reset_data["user_accounts"][5]

    r0 = Path(normalize_absolute_path(temp_path(tmpdir)))
    r0.mkdir()
    r0_f0 = r0 / "f0.txt"
    r0_f0_data = b"file1"
    r0_f0.write_bytes(r0_f0_data)
    r0_f1 = r0 / "f1.txt"
    r0_f1.write_bytes(b"file2")
    r0_f2 = r0 / "f2.odt"
    r0_f2.write_bytes(b"file3")
    r0_f3 = r0 / "f3.rtf"
    r0_f3.write_bytes(b"file4")

    u0_kr = testing_server_user0["_acc_kr"]
    u0wsc = await testing_client_manager.create_logged_in_wsc(testing_server_user0)
    assert not u0wsc.ws.closed

    whp = 12151

    def _mut_loc_conf(c_):
        c_["cool_wopi_port"] = whp
        c_["cool_server_port"] = wsd_mock_server.port

    async with ConfigTestingNanoInstanceHandler(
        reset_user_app_dirs,
        dev_nano_root_dir,
        testing_server_manager,
        local_config_mut=_mut_loc_conf,
    ).run(with_document_server=True) as (pm, debug_info, session, dbg_config):
        pm: DevCoreProcessManager

        await asyncio.sleep(0.25)

        u1wsc = await testing_client_manager.create_logged_in_wsc(u1_data)
        u2wsc = await testing_client_manager.create_logged_in_wsc(u2_data)
        u3wsc = await testing_client_manager.create_logged_in_wsc(u3_data)
        anon_s = await testing_client_manager.session("anon")
        anon_wsc = await testing_client_manager.wsc(anon_s)
        anon_kr = NanoSelfAccountKeyring.generate_anonymous()

        dbg_state = (await pm.get_wait_dbg_config())["state"]
        gateway_port = dbg_state["gateway_port"]
        gateway_key = dbg_state["gateway_key"]
        # successful login
        session_id = await do_gateway_login(
            testing_server_manager, pm, debug_info, gateway_port, gateway_key, testing_server_user0
        )
        await testing_server_manager.wait_nano_bind(u0_id, {session_id: set()})
        await pm.wait_dbg_states(
            [
                ("gateway_port", gateway_port),
                ("gateway_key", gateway_key),
                ("gateway_status", "active"),
            ],
            timeout=15.0,
        )
        assert session_id == pm.get_session_id_from_continue_token()
        await testing_server_manager.wait_nano_bind(u0_id, {session_id: set()})
        nano_id = await testing_server_manager.get_nano_id_by_nano_session_id(u0_id, session_id)
        assert nano_id is not None

        # setup admin pw
        a_pwd = base64.b64encode(os.urandom(9)).decode()
        await testing_client_manager.nano_request_response(
            msgpack_dumps(
                await testing_client_manager.prepare_request_with_mutation_id(
                    {"type": "A-CH-PWD", "new_password": a_pwd},
                    "dbg0",
                    None,
                    nano_id,
                    u0wsc,
                    u0_kr,
                    u0_kr,
                )
            ),
            None,
            nano_id,
            u0wsc,
            u0_kr,
            u0_kr,
        )
        a_sess = await testing_client_manager.new_admin_session_request(
            a_pwd, "dbg0", None, nano_id, u0wsc, u0_kr, u0_kr
        )
        await testing_client_manager.single_response_admin_request(
            {"type": "A-D-CREATE", "path": str(r0)}, "dbg0", a_sess, None, nano_id, u0wsc, u0_kr, u0_kr
        )
        await testing_client_manager.single_response_admin_request(
            {
                "type": "A-D-ATTACH",
                "path": str(r0),
                "target_room_id": room0_sr32,
                "room_blocks": room_blocks_from_room_data(room0),
            },
            "dbg0",
            a_sess,
            None,
            nano_id,
            u0wsc,
            u0_kr,
            u0_kr,
        )
        await asyncio.sleep(0.33)  # a little time is needed so serve advertisements become available for routing
        room_perm = {
            "type": "ROOM-CFG",
            "room_id": room0_sr32,
            "member_flags": {
                # NOTE: flag changes on owner are N/A, but not forbidden; this is here to test key-crypto
                u0_id_sr32: GroupConfigBlockMemberFlags.ACTIVE | GroupConfigBlockMemberFlags.ADMINISTRATOR,
                safe_radix32.encode_fw(u1_data["account_id"]): GroupConfigBlockMemberFlags.ACTIVE,
                safe_radix32.encode_fw(u2_data["account_id"]): GroupConfigBlockMemberFlags.ACTIVE_NEGATE,
                safe_radix32.encode_fw(u3_data["account_id"]): GroupConfigBlockMemberFlags.ACTIVE
                | GroupConfigBlockMemberFlags.ADMINISTRATOR
                | GroupConfigBlockMemberFlags.DRIVE_WRITE_NEGATE,  # NOTE: A previous test sets this, so we undo it.
                safe_radix32.encode_fw(u4_data["account_id"]): GroupConfigBlockMemberFlags.ACTIVE
                | GroupConfigBlockMemberFlags.DRIVE_WRITE,
            },
            "group_flags": GroupConfigBlockFlags.ALLOW_ANONYMOUS_NEGATE,
        }
        await testing_client_manager.nano_request_response(
            msgpack_dumps(
                await testing_client_manager.prepare_request_with_mutation_id(
                    room_perm, "dbg0", room0_id, None, u0wsc, u0_kr, u0_kr
                )
            ),
            room0_id,
            None,
            u0wsc,
            u0_kr,
            u0_kr,
        )
        await asyncio.sleep(0.2)  # so change is reflected in gw

        wsd_ver, u0_f0_id, u0_f0_at = await testing_client_manager.document_ws_prepare(
            r0_f0.name, room0_id, u0wsc, u0_kr, u0_kr
        )
        _, u0_f0_id2, u0_f0_at2 = await testing_client_manager.document_ws_prepare(
            r0_f0.name, room0_id, u0wsc, u0_kr, u0_kr, check_wsd_ver=wsd_ver
        )
        assert u0_f0_id == u0_f0_id2
        assert u0_f0_at == u0_f0_at2  # NOTE: this is not required, if it would expire it would be different
        _, u0_f1_id, u0_f1_at = await testing_client_manager.document_ws_prepare(
            r0_f1.name, room0_id, u0wsc, u0_kr, u0_kr, check_wsd_ver=wsd_ver
        )
        assert u0_f1_id != u0_f0_id
        assert u0_f1_at != u0_f0_at
        r0_f1.unlink()
        _, u0_f2_id, u0_f2_at = await testing_client_manager.document_ws_prepare(
            r0_f2.name, room0_id, u0wsc, u0_kr, u0_kr, check_wsd_ver=wsd_ver
        )
        assert u0_f2_id != u0_f0_id
        assert u0_f2_at != u0_f0_at
        r0_f2.unlink()
        _, u0_f3_id, u0_f3_at = await testing_client_manager.document_ws_prepare(
            r0_f3.name, room0_id, u0wsc, u0_kr, u0_kr, check_wsd_ver=wsd_ver
        )
        assert u0_f3_id != u0_f0_id
        assert u0_f3_at != u0_f0_at
        r0_f3.unlink()

        #  Test pre-signaling no-template for empty file.
        r0_f1.touch()
        await testing_client_manager.document_ws_prepare(
            r0_f1.name, room0_id, u0wsc, u0_kr, u0_kr, check_open_requires_template=True
        )
        r0_f1.unlink()
        r0_f2.touch()
        await testing_client_manager.document_ws_prepare(
            r0_f2.name, room0_id, u0wsc, u0_kr, u0_kr, check_wsd_ver=wsd_ver
        )
        r0_f2.unlink()

        async def _get_internal_response(
            a_id_: int, r_id_: int, req_: Optional[dict] = None, load_avatar_: bool = False
        ):
            rp_ = {"type": "debug-intern-auth", "i_account_id": a_id_, "i_room_id": r_id_, "i_request": req_}
            if load_avatar_:
                rp_["i_avatar"] = True
            r_ = await testing_client_manager.prepare_request_with_mutation_id(
                rp_, "dbg0", None, nano_id, u0wsc, u0_kr, u0_kr
            )
            _, resps_ = await testing_client_manager.nano_request_responses_raw(
                msgpack_dumps(r_), None, nano_id, u0wsc, u0_kr, u0_kr
            )
            assert len(resps_) == 1
            assert isinstance(resps_[0], dict) and "response" in resps_[0], str(resps_[0])
            assert set(resps_[0]["response"].keys()) == {"result"}
            assert isinstance(resps_[0]["response"]["result"], dict)
            return resps_[0]["response"]["result"]

        part_size = 500_000
        _sentinel = object()

        async def _get_internal_room_auth(
            a_id_: int, r_id_: int, avatar_name_: Optional[str] = _sentinel, avatar_key_: Optional[str] = None
        ):
            load_avatar_ = avatar_name_ is not _sentinel
            result_ = await _get_internal_response(a_id_, r_id_, load_avatar_=load_avatar_)
            assert set(result_.keys()) == {"context"}
            context_ = result_["context"]
            if context_:
                ks_ = {"part_size", "room", "owner_account_id", "account_id"}
                if load_avatar_:
                    ks_.update(["avatar_name", "avatar_key"])
                assert set(context_.keys()) == ks_
                assert context_["part_size"] == part_size
                assert context_["account_id"] == a_id_
                if load_avatar_:
                    assert context_["avatar_name"] == avatar_name_
                    assert context_["avatar_key"] == avatar_key_
                assert context_["owner_account_id"] == u0_id
                return context_["room"]
            return {}

        async def _check_internal_room_auth(
            a_id_: int,
            r_id_: int,
            target_: dict,
            avatar_name_: Optional[str] = _sentinel,
            avatar_key_: Optional[str] = None,
        ):
            r_ = await _get_internal_room_auth(a_id_, r_id_, avatar_name_=avatar_name_, avatar_key_=avatar_key_)
            assert r_ == target_, str(r_)

        _room_base = {
            NanoRequestContext.ROOM_ID: room0_id,
            NanoRequestContext.ROOM_APIS: {
                NanoServeAPI.DRIVE_CURRENT | NanoServeAPI.DRIVE_EXT_DOCUMENT_CURRENT: {"path": str(r0)}
            },
            NanoRequestContext.ROOM_CONFIG_GROUP: GroupConfigBlockFlags.ALLOW_ANONYMOUS_NEGATE,
        }
        # debug internal-auth from gw
        await _check_internal_room_auth(0, 0, {})
        await _check_internal_room_auth(u0_id, 0, {})
        await _check_internal_room_auth(0, room0_id, {})
        await _check_internal_room_auth(
            u0_id,
            room0_id,
            {**_room_base, "member": GroupConfigBlockMemberFlags.ACTIVE | GroupConfigBlockMemberFlags.ADMINISTRATOR},
            testing_server_user0["avatar_name"],
            testing_server_user0["avatar_image_key"],
        )
        await _check_internal_room_auth(
            u1_data["account_id"],
            room0_id,
            {**_room_base, "member": GroupConfigBlockMemberFlags.ACTIVE},
            u1_data["avatar_name"],
            u1_data["avatar_image_key"],
        )
        await _check_internal_room_auth(u2_data["account_id"], room0_id, {})
        await _check_internal_room_auth(
            u3_data["account_id"],
            room0_id,
            {
                **_room_base,
                "member": GroupConfigBlockMemberFlags.ACTIVE
                | GroupConfigBlockMemberFlags.ADMINISTRATOR
                | GroupConfigBlockMemberFlags.DRIVE_WRITE_NEGATE,
            },
            u3_data["avatar_name"],
            u3_data["avatar_image_key"],
        )
        assert "avatar_name" not in u4_data
        await _check_internal_room_auth(
            u4_data["account_id"],
            room0_id,
            {**_room_base, "member": GroupConfigBlockMemberFlags.ACTIVE | GroupConfigBlockMemberFlags.DRIVE_WRITE},
            "",
        )
        await _check_internal_room_auth(99999999, room0_id, {})
        # nano api access
        _, u1_f0_id, u1_f0_at = await testing_client_manager.document_ws_prepare(
            r0_f0.name, room0_id, u1wsc, u1_data["_acc_kr"], u0_kr, room0_kr, check_wsd_ver=wsd_ver
        )
        await testing_client_manager.document_ws_prepare(
            r0_f0.name,
            room0_id,
            u2wsc,
            u2_data["_acc_kr"],
            u0_kr,
            room0_kr,
            server_error="Invalid argument!",
        )
        await testing_client_manager.document_ws_prepare(
            r0_f0.name,
            room0_id,
            anon_wsc,
            anon_kr,
            u0_kr,
            room0_kr,
            ephemeral_pk=anon_kr.get_public_key(anon_kr.CURVE25519),
            server_error="Invalid argument!",
        )
        # general internal request processing
        assert await _get_internal_response(
            99999999, room0_id, {"type": "debug-intern-auth", "i_account_id": 11, "i_room_id": 12}
        ) == {"e": 4206}
        assert await _get_internal_response(
            u1_data["account_id"],
            room0_id,
            {"type": "debug-intern-auth", "i_account_id": 13, "i_room_id": 14},
            load_avatar_=True,
        ) == {
            "context": {
                NanoRequestContext.PART_SIZE: part_size,
                NanoRequestContext.ACCOUNT_ID: u1_data["account_id"],
                NanoRequestContext.ACCOUNT_AVATAR_NAME: u1_data["avatar_name"],
                NanoRequestContext.ACCOUNT_AVATAR_KEY: u1_data["avatar_image_key"],
                NanoRequestContext.OWNER_ACCOUNT_ID: u0_id,
                NanoRequestContext.ROOM: {**_room_base, "member": GroupConfigBlockMemberFlags.ACTIVE},
            },
            "response": {"debug": "echo_internal", "account_id": 13, "room_id": 14, "request": None},
        }
        # NOTE: Testing the internal-compatible requests here is necessary, as the document server is the only one
        # that has the internal-request handler setup only now.
        assert await _get_internal_response(
            u4_data["account_id"], room0_id, {"type": "FS-L-G", "path": r0_f0.name}
        ) == {
            "context": {
                "part_size": part_size,
                "account_id": u4_data["account_id"],
                "owner_account_id": u0_id,
                "room": {
                    **_room_base,
                    "member": GroupConfigBlockMemberFlags.ACTIVE | GroupConfigBlockMemberFlags.DRIVE_WRITE,
                },
            },
            "response": {"t": None},
        }
        assert await _get_internal_response(
            u1_data["account_id"], room0_id, {"type": "FS-L-G", "path": r0_f0.name}
        ) == {"e": 4216}
        assert await _get_internal_response(
            u4_data["account_id"], room0_id, {"type": "FS-L-X", "path": r0_f0.name, "op": "create", "token": "tl2"}
        ) == {
            "context": {
                "part_size": part_size,
                "account_id": u4_data["account_id"],
                "owner_account_id": u0_id,
                "room": {
                    **_room_base,
                    "member": GroupConfigBlockMemberFlags.ACTIVE | GroupConfigBlockMemberFlags.DRIVE_WRITE,
                },
            },
            "response": {},
        }
        assert await _get_internal_response(
            u4_data["account_id"], room0_id, {"type": "FS-L-X", "path": r0_f0.name, "op": "create", "token": "ws"}
        ) == {
            "context": {
                "part_size": part_size,
                "account_id": u4_data["account_id"],
                "owner_account_id": u0_id,
                "room": {
                    **_room_base,
                    "member": GroupConfigBlockMemberFlags.ACTIVE | GroupConfigBlockMemberFlags.DRIVE_WRITE,
                },
            },
            "response": {"c": "tl2"},
        }
        assert await _get_internal_response(
            u4_data["account_id"], room0_id, {"type": "FS-L-G", "path": r0_f0.name}
        ) == {
            "context": {
                "part_size": part_size,
                "account_id": u4_data["account_id"],
                "owner_account_id": u0_id,
                "room": {
                    **_room_base,
                    "member": GroupConfigBlockMemberFlags.ACTIVE | GroupConfigBlockMemberFlags.DRIVE_WRITE,
                },
            },
            "response": {"t": "tl2"},
        }

        # with anonymous
        perm_req1 = {
            "type": "ROOM-CFG",
            "room_id": room0_sr32,
            "group_flags": GroupConfigBlockFlags.ALLOW_ANONYMOUS,
        }
        await testing_client_manager.nano_request_response(
            msgpack_dumps(
                await testing_client_manager.prepare_request_with_mutation_id(
                    perm_req1, "dbg0", room0_id, None, u0wsc, u0_kr, u0_kr
                )
            ),
            room0_id,
            None,
            u0wsc,
            u0_kr,
            u0_kr,
        )
        _room_base["group"] = GroupConfigBlockFlags.ALLOW_ANONYMOUS
        await asyncio.sleep(0.2)  # so change is reflected in gw

        await _check_internal_room_auth(0, 0, {})
        await _check_internal_room_auth(u0_id, 0, {})
        await _check_internal_room_auth(0, room0_id, {**_room_base, "member": 0}, "Anonymous")
        await _check_internal_room_auth(
            u0_id,
            room0_id,
            {**_room_base, "member": GroupConfigBlockMemberFlags.ACTIVE | GroupConfigBlockMemberFlags.ADMINISTRATOR},
        )
        await _check_internal_room_auth(
            u1_data["account_id"], room0_id, {**_room_base, "member": GroupConfigBlockMemberFlags.ACTIVE}
        )
        await _check_internal_room_auth(
            u2_data["account_id"],
            room0_id,
            {**_room_base, "member": 0},
            u2_data["avatar_name"],
            u2_data["avatar_image_key"],
        )
        await _check_internal_room_auth(
            u3_data["account_id"],
            room0_id,
            {
                **_room_base,
                "member": GroupConfigBlockMemberFlags.ACTIVE
                | GroupConfigBlockMemberFlags.ADMINISTRATOR
                | GroupConfigBlockMemberFlags.DRIVE_WRITE_NEGATE,
            },
            u3_data["avatar_name"],
            u3_data["avatar_image_key"],
        )
        await _check_internal_room_auth(
            u4_data["account_id"],
            room0_id,
            {**_room_base, "member": GroupConfigBlockMemberFlags.ACTIVE | GroupConfigBlockMemberFlags.DRIVE_WRITE},
        )
        await _check_internal_room_auth(99999999, room0_id, {**_room_base, "member": 0})
        _, u2_f0_id, u2_f0_at = await testing_client_manager.document_ws_prepare(
            r0_f0.name,
            room0_id,
            u2wsc,
            u2_data["_acc_kr"],
            u0_kr,
            room0_kr,
            check_wsd_ver=wsd_ver,
        )  # NOTE: as anon
        _, anon_f0_id, anon_f0_at = await testing_client_manager.document_ws_prepare(
            r0_f0.name,
            room0_id,
            anon_wsc,
            anon_kr,
            u0_kr,
            room0_kr,
            check_wsd_ver=wsd_ver,
            ephemeral_pk=anon_kr.get_public_key(anon_kr.CURVE25519),
        )

        assert u0_f0_id == u1_f0_id == u2_f0_id == anon_f0_id
        assert len({u0_f0_at, u1_f0_at, u2_f0_at, anon_f0_at}) == 4

        # test the WOPI-host api
        u0_wat_f3 = WopiApiTester(session, whp, u0_f3_id, u0_f3_at, r0_f3, False, u0_id, u0_id, testing_server_user0)
        u0_wat_f2 = WopiApiTester(session, whp, u0_f2_id, u0_f2_at, r0_f2, False, u0_id, u0_id, testing_server_user0)
        u0_wat_f1 = WopiApiTester(session, whp, u0_f1_id, u0_f1_at, r0_f1, False, u0_id, u0_id, testing_server_user0)
        u0_wat_f0 = WopiApiTester(session, whp, u0_f0_id, u0_f0_at, r0_f0, False, u0_id, u0_id, testing_server_user0)
        u1_wat_f0 = WopiApiTester(session, whp, u1_f0_id, u1_f0_at, r0_f0, True, u0_id, u1_data["account_id"], u1_data)
        u2_wat_f0 = WopiApiTester(session, whp, u2_f0_id, u2_f0_at, r0_f0, True, u0_id, u2_data["account_id"], u2_data)
        anon_wat_f0 = WopiApiTester(
            session, whp, anon_f0_id, anon_f0_at, r0_f0, True, u0_id, 0, {"avatar_name": "Anonymous"}
        )
        u0_wat_ax = WopiApiTester(session, whp, u0_f0_id, "x", r0_f0, False, u0_id, u0_id)
        u0_wat_fx = WopiApiTester(session, whp, "x", u0_f0_at, r0_f0, False, u0_id, u0_id)
        u0_wat_f01 = WopiApiTester(session, whp, u0_f0_id, u0_f1_at, r0_f0, False, u0_id, u0_id)

        await u0_wat_f2.check_file_info(404)
        await u0_wat_f1.check_file_info(404)
        await u0_wat_f0.check_file_info()
        await u1_wat_f0.check_file_info()
        await u2_wat_f0.check_file_info()
        await anon_wat_f0.check_file_info()
        await u0_wat_ax.check_file_info(401)
        await u0_wat_fx.check_file_info(401)
        await u0_wat_f01.check_file_info(401)

        await u0_wat_f2.get_lock(None, 404)
        await u0_wat_f1.get_lock(None, 404)
        await u0_wat_f0.get_lock("tl2")
        await u1_wat_f0.get_lock(None, 404)
        await u2_wat_f0.get_lock(None, 404)
        await anon_wat_f0.get_lock(None, 404)
        await u0_wat_ax.get_lock(None, 401)
        await u0_wat_fx.get_lock(None, 401)
        await u0_wat_f01.get_lock(None, 401)

        assert await u0_wat_f2.lock("xy", 404) is None
        assert await u0_wat_f1.lock("xy", 404) is None
        assert await u0_wat_f0.lock("xy", 409) == "tl2"
        assert await u1_wat_f0.lock("xy", 404) is None
        assert await u2_wat_f0.lock("xy", 404) is None
        assert await anon_wat_f0.lock("xy", 404) is None
        assert await u0_wat_ax.lock("xy", 401) is None
        assert await u0_wat_fx.lock("xy", 401) is None
        assert await u0_wat_f01.lock("xy", 401) is None

        assert await u0_wat_f2.lock("xy", 404, "ab") is None
        assert await u0_wat_f1.lock("xy", 404, "ab") is None
        assert await u0_wat_f0.lock("xy", 409, "ab") == "tl2"
        assert await u1_wat_f0.lock("xy", 404, "ab") is None
        assert await u2_wat_f0.lock("xy", 404, "ab") is None
        assert await anon_wat_f0.lock("xy", 404, "ab") is None
        assert await u0_wat_ax.lock("xy", 401, "ab") is None
        assert await u0_wat_fx.lock("xy", 401, "ab") is None
        assert await u0_wat_f01.lock("xy", 401, "ab") is None

        assert await u0_wat_f2.unlock("xy", 404) is None
        assert await u0_wat_f1.unlock("xy", 404) is None
        assert await u0_wat_f0.unlock("xy", 409) == "tl2"
        assert await u1_wat_f0.unlock("xy", 404) is None
        assert await u2_wat_f0.unlock("xy", 404) is None
        assert await anon_wat_f0.unlock("xy", 404) is None
        assert await u0_wat_ax.unlock("xy", 401) is None
        assert await u0_wat_fx.unlock("xy", 401) is None
        assert await u0_wat_f01.unlock("xy", 401) is None

        assert await u0_wat_f2.refresh_lock("xy", 404) is None
        assert await u0_wat_f1.refresh_lock("xy", 404) is None
        assert await u0_wat_f0.refresh_lock("xy", 409) == "tl2"
        assert await u1_wat_f0.refresh_lock("xy", 404) is None
        assert await u2_wat_f0.refresh_lock("xy", 404) is None
        assert await anon_wat_f0.refresh_lock("xy", 404) is None
        assert await u0_wat_ax.refresh_lock("xy", 401) is None
        assert await u0_wat_fx.refresh_lock("xy", 401) is None
        assert await u0_wat_f01.refresh_lock("xy", 401) is None

        await u0_wat_f2.get_file(404)
        await u0_wat_f1.get_file(404)
        assert await u0_wat_f0.get_file() == r0_f0_data
        assert await u1_wat_f0.get_file() == r0_f0_data
        assert await u2_wat_f0.get_file() == r0_f0_data
        assert await anon_wat_f0.get_file() == r0_f0_data
        await u0_wat_ax.get_file(401)
        await u0_wat_fx.get_file(401)
        await u0_wat_f01.get_file(401)

        assert await u0_wat_f2.put_file("xy", b"xx", 404) == (None, None)
        assert await u0_wat_f1.put_file("xy", b"xx", 404) == (None, None)
        assert await u0_wat_f0.put_file("xy", b"xx", 409) == ("tl2", None)
        assert await u1_wat_f0.put_file("xy", b"xx", 404) == (None, None)
        assert await u2_wat_f0.put_file("xy", b"xx", 404) == (None, None)
        assert await anon_wat_f0.put_file("xy", b"xx", 404) == (None, None)
        assert await u0_wat_ax.put_file("xy", b"xx", 401) == (None, None)
        assert await u0_wat_fx.put_file("xy", b"xx", 401) == (None, None)
        assert await u0_wat_f01.put_file("xy", b"xx", 401) == (None, None)

        assert await u0_wat_f2.put_relative_file("xy", "f6.txt", b"xxg-wet") is not None  # allow saving from deleted
        assert await u0_wat_f1.put_relative_file("xy", "f3.txt", b"xxg-wat") is not None  # allow saving from deleted
        assert await u0_wat_f0.put_relative_file("xy", "f4.txt", b"xx-rel-me") is not None
        assert await u0_wat_f0.put_relative_file("xy", "f4.txt", b"qq-auto-n", auto_names=True) is not None
        assert await u1_wat_f0.put_relative_file("xy", "f5.txt", b"xx", 404) is None
        assert await u2_wat_f0.put_relative_file("xy", "f5.txt", b"xx", 404) is None
        assert await anon_wat_f0.put_relative_file("xy", "f5.txt", b"xx", 404) is None
        assert await u0_wat_ax.put_relative_file("xy", "f5.txt", b"xx", 401) is None
        assert await u0_wat_fx.put_relative_file("xy", "f5.txt", b"xx", 401) is None
        assert await u0_wat_f01.put_relative_file("xy", "f5.txt", b"xx", 401) is None

        # more successful locking
        dn = datetime.now().isoformat()
        assert await u0_wat_f0.unlock("tl2") is None
        assert await u0_wat_f0.lock("tl3") is None
        assert await u0_wat_f0.refresh_lock("tl3") is None
        assert await u0_wat_f0.lock("tl4", old_token="tl3") is None
        assert await u0_wat_f0.put_file("tl4", put_data := b"put-me") == (None, u0_wat_f0.put_result())
        assert r0_f0.read_bytes() == put_data
        # NOTE: There is no external-change conflict handling if the client does not specify the COOL-Timestamp header.
        assert await u0_wat_f0.put_file("tl4", put_data := b"put-wat", forced_ts="") == (None, u0_wat_f0.put_result())
        assert r0_f0.read_bytes() == put_data
        assert await u0_wat_f0.put_file("tl4", b"xx", 409, forced_ts=dn) == (None, {"COOLStatusCode": 1010})
        r0_f0.unlink()  # NOTE: Removing locked file produces unexpected state for editor & locking.
        # NOTE: Even if PutFile were to accept creating a new file, it would fail later because the internal GetLock
        # check returns 404. To save in this state PutRelativeFile can be utilized.
        assert await u0_wat_f0.put_file("tl4", b"xx", 404, forced_ts=dn) == (None, None)
        assert await u0_wat_f0.put_file("tl4", b"xx", 404, forced_ts="") == (None, None)
        # NOTE: Even PutRelativeFile will fail to save the original filename because of the broken lock, but an
        # internal autoname & retry will succeed to save to a different path, if the directory still exists.
        f0_alt_name = (
            await u0_wat_f0.put_relative_file("tl4", u0_wat_f0.path.name, put_data := b"put-you", auto_names=True)
        )["Name"]
        assert (r0_f0.parent / f0_alt_name).read_bytes() == put_data
        assert r0_f0.read_bytes() == b""  # Re-created by PutRelativeFile, but a broken lock prevented writing.
        assert await u0_wat_f0.unlock("tl4") is None
        assert await u0_wat_f0.put_file("xy", b"xx", 409) == ("", None)

        # template-source checkfileinfo
        async def _debug_store_template(file_path: str, template: str, templ_by_path: str = None):
            rp_ = {
                "type": "debug-store-wopi-tmpl",
                "room_id": room0_sr32,
                "file_path": file_path,
                "tmpl": template,
                "templ_by_path": templ_by_path,
            }
            r_ = await testing_client_manager.prepare_request_with_mutation_id(
                rp_, "dbg0", room0_id, None, u0wsc, u0_kr, u0_kr
            )
            _, resps_ = await testing_client_manager.nano_request_responses_raw(
                msgpack_dumps(r_), room0_id, None, u0wsc, u0_kr, u0_kr
            )
            assert len(resps_) == 1
            assert set(resps_[0]["response"].keys()) == {"result"}
            return resps_[0]["response"]["result"]

        await u0_wat_f3.check_file_info(404)
        await u0_wat_f2.check_file_info(404)
        await u0_wat_f1.check_file_info(404)
        r0_f3.touch()
        r0_f2.touch()
        r0_f1.touch()
        await u0_wat_f3.check_file_info(404)  # no template for ".rtf"
        await u0_wat_f2.check_file_info(template="empty/writer.ott")
        await u0_wat_f1.check_file_info()  # ".txt" can't be auto-templated because empty file state is valid as-is
        tmpl_src = await _debug_store_template(r0_f3.as_posix(), "not-found")
        assert not tmpl_src
        await u0_wat_f3.check_file_info(404)
        tmpl_src = await _debug_store_template(r0_f1.as_posix(), "not-found")
        assert not tmpl_src
        await u0_wat_f1.check_file_info()
        # NOTE: Forcing template for different extension only works with debug handler.
        tmpl_src = await _debug_store_template(r0_f3.as_posix(), "empty", "valami.ods")
        assert tmpl_src and tmpl_src.endswith(".ots")
        await u0_wat_f3.check_file_info(template=tmpl_src)
        r0_f1.unlink()
        r0_f2.unlink()
        r0_f3.unlink()
        await u0_wat_f3.check_file_info(404)
        await u0_wat_f2.check_file_info(404)
        await u0_wat_f1.check_file_info(404)

        # TODO: test document_ws_* handler auth consistency (with wopi token permissions)
        # TODO: test document_ws_* handler error cases

        # Test the WS transport to (echo-debug) document server.
        async with EchoDocumentWsSessionTester(part_size, testing_client_manager, room0_id, u0wsc, u0_kr, u0_kr).run(
            EchoDocumentWsSessionTester.create_wsd_ws_url(u0_f0_id, u0_f0_at)
        ) as wst0:
            wst0: EchoDocumentWsSessionTester

            prefix = b"x "  # needed for message translation parsing to pass
            wst0.send(prefix + os.urandom(89))
            await wst0.wait_drain_queues()
            for s in [5, 1, 50, 150, 0, 37, 13]:
                wst0.send(prefix + os.urandom(s))
            await wst0.wait_drain_queues()
            for s in [5, 1, 50, 150, 0, 37, 13]:
                wst0.send(prefix + os.urandom(1000) * s)
            await wst0.wait_drain_queues()
            for s in [300, 3, 400, 2, 420, 501, 441, 5, 13, 600, 150]:
                wst0.send(prefix + os.urandom(1000) * s)
            await wst0.wait_drain_queues()
            for s in [1300, 1400, 2501, 800, 3100]:
                wst0.send(prefix + os.urandom(1000) * s)
                await wst0.wait_drain_queues()  # all shuffled makes the command buffer close the session

            # test little size delay
            for _ in range(10):
                wst0.send(prefix + os.urandom(3) * random.randint(0, 33333))
                await wst0.wait_drain_queues(0.31)

            wst0.do_shuffle = False
            # large/random data
            for _ in range(5):
                for _ in range(11):
                    wst0.send(prefix + os.urandom(37) * random.randint(0, 94594))
                await asyncio.sleep(1.0 + random.random() * 1.5)
            await wst0.wait_drain_queues()


def test_server_document_utilities():
    for msg, tokens_ref in [
        (b"resetaccesstoken asd", [b"resetaccesstoken", b"asd"]),
        (b"resetaccesstoken asd\n", [b"resetaccesstoken", b"asd"]),
        (b" resetaccesstoken asd ", [b"resetaccesstoken", b"asd"]),
        (b" resetaccesstoken asd\n", [b"resetaccesstoken", b"asd"]),
        (b"  resetaccesstoken  asd \n", [b"resetaccesstoken", b"asd"]),
        (b"  resetaccesstoken\n  asd \n", [b"resetaccesstoken"]),
        (b"  resetaccesstoken  \n  asd \n", [b"resetaccesstoken"]),
        (b"resetaccesstoken  \n  asd \n", [b"resetaccesstoken"]),
        (b"resetaccesstoken \n \n  asd \n", [b"resetaccesstoken"]),
        (b"resetaccesstoken", [b"resetaccesstoken"]),
        (b"resetaccesstoken\n", [b"resetaccesstoken"]),
        (b"resetaccesstoken\n ", [b"resetaccesstoken"]),
        (b" resetaccesstoken \n", [b"resetaccesstoken"]),
        (b" load url=asd   yes \n", [b"load", b"url=asd", b"yes"]),
    ]:
        tokens = list(DocumentWsSession.wsd_command_tokenizer(msg))
        assert tokens, f"{msg} {tokens} {tokens_ref}"
        assert tokens == tokens_ref, f"{msg} {tokens} {tokens_ref}"
