import asyncio
import base64
import os
import socket
import sys
import time
import uuid
from contextlib import asynccontextmanager
from datetime import datetime, date, timedelta
from pathlib import Path

import pytest
import safe_radix32
from clarabot_crypto.license.file import LicenseFile
from clarabot_nano_common.utility.path.common import normalize_absolute_path
from clarabot_nano_common.utility.serialize import msgpack_dumps

from clarabot_nano.utility import parse_environment_flag
from clarabot_nano_tests.control_api.testing.client import ControlApiTestingClient
from clarabot_nano_tests.testing.client.gql_ws import GraphQlWebSocketClient
from clarabot_nano_tests.testing.client.manager import GraphQLClientManager
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.file import temp_path
from clarabot_nano_tests.testing.server.manager import DevServerManager

HERE = os.path.abspath(os.path.dirname(__file__))
TEMPLATE_HOOK_SCRIPT = Path(HERE, "testing", "vendor_hook_script.py").read_text()


@pytest.fixture
def control_api_path():
    control_api_path = (
        "\\\\.\\pipe\\" if sys.platform.startswith("win") else "/tmp/"
    ) + "cbnano-dev-control-api.socket"
    if os.path.exists(control_api_path):
        os.unlink(control_api_path)
    try:
        yield control_api_path
    finally:
        if os.path.exists(control_api_path):
            os.unlink(control_api_path)


@asynccontextmanager
async def headless_control_api_nano(
    tmpdir,
    testing_server_manager: DevServerManager,
    reset_user_app_dirs,
    dev_nano_root_dir,
    testing_client_manager: GraphQLClientManager,
    testing_server_user0,
    testing_server_user_1,
    control_api_path,
    local_configs: dict = None,
):
    room0 = testing_server_user0["rooms"][0]
    room0_sr32 = safe_radix32.encode_fw(room0["id"])
    await testing_server_manager.call_nano_clear_sessions([room0_sr32])
    await clear_nano_slots(
        testing_server_manager, [testing_server_user0["account_id"], testing_server_user_1["account_id"]]
    )

    r0 = Path(normalize_absolute_path(temp_path(tmpdir)))
    r0.mkdir()

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

    alt_group_name = None
    if not sys.platform.startswith("win"):
        import pwd
        import grp

        uname = pwd.getpwuid(os.getuid()).pw_name
        for g in grp.getgrall():
            if uname in g.gr_mem and g.gr_name != uname:
                alt_group_name = g.gr_name
                break
        else:
            alt_group_name = uname  # at elast cover the code path

    def _mut_loc_conf(c_):
        c_["control_api_path"] = control_api_path
        c_["control_api_mode"] = 0o750
        c_["control_api_group"] = alt_group_name
        if local_configs:
            c_.update(local_configs)

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

        await asyncio.sleep(0.25)
        capi = ControlApiTestingClient(control_api_path)
        try:
            async with capi.stream_status() as coll:
                if not sys.platform.startswith("win"):
                    s = os.stat(control_api_path)
                    assert (s.st_mode & 0o777) == 0o750
                    g = grp.getgrgid(s.st_gid)
                    assert g and g.gr_name == alt_group_name

                await asyncio.sleep(2.5)
                c = coll.get()
                assert c.pop("rps_count") > 0
                # test disconnect works okay

            yield r0, u0_kr, u0wsc, pm, capi
        finally:
            await capi.close()


@pytest.mark.skipif(parse_environment_flag("CBNANO_TEST_WITHOUT_SERVER"), reason="server-dependant tests disabled")
@pytest.mark.asyncio
async def test_control_api_base(
    tmpdir,
    testing_server_manager: DevServerManager,
    reset_user_app_dirs,
    dev_nano_root_dir,
    testing_client_manager: GraphQLClientManager,
    testing_server_user0,
    testing_server_user_1,
    control_api_path,
):
    room0 = testing_server_user0["rooms"][0]
    room0_sr32 = safe_radix32.encode_fw(room0["id"])

    async with headless_control_api_nano(
        tmpdir,
        testing_server_manager,
        reset_user_app_dirs,
        dev_nano_root_dir,
        testing_client_manager,
        testing_server_user0,
        testing_server_user_1,
        control_api_path,
    ) as (r0, u0_kr, u0wsc, pm, capi):
        r0: Path
        u0wsc: GraphQlWebSocketClient
        pm: DevCoreProcessManager
        capi: ControlApiTestingClient

        async with capi.stream_status() as coll:
            await asyncio.sleep(2.5)
            c = coll.get()
            assert c.pop("rps_count") > 0
            assert c == {
                "rps_sum": 0,
                "idx_works": [],
                "errors": [],
                "state": {
                    "name": socket.gethostname(),
                    "gateway_status": "waiting-config",
                    "indexer_status": "ready",
                    "gateway_update": -1,
                    "vendor": "Clarabot",
                    "license": None,
                    "device_id": None,
                    "license_local_validity": "missing",
                    "license_server_validity": "unknown",
                },
            }

            await asyncio.sleep(0.5)
            resp = await capi.do_login("test@not.valid", "passx")
            assert resp == {"result": "login failed"}
            resp = await capi.do_login(testing_server_user_1["email"], testing_server_user_1["password"])
            assert resp == {"result": "2faCode-required"}
            resp = await capi.do_login(testing_server_user_1["email"], testing_server_user_1["password"], "10101")
            assert resp == {"result": "login successful"}

            await capi.wait_gateway_active()

            await asyncio.sleep(1.5)
            resp = await capi.do_reset()
            assert resp == {"result": "ok"}
            await asyncio.sleep(1.5)
            c = coll.get()
            assert c.pop("rps_count") >= 0
            assert c == {
                "rps_sum": 0,
                "idx_works": [],
                "errors": [],
                "state": {
                    "name": socket.gethostname(),
                    "gateway_status": "waiting-config",
                    "indexer_status": "ready",
                    "gateway_update": 0,
                    "vendor": "Clarabot",
                    "license": None,
                    "device_id": None,
                    "license_local_validity": "missing",
                    "license_server_validity": "unknown",
                },
            }

            # login with user that has rooms
            resp = await capi.do_login(testing_server_user0["email"], testing_server_user0["password"])
            assert resp == {"result": "login successful"}

            await capi.wait_gateway_active()

            # attach drive to room
            session_id = pm.get_session_id_from_continue_token()
            await testing_server_manager.wait_nano_bind(testing_server_user0["account_id"], {session_id: set()})
            nano_id = await testing_server_manager.get_nano_id_by_nano_session_id(
                testing_server_user0["account_id"], session_id
            )
            assert nano_id is not None
            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,
            )

            f0 = r0 / "f0"
            f0.write_bytes(b"bbb")
            await asyncio.sleep(0.3)
            d0 = r0 / "d0"
            d0.mkdir()
            await asyncio.sleep(0.3)
            f1 = d0 / "f1"
            f1.write_bytes(b"ccc")

            # for error log
            await testing_client_manager.single_response_admin_request(
                {"type": "A-D-CREATE", "path": "not-absolute"},
                "dbg0",
                a_sess,
                None,
                nano_id,
                u0wsc,
                u0_kr,
                u0_kr,
                error=4215,
            )

            await asyncio.sleep(2.0)
            c = coll.get()
            assert c.pop("rps_count") > 0
            assert c.pop("rps_sum") > 0
            es = c.pop("errors")
            assert len(es) == 1
            assert "Path is not absolute" in es[0] and "Failed to update config: drive:create" in es[0]
            assert c == {
                "idx_works": ["f0", "f1"],
                "state": {
                    "name": socket.gethostname(),
                    "gateway_status": "active",
                    "indexer_status": "ready",
                    "gateway_update": 0,
                    "vendor": "Clarabot",
                    "license": None,
                    "device_id": None,
                    "license_local_validity": "missing",
                    "license_server_validity": "unknown",
                },
            }
            new_name = f"Nano{int(time.time()*1000)}"
            resp = await capi.do_rename(new_name)
            assert resp == {"result": "ok"}
            c = coll.get()
            assert c["state"]["name"] == new_name, str(c)


@pytest.fixture
async def migrating_licenses(testing_server_manager):
    tmp = await testing_server_manager.get_nano_file_licenses()
    migrating = tmp["vendor_licenses_auto_migrating"]  # vendor -> skpk
    licenses = tmp["vendor_licenses"]  # vendor -> [pk]
    return migrating, licenses


def hook_data_prepare(tmpdir, vendor):
    hook_data_path = Path(normalize_absolute_path(temp_path(tmpdir)))
    hook_script_path = Path(normalize_absolute_path(temp_path(tmpdir)))
    script = TEMPLATE_HOOK_SCRIPT
    script = script.replace("LICENSE_PATH: str", f"LICENSE_PATH = {str(hook_data_path)!r}")
    script = script.replace("VENDOR: str", f"VENDOR = {vendor!r}")
    hook_script_path.write_text(script)
    return hook_data_path, hook_script_path


@pytest.mark.skipif(parse_environment_flag("CBNANO_TEST_WITHOUT_SERVER"), reason="server-dependant tests disabled")
@pytest.mark.asyncio
async def test_control_api_vendor_licensing(
    tmpdir,
    testing_server_manager: DevServerManager,
    reset_user_app_dirs,
    dev_nano_root_dir,
    testing_client_manager: GraphQLClientManager,
    testing_server_user0,
    testing_server_user_1,
    control_api_path,
    migrating_licenses,
):
    room0 = testing_server_user0["rooms"][0]
    room0_sr32 = safe_radix32.encode_fw(room0["id"])

    migrating, licenses = migrating_licenses

    vendor = "Other"
    assert vendor not in migrating
    assert vendor in licenses
    lic_pk = licenses["Other"][0]
    lic_skpk = migrating["_Other"]

    hook_data_path, hook_script_path = hook_data_prepare(tmpdir, vendor)

    async with headless_control_api_nano(
        tmpdir,
        testing_server_manager,
        reset_user_app_dirs,
        dev_nano_root_dir,
        testing_client_manager,
        testing_server_user0,
        testing_server_user_1,
        control_api_path,
        local_configs={"hook_vendor_data": [sys.executable, str(hook_script_path)]},
    ) as (r0, u0_kr, u0wsc, pm, capi):
        r0: Path
        u0wsc: GraphQlWebSocketClient
        pm: DevCoreProcessManager
        capi: ControlApiTestingClient

        async with capi.stream_status() as coll:
            await asyncio.sleep(2.5)

            resp = await capi.do_login(testing_server_user0["email"], testing_server_user0["password"])
            assert resp == {"result": "login successful"}
            await coll.wait_state(
                {
                    "vendor": vendor,
                    "license": None,
                    "device_id": str(uuid.getnode()),
                    "license_local_validity": "missing",
                    "license_server_validity": "rejected",
                }
            )

            resp = await capi.do_new_license("asdfasdf\r\n{}\n")
            assert resp == {"result": "ok"}, str(resp)
            now = datetime.utcnow()
            lic55 = LicenseFile(
                55, (now - timedelta(days=1)).date(), (now + timedelta(days=20)).date(), [], {"vendor": "asdf"}
            )
            resp = await capi.do_new_license(lic55.dumps(lic_skpk, check_load_validity=False))
            assert resp == {"result": "ok"}, str(resp)
            await coll.wait_state(
                {
                    "vendor": vendor,
                    "license": None,
                    "device_id": str(uuid.getnode()),
                    "license_local_validity": "missing",
                    "license_server_validity": "rejected",
                }
            )

            lic55 = LicenseFile(
                55, (now - timedelta(days=1)).date(), (now + timedelta(days=20)).date(), [], {"vendor": vendor}
            )
            resp = await capi.do_new_license(lic55.dumps(lic_skpk))
            assert resp == {"result": "ok"}, str(resp)
            c = await coll.wait_state(
                {
                    "vendor": vendor,
                    "device_id": str(uuid.getnode()),
                    "license_local_validity": "valid",
                    "license_server_validity": "accepted",
                }
            )
            assert isinstance(c["state"].get("license"), str)

            resp = await capi.do_new_license("")
            assert resp == {"result": "ok"}, str(resp)
            await coll.wait_state(
                {
                    "vendor": vendor,
                    "license": None,
                    "device_id": str(uuid.getnode()),
                    "license_local_validity": "missing",
                    "license_server_validity": "rejected",
                }
            )


@pytest.mark.skipif(parse_environment_flag("CBNANO_TEST_WITHOUT_SERVER"), reason="server-dependant tests disabled")
@pytest.mark.asyncio
async def test_control_api_vendor_auto_migrating(
    tmpdir,
    testing_server_manager: DevServerManager,
    reset_user_app_dirs,
    dev_nano_root_dir,
    testing_client_manager: GraphQLClientManager,
    testing_server_user0,
    testing_server_user_1,
    control_api_path,
    migrating_licenses,
):
    room0 = testing_server_user0["rooms"][0]
    room0_sr32 = safe_radix32.encode_fw(room0["id"])

    migrating, licenses = migrating_licenses

    vendor = "Migraine"
    assert vendor in migrating
    assert vendor in licenses
    lic_pk = licenses["Migraine"][0]
    lic_skpk = migrating["Migraine"]

    hook_data_path, hook_script_path = hook_data_prepare(tmpdir, vendor)

    async with headless_control_api_nano(
        tmpdir,
        testing_server_manager,
        reset_user_app_dirs,
        dev_nano_root_dir,
        testing_client_manager,
        testing_server_user0,
        testing_server_user_1,
        control_api_path,
        local_configs={"hook_vendor_data": [sys.executable, str(hook_script_path)]},
    ) as (r0, u0_kr, u0wsc, pm, capi):
        r0: Path
        u0wsc: GraphQlWebSocketClient
        pm: DevCoreProcessManager
        capi: ControlApiTestingClient

        async with capi.stream_status() as coll:
            await asyncio.sleep(2.5)

            resp = await capi.do_login(testing_server_user0["email"], testing_server_user0["password"])
            assert resp == {"result": "login successful"}

            c = await coll.wait_state(
                {
                    "vendor": vendor,
                    "device_id": str(uuid.getnode()),
                    "license_local_validity": "valid",
                    "license_server_validity": "accepted",
                }
            )
            assert isinstance(c["state"].get("license"), str)
