import asyncio
import hashlib
import os
import random
import sys
import time
from contextlib import contextmanager
from io import StringIO
from pathlib import Path
from typing import Optional

import pytest
import safe_radix32
from clarabot_crypto.context.account_signature import ACCOUNT_SIGNATURE_CRYPTO_CONTEXT, AccountSignatureConfigLoadArgs
from clarabot_crypto.context.nano_data import NANO_DATA_CRYPTO_CONTEXT, NanoDataConfigLoadArgs
from clarabot_crypto.context.nano_own import NANO_OWN_CRYPTO_CONTEXT, NanoOwnConfigLoadArgs
from clarabot_nano_common.common.config.constants import NanoServeAPI
from clarabot_nano_common.common.crypto.keyring import NanoSelfAccountKeyring, NanoRoomKeyring
from clarabot_nano_common.common.error import log_escape_str
from clarabot_nano_common.common.handler.registry import NanoHandlerClassRegistry
from clarabot_nano_common.common.protocol.errors import (
    NANO_REQUEST_ERROR_UNKNOWN_TYPE,
    NANO_REQUEST_ERROR,
    NANO_REQUEST_ERROR_DENIED,
    NANO_REQUEST_ERROR_PART_RANGE,
    NANO_REQUEST_ERROR_CONFLICT,
    NANO_REQUEST_ERROR_TEMPORARY_INTERNAL_FAILURE,
    NANO_REQUEST_ERROR_DEBUG,
    NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST,
    NANO_REQUEST_ERROR_HANDLER_FAILURE,
    NANO_REQUEST_ERROR_ADMIN_SESSION,
)
from clarabot_nano_common.common.protocol.server import NanoServerResponse
from clarabot_nano_common.server.base.config import ServerConfig
from clarabot_nano_common.server.base.handler.filesystem.base import AbstractFileSystemHandler
from clarabot_nano_common.utility.serialize import msgpack_dumps, msgpack_loads, uint2bytes

from clarabot_nano.common.crypto.admin_password import ADMIN_PASSWORD_CONTEXT
from clarabot_nano.common.zmq.constants import REPLY_MESSAGE
from clarabot_nano.development.spawn.server.general.dbg_handlers import (
    DebugServerHandleHelper,
    DebugServerEchoSPHandler,
    DebugServerEchoCHandler,
    DebugServerEchoCAdminHandler,
)
from clarabot_nano.server.base.process import ServerProcess
from clarabot_nano_tests.testing.file import temp_path


class _ErrorTestingAbstractFileSystemHandler(AbstractFileSystemHandler):
    HANDLER_KEY = "FS-DBG"

    def build_error_message(self, buf: StringIO) -> None:
        buf.write(log_escape_str(self.get_drive_root_for_error_message()))


class _ErrorTestingServerProcess(ServerProcess):
    def __init__(self, tmpdir):
        # NOTE: purposefully not calling super inits
        self._helper = DebugServerHandleHelper(None, {})
        self._handler_registry = NanoHandlerClassRegistry(
            [
                DebugServerEchoSPHandler,
                DebugServerEchoCHandler,
                DebugServerEchoCAdminHandler,
                _ErrorTestingAbstractFileSystemHandler,
            ],
        )
        self.admin_password = "00000"
        self._config = {
            "local": {
                "data_dir": str(temp_path(tmpdir)),
                "remote_admin_policy": "restrict",
                "path_unicode_normalization": "default",
                "restrict_sensitive_path_access": True,
                "drive_restriction_paths": [],
                "cool_wopi_port": None,
            },
            "remote": {
                "name": "my-name",
                "remote_files_restriction": True,
                "secrets": [os.urandom(32) for _ in range(8)],
                "admin_password": ADMIN_PASSWORD_CONTEXT.hash(self.admin_password),
            },
            "state": {},
        }
        self._extracted_config = ServerConfig(self._config)
        # debug
        self._resps = []

    @contextmanager
    def change_config(self):
        yield self._config
        self._extracted_config = ServerConfig(self._config)

    def set_helper_dbg(self, dbg: Optional[dict] = None):
        if dbg:
            self._helper.dbg = dbg
        else:
            self._helper.dbg = dict()

    async def dbg_req_proc_err(
        self, gateway_context, gateway_payload, error, helper_dbg: Optional[dict] = None, set_request_head=True
    ):
        gateway_context = msgpack_loads(gateway_context)
        if set_request_head:
            gateway_context["request_head"] = gateway_payload[:128]
        else:
            gateway_context.pop("request_head", None)
        rs = await self.dbg_req_proc(msgpack_dumps(gateway_context), gateway_payload, helper_dbg)
        assert len(rs) == 1, f"{rs}"
        try:
            assert msgpack_loads(rs[0]) == {NanoServerResponse.ERROR: error}, f"{rs}"
        except AssertionError:
            raise
        except Exception:
            assert rs[0] == msgpack_dumps({NanoServerResponse.ERROR: error}), f"{rs}"

    async def dbg_req_proc(self, gateway_context, gateway_payload, helper_dbg: Optional[dict] = None):
        self.set_helper_dbg(helper_dbg)
        assert not self.get_responses()
        op_id = os.urandom(4)
        req = [b"", op_id]
        if gateway_context is None:
            assert gateway_payload is None
        else:
            req.append(gateway_context)
            if gateway_payload is not None:
                req.append(gateway_payload)
        await self._process_gateway_request(self, self._helper, False, req)
        rs = self.get_responses()
        assert len(rs) == 1, f"{rs}"
        assert list(rs[0])[:2] == [REPLY_MESSAGE, op_id], f"{rs[0]}"
        return rs[0][2:]

    def get_responses(self):
        tmp = self._resps
        self._resps = []
        return tmp

    # composite object mocking methods
    def send_multipart(self, parts):
        """
        Mocks the gateway_client.send_multipart()
        """
        self._resps.append(parts)


@pytest.mark.asyncio
async def test_server_request_errors(tmpdir):
    r0 = Path(str(temp_path(tmpdir)))
    r0.mkdir()
    r1 = Path(str(temp_path(tmpdir)))
    r1.mkdir()
    p = _ErrorTestingServerProcess(tmpdir)
    tmp = random.randint(100, 10000000)
    ids = [tmp + i for i in range(10)]

    room3_kr = NanoRoomKeyring.generate(safe_radix32.encode_fw(ids[3]))
    owner_kr = NanoSelfAccountKeyring.generate(safe_radix32.encode_fw(ids[0]))

    payload_adm = msgpack_dumps({"type": DebugServerEchoCAdminHandler.HANDLER_KEY})

    dbg_errors = [
        (NANO_REQUEST_ERROR, {"processing_fail": {"unexpected": True}}),
        (NANO_REQUEST_ERROR_DEBUG, {"processing_fail": {}}),
        (NANO_REQUEST_ERROR_DEBUG, {"processing_fail": {"maybe_conflict": True}}),
        (NANO_REQUEST_ERROR_DEBUG, {"processing_fail": {}, "data_source_fail": True}),
        (NANO_REQUEST_ERROR_CONFLICT, {"processing_fail": {"maybe_conflict": True}, "data_source_fail": True}),
    ]

    # preparse errors
    for context, payload in [
        (None, None),
        (b"xxxx", None),
        (msgpack_dumps([]), None),
        (msgpack_dumps([]), b"xxxx"),
        (msgpack_dumps([]), msgpack_dumps([])),
        (msgpack_dumps({}), msgpack_dumps([])),
    ]:
        assert await p.dbg_req_proc(context, payload) == [msgpack_dumps({})]

    # incomplete context errors 0 (preload-unexpected)
    for context, payload in [
        (msgpack_dumps({}), msgpack_dumps({})),
        (msgpack_dumps({"owner_kr": owner_kr.dump_internal(), "peer_kr": {"nothing": None}}), msgpack_dumps({})),
        (msgpack_dumps({"owner_kr": owner_kr.dump_internal(), "room": [None]}), msgpack_dumps({})),
        (msgpack_dumps({"owner_kr": owner_kr.dump_internal(), "room": {"nothing": None}}), msgpack_dumps({})),
    ]:
        await p.dbg_req_proc_err(context, payload, NANO_REQUEST_ERROR)

    # handler type errors
    for context, payload in [
        (msgpack_dumps({"owner_kr": owner_kr.dump_internal()}), msgpack_dumps({})),
        (msgpack_dumps({"owner_kr": owner_kr.dump_internal()}), msgpack_dumps({"type": None})),
        (msgpack_dumps({"owner_kr": owner_kr.dump_internal()}), msgpack_dumps({"type": []})),
    ]:
        await p.dbg_req_proc_err(context, payload, NANO_REQUEST_ERROR_UNKNOWN_TYPE)

    # incomplete context errors 1 (handler-unexpected)
    for context, payload in [
        (msgpack_dumps({"owner_kr": owner_kr.dump_internal()}), payload_adm),
        (msgpack_dumps({"owner_kr": owner_kr.dump_internal(), "account_id": ids[0]}), payload_adm),
        (msgpack_dumps({"owner_kr": owner_kr.dump_internal(), "owner_account_id": ids[0]}), payload_adm),
    ]:
        await p.dbg_req_proc_err(context, payload, NANO_REQUEST_ERROR)

    context_base = {
        "owner_kr": owner_kr.dump_internal(),
        "peer_kr": None,
        "part_size": 1000000,
        "part_approx": [750000, 250000],
        "s_hmac_parts": [24, 48],
    }

    # not owner error
    for context, payload in [
        (
            msgpack_dumps({**context_base, "account_id": ids[0], "owner_account_id": ids[1]}),
            payload_adm,
        ),
    ]:
        await p.dbg_req_proc_err(context, payload, NANO_REQUEST_ERROR_DENIED)

    admin_payload = msgpack_dumps({"type": DebugServerEchoCAdminHandler.HANDLER_KEY})
    # missing admin session
    await p.dbg_req_proc_err(
        msgpack_dumps({**context_base, "account_id": ids[0], "owner_account_id": ids[0]}),
        admin_payload,
        NANO_REQUEST_ERROR_ADMIN_SESSION,
        set_request_head=False,
    )
    # incomplete context errors (unexpected)
    for context in [
        msgpack_dumps({**context_base, "account_id": ids[0], "owner_account_id": ids[0], "admin_st": 1}),
        msgpack_dumps({**context_base, "account_id": ids[0], "owner_account_id": ids[0], "admin_st": 1, "part": None}),
    ]:
        await p.dbg_req_proc_err(context, admin_payload, NANO_REQUEST_ERROR, set_request_head=False)

    # explicit part error for single handler
    await p.dbg_req_proc_err(
        msgpack_dumps({**context_base, "account_id": ids[0], "part": 1}),
        msgpack_dumps({"type": DebugServerEchoSPHandler.HANDLER_KEY}),
        NANO_REQUEST_ERROR_PART_RANGE,
    )

    payload_cache = msgpack_dumps({"type": DebugServerEchoCHandler.HANDLER_KEY, "cacheable": True, "count": 2})
    payload_transfer = msgpack_dumps({"type": DebugServerEchoCHandler.HANDLER_KEY, "cacheable": False, "count": 2})

    # cache_key missing
    await p.dbg_req_proc_err(
        msgpack_dumps(
            {
                **context_base,
                "account_id": ids[0],
                "part": 3,
            }
        ),
        payload_cache,
        NANO_REQUEST_ERROR,
    )
    # cache_key mismatch
    await p.dbg_req_proc_err(
        msgpack_dumps(
            {
                **context_base,
                "account_id": ids[0],
                "cache_key": None,
                "part": 3,
            }
        ),
        payload_cache,
        NANO_REQUEST_ERROR_CONFLICT,
    )
    # transfer_key missing
    await p.dbg_req_proc_err(
        msgpack_dumps({**context_base, "account_id": ids[0], "cache_key": None, "part": 3}),
        payload_transfer,
        NANO_REQUEST_ERROR,
    )
    # transfer_key mismatch
    await p.dbg_req_proc_err(
        msgpack_dumps(
            {
                **context_base,
                "account_id": ids[0],
                "cache_key": None,
                "transfer_key": None,
                "part": 3,
            }
        ),
        payload_transfer,
        NANO_REQUEST_ERROR_CONFLICT,
    )

    # initial request conflict errors (internal-only)
    for payload in [payload_cache, payload_transfer]:
        await p.dbg_req_proc_err(
            msgpack_dumps(
                {
                    **context_base,
                    "account_id": ids[0],
                    "cache_key": None,
                    "transfer_key": None,
                    "part": None,
                    "request_head": payload[:128],
                }
            ),
            payload,
            NANO_REQUEST_ERROR_CONFLICT,
            {"data_source_fail": True},
        )

    # part range and conflict errors
    for payload in [payload_cache, payload_transfer]:
        cacheable = payload == payload_cache
        target_key = NanoServerResponse.CACHE_KEY if cacheable else NanoServerResponse.TRANSFER_KEY
        other_key = NanoServerResponse.TRANSFER_KEY if cacheable else NanoServerResponse.CACHE_KEY
        response0 = msgpack_loads(
            (
                await p.dbg_req_proc(
                    msgpack_dumps(
                        {
                            **context_base,
                            "account_id": ids[0],
                            "cache_key": None,
                            "transfer_key": None,
                            "part": None,
                            "request_head": payload[:128],
                        }
                    ),
                    payload,
                )
            )[0]
        )
        assert response0.get(target_key), str(response0)
        assert not response0.get(other_key), str(response0)
        for part in [0, 2]:
            await p.dbg_req_proc_err(
                msgpack_dumps(
                    {
                        **context_base,
                        "account_id": ids[0],
                        "cache_key": response0[target_key] if cacheable else None,
                        "transfer_key": response0[target_key] if not cacheable else None,
                        "part": part,
                    }
                ),
                payload,
                NANO_REQUEST_ERROR_PART_RANGE,
            )
        await p.dbg_req_proc_err(
            msgpack_dumps(
                {
                    **context_base,
                    "account_id": ids[0],
                    "cache_key": response0[target_key] if cacheable else None,
                    "transfer_key": response0[target_key] if not cacheable else None,
                    "part": 1,
                }
            ),
            payload,
            NANO_REQUEST_ERROR_CONFLICT,
            {"data_source_fail": True},
        )

    # handler exceptions (unexpected, coordinated, maybe conflict)
    for cacheable in (True, False):
        target_key = NanoServerResponse.CACHE_KEY if cacheable else NanoServerResponse.TRANSFER_KEY
        other_key = NanoServerResponse.TRANSFER_KEY if cacheable else NanoServerResponse.CACHE_KEY
        # unexpected error at init
        payload = msgpack_dumps(
            {
                "type": DebugServerEchoCHandler.HANDLER_KEY,
                "cacheable": cacheable,
                "count": 2,
            }
        )
        for err, dbg in dbg_errors:
            await p.dbg_req_proc_err(
                msgpack_dumps(
                    {
                        **context_base,
                        "account_id": ids[0],
                        "cache_key": None,
                        "transfer_key": None,
                        "part": None,
                        "request_head": payload[:128],
                    }
                ),
                payload,
                err,
                dbg,
            )
        # init fine
        response0, response1 = await p.dbg_req_proc(
            msgpack_dumps(
                {
                    **context_base,
                    "account_id": ids[0],
                    "cache_key": None,
                    "transfer_key": None,
                    "part": None,
                    "request_head": payload[:128],
                }
            ),
            payload,
        )
        response0 = msgpack_loads(response0)
        assert response0[target_key]
        assert not response0[other_key]
        response1 = NANO_OWN_CRYPTO_CONTEXT.load(NanoOwnConfigLoadArgs(response1, owner_kr, time.time()))
        response1 = msgpack_loads(response1)
        assert response1.get("PK"), str(response1)
        part_key = response1.get("PK")
        assert (
            msgpack_loads(
                (
                    await p.dbg_req_proc(
                        msgpack_dumps(
                            {
                                **context_base,
                                "account_id": ids[0],
                                "cache_key": response0[target_key] if cacheable else None,
                                "transfer_key": response0[target_key] if not cacheable else None,
                                "part": 1,
                                "request_head": payload[:128],
                            }
                        ),
                        payload,
                    )
                )[0]
            ).get(NanoServerResponse.ERROR)
            is None
        )
        for err, dbg in dbg_errors:
            await p.dbg_req_proc_err(
                msgpack_dumps(
                    {
                        **context_base,
                        "account_id": ids[0],
                        "cache_key": response0[target_key] if cacheable else None,
                        "transfer_key": response0[target_key] if not cacheable else None,
                        "part": None,
                        "request_head": payload[:128],
                    }
                ),
                payload,
                err,
                dbg,
            )

    # test a response with s_hmac signature
    respo0, respo1 = await p.dbg_req_proc(
        msgpack_dumps(
            {
                **context_base,
                "account_id": ids[0],
                "cache_key": response0[target_key] if cacheable else None,
                "transfer_key": response0[target_key] if not cacheable else None,
                "part": 1,
                "request_head": payload[:128],
            }
        ),
        payload,
        {"s_hmac_data": (s_hmac_data := os.urandom(49))},
    )
    assert msgpack_loads(respo0).get(NanoServerResponse.ERROR) is None
    respo1 = NANO_DATA_CRYPTO_CONTEXT.load(NanoDataConfigLoadArgs(respo1, part_key))
    respo1 = msgpack_loads(respo1)
    shmac = msgpack_loads(respo1["MAC"])
    s_hmac_data += uint2bytes(1)
    fingerprint = hashlib.sha512(shmac["s"] + s_hmac_data).digest()[:32]
    assert shmac["c"] == len(s_hmac_data)
    assert shmac["f"] == fingerprint
    assert (
        ACCOUNT_SIGNATURE_CRYPTO_CONTEXT.load(AccountSignatureConfigLoadArgs(respo1["MACS"], owner_kr, respo1["MAC"]))
        == respo1["MAC"]
    )

    # task cancellation
    payload = msgpack_dumps({"type": DebugServerEchoCHandler.HANDLER_KEY, "cacheable": True, "delay": 10})
    t = asyncio.create_task(
        p.dbg_req_proc(
            msgpack_dumps(
                {
                    **context_base,
                    "account_id": ids[0],
                    "cache_key": None,
                    "transfer_key": None,
                    "part": None,
                    "request_head": payload[:128],
                }
            ),
            payload,
        )
    )
    await asyncio.sleep(0.55)
    assert not t.done(), f"{t.result()}"
    t.cancel()
    with pytest.raises(asyncio.CancelledError):
        await t
    rs = p.get_responses()
    assert len(rs) == 1, f"{rs}"
    assert len(rs[0]) == 3, f"{rs}"
    assert rs[0][2] == msgpack_dumps({NanoServerResponse.ERROR: NANO_REQUEST_ERROR_TEMPORARY_INTERNAL_FAILURE})

    # FS-BASE
    payload = msgpack_dumps({"type": _ErrorTestingAbstractFileSystemHandler.HANDLER_KEY})
    for room, err in [
        (None, NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST),
        (
            {"id": ids[3], "room_apis": {0: {"dbg": True}}, "kr": room3_kr.dump_internal(), "member": 1, "group": 0},
            NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST,
        ),
        (
            {
                "id": ids[3],
                "room_apis": {NanoServeAPI.DRIVE_CURRENT: {"miss": None}},
                "kr": room3_kr.dump_internal(),
                "member": 1,
                "group": 0,
            },
            NANO_REQUEST_ERROR_HANDLER_FAILURE,
        ),
        (
            {
                "id": ids[3],
                "room_apis": {
                    NanoServeAPI.DRIVE_CURRENT: {
                        "path": "C:/._never_exists_XY" if sys.platform.startswith("win") else "/._never_exists_XY",
                    }
                },
                "kr": room3_kr.dump_internal(),
                "member": 1,
                "group": 0,
            },
            NANO_REQUEST_ERROR_HANDLER_FAILURE,
        ),
    ]:
        await p.dbg_req_proc_err(
            msgpack_dumps(
                {
                    **context_base,
                    "account_id": ids[0],
                    "cache_key": None,
                    "transfer_key": None,
                    "part": None,
                    "room": room,
                    "request_head": payload[:128],
                }
            ),
            payload,
            err,
        )

    # test drive inaccessibility by path restriction
    def _restrict_context(r_):
        return msgpack_dumps(
            {
                **context_base,
                "account_id": ids[0],
                "cache_key": None,
                "transfer_key": None,
                "part": None,
                "room": {
                    "id": ids[3],
                    "room_apis": {NanoServeAPI.DRIVE_CURRENT: {"path": str(r_)}},
                    "kr": room3_kr.dump_internal(),
                    "member": 1,
                    "group": 0,
                },
                "request_head": payload[:128],
            }
        )

    await p.dbg_req_proc_err(_restrict_context(r0), payload, NANO_REQUEST_ERROR)  # generic error (missing impl)
    await p.dbg_req_proc_err(_restrict_context(r1), payload, NANO_REQUEST_ERROR)  # generic error (missing impl)
    with p.change_config() as c:
        c["local"]["drive_restriction_paths"].append(str(r1))
    await p.dbg_req_proc_err(_restrict_context(r0), payload, NANO_REQUEST_ERROR_HANDLER_FAILURE)
    await p.dbg_req_proc_err(_restrict_context(r1), payload, NANO_REQUEST_ERROR)
    with p.change_config() as c:
        c["local"]["drive_restriction_paths"].append(str(r0))
    await p.dbg_req_proc_err(_restrict_context(r0), payload, NANO_REQUEST_ERROR)
    await p.dbg_req_proc_err(_restrict_context(r1), payload, NANO_REQUEST_ERROR)
    with p.change_config() as c:
        c["local"]["drive_restriction_paths"].remove(str(r1))
    await p.dbg_req_proc_err(_restrict_context(r0), payload, NANO_REQUEST_ERROR)
    await p.dbg_req_proc_err(_restrict_context(r1), payload, NANO_REQUEST_ERROR_HANDLER_FAILURE)
