"""Tests for pyps4_2ndscreen/ddp.py"""
from unittest.mock import patch
import asyncio
import logging

import pytest
from pyps4_2ndscreen import ddp
from pyps4_2ndscreen.credential import get_ddp_message
from pyps4_2ndscreen.ps4 import Ps4Async as ps4, STATUS_STANDBY

logging.basicConfig(level=logging.DEBUG)
_LOGGER = logging.getLogger(__name__)


MODULE_NAME = "pyps4_2ndscreen"

MOCK_CREDS = "123412341234abcd12341234abcd12341234abcd12341234abcd12341234abcd"
MOCK_NAME = "ha_ps4_name"
MOCK_HOST = "192.168.0.2"
MOCK_HOST_NAME = "Fake PS4"
MOCK_HOST_ID = "A0000A0AA000"
MOCK_HOST_TYPE = "PS4"
MOCK_STATUS_REST = "Server Standby"
MOCK_STATUS_ON = "Ok"
MOCK_STANDBY_CODE = 620
MOCK_ON_CODE = 200
MOCK_TCP_PORT = 997
MOCK_DDP_PORT = 987
MOCK_DDP_VERSION = "00020020"
MOCK_SYSTEM_VERSION = "01000000"
MOCK_RANDOM_PORT = 1234
MOCK_TITLE_ID = "CUSA00000"
MOCK_TITLE_NAME = "Random Game"

MOCK_DDP_VER = 'device-discovery-protocol-version:{}\n'.format(
    MOCK_DDP_VERSION)

MOCK_DDP_MESSAGE = '{} * HTTP/1.1\n{}'
MOCK_DDP_CRED_DATA = 'user-credential:{}\nclient-type:a\nauth-type:C\n{}'

MOCK_DDP_DICT = {
    "host-type": MOCK_HOST_TYPE,
    "host-ip": MOCK_HOST,
    "host-request-port": MOCK_TCP_PORT,
    "running-app-name": MOCK_TITLE_NAME,
    "running-app-titleid": MOCK_TITLE_ID,
    "host-id": MOCK_HOST_ID,
    "host-name": MOCK_HOST_NAME,
    "status": MOCK_STATUS_ON,
    "status_code": MOCK_ON_CODE,
    "device-discovery-protocol-version": MOCK_DDP_VERSION,
    "system-version": MOCK_SYSTEM_VERSION,
}

MOCK_DDP_RESPONSE = '''
    HTTP/1.1 {} {}\n
    host-id:{}\n
    host-type:{}\n
    host-name:{}\n
    host-request-port:{}\n
    running-app-name:{}\n
    running-app-titleid:{}\n
    device-discovery-protocol-version:{}\n
    system-version:{}\n
'''.format(
    MOCK_ON_CODE,
    MOCK_STATUS_ON,
    MOCK_HOST_ID,
    MOCK_HOST_TYPE,
    MOCK_HOST_NAME,
    MOCK_TCP_PORT,
    MOCK_TITLE_NAME,
    MOCK_TITLE_ID,
    MOCK_DDP_VERSION,
    MOCK_SYSTEM_VERSION,
)

MOCK_DDP_PROTO_HOST = '127.0.0.2'
MOCK_DDP_PROTO_HOST2 = '127.0.0.3'
MOCK_DDP_PROTO_PORT = 9041  # Random port. Otherwise need sudo.

MOCK_DDP_PROTO_CREDS =\
    '223412341234abcd12341234abcd12341234abcd12341234abcd12341234abcd'
MOCK_DDP_PROTO_CREDS2 =\
    '323412341234abcd12341234abcd12341234abcd12341234abcd12341234abcd'

MOCK_DDP_PROTO_STANDBY = '620 Server Standby'
MOCK_DDP_PROTO_RESPONSE = {
    'host-id': "123456789A",
    'host-type': 'PS4',
    'host-name': 'FakePs4',
    'host-request-port': MOCK_DDP_PROTO_PORT
}

DDP_PROTO_TIMEOUT = 3


def test_ddp_messages():
    """Test that DDP messages to send are correct."""
    msg = ddp.get_ddp_search_message()
    msg = msg.split('\n')
    for item in msg:
        assert item in MOCK_DDP_MESSAGE.format(
            ddp.DDP_TYPE_SEARCH, MOCK_DDP_VER)

    cred_data = MOCK_DDP_CRED_DATA.format(MOCK_CREDS, MOCK_DDP_VER)

    msg = ddp.get_ddp_launch_message(MOCK_CREDS)
    msg = msg.split('\n')
    for item in msg:
        assert item in MOCK_DDP_MESSAGE.format(ddp.DDP_TYPE_LAUNCH, cred_data)

    msg = ddp.get_ddp_wake_message(MOCK_CREDS)
    msg = msg.split('\n')
    for item in msg:
        assert item in MOCK_DDP_MESSAGE.format(ddp.DDP_TYPE_WAKEUP, cred_data)


def test_get_status():
    """Test that get_status returns correctly parsed response."""
    with patch(
        'pyps4_2ndscreen.ddp._send_recv_msg',
        return_value=(
            MOCK_DDP_RESPONSE.encode('utf-8'),
            (MOCK_HOST, MOCK_RANDOM_PORT)),
    ) as mock_send:
        parsed = ddp.get_status(host=MOCK_HOST)

    assert len(mock_send.mock_calls) == 1

    for key, value in parsed.items():
        assert key in MOCK_DDP_DICT
        assert value == parsed[key]
    assert len(parsed) == len(MOCK_DDP_DICT)


class MockDDPProtocol():
    """Mock DDP Server; PS4."""

    def connection_made(self, transport):
        self.transport = transport

    def connection_lost(self, exc):
        """On Connection Lost."""
        if self.transport is not None:
            _LOGGER.error("Mock DDP Transport Closed")
            self.transport.close()

    def datagram_received(self, data, addr):
        message = data.decode()
        _LOGGER.debug("RECV: %s", message)
        msg = get_ddp_message(MOCK_DDP_PROTO_STANDBY, MOCK_DDP_PROTO_RESPONSE)
        msg = msg.encode()
        _LOGGER.debug("Sent: %s to %s", msg, addr)
        self.transport.sendto(msg, addr)

    def close(self):
        """Stop Server."""
        self.transport.close()
        self.transport = None


async def start_mock_ps4(mock_addr):
    """Start a Mock PS4 Server."""
    mock_loop = asyncio.get_event_loop()
    mock_server = mock_loop.create_datagram_endpoint(
        lambda: MockDDPProtocol(), local_addr=(mock_addr, MOCK_DDP_PROTO_PORT),  # noqa: pylint: disable=unnecessary-lambda
        reuse_port=True, allow_broadcast=True)
    _, mock_protocol = await mock_loop.create_task(mock_server)
    return mock_protocol


async def start_mock_instance():
    """Start Tests.

    There are 3 PS4 objects to test, representing 2 PS4 consoles
    1. A PS4 object
    2. A PS4 object bound to the same IP address as #1. This represents the same
    PS4 console, but an additional object using a different account credential.
    3. A PS4 object with a different IP Address. Represents a second PS4 console.
    """
    _, mock_client_protocol = await ddp.async_create_ddp_endpoint()
    assert mock_client_protocol.port == 987
    # Change port to use unpriviliged port.
    mock_client_protocol._set_write_port(MOCK_DDP_PROTO_PORT)  # noqa: pylint: disable=protected-access
    assert mock_client_protocol.port == MOCK_DDP_PROTO_PORT
    mock_protocol1 = await start_mock_ps4(MOCK_DDP_PROTO_HOST)
    mock_protocol2 = await start_mock_ps4(MOCK_DDP_PROTO_HOST2)

    mock_ps4 = ps4(MOCK_DDP_PROTO_HOST, MOCK_DDP_PROTO_CREDS)
    mock_ps4.ddp_protocol = mock_client_protocol

    mock_ps4_creds = ps4(MOCK_DDP_PROTO_HOST, MOCK_DDP_PROTO_CREDS2)
    mock_ps4_creds.ddp_protocol = mock_client_protocol

    mock_ps4_host = ps4(MOCK_DDP_PROTO_HOST2, MOCK_DDP_PROTO_CREDS)
    mock_ps4_host.ddp_protocol = mock_client_protocol

    return (
        mock_client_protocol,
        (mock_ps4, mock_ps4_creds, mock_ps4_host),
        (mock_protocol1, mock_protocol2),
    )


def _add_callbacks(client_protocol, ps4_obj, callbacks):
    client_protocol.add_callback(
        ps4_obj[0], callbacks[0])
    client_protocol.add_callback(
        ps4_obj[1], callbacks[1])
    client_protocol.add_callback(
        ps4_obj[2], callbacks[2])


class CBStatus():
    def __init__(self, ps4_obj):
        self.ps40 = ps4_obj[0]
        self.ps41 = ps4_obj[1]
        self.ps42 = ps4_obj[2]
        self.status0 = None
        self.status1 = None
        self.status2 = None

    def _update0(self):
        self.status0 = self.ps40.status

    def _update1(self):
        self.status1 = self.ps41.status

    def _update2(self):
        self.status2 = self.ps42.status


@pytest.mark.asyncio
async def test_status():
    """Test status update for a PS4 Devices."""
    mock_client_protocol, mock_ps4s, mock_devices = await start_mock_instance()
    mock_ps4 = mock_ps4s[0]
    mock_ps4_creds = mock_ps4s[1]
    mock_ps4_host = mock_ps4s[2]

    _status = CBStatus(mock_ps4s)
    mock_callbacks = (_status._update0, _status._update1, _status._update2)
    _add_callbacks(mock_client_protocol, mock_ps4s, mock_callbacks)

    assert len(mock_client_protocol.callbacks) == 2  # 2 PS4 Devices.
    assert len(mock_client_protocol.callbacks[MOCK_DDP_PROTO_HOST]) == 2  # noqa: 2 Callbacks for 2 PS4 Object.
    assert len(mock_client_protocol.callbacks[MOCK_DDP_PROTO_HOST2]) == 1  # noqa: 1 Callback for other PS4 device.

    mock_ps4.get_status()
    mock_ps4_creds.get_status()
    mock_ps4_host.get_status()
    await asyncio.sleep(DDP_PROTO_TIMEOUT)
    assert mock_ps4.status is not None
    assert _status.status0['status_code'] == STATUS_STANDBY
    assert mock_ps4_creds.status is not None
    assert _status.status1['status_code'] == STATUS_STANDBY
    assert mock_ps4_host.status is not None
    assert _status.status2['status_code'] == STATUS_STANDBY

    mock_client_protocol.remove_callback(
        mock_ps4, mock_callbacks[0])
    assert len(mock_client_protocol.callbacks[MOCK_DDP_PROTO_HOST]) == 1
    # Should be 1 callback for this PS4 device.
    mock_client_protocol.remove_callback(
        mock_ps4_creds, mock_callbacks[1])
    # Device should be removed since no callbacks remaining.
    assert len(mock_client_protocol.callbacks) == 1
    mock_client_protocol.remove_callback(
        mock_ps4_host, mock_callbacks[2])
    assert len(mock_client_protocol.callbacks) == 0

    mock_devices[0].close()
    mock_devices[1].close()
