"""The tests for the DirecTV Media player platform."""
from unittest.mock import call, patch

from datetime import datetime, timedelta
import requests
import pytest

from homeassistant.components.media_player.const import (
    ATTR_MEDIA_CONTENT_ID,
    ATTR_MEDIA_CONTENT_TYPE,
    MEDIA_TYPE_TVSHOW,
    ATTR_MEDIA_ENQUEUE,
    ATTR_MEDIA_DURATION,
    ATTR_MEDIA_TITLE,
    ATTR_MEDIA_POSITION,
    ATTR_MEDIA_SERIES_TITLE,
    ATTR_MEDIA_CHANNEL,
    ATTR_INPUT_SOURCE,
    ATTR_MEDIA_POSITION_UPDATED_AT,
    DOMAIN,
    SERVICE_PLAY_MEDIA,
    SUPPORT_PAUSE,
    SUPPORT_TURN_ON,
    SUPPORT_TURN_OFF,
    SUPPORT_PLAY_MEDIA,
    SUPPORT_STOP,
    SUPPORT_NEXT_TRACK,
    SUPPORT_PREVIOUS_TRACK,
    SUPPORT_PLAY,
)
from homeassistant.components.directv.media_player import (
    ATTR_MEDIA_CURRENTLY_RECORDING,
    ATTR_MEDIA_RATING,
    ATTR_MEDIA_RECORDED,
    ATTR_MEDIA_START_TIME,
    DEFAULT_DEVICE,
    DEFAULT_PORT,
)
from homeassistant.const import (
    ATTR_ENTITY_ID,
    CONF_DEVICE,
    CONF_HOST,
    CONF_NAME,
    CONF_PORT,
    SERVICE_MEDIA_NEXT_TRACK,
    SERVICE_MEDIA_PAUSE,
    SERVICE_MEDIA_PLAY,
    SERVICE_MEDIA_PREVIOUS_TRACK,
    SERVICE_MEDIA_STOP,
    SERVICE_TURN_OFF,
    SERVICE_TURN_ON,
    STATE_OFF,
    STATE_PAUSED,
    STATE_PLAYING,
    STATE_UNAVAILABLE,
)
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util

from tests.common import MockDependency, async_fire_time_changed

CLIENT_ENTITY_ID = "media_player.client_dvr"
MAIN_ENTITY_ID = "media_player.main_dvr"
IP_ADDRESS = "127.0.0.1"

DISCOVERY_INFO = {"host": IP_ADDRESS, "serial": 1234}

LIVE = {
    "callsign": "HASSTV",
    "date": "20181110",
    "duration": 3600,
    "isOffAir": False,
    "isPclocked": 1,
    "isPpv": False,
    "isRecording": False,
    "isVod": False,
    "major": 202,
    "minor": 65535,
    "offset": 1,
    "programId": "102454523",
    "rating": "No Rating",
    "startTime": 1541876400,
    "stationId": 3900947,
    "title": "Using Home Assistant to automate your home",
}

LOCATIONS = [{"locationName": "Main DVR", "clientAddr": DEFAULT_DEVICE}]

RECORDING = {
    "callsign": "HASSTV",
    "date": "20181110",
    "duration": 3600,
    "isOffAir": False,
    "isPclocked": 1,
    "isPpv": False,
    "isRecording": True,
    "isVod": False,
    "major": 202,
    "minor": 65535,
    "offset": 1,
    "programId": "102454523",
    "rating": "No Rating",
    "startTime": 1541876400,
    "stationId": 3900947,
    "title": "Using Home Assistant to automate your home",
    "uniqueId": "12345",
    "episodeTitle": "Configure DirecTV platform.",
}

WORKING_CONFIG = {
    "media_player": {
        "platform": "directv",
        CONF_HOST: IP_ADDRESS,
        CONF_NAME: "Main DVR",
        CONF_PORT: DEFAULT_PORT,
        CONF_DEVICE: DEFAULT_DEVICE,
    }
}


@pytest.fixture
def client_dtv():
    """Fixture for a client device."""
    mocked_dtv = MockDirectvClass("mock_ip")
    mocked_dtv.attributes = RECORDING
    mocked_dtv._standby = False
    return mocked_dtv


@pytest.fixture
def main_dtv():
    """Fixture for main DVR."""
    return MockDirectvClass("mock_ip")


@pytest.fixture
def dtv_side_effect(client_dtv, main_dtv):
    """Fixture to create DIRECTV instance for main and client."""

    def mock_dtv(ip, port, client_addr):
        if client_addr != "0":
            mocked_dtv = client_dtv
        else:
            mocked_dtv = main_dtv
        mocked_dtv._host = ip
        mocked_dtv._port = port
        mocked_dtv._device = client_addr
        return mocked_dtv

    return mock_dtv


@pytest.fixture
def mock_now():
    """Fixture for dtutil.now."""
    return dt_util.utcnow()


@pytest.fixture
def platforms(hass, dtv_side_effect, mock_now):
    """Fixture for setting up test platforms."""
    config = {
        "media_player": [
            {
                "platform": "directv",
                "name": "Main DVR",
                "host": IP_ADDRESS,
                "port": DEFAULT_PORT,
                "device": DEFAULT_DEVICE,
            },
            {
                "platform": "directv",
                "name": "Client DVR",
                "host": IP_ADDRESS,
                "port": DEFAULT_PORT,
                "device": "1",
            },
        ]
    }

    with MockDependency("DirectPy"), patch(
        "DirectPy.DIRECTV", side_effect=dtv_side_effect
    ), patch("homeassistant.util.dt.utcnow", return_value=mock_now):
        hass.loop.run_until_complete(async_setup_component(hass, DOMAIN, config))
        hass.loop.run_until_complete(hass.async_block_till_done())
        yield


async def async_turn_on(hass, entity_id=None):
    """Turn on specified media player or all."""
    data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
    await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)


async def async_turn_off(hass, entity_id=None):
    """Turn off specified media player or all."""
    data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
    await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)


async def async_media_pause(hass, entity_id=None):
    """Send the media player the command for pause."""
    data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
    await hass.services.async_call(DOMAIN, SERVICE_MEDIA_PAUSE, data)


async def async_media_play(hass, entity_id=None):
    """Send the media player the command for play/pause."""
    data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
    await hass.services.async_call(DOMAIN, SERVICE_MEDIA_PLAY, data)


async def async_media_stop(hass, entity_id=None):
    """Send the media player the command for stop."""
    data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
    await hass.services.async_call(DOMAIN, SERVICE_MEDIA_STOP, data)


async def async_media_next_track(hass, entity_id=None):
    """Send the media player the command for next track."""
    data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
    await hass.services.async_call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data)


async def async_media_previous_track(hass, entity_id=None):
    """Send the media player the command for prev track."""
    data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
    await hass.services.async_call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)


async def async_play_media(hass, media_type, media_id, entity_id=None, enqueue=None):
    """Send the media player the command for playing media."""
    data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id}

    if entity_id:
        data[ATTR_ENTITY_ID] = entity_id

    if enqueue:
        data[ATTR_MEDIA_ENQUEUE] = enqueue

    await hass.services.async_call(DOMAIN, SERVICE_PLAY_MEDIA, data)


class MockDirectvClass:
    """A fake DirecTV DVR device."""

    def __init__(self, ip, port=8080, clientAddr="0"):
        """Initialize the fake DirecTV device."""
        self._host = ip
        self._port = port
        self._device = clientAddr
        self._standby = True
        self._play = False

        self._locations = LOCATIONS

        self.attributes = LIVE

    def get_locations(self):
        """Mock for get_locations method."""
        test_locations = {
            "locations": self._locations,
            "status": {
                "code": 200,
                "commandResult": 0,
                "msg": "OK.",
                "query": "/info/getLocations",
            },
        }

        return test_locations

    def get_standby(self):
        """Mock for get_standby method."""
        return self._standby

    def get_tuned(self):
        """Mock for get_tuned method."""
        if self._play:
            self.attributes["offset"] = self.attributes["offset"] + 1

        test_attributes = self.attributes
        test_attributes["status"] = {
            "code": 200,
            "commandResult": 0,
            "msg": "OK.",
            "query": "/tv/getTuned",
        }
        return test_attributes

    def key_press(self, keypress):
        """Mock for key_press method."""
        if keypress == "poweron":
            self._standby = False
            self._play = True
        elif keypress == "poweroff":
            self._standby = True
            self._play = False
        elif keypress == "play":
            self._play = True
        elif keypress == "pause" or keypress == "stop":
            self._play = False

    def tune_channel(self, source):
        """Mock for tune_channel method."""
        self.attributes["major"] = int(source)


async def test_setup_platform_config(hass):
    """Test setting up the platform from configuration."""
    with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass):

        await async_setup_component(hass, DOMAIN, WORKING_CONFIG)
        await hass.async_block_till_done()

    state = hass.states.get(MAIN_ENTITY_ID)
    assert state
    assert len(hass.states.async_entity_ids("media_player")) == 1


async def test_setup_platform_discover(hass):
    """Test setting up the platform from discovery."""
    with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass):

        hass.async_create_task(
            async_load_platform(
                hass, DOMAIN, "directv", DISCOVERY_INFO, {"media_player": {}}
            )
        )
        await hass.async_block_till_done()

    state = hass.states.get(MAIN_ENTITY_ID)
    assert state
    assert len(hass.states.async_entity_ids("media_player")) == 1


async def test_setup_platform_discover_duplicate(hass):
    """Test setting up the platform from discovery."""
    with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass):

        await async_setup_component(hass, DOMAIN, WORKING_CONFIG)
        await hass.async_block_till_done()
        hass.async_create_task(
            async_load_platform(
                hass, DOMAIN, "directv", DISCOVERY_INFO, {"media_player": {}}
            )
        )
        await hass.async_block_till_done()

    state = hass.states.get(MAIN_ENTITY_ID)
    assert state
    assert len(hass.states.async_entity_ids("media_player")) == 1


async def test_setup_platform_discover_client(hass):
    """Test setting up the platform from discovery."""
    LOCATIONS.append({"locationName": "Client 1", "clientAddr": "1"})
    LOCATIONS.append({"locationName": "Client 2", "clientAddr": "2"})

    with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass):

        await async_setup_component(hass, DOMAIN, WORKING_CONFIG)
        await hass.async_block_till_done()

        hass.async_create_task(
            async_load_platform(
                hass, DOMAIN, "directv", DISCOVERY_INFO, {"media_player": {}}
            )
        )
        await hass.async_block_till_done()

    del LOCATIONS[-1]
    del LOCATIONS[-1]
    state = hass.states.get(MAIN_ENTITY_ID)
    assert state
    state = hass.states.get("media_player.client_1")
    assert state
    state = hass.states.get("media_player.client_2")
    assert state

    assert len(hass.states.async_entity_ids("media_player")) == 3


async def test_supported_features(hass, platforms):
    """Test supported features."""
    # Features supported for main DVR
    state = hass.states.get(MAIN_ENTITY_ID)
    assert (
        SUPPORT_PAUSE
        | SUPPORT_TURN_ON
        | SUPPORT_TURN_OFF
        | SUPPORT_PLAY_MEDIA
        | SUPPORT_STOP
        | SUPPORT_NEXT_TRACK
        | SUPPORT_PREVIOUS_TRACK
        | SUPPORT_PLAY
        == state.attributes.get("supported_features")
    )

    # Feature supported for clients.
    state = hass.states.get(CLIENT_ENTITY_ID)
    assert (
        SUPPORT_PAUSE
        | SUPPORT_PLAY_MEDIA
        | SUPPORT_STOP
        | SUPPORT_NEXT_TRACK
        | SUPPORT_PREVIOUS_TRACK
        | SUPPORT_PLAY
        == state.attributes.get("supported_features")
    )


async def test_check_attributes(hass, platforms, mock_now):
    """Test attributes."""
    next_update = mock_now + timedelta(minutes=5)
    with patch("homeassistant.util.dt.utcnow", return_value=next_update):
        async_fire_time_changed(hass, next_update)
        await hass.async_block_till_done()

    # Start playing TV
    with patch("homeassistant.util.dt.utcnow", return_value=next_update):
        await async_media_play(hass, CLIENT_ENTITY_ID)
        await hass.async_block_till_done()

    state = hass.states.get(CLIENT_ENTITY_ID)
    assert state.state == STATE_PLAYING

    assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == RECORDING["programId"]
    assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_TVSHOW
    assert state.attributes.get(ATTR_MEDIA_DURATION) == RECORDING["duration"]
    assert state.attributes.get(ATTR_MEDIA_POSITION) == 2
    assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) == next_update
    assert state.attributes.get(ATTR_MEDIA_TITLE) == RECORDING["title"]
    assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) == RECORDING["episodeTitle"]
    assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "{} ({})".format(
        RECORDING["callsign"], RECORDING["major"]
    )
    assert state.attributes.get(ATTR_INPUT_SOURCE) == RECORDING["major"]
    assert (
        state.attributes.get(ATTR_MEDIA_CURRENTLY_RECORDING) == RECORDING["isRecording"]
    )
    assert state.attributes.get(ATTR_MEDIA_RATING) == RECORDING["rating"]
    assert state.attributes.get(ATTR_MEDIA_RECORDED)
    assert state.attributes.get(ATTR_MEDIA_START_TIME) == datetime(
        2018, 11, 10, 19, 0, tzinfo=dt_util.UTC
    )

    # Test to make sure that ATTR_MEDIA_POSITION_UPDATED_AT is not
    # updated if TV is paused.
    with patch(
        "homeassistant.util.dt.utcnow", return_value=next_update + timedelta(minutes=5)
    ):
        await async_media_pause(hass, CLIENT_ENTITY_ID)
        await hass.async_block_till_done()

    state = hass.states.get(CLIENT_ENTITY_ID)
    assert state.state == STATE_PAUSED
    assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) == next_update


async def test_main_services(hass, platforms, main_dtv, mock_now):
    """Test the different services."""
    next_update = mock_now + timedelta(minutes=5)
    with patch("homeassistant.util.dt.utcnow", return_value=next_update):
        async_fire_time_changed(hass, next_update)
        await hass.async_block_till_done()
    # DVR starts in off state.
    state = hass.states.get(MAIN_ENTITY_ID)
    assert state.state == STATE_OFF

    # All these should call key_press in our class.
    with patch.object(
        main_dtv, "key_press", wraps=main_dtv.key_press
    ) as mock_key_press, patch.object(
        main_dtv, "tune_channel", wraps=main_dtv.tune_channel
    ) as mock_tune_channel, patch.object(
        main_dtv, "get_tuned", wraps=main_dtv.get_tuned
    ) as mock_get_tuned, patch.object(
        main_dtv, "get_standby", wraps=main_dtv.get_standby
    ) as mock_get_standby:

        # Turn main DVR on. When turning on DVR is playing.
        await async_turn_on(hass, MAIN_ENTITY_ID)
        await hass.async_block_till_done()
        assert mock_key_press.called
        assert mock_key_press.call_args == call("poweron")
        state = hass.states.get(MAIN_ENTITY_ID)
        assert state.state == STATE_PLAYING

        # Pause live TV.
        await async_media_pause(hass, MAIN_ENTITY_ID)
        await hass.async_block_till_done()
        assert mock_key_press.called
        assert mock_key_press.call_args == call("pause")
        state = hass.states.get(MAIN_ENTITY_ID)
        assert state.state == STATE_PAUSED

        # Start play again for live TV.
        await async_media_play(hass, MAIN_ENTITY_ID)
        await hass.async_block_till_done()
        assert mock_key_press.called
        assert mock_key_press.call_args == call("play")
        state = hass.states.get(MAIN_ENTITY_ID)
        assert state.state == STATE_PLAYING

        # Change channel, currently it should be 202
        assert state.attributes.get("source") == 202
        await async_play_media(hass, "channel", 7, MAIN_ENTITY_ID)
        await hass.async_block_till_done()
        assert mock_tune_channel.called
        assert mock_tune_channel.call_args == call("7")
        state = hass.states.get(MAIN_ENTITY_ID)
        assert state.attributes.get("source") == 7

        # Stop live TV.
        await async_media_stop(hass, MAIN_ENTITY_ID)
        await hass.async_block_till_done()
        assert mock_key_press.called
        assert mock_key_press.call_args == call("stop")
        state = hass.states.get(MAIN_ENTITY_ID)
        assert state.state == STATE_PAUSED

        # Turn main DVR off.
        await async_turn_off(hass, MAIN_ENTITY_ID)
        await hass.async_block_till_done()
        assert mock_key_press.called
        assert mock_key_press.call_args == call("poweroff")
        state = hass.states.get(MAIN_ENTITY_ID)
        assert state.state == STATE_OFF

        # There should have been 6 calls to check if DVR is in standby
        assert main_dtv.get_standby.call_count == 6
        assert mock_get_standby.call_count == 6
        # There should be 5 calls to get current info (only 1 time it will
        # not be called as DVR is in standby.)
        assert main_dtv.get_tuned.call_count == 5
        assert mock_get_tuned.call_count == 5


async def test_available(hass, platforms, main_dtv, mock_now):
    """Test available status."""
    next_update = mock_now + timedelta(minutes=5)
    with patch("homeassistant.util.dt.utcnow", return_value=next_update):
        async_fire_time_changed(hass, next_update)
        await hass.async_block_till_done()

    # Confirm service is currently set to available.
    state = hass.states.get(MAIN_ENTITY_ID)
    assert state.state != STATE_UNAVAILABLE

    # Make update fail 1st time
    next_update = next_update + timedelta(minutes=5)
    with patch.object(
        main_dtv, "get_standby", side_effect=requests.RequestException
    ), patch("homeassistant.util.dt.utcnow", return_value=next_update):
        async_fire_time_changed(hass, next_update)
        await hass.async_block_till_done()

    state = hass.states.get(MAIN_ENTITY_ID)
    assert state.state != STATE_UNAVAILABLE

    # Make update fail 2nd time within 1 minute
    next_update = next_update + timedelta(seconds=30)
    with patch.object(
        main_dtv, "get_standby", side_effect=requests.RequestException
    ), patch("homeassistant.util.dt.utcnow", return_value=next_update):
        async_fire_time_changed(hass, next_update)
        await hass.async_block_till_done()

    state = hass.states.get(MAIN_ENTITY_ID)
    assert state.state != STATE_UNAVAILABLE

    # Make update fail 3rd time more then a minute after 1st failure
    next_update = next_update + timedelta(minutes=1)
    with patch.object(
        main_dtv, "get_standby", side_effect=requests.RequestException
    ), patch("homeassistant.util.dt.utcnow", return_value=next_update):
        async_fire_time_changed(hass, next_update)
        await hass.async_block_till_done()

    state = hass.states.get(MAIN_ENTITY_ID)
    assert state.state == STATE_UNAVAILABLE

    # Recheck state, update should work again.
    next_update = next_update + timedelta(minutes=5)
    with patch("homeassistant.util.dt.utcnow", return_value=next_update):
        async_fire_time_changed(hass, next_update)
        await hass.async_block_till_done()
    state = hass.states.get(MAIN_ENTITY_ID)
    assert state.state != STATE_UNAVAILABLE
