from datetime import timedelta
from backup.model.backups import Backup
import pytest

from backup.util import GlobalInfo
from backup.ha import HaUpdater
from backup.ha.haupdater import REASSURING_MESSAGE
from .faketime import FakeTime
from .helpers import HelperTestSource
from dev.simulationserver import SimulationServer
from backup.logger import getLast
from backup.util import Estimator
from dev.simulated_supervisor import SimulatedSupervisor, URL_MATCH_CORE_API
from dev.request_interceptor import RequestInterceptor
from backup.model import Coordinator
from backup.config import Config, Setting

STALE_ATTRIBUTES = {
    "friendly_name": "Backups Stale",
    "device_class": "problem"
}


@pytest.fixture
def source():
    return HelperTestSource("Source")


@pytest.fixture
def dest():
    return HelperTestSource("Dest")


@pytest.mark.asyncio
async def test_init(updater: HaUpdater, global_info, supervisor: SimulatedSupervisor, server, time: FakeTime):
    await updater.update()
    assert not updater._stale()
    assert updater._state() == "waiting"
    verifyEntity(supervisor, "binary_sensor.backups_stale",
                 "off", STALE_ATTRIBUTES)
    verifyEntity(supervisor, "sensor.backup_state", "waiting", {
        'friendly_name': 'Backup State',
        'last_backup': 'Never',
        'next_backup': time.now().isoformat(),
        'last_uploaded': 'Never',
        'backups': [],
        'backups_in_google_drive': 0,
        'free_space_in_google_drive': "",
        'backups_in_home_assistant': 0,
        'size_in_google_drive': "0.0 B",
        'size_in_home_assistant': '0.0 B'
    })
    assert supervisor.getNotification() is None

    global_info.success()
    assert not updater._stale()
    assert updater._state() == "backed_up"


@pytest.mark.asyncio
async def test_init_failure(updater: HaUpdater, global_info: GlobalInfo, time: FakeTime, server, supervisor: SimulatedSupervisor):
    await updater.update()
    assert not updater._stale()
    assert updater._state() == "waiting"

    global_info.failed(Exception())
    assert not updater._stale()
    assert updater._state() == "backed_up"
    assert supervisor.getNotification() is None

    time.advanceDay()
    assert updater._stale()
    assert updater._state() == "error"
    await updater.update()
    assert supervisor.getNotification() == {
        'message': 'The add-on is having trouble making backups and needs attention.  Please visit the add-on status page for details.',
        'title': 'Home Assistant Google Drive Backup is Having Trouble',
        'notification_id': 'backup_broken'
    }


@pytest.mark.asyncio
async def test_failure_backoff_502(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor):
    interceptor.setError(URL_MATCH_CORE_API, 502)
    for x in range(9):
        await updater.update()
    assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300]

    interceptor.clear()
    await updater.update()
    assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300]


@pytest.mark.asyncio
async def test_failure_backoff_510(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor):
    interceptor.setError(URL_MATCH_CORE_API, 502)
    for x in range(9):
        await updater.update()
    assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300]

    interceptor.clear()
    await updater.update()
    assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300]


@pytest.mark.asyncio
async def test_failure_backoff_other(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor):
    interceptor.setError(URL_MATCH_CORE_API, 400)
    for x in range(9):
        await updater.update()
    assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300]
    interceptor.clear()
    await updater.update()
    assert time.sleeps == [60, 120, 240, 300, 300, 300, 300, 300, 300]


@pytest.mark.asyncio
async def test_update_backups(updater: HaUpdater, server, time: FakeTime, supervisor: SimulatedSupervisor):
    await updater.update()
    assert not updater._stale()
    assert updater._state() == "waiting"
    verifyEntity(supervisor, "binary_sensor.backups_stale",
                 "off", STALE_ATTRIBUTES)
    verifyEntity(supervisor, "sensor.backup_state", "waiting", {
        'friendly_name': 'Backup State',
        'last_backup': 'Never',
        'next_backup': time.now().isoformat(),
        'last_uploaded': 'Never',
        'backups': [],
        'backups_in_google_drive': 0,
        'backups_in_home_assistant': 0,
        'size_in_home_assistant': "0.0 B",
        'size_in_google_drive': "0.0 B",
        'free_space_in_google_drive': ''
    })


@pytest.mark.asyncio
async def test_update_backups_no_next_backup(updater: HaUpdater, server, time: FakeTime, supervisor: SimulatedSupervisor, config: Config):
    config.override(Setting.DAYS_BETWEEN_BACKUPS, 0)
    await updater.update()
    assert not updater._stale()
    assert updater._state() == "waiting"
    verifyEntity(supervisor, "binary_sensor.backups_stale",
                 "off", STALE_ATTRIBUTES)
    verifyEntity(supervisor, "sensor.backup_state", "waiting", {
        'friendly_name': 'Backup State',
        'last_backup': 'Never',
        'next_backup': None,
        'last_uploaded': 'Never',
        'backups': [],
        'backups_in_google_drive': 0,
        'backups_in_home_assistant': 0,
        'size_in_home_assistant': "0.0 B",
        'size_in_google_drive': "0.0 B",
        'free_space_in_google_drive': ''
    })


@pytest.mark.asyncio
async def test_update_backups_sync(updater: HaUpdater, server, time: FakeTime, backup: Backup, supervisor: SimulatedSupervisor, config: Config):
    await updater.update()
    assert not updater._stale()
    assert updater._state() == "backed_up"
    verifyEntity(supervisor, "binary_sensor.backups_stale",
                 "off", STALE_ATTRIBUTES)
    date = '1985-12-06T05:00:00+00:00'
    verifyEntity(supervisor, "sensor.backup_state", "backed_up", {
        'friendly_name': 'Backup State',
        'last_backup': date,
        'last_uploaded': date,
        'next_backup': (backup.date() + timedelta(days=config.get(Setting.DAYS_BETWEEN_BACKUPS))).isoformat(),
        'backups': [{
            'date': date,
            'name': backup.name(),
            'size': backup.sizeString(),
            'state': backup.status(),
            'slug': backup.slug()
        }
        ],
        'backups_in_google_drive': 1,
        'backups_in_home_assistant': 1,
        'size_in_home_assistant': Estimator.asSizeString(backup.size()),
        'size_in_google_drive': Estimator.asSizeString(backup.size()),
        'free_space_in_google_drive': '4.0 GB'
    })


@pytest.mark.asyncio
async def test_notification_link(updater: HaUpdater, server, time: FakeTime, global_info, supervisor: SimulatedSupervisor):
    await updater.update()
    assert not updater._stale()
    assert updater._state() == "waiting"
    verifyEntity(supervisor, "binary_sensor.backups_stale",
                 "off", STALE_ATTRIBUTES)
    verifyEntity(supervisor, "sensor.backup_state", "waiting", {
        'friendly_name': 'Backup State',
        'last_backup': 'Never',
        'next_backup': time.now().isoformat(),
        'last_uploaded': 'Never',
        'backups': [],
        'backups_in_google_drive': 0,
        'backups_in_home_assistant': 0,
        'size_in_home_assistant': "0.0 B",
        'size_in_google_drive': "0.0 B",
        'free_space_in_google_drive': ''
    })
    assert supervisor.getNotification() is None

    global_info.failed(Exception())
    global_info.url = "http://localhost/test"
    time.advanceDay()
    await updater.update()
    assert supervisor.getNotification() == {
        'message': 'The add-on is having trouble making backups and needs attention.  Please visit the add-on [status page](http://localhost/test) for details.',
        'title': 'Home Assistant Google Drive Backup is Having Trouble',
        'notification_id': 'backup_broken'
    }


@pytest.mark.asyncio
async def test_notification_clears(updater: HaUpdater, server, time: FakeTime, global_info, supervisor: SimulatedSupervisor):
    await updater.update()
    assert not updater._stale()
    assert updater._state() == "waiting"
    assert supervisor.getNotification() is None

    global_info.failed(Exception())
    time.advance(hours=8)
    await updater.update()
    assert supervisor.getNotification() is not None

    global_info.success()
    await updater.update()
    assert supervisor.getNotification() is None


@pytest.mark.asyncio
async def test_publish_for_failure(updater: HaUpdater, server, time: FakeTime, global_info: GlobalInfo, supervisor: SimulatedSupervisor):
    global_info.success()
    await updater.update()
    assert supervisor.getNotification() is None

    time.advance(hours=8)
    global_info.failed(Exception())
    await updater.update()
    assert supervisor.getNotification() is not None

    time.advance(hours=8)
    global_info.failed(Exception())
    await updater.update()
    assert supervisor.getNotification() is not None

    global_info.success()
    await updater.update()
    assert supervisor.getNotification() is None


@pytest.mark.asyncio
async def test_failure_logging(updater: HaUpdater, server, time: FakeTime, interceptor: RequestInterceptor):
    interceptor.setError(URL_MATCH_CORE_API, 501)
    assert getLast() is None
    await updater.update()
    assert getLast() is None

    time.advance(minutes=1)
    await updater.update()
    assert getLast() is None

    time.advance(minutes=5)
    await updater.update()
    assert getLast().msg == REASSURING_MESSAGE.format(501)

    last_log = getLast()
    time.advance(minutes=5)
    await updater.update()
    assert getLast() is not last_log
    assert getLast().msg == REASSURING_MESSAGE.format(501)

    last_log = getLast()
    interceptor.clear()
    await updater.update()
    assert getLast() is last_log


@pytest.mark.asyncio
async def test_publish_retries(updater: HaUpdater, server: SimulationServer, time: FakeTime, backup, drive, supervisor: SimulatedSupervisor):
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") is not None

    # Shoudlnt update after 59 minutes
    supervisor.clearEntities()
    time.advance(minutes=59)
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") is None

    # after that it should
    supervisor.clearEntities()
    time.advance(minutes=2)
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") is not None

    supervisor.clearEntities()
    await drive.delete(backup)
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") is not None


@pytest.mark.asyncio
async def test_ignored_backups(updater: HaUpdater, time: FakeTime, server: SimulationServer, backup: Backup, supervisor: SimulatedSupervisor, coord: Coordinator, config: Config):
    config.override(Setting.IGNORE_OTHER_BACKUPS, True)
    time.advance(hours=1)
    await supervisor.createBackup({'name': "test_backup"}, date=time.now())
    await coord.sync()
    await updater.update()
    state = supervisor.getAttributes("sensor.backup_state")
    assert state["backups_in_google_drive"] == 1
    assert state["backups_in_home_assistant"] == 1
    assert len(state["backups"]) == 1
    assert state['last_backup'] == backup.date().isoformat()


@pytest.mark.asyncio
async def test_update_backups_old_names(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config):
    config.override(Setting.CALL_BACKUP_SNAPSHOT, True)
    await updater.update()
    assert not updater._stale()
    assert updater._state() == "backed_up"
    verifyEntity(supervisor, "binary_sensor.snapshots_stale",
                 "off", {"friendly_name": "Snapshots Stale",
                         "device_class": "problem"})
    date = '1985-12-06T05:00:00+00:00'
    verifyEntity(supervisor, "sensor.snapshot_backup", "backed_up", {
        'friendly_name': 'Snapshot State',
        'last_snapshot': date,
        'snapshots': [{
            'date': date,
            'name': backup.name(),
            'size': backup.sizeString(),
            'state': backup.status(),
            'slug': backup.slug()
        }
        ],
        'snapshots_in_google_drive': 1,
        'snapshots_in_home_assistant': 1,
        'snapshots_in_hassio': 1,
        'size_in_home_assistant': Estimator.asSizeString(backup.size()),
        'size_in_google_drive': Estimator.asSizeString(backup.size())
    })


@pytest.mark.asyncio
async def test_drive_free_space(updater: HaUpdater, time: FakeTime, server: SimulationServer, supervisor: SimulatedSupervisor, coord: Coordinator, config: Config):
    await updater.update()
    state = supervisor.getAttributes("sensor.backup_state")
    assert state["free_space_in_google_drive"] == ""

    await coord.sync()
    await updater.update()
    state = supervisor.getAttributes("sensor.backup_state")
    assert state["free_space_in_google_drive"] == "4.0 GB"


@pytest.mark.asyncio
async def test_stale_backup_is_error(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config):
    config.override(Setting.DAYS_BETWEEN_BACKUPS, 1)
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") == "backed_up"

    time.advance(days=1)
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") == "backed_up"

    time.advance(days=1)
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") == "error"

    time.advance(days=1)
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") == "error"


@pytest.mark.asyncio
async def test_stale_backup_ignores_pending(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config, coord: Coordinator):
    config.override(Setting.DAYS_BETWEEN_BACKUPS, 1)

    config.override(Setting.NEW_BACKUP_TIMEOUT_SECONDS, 1)
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") == "backed_up"

    time.advance(days=2)
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") == "error"

    async with supervisor._backup_inner_lock:
        await coord.sync()
        assert coord.getBackup("pending") is not None
        await updater.update()
        assert supervisor.getEntity("sensor.backup_state") == "error"


@pytest.mark.asyncio
async def test_stale_backups_fine_for_no_creation(updater: HaUpdater, server, backup: Backup, time: FakeTime, supervisor: SimulatedSupervisor, config: Config, coord: Coordinator):
    config.override(Setting.DAYS_BETWEEN_BACKUPS, 0)
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") == "backed_up"

    # backups shouldn't become stale because the addon doesn't create them.
    time.advance(days=100)
    await updater.update()
    assert supervisor.getEntity("sensor.backup_state") == "backed_up"


def verifyEntity(backend: SimulatedSupervisor, name, state, attributes):
    assert backend.getEntity(name) == state
    assert backend.getAttributes(name) == attributes
