import os
import pytest
import socket
import subprocess
import sys
import time

examples_dir = os.path.join(os.path.dirname(__file__), '..', 'examples')
hostname = 'localhost'


processes = []

@pytest.fixture
def cleanup():
    yield
    for p in processes:
        p.kill()


def run_subprocesses(address, server, client, wildcard_server=False, ipv4_force=True):
    server_attempt = 0
    server_attempts = 2
    done = False
    addr, port = address.split(':')
    c_address = address
    s_address = address
    while not done:
        assert server_attempt < server_attempts, "Failed {} server attempts".format(server_attempts)
        server_attempt += 1

        # Force ipv4 for tests (known issues on GitHub Actions with IPv6 for some targets)
        if 'unix' not in addr and ipv4_force:
            addr = socket.gethostbyname(addr)
            c_address = '{}:{}'.format(addr, port)
            s_address = c_address
            if wildcard_server:
                s_address = '*:{}'.format(port) # Use wildcard address for server
            print("Forcing ipv4 -> {} => {} {}".format(address, c_address, s_address))

        # Start server
        cmd = [sys.executable, os.path.join(examples_dir, server), s_address]
        serverp = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
        print("Server started (Attempt #{})".format(server_attempt))
        processes.append(serverp)
        retries = 300
        # Loop until we have a socket connection to the server (with timeout)
        while True:
            try:
                if 'unix' in address:
                    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
                    result = sock.connect_ex(port)
                    if result == 0:
                        break
                else:
                    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                    result = sock.connect_ex((addr, int(port)))
                    if result == 0:
                        break
                    sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
                    result = sock.connect_ex((addr, int(port)))
                    if result == 0:
                        break
            except socket.gaierror as err:
                print("gaierror: {}".format(err))
            # Give the server some small amount of time to start listening
            time.sleep(0.1)
            retries -= 1
            if retries == 0:
                serverp.kill()
                print("Timed out waiting for server to start")
                break

            if serverp.poll() is not None:
                print("Server exited prematurely: {}".format(serverp.returncode))
                break

        # 3 tries per server try
        client_attempt = 0
        client_attempts = 3
        while not done:
            if client_attempt >= client_attempts:
                print("Failed {} client attempts".format(client_attempts))
                break
            client_attempt += 1

            # Start client
            cmd = [sys.executable, os.path.join(examples_dir, client), c_address]
            clientp = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
            print("Client started (Attempt #{})".format(client_attempt))
            processes.append(clientp)

            retries = 30 * 10
            # Loop until the client is finished (with timeout)
            while True:
                if clientp.poll() == 0:
                    done = True
                    break

                if clientp.poll() is not None:
                    print("Client exited prematurely: {}".format(clientp.returncode))
                    break
                time.sleep(0.1)
                retries -= 1
                if retries == 0:
                    print("Timed out waiting for client to finish")
                    clientp.kill()
                    break

        serverp.kill()

    serverp.kill()


def test_async_calculator_example(cleanup):
    address = '{}:36432'.format(hostname)
    server = 'async_calculator_server.py'
    client = 'async_calculator_client.py'
    run_subprocesses(address, server, client)


@pytest.mark.xfail(reason="Some versions of python don't like to share ports, don't worry if this fails")
def test_thread_example(cleanup):
    address = '{}:36433'.format(hostname)
    server = 'thread_server.py'
    client = 'thread_client.py'
    run_subprocesses(address, server, client, wildcard_server=True)


def test_addressbook_example(cleanup):
    proc = subprocess.Popen([sys.executable, os.path.join(examples_dir, 'addressbook.py')])
    ret = proc.wait()
    assert ret == 0


@pytest.mark.skipif(sys.platform == "win32", reason="Asyncio bug with libcapnp timer, likely due to asyncio starving some event loop. See https://github.com/capnproto/pycapnp/issues/196")
def test_async_example(cleanup):
    address = '{}:36434'.format(hostname)
    server = 'async_server.py'
    client = 'async_client.py'
    run_subprocesses(address, server, client)


@pytest.mark.skipif(sys.platform == "win32", reason="Asyncio bug with libcapnp timer, likely due to asyncio starving some event loop. See https://github.com/capnproto/pycapnp/issues/196")
def test_ssl_async_example(cleanup):
    address = '{}:36435'.format(hostname)
    server = 'async_ssl_server.py'
    client = 'async_ssl_client.py'
    run_subprocesses(address, server, client, ipv4_force=False)


@pytest.mark.skipif(sys.platform == "win32", reason="Asyncio bug with libcapnp timer, likely due to asyncio starving some event loop. See https://github.com/capnproto/pycapnp/issues/196")
def test_ssl_reconnecting_async_example(cleanup):
    address = '{}:36436'.format(hostname)
    server = 'async_ssl_server.py'
    client = 'async_reconnecting_ssl_client.py'
    run_subprocesses(address, server, client, ipv4_force=False)


def test_async_ssl_calculator_example(cleanup):
    address = '{}:36437'.format(hostname)
    server = 'async_ssl_calculator_server.py'
    client = 'async_ssl_calculator_client.py'
    run_subprocesses(address, server, client, ipv4_force=False)
