import asyncio
import hashlib
import os
import random
import sys
import time
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.config.constants import NanoServeAPI
from clarabot_nano.common.crypto.admin_password import ADMIN_PASSWORD_CONTEXT
from clarabot_nano.common.crypto.keyring import NanoSelfAccountKeyring, NanoRoomKeyring
from clarabot_nano.common.handler.registry import NanoHandlerClassRegistry
from clarabot_nano.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,
)
from clarabot_nano.common.protocol.server import NanoServerResponse
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.config import ServerConfig
from clarabot_nano.server.base.handler.filesystem.base import AbstractFileSystemHandler
from clarabot_nano.server.base.process import ServerProcess
from clarabot_nano.utility.serialize import msgpack_dumps, msgpack_loads, uint2bytes
from clarabot_nano_tests.testing.file import temp_path


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


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._extracted_config = ServerConfig(
            {
                "local": {
                    "data_dir": str(temp_path(tmpdir)),
                    "remote_admin_policy": "restrict",
                    "path_unicode_normalization": "default",
                    "restrict_sensitive_path_access": True,
                },
                "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": {},
            }
        )
        # debug
        self._resps = []

    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}"
        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, 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):
    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, "admin_password": p.admin_password}
    )
    # incomplete context errors (unexpected)
    for context in [
        msgpack_dumps({**context_base, "account_id": ids[0], "owner_account_id": ids[0]}),
        msgpack_dumps({**context_base, "account_id": ids[0], "owner_account_id": ids[0], "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_V2: {"miss": None}},
                "kr": room3_kr.dump_internal(),
                "member": 1,
                "group": 0,
            },
            NANO_REQUEST_ERROR_HANDLER_FAILURE,
        ),
        (
            {
                "id": ids[3],
                "room_apis": {
                    NanoServeAPI.DRIVE_V2: {
                        "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,
        )
