"""Test scaffolding: async test base class and assertion helpers."""
import ast
import asyncio
import os
import unittest
import socket
import hashlib

from ..errors import *
from ..settings import *
from .utils import *
from .cmd_tools import *
from ..net.net_utils import *
from ..net.address import *
from ..nic.interface import *
from ..nic.select_interface import *
from ..net.asyncio.event_loop import *

# Loads interface info on Windows.
# Make it available for all tests.
from ..net.pipe.pipe import *
from ..protocol.stun.stun_client import *
from ..protocol.stun.stun_defs import RFC3489
from ..net.net_defs import UDP, NET_CONF, VALID_AFS
from ..install import *


# Intentionally no __all__ defined on this module.
#
# test_init.py is test scaffolding: it re-exports a wide net of stdlib modules
# (unittest, asyncio, os, struct, sys, time, socket) and library names via
# `from X import *` at the top of the file so test suites can write
# `from aionetiface import *` and then use e.g. `unittest.IsolatedAsyncioTestCase`
# or `copy.deepcopy(...)` directly.  Several tests and downstream modules rely
# on that historical behaviour, so adding an explicit __all__ would break them.


aionetiface_NET_ADDR_BYTES = b"0,3-[0,158.69.27.176,158.69.27.176,10001,1,1,0]-[0,2607:5300:0060:80b0:0000:0000:0000:0001,fe80:0000:0000:0000:f816:3eff:feae:b2d9,10001,1,1,0]-aionetiface_test_node"

# Only load the test interface on the right machine.
# Otherwise the name (probably) won't exist.
aionetiface_IFS = []

if hasattr(unittest, "IsolatedAsyncioTestCase"):

    class PatchedAsyncTest(unittest.IsolatedAsyncioTestCase):  # type: ignore[misc]
        def _setupAsyncioLoop(self):
            assert self._asyncioTestLoop is None
            loop = CustomEventLoop()
            asyncio.set_event_loop(loop)
            loop.set_debug(False)
            self._asyncioTestLoop = loop
            fut = loop.create_future()
            self._asyncioCallsTask = loop.create_task(self._asyncioLoopRunner(fut))
            loop.run_until_complete(safe_run(fut))

else:
    PatchedAsyncTest = None


# Basic echo client test.
async def check_pipe(pipe, dest_tup=None):
    """Send an echo request over the pipe and return True if the response matches."""
    # Sanity check.
    data = b"Meow"
    if pipe is None:
        log("> Check pipe: pipe is none")
        return False

    # Indicate any message is acceptable to queue.
    pipe.subscribe(sub=SUB_ALL)

    # Request peer to echo back the data sent.
    if dest_tup is None:
        if hasattr(pipe, "get_first_peer_tup"):
            dest_tup = pipe.get_first_peer_tup()
        else:
            dest_tup = pipe.sock.getpeername()

    # Give any pipes time to add callback handlers.
    # This prevents race conditions in processing messages.
    await asyncio.sleep(4)
    await pipe.echo(data, dest_tup)

    # Wait for message with timeout.
    buf = await pipe.recv(timeout=3)
    if buf is None:
        log("Check pipe: recv buf is none")
        return False

    # May not have echo process hooked up.
    if data in buf:
        return True
    log(fstr("Check pipe: invalid recv buf {0}", (buf,)))
    return False


# If a random node ID is generated and subbed to over and over.
# The server may limit new subs from the same IP.
# But if all test nodes use the same IDs the there will be collisons.
# Hence generate a unique IQ deterministically.
def node_name(x, i):
    """Derive a deterministic 10-byte node identifier from a seed and the interface's MAC address."""
    assert i.resolved
    name_base = to_b(
        fstr(
            "{0} {1}",
            (
                i.mac,
                socket.gethostname(),
            ),
        )
    )
    node_name = hashlib.sha256(x + name_base).hexdigest()
    return to_b(node_name)[:10]


class FakeSTUNClient:
    def __init__(
        self,
        dest=None,
        proto=UDP,
        mode=RFC3489,
        conf=None,
    ):
        if conf is None:
            conf = NET_CONF
        self.rip = "1.3.3.7"
        self.sock = None
        self.mappings = []  # [local, mapped] ...
        self.p = 0
        self.wan_ip = None
        self.interface = None

    def rand_server(self):
        return None

    def set_mappings(self, mappings):
        self.mappings = mappings
        self.p = 0

    def set_wan_ip(self, wan_ip):
        self.wan_ip = wan_ip

    async def get_wan_ip(self, pipe=None):
        return ip_norm(self.wan_ip)

    async def get_mapping(self, pipe=None):
        local, mapped = self.mappings[self.p]
        out = [local, mapped, self.sock]
        self.p = (self.p + 1) % len(self.mappings)

        return out


async def duel_if_setup(netifaces):
    """Return two interfaces sharing a common address family, or None if unavailable."""
    # Load interface list
    if_names = await list_interfaces(netifaces)
    ifs = await load_interfaces(if_names, Interface)

    for af in VALID_AFS:
        found = []
        for nic in ifs:
            if af in nic.supported():
                found.append(nic)

        if len(found) >= 2:
            return found[:2], af


class FakeNetifaces:
    def __init__(self):
        self.addr_info = None

    def set_addr_info(self, addr_info):
        self.addr_info = addr_info

    def ifaddresses(self, if_name):
        return self.addr_info

    def get_nic_id(self, af, if_name):
        """Parity with Netifaces.get_nic_id; tests echo if_name back."""
        return if_name


def get_cached_if():
    """Load and return a previously cached interface dict from disk, or None if absent."""
    install_path = get_aionetiface_install_root()
    nic_path = os.path.join(install_path, "if_info")
    if not os.path.exists(nic_path):
        return None

    with open(nic_path, "rb") as f:
        buf = f.read()
        return ast.literal_eval(buf)
