from datetime import (
    datetime,
    time,
)
from threading import Condition
from time import sleep

import freezegun
from assertpy import assert_that
from maintenance_util import (
    MaintenanceWindow,
    MaintenanceWindowManager,
)
from testdoubleutil import ForeverSleeper
from timeutil import (
    AwakableSleeper,
)


def test_run_func_on_start_by_default():
    func_stub = FuncStub()

    manager = MaintenanceWindowManager(
        maintenance_window=MaintenanceWindow(
            start_time=time(hour=4, minute=0, second=0, microsecond=0),
            end_time=time(hour=6, minute=0, second=0, microsecond=0),
        ),
        func=func_stub.func,
    )
    now = datetime.now().replace(hour=7)
    with freezegun.freeze_time(now):
        manager.start()

    assert_that(func_stub.called).is_true()

def test_do_not_run_func_on_start_when_requested_to():
    func_stub = FuncStub()

    manager = MaintenanceWindowManager(
        maintenance_window=MaintenanceWindow(
            start_time=time(hour=4, minute=0, second=0, microsecond=0),
            end_time=time(hour=6, minute=0, second=0, microsecond=0),
        ),
        func=func_stub.func,
        run_on_start=False,
    )
    now = datetime.now().replace(hour=7)
    with freezegun.freeze_time(now):
        manager.start()

    assert_that(func_stub.called).is_false()


def test_sleeps_until_maintenance_window_starts():
    sleep_condition = Condition()
    sleeper = NotifiableSleeper(condition=sleep_condition)

    window = MaintenanceWindow(
        start_time=time(hour=4, minute=0, second=0, microsecond=0),
        end_time=time(hour=6, minute=0, second=0, microsecond=0),
    )
    manager = MaintenanceWindowManager(
        maintenance_window=window,
        func=FuncStub().func,
        sleeper=sleeper,
    )
    now = datetime.now().replace(hour=7)
    with freezegun.freeze_time(now):
        with sleep_condition:
            manager.start()
            assert_that(sleep_condition.wait(timeout=1)).is_true()
            assert_that(sleeper.slept_for).is_equal_to(window.seconds_to_next())

def test_run_func_on_maintenance_window():
    func_condition = Condition()
    func_stub = NotifiableFunc(condition=func_condition)

    sleep_condition = Condition()
    sleeper = NotifiableSleeper(condition=sleep_condition)

    window = MaintenanceWindow(
        start_time=time(hour=4, minute=0, second=0, microsecond=0),
        end_time=time(hour=6, minute=0, second=0, microsecond=0),
    )
    manager = MaintenanceWindowManager(
        maintenance_window=window,
        func=func_stub.func,
        sleeper=sleeper,
    )
    now = datetime.now().replace(hour=7)
    with freezegun.freeze_time(now) as frozen_time:
        with sleep_condition:
            with func_condition:
                manager.start()
                for i in range(3):
                    assert_that(sleep_condition.wait(timeout=1)).is_true()
                    frozen_time.tick(delta=window.seconds_to_next())
                    assert_that(func_condition.wait(timeout=1)).is_true()

    manager.stop()

def test_stops_even_if_manager_is_sleeping():
    func_condition = Condition()
    func_stub = NotifiableFunc(condition=func_condition)

    sleep_condition = Condition()
    sleeper = ForeverSleeper(when_sleep_condition=sleep_condition)

    window = MaintenanceWindow(
        start_time=time(hour=4, minute=0, second=0, microsecond=0),
        end_time=time(hour=6, minute=0, second=0, microsecond=0),
    )
    manager = MaintenanceWindowManager(
        maintenance_window=window,
        func=func_stub.func,
        sleeper=sleeper,
    )
    now = datetime.now().replace(hour=7)
    with freezegun.freeze_time(now):
        with sleep_condition:
            with func_condition:
                manager.start()
                assert_that(sleep_condition.wait(timeout=1)).is_true()
                manager.stop()


class FuncStub(object):
    def __init__(self):
        self.called = False

    def func(self):
        self.called = True

class NotifiableFunc(object):
    def __init__(self, condition: Condition):
        self.condition = condition

    def func(self):
        with self.condition:
            self.condition.notify_all()

class NotifiableSleeper(AwakableSleeper):
    def __init__(self, condition: Condition):
        self.condition = condition
        self.slept_for = 0

    def sleep(self, secs):
        with self.condition:
            self.condition.notify_all()

        self.slept_for = secs
        sleep(0)

    def hibernate(self, secs):
        pass

    def wake_up(self):
        pass
