# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
#
# This file is part of Timeline.
#
# Timeline is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Timeline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Timeline.  If not, see <http://www.gnu.org/licenses/>.


from unittest.mock import Mock
from unittest.mock import sentinel
from collections import namedtuple


from timelinelib.calendar.gregorian.time import GregorianDelta
from timelinelib.calendar.gregorian.time import GregorianTime
from timelinelib.calendar.timetype import TimeType
from timelinelib.canvas.data.memorydb.db import MemoryDB
from timelinelib.canvas import TimelineCanvas
from timelinelib.canvas.timelinecanvascontroller import TimelineCanvasController
from timelinelib.canvas.timelinecanvascontroller import CHOICE_WHOLE_PERIOD
from timelinelib.canvas.timelinecanvascontroller import CHOICE_VISIBLE_PERIOD
from timelinelib.test.cases.wxapp import WxAppTestCase
from timelinelib.canvas.drawing.viewproperties import ViewProperties
from timelinelib.wxgui.cursor import Cursor
from timelinelib.canvas.data.event import Event
from timelinelib.canvas.data.timeperiod import TimePeriod


SCENE = sentinel.SCENE


class TimelineCanvasControllerTestCase(WxAppTestCase):

    def setUp(self):
        WxAppTestCase.setUp(self)
        self.setUpView()
        self.setUpDb()
        self.drawer = Mock()
        self.drawer.scene = SCENE
        self.controller = TimelineCanvasController(self.view, False, self.drawer)
        self.time_type = self.db.get_time_type()

    def setUpView(self):
        self.view = Mock(TimelineCanvas)
        self.view.GetDividerPosition.return_value = 0
        self.view.PostEvent = lambda e: None # Don't store the wx event since it causes a segfault when garbage collected

    def setUpDb(self):
        self.db = MemoryDB()
        self.db.set_time_type(ATimeType())


class describe_navigate(TimelineCanvasControllerTestCase):

    def test_setting_period_works(self):
        self.controller.navigate(lambda tp: time_period(10, 11))
        self.assertEqual(self.controller.time_period, time_period(10, 11))

    def test_setting_too_narrow_period_gives_error(self):
        self.assertNavigationFails(
            lambda tp: time_period(15, 15),
            "⟪Can't zoom deeper than 1⟫"
        )

    def test_setting_too_left_period_gives_error(self):
        self.assertNavigationFails(
            lambda tp: time_period(0, 2),
            "⟪Can't scroll more to the left⟫"
        )

    def test_setting_too_right_period_gives_error(self):
        self.assertNavigationFails(
            lambda tp: time_period(20, 21),
            "⟪Can't scroll more to the right⟫"
        )

    def assertNavigationFails(self, navigate_fn, pattern):
        try:
            self.controller.navigate(navigate_fn)
        except ValueError as e:
            self.assertEqual(pattern, str(e))
            self.assertEqual(
                self.controller.time_period,
                self.original_period
            )
        else:
            self.fail("ValueError not raised")

    def setUp(self):
        TimelineCanvasControllerTestCase.setUp(self)
        self.controller.timeline = self.db
        self.original_period = self.controller.time_period


class describe_properties(TimelineCanvasControllerTestCase):

    def test_has_scene_property(self):
        self.assertEqual(SCENE, self.controller.scene)

    def test_getting_time_preriod_raises_exception_if_no_timeline_exists(self):
        self.controller._timeline = None
        try:
            time_period = self.controller.time_period
            self.fail("Expected Exception")
        except Exception as ex:
            self.assertEqual("⟪No timeline set⟫", str(ex))

    def test_fast_draw_can_be_set(self):
        self.controller.use_fast_draw(True)
        self.assertTrue(self.controller._fast_draw)

    def test_fast_draw_can_be_reset(self):
        self.controller.use_fast_draw(False)
        self.assertFalse(self.controller._fast_draw)

    def test_appearence_can_be_set(self):
        # Given
        appearance = Mock()
        # When
        self.controller.appearance = appearance
        # Then
        self.assertEqual(appearance, self.controller.appearance)

    def test_appearence_cannot_be_set_to_none(self):
        # Given
        appearance = Mock()
        self.controller.appearance = appearance
        self.assertEqual(appearance, self.controller.appearance)
        # When
        self.controller.appearance = None
        # Then
        self.assertEqual(appearance, self.controller.appearance)

    def test_when_appearence_is_set_the_timeline_is_redrawn(self):
        appearance = Mock()
        self.controller.appearance = appearance
        appearance.listen_for_any.assert_called_with(self.controller._redraw_timeline)

    def test_time_at_point_x_can_be_retrieved(self):
        # Given
        x = 100
        time_value = 77
        self.drawer.get_time.return_value = time_value
        # When
        result = self.controller.get_time(x)
        # Then
        self.assertEqual(time_value, result)
        self.drawer.get_time.assert_called_with(x)

    def test_event_with_rect_at_point_xy_can_be_retrieved(self):
        # Given
        x = 100
        y = 102
        self.drawer.event_with_rect_at.return_value = sentinel.EVENT_WITH_RECT
        # When
        result = self.controller.event_with_rect_at(x, y)
        # Then
        self.assertEqual(sentinel.EVENT_WITH_RECT, result)
        self.drawer.event_with_rect_at.assert_called_with(x, y, self.controller._view_properties)

    def test_event_at_point_xy_can_be_retrieved(self):
        # Given
        self.drawer.event_at.return_value = sentinel.EVENT
        # When
        result = self.controller.event_at(100, 102)
        # Then
        self.assertEqual(sentinel.EVENT, result)
        self.drawer.event_at.assert_called_with(100, 102, False)

    def test_event_can_be_selected(self):
        # When
        self.controller.set_selected(sentinel.EVENT, True)
        # Then
        self.controller._view_properties.set_selected.assert_called_with(sentinel.EVENT, True)

    def test_can_selecte_all_events(self):
        # When
        self.controller.select_all_events()
        # Then
        self.controller._view_properties.select_all_events.assert_called_with()

    def test_event_can_clear_selected(self):
        # When
        self.controller.clear_selected()
        # Then
        self.controller._view_properties.clear_selected.assert_called_with()

    def test_selection_state_of_event_can_be_detected(self):
        # Given
        self.controller._view_properties.is_selected.return_value = True
        # When
        result = self.controller.is_selected(sentinel.EVENT)
        # Then
        self.assertTrue(result)
        self.controller._view_properties.is_selected.assert_called_with(sentinel.EVENT)

    def test_hovered_event_can_be_set(self):
        # When
        self.controller.set_hovered_event(sentinel.EVENT)
        # Then
        self.controller._view_properties.change_hovered_event.assert_called_with(sentinel.EVENT)

    def test_hovered_event_can_be_found(self):
        # When
        result = self.controller.get_hovered_event()
        # Then
        self.assertEqual(sentinel.EVENT, result)

    def test_selection_rect_can_be_set(self):
        # Given
        cursor = Mock(Cursor)
        cursor.rect = sentinel.RECT
        # When
        self.controller.set_selection_rect(cursor)
        # Then
        self.controller._view_properties.set_selection_rect.assert_called_with(sentinel.RECT)
        self.assertTrue(self.controller._fast_draw)

    def test_selection_rect_can_be_removed(self):
        # When
        self.controller.remove_selection_rect()
        # Then
        self.controller._view_properties.set_selection_rect.assert_called_with(None)

    def test_hscroll_amount_can_be_retrieved(self):
        # When
        result = self.controller.get_hscroll_amount()
        # Then
        self.assertEqual(sentinel.HSCROLL, result)

    def test_hscroll_amount_can_be_set(self):
        # When
        self.controller.set_hscroll_amount(sentinel.HSCROLL)
        # Then
        self.assertEqual(sentinel.HSCROLL, self.controller._view_properties.hscroll_amount)

    def test_period_selection_can_be_set(self):
        # Given
        Period = namedtuple('Period', ['start_time', 'end_time'])
        period = Period('start', 'end')
        # When
        self.controller.set_period_selection(period)
        # Then
        self.assertEqual(('start', 'end'), self.controller._view_properties.period_selection)

    def test_period_selection_can_be_reset(self):
        # When
        self.controller.set_period_selection(None)
        # Then
        self.assertEqual(None, self.controller._view_properties.period_selection)

    def test_event_has_sticky_ballon_is_deteactable(self):
        # Given
        self.controller._view_properties.event_has_sticky_balloon.return_value = True
        # When
        result = self.controller.event_has_sticky_balloon(sentinel.EVENT)
        # Then
        self.assertTrue(result)

    def test_event_has_not_sticky_ballon_is_deteactable(self):
        # Given
        self.controller._view_properties.event_has_sticky_balloon.return_value = False
        # When
        result = self.controller.event_has_sticky_balloon(sentinel.EVENT)
        # Then
        self.assertFalse(result)

    def test_stickyness_on_ballon_can_be_set(self):
        # When
        self.controller.set_event_sticky_balloon(sentinel.EVENT, True)
        # Then
        self.controller._view_properties.set_event_has_sticky_balloon.assert_called_with(sentinel.EVENT, True)

    def test_ballon_at_cursor_can_be_detected(self):
        # Given
        cursor = Mock(Cursor)
        cursor.pos = (1, 2)
        self.controller._drawing_algorithm.balloon_at.return_value = sentinel.BALLON
        # When
        result = self.controller.balloon_at(cursor)
        # Then
        self.assertEqual(sentinel.BALLON, result)
        self.controller._drawing_algorithm.balloon_at.assert_called_with(1, 2)

    def test_can_add_highlight(self):
        # When
        self.controller.add_highlight(sentinel.EVENT, False)
        # Then
        self.controller._view_properties.add_highlight.assert_called_with(sentinel.EVENT, False)

    def test_can_tick_highlights(self):
        # When
        self.controller.tick_highlights()
        # Then
        self.controller._view_properties.tick_highlights.assert_called_with(limit=15)

    def test_can_detect_existing_highlights(self):
        # Given
        self.controller._view_properties.has_higlights.return_value = True
        # When
        result = self.controller.has_higlights()
        # Then
        self.assertTrue(result)

    def test_can_detect_nonexisting_highlights(self):
        # Given
        self.controller._view_properties.has_higlights.return_value = False
        # When
        result = self.controller.has_higlights()
        # Then
        self.assertFalse(result)

    def test_can_snap(self):
        # When
        result = self.controller.snap(sentinel.TIME)
        # Then
        self.assertTrue(result)
        self.controller._drawing_algorithm.snap.assert_called_with(sentinel.TIME)

    def test_events_in_rect_can_be_retrieved(self):
        # Given
        self.controller._drawing_algorithm.get_events_in_rect.return_value = sentinel.EVENTS
        # When
        result = self.controller.get_events_in_rect(sentinel.RECT)
        # Then
        self.assertEqual(sentinel.EVENTS, result)
        self.controller._drawing_algorithm.get_events_in_rect.assert_called_with(sentinel.RECT)

    def test_hidden_events_can_be_retrieved(self):
        # Given
        self.controller._drawing_algorithm.get_hidden_event_count.return_value = 9
        # When
        result = self.controller.get_hidden_event_count()
        # Then
        self.assertEqual(9, result)
        self.controller._drawing_algorithm.get_hidden_event_count.assert_called_with()

    def test_font_size_can_be_incremented(self):
        # Given
        self.controller._drawing_algorithm.increment_font_size.return_value = sentinel.FONT
        # When
        result = self.controller.increment_font_size()
        # Then
        self.assertEqual(sentinel.FONT, result)
        self.controller._drawing_algorithm.increment_font_size.assert_called_with()

    def test_font_size_can_be_decremented(self):
        # Given
        self.controller._drawing_algorithm.decrement_font_size.return_value = sentinel.FONT
        # When
        result = self.controller.decrement_font_size()
        # Then
        self.assertEqual(sentinel.FONT, result)
        self.controller._drawing_algorithm.decrement_font_size.assert_called_with()

    def test_closest_overlapping_event_can_be_retrieved(self):
        # Given
        self.controller._drawing_algorithm.get_closest_overlapping_event.return_value = sentinel.EVENT1
        # When
        result = self.controller.get_closest_overlapping_event(sentinel.EVENT2, True)
        # Then
        self.assertEqual(sentinel.EVENT1, result)
        self.controller._drawing_algorithm.get_closest_overlapping_event.assert_called_with(sentinel.EVENT2, up=True)

    def test_can_detect_if_event_is_a_period_event(self):
        # Given
        event = Mock(Event)
        event.get_time_period.return_value = sentinel.TIME_PERIOD
        self.controller._drawing_algorithm.event_is_period.return_value = True
        # When
        result = self.controller.event_is_period(event)
        # Then
        self.assertTrue(result)
        self.controller._drawing_algorithm.event_is_period.assert_called_with(sentinel.TIME_PERIOD)

    def test_selected_events_can_be_retrieved(self):
        # Given
        timeline = Mock(MemoryDB)
        timeline.find_event_with_ids.return_value = sentinel.EVENTS
        self.controller._timeline = timeline
        # When
        result = self.controller.get_selected_events()
        # Then
        self.assertEqual(sentinel.EVENTS, result)
        timeline.find_event_with_ids.assert_called_with(sentinel.SELECTED_EVENT_IDS)

    def test_events_in_rect_can_be_selected(self):
        # Given
        self.controller._drawing_algorithm.get_events_in_rect.return_value = sentinel.EVENTS
        # When
        self.controller.select_events_in_rect(sentinel.RECT)
        # Then
        self.controller._view_properties.set_all_selected.asseert_called_with(sentinel.EVENTS)
        self.controller._drawing_algorithm.get_events_in_rect.assert_called_with(sentinel.RECT)

    def test_events_can_be_filtered(self):
        # Given
        event1 = Mock(Event)
        event1.get_time_period.return_value = 1
        event2 = Mock(Event)
        event2.get_time_period.return_value = 1
        events = [event1, event2]
        self.controller._view_properties.filter_events.return_value = events
        time_period = Mock(TimePeriod)
        time_period.overlaps.return_value = True
        self.controller._view_properties.displayed_period = time_period
        # When
        result = self.controller.filter_events(events, CHOICE_WHOLE_PERIOD)
        # Then
        self.assertEqual(events, result)
        self.controller._view_properties.filter_events.assert_called_with(events)
        # When
        result = self.controller.filter_events(events, CHOICE_VISIBLE_PERIOD)
        # Then
        self.assertEqual(events, result)
        self.controller._view_properties.filter_events.assert_called_with(events)

    def test_event_handler_for_redrawing(self):
        # When
        self.controller._timeline_changed(sentinel.STATE_CHANGE)
        # Then
        self.controller._view.RedrawSurface.assert_called()

    def setUp(self):
        TimelineCanvasControllerTestCase.setUp(self)
        self.controller.timeline = self.db
        self.original_period = self.controller.time_period
        view_properties = Mock(ViewProperties)
        view_properties.displayed_period = sentinel.DISPLAYED_PERIOD
        view_properties.has_higlights.return_value = True
        view_properties.hscroll_amount = sentinel.HSCROLL
        view_properties.hovered_event = sentinel.EVENT
        view_properties.get_selected_event_ids.return_value = sentinel.SELECTED_EVENT_IDS
        self.controller._view_properties = view_properties


class ATimeType(TimeType):

    def get_min_time(self):
        return GregorianTime(10, 0)

    def get_max_time(self):
        return GregorianTime(20, 0)

    def format_period(self, period):
        return "%s to %s" % (period.start_time, period.end_time)

    def get_min_zoom_delta(self):
        return (GregorianDelta(1), "⟪Can't zoom deeper than 1⟫")

    def now(self):
        return GregorianTime(0, 0)

    def __eq__(self, other):
        return isinstance(other, ATimeType)

    def __ne__(self, other):
        return not (self == other)


def time_period(start, end):
    return TimePeriod(GregorianTime(start, 0), GregorianTime(end, 0))
