"""
Test LMS Notes
"""
import random
from datetime import datetime
from unittest import skip
from uuid import uuid4

from nose.plugins.attrib import attr

from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
from common.test.acceptance.fixtures.edxnotes import EdxNotesFixture, Note, Range
from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
from common.test.acceptance.pages.lms.course_home import CourseHomePage
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.edxnotes import EdxNotesPage, EdxNotesPageNoContent, EdxNotesUnitPage
from common.test.acceptance.tests.helpers import EventsTestMixin, UniqueCourseTest


class EdxNotesTestMixin(UniqueCourseTest):
    """
    Creates a course with initial data and contains useful helper methods.
    """
    def setUp(self):
        """
        Initialize pages and install a course fixture.
        """
        super(EdxNotesTestMixin, self).setUp()
        self.courseware_page = CoursewarePage(self.browser, self.course_id)
        self.course_home_page = CourseHomePage(self.browser, self.course_id)
        self.note_unit_page = EdxNotesUnitPage(self.browser, self.course_id)
        self.notes_page = EdxNotesPage(self.browser, self.course_id)

        self.username = str(uuid4().hex)[:5]
        self.email = "{}@email.com".format(self.username)

        self.selector = "annotate-id"
        self.edxnotes_fixture = EdxNotesFixture()
        self.course_fixture = CourseFixture(
            self.course_info["org"], self.course_info["number"],
            self.course_info["run"], self.course_info["display_name"]
        )

        self.course_fixture.add_advanced_settings({
            u"edxnotes": {u"value": True}
        })

        self.course_fixture.add_children(
            XBlockFixtureDesc("chapter", "Test Section 1").add_children(
                XBlockFixtureDesc("sequential", "Test Subsection 1").add_children(
                    XBlockFixtureDesc("vertical", "Test Unit 1").add_children(
                        XBlockFixtureDesc(
                            "html",
                            "Test HTML 1",
                            data="""
                                <p><span class="{}">Annotate this!</span></p>
                                <p>Annotate this</p>
                            """.format(self.selector)
                        ),
                        XBlockFixtureDesc(
                            "html",
                            "Test HTML 2",
                            data="""<p><span class="{}">Annotate this!</span></p>""".format(self.selector)
                        ),
                    ),
                    XBlockFixtureDesc("vertical", "Test Unit 2").add_children(
                        XBlockFixtureDesc(
                            "html",
                            "Test HTML 3",
                            data="""<p><span class="{}">Annotate this!</span></p>""".format(self.selector)
                        ),
                    ),
                ),
                XBlockFixtureDesc("sequential", "Test Subsection 2").add_children(
                    XBlockFixtureDesc("vertical", "Test Unit 3").add_children(
                        XBlockFixtureDesc(
                            "html",
                            "Test HTML 4",
                            data="""
                                <p><span class="{}">Annotate this!</span></p>
                            """.format(self.selector)
                        ),
                    ),
                ),
            ),
            XBlockFixtureDesc("chapter", "Test Section 2").add_children(
                XBlockFixtureDesc("sequential", "Test Subsection 3").add_children(
                    XBlockFixtureDesc("vertical", "Test Unit 4").add_children(
                        XBlockFixtureDesc(
                            "html",
                            "Test HTML 5",
                            data="""
                                <p><span class="{}">Annotate this!</span></p>
                            """.format(self.selector)
                        ),
                        XBlockFixtureDesc(
                            "html",
                            "Test HTML 6",
                            data="""<p><span class="{}">Annotate this!</span></p>""".format(self.selector)
                        ),
                    ),
                ),
            )).install()

        self.addCleanup(self.edxnotes_fixture.cleanup)

        AutoAuthPage(self.browser, username=self.username, email=self.email, course_id=self.course_id).visit()

    def _add_notes(self):
        xblocks = self.course_fixture.get_nested_xblocks(category="html")
        notes_list = []
        for index, xblock in enumerate(xblocks):
            notes_list.append(
                Note(
                    user=self.username,
                    usage_id=xblock.locator,
                    course_id=self.course_fixture._course_key,
                    ranges=[Range(startOffset=index, endOffset=index + 5)]
                )
            )

        self.edxnotes_fixture.create_notes(notes_list)
        self.edxnotes_fixture.install()


@attr(shard=4)
class EdxNotesDefaultInteractionsTest(EdxNotesTestMixin):
    """
    Tests for creation, editing, deleting annotations inside annotatable components in LMS.
    """
    def create_notes(self, components, offset=0):
        self.assertGreater(len(components), 0)
        index = offset
        for component in components:
            for note in component.create_note(".{}".format(self.selector)):
                note.text = "TEST TEXT {}".format(index)
                index += 1

    def edit_notes(self, components, offset=0):
        self.assertGreater(len(components), 0)
        index = offset
        for component in components:
            self.assertGreater(len(component.notes), 0)
            for note in component.edit_note():
                note.text = "TEST TEXT {}".format(index)
                index += 1

    def edit_tags_in_notes(self, components, tags):
        self.assertGreater(len(components), 0)
        index = 0
        for component in components:
            self.assertGreater(len(component.notes), 0)
            for note in component.edit_note():
                note.tags = tags[index]
                index += 1
        self.assertEqual(index, len(tags), "Number of supplied tags did not match components")

    def remove_notes(self, components):
        self.assertGreater(len(components), 0)
        for component in components:
            self.assertGreater(len(component.notes), 0)
            component.remove_note()

    def assert_notes_are_removed(self, components):
        for component in components:
            self.assertEqual(0, len(component.notes))

    def assert_text_in_notes(self, notes):
        actual = [note.text for note in notes]
        expected = ["TEST TEXT {}".format(i) for i in xrange(len(notes))]
        self.assertEqual(expected, actual)

    def assert_tags_in_notes(self, notes, expected_tags):
        actual = [note.tags for note in notes]
        expected = [expected_tags[i] for i in xrange(len(notes))]
        self.assertEqual(expected, actual)

    def test_can_create_notes(self):
        """
        Scenario: User can create notes.
        Given I have a course with 3 annotatable components
        And I open the unit with 2 annotatable components
        When I add 2 notes for the first component and 1 note for the second
        Then I see that notes were correctly created
        When I change sequential position to "2"
        And I add note for the annotatable component on the page
        Then I see that note was correctly created
        When I refresh the page
        Then I see that note was correctly stored
        When I change sequential position to "1"
        Then I see that notes were correctly stored on the page
        """
        self.note_unit_page.visit()

        components = self.note_unit_page.components
        self.create_notes(components)
        self.assert_text_in_notes(self.note_unit_page.notes)

        self.courseware_page.go_to_sequential_position(2)
        components = self.note_unit_page.components
        self.create_notes(components)

        components = self.note_unit_page.refresh()
        self.assert_text_in_notes(self.note_unit_page.notes)

        self.courseware_page.go_to_sequential_position(1)
        components = self.note_unit_page.components
        self.assert_text_in_notes(self.note_unit_page.notes)

    def test_can_edit_notes(self):
        """
        Scenario: User can edit notes.
        Given I have a course with 3 components with notes
        And I open the unit with 2 annotatable components
        When I change text in the notes
        Then I see that notes were correctly changed
        When I change sequential position to "2"
        And I change the note on the page
        Then I see that note was correctly changed
        When I refresh the page
        Then I see that edited note was correctly stored
        When I change sequential position to "1"
        Then I see that edited notes were correctly stored on the page
        """
        self._add_notes()
        self.note_unit_page.visit()

        components = self.note_unit_page.components
        self.edit_notes(components)
        self.assert_text_in_notes(self.note_unit_page.notes)

        self.courseware_page.go_to_sequential_position(2)
        components = self.note_unit_page.components
        self.edit_notes(components)
        self.assert_text_in_notes(self.note_unit_page.notes)

        components = self.note_unit_page.refresh()
        self.assert_text_in_notes(self.note_unit_page.notes)

        self.courseware_page.go_to_sequential_position(1)
        components = self.note_unit_page.components
        self.assert_text_in_notes(self.note_unit_page.notes)

    def test_can_delete_notes(self):
        """
        Scenario: User can delete notes.
        Given I have a course with 3 components with notes
        And I open the unit with 2 annotatable components
        When I remove all notes on the page
        Then I do not see any notes on the page
        When I change sequential position to "2"
        And I remove all notes on the page
        Then I do not see any notes on the page
        When I refresh the page
        Then I do not see any notes on the page
        When I change sequential position to "1"
        Then I do not see any notes on the page
        """
        self._add_notes()
        self.note_unit_page.visit()

        components = self.note_unit_page.components
        self.remove_notes(components)
        self.assert_notes_are_removed(components)

        self.courseware_page.go_to_sequential_position(2)
        components = self.note_unit_page.components
        self.remove_notes(components)
        self.assert_notes_are_removed(components)

        components = self.note_unit_page.refresh()
        self.assert_notes_are_removed(components)

        self.courseware_page.go_to_sequential_position(1)
        components = self.note_unit_page.components
        self.assert_notes_are_removed(components)

    def test_can_create_note_with_tags(self):
        """
        Scenario: a user of notes can define one with tags
        Given I have a course with 3 annotatable components
        And I open the unit with 2 annotatable components
        When I add a note with tags for the first component
        And I refresh the page
        Then I see that note was correctly stored with its tags
        """
        self.note_unit_page.visit()

        components = self.note_unit_page.components
        for note in components[0].create_note(".{}".format(self.selector)):
            note.tags = ["fruit", "tasty"]

        self.note_unit_page.refresh()
        self.assertEqual(["fruit", "tasty"], self.note_unit_page.notes[0].tags)

    def test_can_change_tags(self):
        """
        Scenario: a user of notes can edit tags on notes
        Given I have a course with 3 components with notes
        When I open the unit with 2 annotatable components
        And I edit tags on the notes for the 2 annotatable components
        Then I see that the tags were correctly changed
        And I again edit tags on the notes for the 2 annotatable components
        And I refresh the page
        Then I see that the tags were correctly changed
        """
        self._add_notes()
        self.note_unit_page.visit()

        components = self.note_unit_page.components
        self.edit_tags_in_notes(components, [["hard"], ["apple", "pear"]])
        self.assert_tags_in_notes(self.note_unit_page.notes, [["hard"], ["apple", "pear"]])

        self.edit_tags_in_notes(components, [[], ["avocado"]])
        self.assert_tags_in_notes(self.note_unit_page.notes, [[], ["avocado"]])

        self.note_unit_page.refresh()
        self.assert_tags_in_notes(self.note_unit_page.notes, [[], ["avocado"]])

    def test_sr_labels(self):
        """
        Scenario: screen reader labels exist for text and tags fields
        Given I have a course with 3 components with notes
        When I open the unit with 2 annotatable components
        And I open the editor for each note
        Then the text and tags fields both have screen reader labels
        """
        self._add_notes()
        self.note_unit_page.visit()

        # First note is in the first annotatable component, will have field indexes 0 and 1.
        for note in self.note_unit_page.components[0].edit_note():
            self.assertTrue(note.has_sr_label(0, 0, "Note"))
            self.assertTrue(note.has_sr_label(1, 1, "Tags (space-separated)"))

        # Second note is in the second annotatable component, will have field indexes 2 and 3.
        for note in self.note_unit_page.components[1].edit_note():
            self.assertTrue(note.has_sr_label(0, 2, "Note"))
            self.assertTrue(note.has_sr_label(1, 3, "Tags (space-separated)"))


@attr(shard=4)
class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
    """
    Tests for Notes page.
    """
    def _add_notes(self, notes_list):
        self.edxnotes_fixture.create_notes(notes_list)
        self.edxnotes_fixture.install()

    def _add_default_notes(self, tags=None, extra_notes=0):
        """
        Creates 5 test notes by default & number of extra_notes will be created if specified.
        If tags are not specified, will populate the notes with some test tag data.
        If tags are specified, they will be used for each of the 3 notes that have tags.
        """
        xblocks = self.course_fixture.get_nested_xblocks(category="html")
        # pylint: disable=attribute-defined-outside-init
        self.raw_note_list = [
            Note(
                usage_id=xblocks[4].locator,
                user=self.username,
                course_id=self.course_fixture._course_key,
                text="First note",
                quote="Annotate this",
                updated=datetime(2011, 1, 1, 1, 1, 1, 1).isoformat(),
            ),
            Note(
                usage_id=xblocks[2].locator,
                user=self.username,
                course_id=self.course_fixture._course_key,
                text="",
                quote=u"Annotate this",
                updated=datetime(2012, 1, 1, 1, 1, 1, 1).isoformat(),
                tags=["Review", "cool"] if tags is None else tags
            ),
            Note(
                usage_id=xblocks[0].locator,
                user=self.username,
                course_id=self.course_fixture._course_key,
                text="Third note",
                quote="Annotate this",
                updated=datetime(2013, 1, 1, 1, 1, 1, 1).isoformat(),
                ranges=[Range(startOffset=0, endOffset=18)],
                tags=["Cool", "TODO"] if tags is None else tags
            ),
            Note(
                usage_id=xblocks[3].locator,
                user=self.username,
                course_id=self.course_fixture._course_key,
                text="Fourth note",
                quote="",
                updated=datetime(2014, 1, 1, 1, 1, 1, 1).isoformat(),
                tags=["review"] if tags is None else tags
            ),
            Note(
                usage_id=xblocks[1].locator,
                user=self.username,
                course_id=self.course_fixture._course_key,
                text="Fifth note",
                quote="Annotate this",
                updated=datetime(2015, 1, 1, 1, 1, 1, 1).isoformat()
            ),
        ]
        if extra_notes > 0:
            for __ in range(extra_notes):
                self.raw_note_list.append(
                    Note(
                        usage_id=xblocks[random.choice([0, 1, 2, 3, 4, 5])].locator,
                        user=self.username,
                        course_id=self.course_fixture._course_key,  # pylint: disable=protected-access
                        text="Fourth note",
                        quote="",
                        updated=datetime(2014, 1, 1, 1, 1, 1, 1).isoformat(),
                        tags=["review"] if tags is None else tags
                    )
                )
        self._add_notes(self.raw_note_list)

    def assertNoteContent(self, item, text=None, quote=None, unit_name=None, time_updated=None, tags=None):
        """ Verifies the expected properties of the note. """
        self.assertEqual(text, item.text)
        if item.quote is not None:
            self.assertIn(quote, item.quote)
        else:
            self.assertIsNone(quote)
        self.assertEqual(unit_name, item.unit_name)
        self.assertEqual(time_updated, item.time_updated)
        self.assertEqual(tags, item.tags)

    def assertChapterContent(self, item, title=None, subtitles=None):
        """
        Verifies the expected title and subsection titles (subtitles) for the given chapter.
        """
        self.assertEqual(item.title, title)
        self.assertEqual(item.subtitles, subtitles)

    def assertGroupContent(self, item, title=None, notes=None):
        """
        Verifies the expected title and child notes for the given group.
        """
        self.assertEqual(item.title, title)
        self.assertEqual(item.notes, notes)

    def assert_viewed_event(self, view=None):
        """
        Verifies that the correct view event was captured for the Notes page.
        """
        # There will always be an initial event for "Recent Activity" because that is the default view.
        # If view is something besides "Recent Activity", expect 2 events, with the second one being
        # the view name passed in.
        if view == 'Recent Activity':
            view = None
        actual_events = self.wait_for_events(
            event_filter={'event_type': 'edx.course.student_notes.notes_page_viewed'},
            number_of_matches=1 if view is None else 2
        )
        expected_events = [{'event': {'view': 'Recent Activity'}}]
        if view:
            expected_events.append({'event': {'view': view}})
        self.assert_events_match(expected_events, actual_events)

    def assert_unit_link_event(self, usage_id, view):
        """
        Verifies that the correct used_unit_link event was captured for the Notes page.
        """
        actual_events = self.wait_for_events(
            event_filter={'event_type': 'edx.course.student_notes.used_unit_link'},
            number_of_matches=1
        )
        expected_events = [
            {'event': {'component_usage_id': usage_id, 'view': view}}
        ]
        self.assert_events_match(expected_events, actual_events)

    def assert_search_event(self, search_string, number_of_results):
        """
        Verifies that the correct searched event was captured for the Notes page.
        """
        actual_events = self.wait_for_events(
            event_filter={'event_type': 'edx.course.student_notes.searched'},
            number_of_matches=1
        )
        expected_events = [
            {'event': {'search_string': search_string, 'number_of_results': number_of_results}}
        ]
        self.assert_events_match(expected_events, actual_events)

    def _verify_pagination_info(
            self,
            notes_count_on_current_page,
            header_text,
            previous_button_enabled,
            next_button_enabled,
            current_page_number,
            total_pages
    ):
        """
        Verify pagination info
        """
        self.assertEqual(self.notes_page.count(), notes_count_on_current_page)
        self.assertEqual(self.notes_page.get_pagination_header_text(), header_text)

        if total_pages > 1:
            self.assertEqual(self.notes_page.footer_visible, True)
            self.assertEqual(self.notes_page.is_previous_page_button_enabled(), previous_button_enabled)
            self.assertEqual(self.notes_page.is_next_page_button_enabled(), next_button_enabled)
            self.assertEqual(self.notes_page.get_current_page_number(), current_page_number)
            self.assertEqual(self.notes_page.get_total_pages, total_pages)
        else:
            self.assertEqual(self.notes_page.footer_visible, False)

    def search_and_verify(self):
        """
        Add, search and verify notes.
        """
        self._add_default_notes(extra_notes=22)
        self.notes_page.visit()
        # Run the search
        self.notes_page.search("note")
        # No error message appears
        self.assertFalse(self.notes_page.is_error_visible)
        self.assertIn(u"Search Results", self.notes_page.tabs)

        self.assertEqual(self.notes_page.get_total_pages, 2)

    def test_no_content(self):
        """
        Scenario: User can see `No content` message.
        Given I have a course without notes
        When I open Notes page
        Then I see only "You do not have any notes within the course." message
        """
        notes_page_empty = EdxNotesPageNoContent(self.browser, self.course_id)
        notes_page_empty.visit()
        self.assertIn(
            "You have not made any notes in this course yet. Other students in this course are using notes to:",
            notes_page_empty.no_content_text)

    def test_notes_works_correctly_with_xss(self):
        """
        Scenario: Note text & tags should be HTML and JS escaped
        Given I am enrolled in a course with notes enabled
        When I visit the Notes page, with a Notes text and tag containing HTML characters like < and >
        Then the text and tags appear as expected due to having been properly escaped
        """
        xblocks = self.course_fixture.get_nested_xblocks(category="html")
        self._add_notes([
            Note(
                usage_id=xblocks[0].locator,
                user=self.username,
                course_id=self.course_fixture._course_key,  # pylint: disable=protected-access
                text='<script>alert("XSS")</script>',
                quote="quote",
                updated=datetime(2014, 1, 1, 1, 1, 1, 1).isoformat(),
                tags=['<script>alert("XSS")</script>']
            ),
            Note(
                usage_id=xblocks[1].locator,
                user=self.username,
                course_id=self.course_fixture._course_key,  # pylint: disable=protected-access
                text='<b>bold</b>',
                quote="quote",
                updated=datetime(2014, 2, 1, 1, 1, 1, 1).isoformat(),
                tags=['<i>bold</i>']
            )
        ])
        self.notes_page.visit()

        notes = self.notes_page.notes
        self.assertEqual(len(notes), 2)

        self.assertNoteContent(
            notes[0],
            quote=u"quote",
            text='<b>bold</b>',
            unit_name="Test Unit 1",
            time_updated="Feb 01, 2014 at 01:01 UTC",
            tags=['<i>bold</i>']
        )

        self.assertNoteContent(
            notes[1],
            quote=u"quote",
            text='<script>alert("XSS")</script>',
            unit_name="Test Unit 1",
            time_updated="Jan 01, 2014 at 01:01 UTC",
            tags=['<script>alert("XSS")</script>']
        )

    def test_recent_activity_view(self):
        """
        Scenario: User can view all notes by recent activity.
        Given I have a course with 5 notes
        When I open Notes page
        Then I see 5 notes sorted by the updated date
        And I see correct content in the notes
        And an event has fired indicating that the Recent Activity view was selected
        """
        self._add_default_notes()
        self.notes_page.visit()
        notes = self.notes_page.notes
        self.assertEqual(len(notes), 5)

        self.assertNoteContent(
            notes[0],
            quote=u"Annotate this",
            text=u"Fifth note",
            unit_name="Test Unit 1",
            time_updated="Jan 01, 2015 at 01:01 UTC"
        )

        self.assertNoteContent(
            notes[1],
            text=u"Fourth note",
            unit_name="Test Unit 3",
            time_updated="Jan 01, 2014 at 01:01 UTC",
            tags=["review"]
        )

        self.assertNoteContent(
            notes[2],
            quote="Annotate this",
            text=u"Third note",
            unit_name="Test Unit 1",
            time_updated="Jan 01, 2013 at 01:01 UTC",
            tags=["Cool", "TODO"]
        )

        self.assertNoteContent(
            notes[3],
            quote=u"Annotate this",
            unit_name="Test Unit 2",
            time_updated="Jan 01, 2012 at 01:01 UTC",
            tags=["Review", "cool"]
        )

        self.assertNoteContent(
            notes[4],
            quote=u"Annotate this",
            text=u"First note",
            unit_name="Test Unit 4",
            time_updated="Jan 01, 2011 at 01:01 UTC"
        )

        self.assert_viewed_event()

    def test_course_structure_view(self):
        """
        Scenario: User can view all notes by location in Course.
        Given I have a course with 5 notes
        When I open Notes page
        And I switch to "Location in Course" view
        Then I see 2 groups, 3 sections and 5 notes
        And I see correct content in the notes and groups
        And an event has fired indicating that the Location in Course view was selected
        """
        self._add_default_notes()
        self.notes_page.visit().switch_to_tab("structure")

        notes = self.notes_page.notes
        groups = self.notes_page.chapter_groups
        sections = self.notes_page.subsection_groups
        self.assertEqual(len(notes), 5)
        self.assertEqual(len(groups), 2)
        self.assertEqual(len(sections), 3)

        self.assertChapterContent(
            groups[0],
            title=u"Test Section 1",
            subtitles=[u"Test Subsection 1", u"Test Subsection 2"]
        )

        self.assertGroupContent(
            sections[0],
            title=u"Test Subsection 1",
            notes=[u"Fifth note", u"Third note", None]
        )

        self.assertNoteContent(
            notes[0],
            quote=u"Annotate this",
            text=u"Fifth note",
            unit_name="Test Unit 1",
            time_updated="Jan 01, 2015 at 01:01 UTC"
        )

        self.assertNoteContent(
            notes[1],
            quote=u"Annotate this",
            text=u"Third note",
            unit_name="Test Unit 1",
            time_updated="Jan 01, 2013 at 01:01 UTC",
            tags=["Cool", "TODO"]
        )

        self.assertNoteContent(
            notes[2],
            quote=u"Annotate this",
            unit_name="Test Unit 2",
            time_updated="Jan 01, 2012 at 01:01 UTC",
            tags=["Review", "cool"]
        )

        self.assertGroupContent(
            sections[1],
            title=u"Test Subsection 2",
            notes=[u"Fourth note"]
        )

        self.assertNoteContent(
            notes[3],
            text=u"Fourth note",
            unit_name="Test Unit 3",
            time_updated="Jan 01, 2014 at 01:01 UTC",
            tags=["review"]
        )

        self.assertChapterContent(
            groups[1],
            title=u"Test Section 2",
            subtitles=[u"Test Subsection 3"],
        )

        self.assertGroupContent(
            sections[2],
            title=u"Test Subsection 3",
            notes=[u"First note"]
        )

        self.assertNoteContent(
            notes[4],
            quote=u"Annotate this",
            text=u"First note",
            unit_name="Test Unit 4",
            time_updated="Jan 01, 2011 at 01:01 UTC"
        )

        self.assert_viewed_event('Location in Course')

    def test_tags_view(self):
        """
        Scenario: User can view all notes by associated tags.
        Given I have a course with 5 notes and I am viewing the Notes page
        When I switch to the "Tags" view
        Then I see 4 tag groups
        And I see correct content in the notes and groups
        And an event has fired indicating that the Tags view was selected
        """
        self._add_default_notes()
        self.notes_page.visit().switch_to_tab("tags")

        notes = self.notes_page.notes
        groups = self.notes_page.tag_groups
        self.assertEqual(len(notes), 7)
        self.assertEqual(len(groups), 4)

        # Tag group "cool"
        self.assertGroupContent(
            groups[0],
            title=u"cool (2)",
            notes=[u"Third note", None]
        )

        self.assertNoteContent(
            notes[0],
            quote=u"Annotate this",
            text=u"Third note",
            unit_name="Test Unit 1",
            time_updated="Jan 01, 2013 at 01:01 UTC",
            tags=["Cool", "TODO"]
        )

        self.assertNoteContent(
            notes[1],
            quote=u"Annotate this",
            unit_name="Test Unit 2",
            time_updated="Jan 01, 2012 at 01:01 UTC",
            tags=["Review", "cool"]
        )

        # Tag group "review"
        self.assertGroupContent(
            groups[1],
            title=u"review (2)",
            notes=[u"Fourth note", None]
        )

        self.assertNoteContent(
            notes[2],
            text=u"Fourth note",
            unit_name="Test Unit 3",
            time_updated="Jan 01, 2014 at 01:01 UTC",
            tags=["review"]
        )

        self.assertNoteContent(
            notes[3],
            quote=u"Annotate this",
            unit_name="Test Unit 2",
            time_updated="Jan 01, 2012 at 01:01 UTC",
            tags=["Review", "cool"]
        )

        # Tag group "todo"
        self.assertGroupContent(
            groups[2],
            title=u"todo (1)",
            notes=["Third note"]
        )

        self.assertNoteContent(
            notes[4],
            quote=u"Annotate this",
            text=u"Third note",
            unit_name="Test Unit 1",
            time_updated="Jan 01, 2013 at 01:01 UTC",
            tags=["Cool", "TODO"]
        )

        # Notes with no tags
        self.assertGroupContent(
            groups[3],
            title=u"[no tags] (2)",
            notes=["Fifth note", "First note"]
        )

        self.assertNoteContent(
            notes[5],
            quote=u"Annotate this",
            text=u"Fifth note",
            unit_name="Test Unit 1",
            time_updated="Jan 01, 2015 at 01:01 UTC"
        )

        self.assertNoteContent(
            notes[6],
            quote=u"Annotate this",
            text=u"First note",
            unit_name="Test Unit 4",
            time_updated="Jan 01, 2011 at 01:01 UTC"
        )

        self.assert_viewed_event('Tags')

    def test_easy_access_from_notes_page(self):
        """
        Scenario: Ensure that the link to the Unit works correctly.
        Given I have a course with 5 notes
        When I open Notes page
        And I click on the first unit link
        Then I see correct text on the unit page and a unit link event was fired
        When go back to the Notes page
        And I switch to "Location in Course" view
        And I click on the second unit link
        Then I see correct text on the unit page and a unit link event was fired
        When go back to the Notes page
        And I switch to "Tags" view
        And I click on the first unit link
        Then I see correct text on the unit page and a unit link event was fired
        When go back to the Notes page
        And I run the search with "Fifth" query
        And I click on the first unit link
        Then I see correct text on the unit page  and a unit link event was fired
        """
        def assert_page(note, usage_id, view):
            """ Verify that clicking on the unit link works properly. """
            quote = note.quote
            note.go_to_unit()
            self.courseware_page.wait_for_page()
            self.assertIn(quote, self.courseware_page.xblock_component_html_content())
            self.assert_unit_link_event(usage_id, view)
            self.reset_event_tracking()

        self._add_default_notes()
        self.notes_page.visit()
        note = self.notes_page.notes[0]
        assert_page(note, self.raw_note_list[4]['usage_id'], "Recent Activity")

        self.notes_page.visit()
        self.notes_page.switch_to_tab("structure")
        note = self.notes_page.notes[1]
        assert_page(note, self.raw_note_list[2]['usage_id'], "Location in Course")

        self.notes_page.visit()
        self.notes_page.switch_to_tab("tags")
        note = self.notes_page.notes[0]
        assert_page(note, self.raw_note_list[2]['usage_id'], "Tags")

        self.notes_page.visit()
        self.notes_page.search("Fifth")
        self.notes_page.wait_for_ajax()
        note = self.notes_page.notes[0]
        assert_page(note, self.raw_note_list[4]['usage_id'], "Search Results")

    def test_search_behaves_correctly(self):
        """
        Scenario: Searching behaves correctly.
        Given I have a course with 5 notes
        When I open Notes page
        When I run the search with "   " query
        Then I see the following error message "Please enter a term in the search field."
        And I do not see "Search Results" tab
        When I run the search with "note" query
        Then I see that error message disappears
        And I see that "Search Results" tab appears with 4 notes found
        And an event has fired indicating that the Search Results view was selected
        And an event has fired recording the search that was performed
        """
        self._add_default_notes()
        self.notes_page.visit()
        # Run the search with whitespaces only
        self.notes_page.search("   ")
        # Displays error message
        self.assertTrue(self.notes_page.is_error_visible)
        self.assertEqual(self.notes_page.error_text, u"Please enter a term in the search field.")
        # Search results tab does not appear
        self.assertNotIn(u"Search Results", self.notes_page.tabs)
        # Run the search with correct query
        self.notes_page.search("note")
        # Error message disappears
        self.assertFalse(self.notes_page.is_error_visible)
        self.assertIn(u"Search Results", self.notes_page.tabs)
        notes = self.notes_page.notes
        self.assertEqual(len(notes), 4)

        self.assertNoteContent(
            notes[0],
            quote=u"Annotate this",
            text=u"Fifth note",
            unit_name="Test Unit 1",
            time_updated="Jan 01, 2015 at 01:01 UTC"
        )

        self.assertNoteContent(
            notes[1],
            text=u"Fourth note",
            unit_name="Test Unit 3",
            time_updated="Jan 01, 2014 at 01:01 UTC",
            tags=["review"]
        )

        self.assertNoteContent(
            notes[2],
            quote="Annotate this",
            text=u"Third note",
            unit_name="Test Unit 1",
            time_updated="Jan 01, 2013 at 01:01 UTC",
            tags=["Cool", "TODO"]
        )

        self.assertNoteContent(
            notes[3],
            quote=u"Annotate this",
            text=u"First note",
            unit_name="Test Unit 4",
            time_updated="Jan 01, 2011 at 01:01 UTC"
        )

        self.assert_viewed_event('Search Results')
        self.assert_search_event('note', 4)

    @skip("scroll to tag functionality is disabled")
    def test_scroll_to_tag_recent_activity(self):
        """
        Scenario: Can scroll to a tag group from the Recent Activity view (default view)
        Given I have a course with 5 notes and I open the Notes page
        When I click on a tag associated with a note
        Then the Tags view tab gets focus and I scroll to the section of notes associated with that tag
        """
        self._add_default_notes(["apple", "banana", "kiwi", "pear", "pumpkin", "squash", "zucchini"])
        self.notes_page.visit()
        self._scroll_to_tag_and_verify("pear", 3)

    @skip("scroll to tag functionality is disabled")
    def test_scroll_to_tag_course_structure(self):
        """
        Scenario: Can scroll to a tag group from the Course Structure view
        Given I have a course with 5 notes and I open the Notes page and select the Course Structure view
        When I click on a tag associated with a note
        Then the Tags view tab gets focus and I scroll to the section of notes associated with that tag
        """
        self._add_default_notes(["apple", "banana", "kiwi", "pear", "pumpkin", "squash", "zucchini"])
        self.notes_page.visit().switch_to_tab("structure")
        self._scroll_to_tag_and_verify("squash", 5)

    @skip("scroll to tag functionality is disabled")
    def test_scroll_to_tag_search(self):
        """
        Scenario: Can scroll to a tag group from the Search Results view
        Given I have a course with 5 notes and I open the Notes page and perform a search
        Then the Search view tab opens and gets focus
        And when I click on a tag associated with a note
        Then the Tags view tab gets focus and I scroll to the section of notes associated with that tag
        """
        self._add_default_notes(["apple", "banana", "kiwi", "pear", "pumpkin", "squash", "zucchini"])
        self.notes_page.visit().search("note")
        self._scroll_to_tag_and_verify("pumpkin", 4)

    @skip("scroll to tag functionality is disabled")
    def test_scroll_to_tag_from_tag_view(self):
        """
        Scenario: Can scroll to a tag group from the Tags view
        Given I have a course with 5 notes and I open the Notes page and select the Tag view
        When I click on a tag associated with a note
        Then I scroll to the section of notes associated with that tag
        """
        self._add_default_notes(["apple", "banana", "kiwi", "pear", "pumpkin", "squash", "zucchini"])
        self.notes_page.visit().switch_to_tab("tags")
        self._scroll_to_tag_and_verify("kiwi", 2)

    def _scroll_to_tag_and_verify(self, tag_name, group_index):
        """ Helper method for all scroll to tag tests """
        self.notes_page.notes[1].go_to_tag(tag_name)

        # Because all the notes (with tags) have the same tags, they will end up ordered alphabetically.
        pear_group = self.notes_page.tag_groups[group_index]
        self.assertEqual(tag_name + " (3)", pear_group.title)
        self.assertTrue(pear_group.scrolled_to_top(group_index))

    def test_tabs_behaves_correctly(self):
        """
        Scenario: Tabs behaves correctly.
        Given I have a course with 5 notes
        When I open Notes page
        Then I see only "Recent Activity", "Location in Course", and "Tags" tabs
        When I run the search with "note" query
        And I see that "Search Results" tab appears with 4 notes found
        Then I switch to "Recent Activity" tab
        And I see all 5 notes
        Then I switch to "Location in Course" tab
        And I see all 2 groups and 5 notes
        When I switch back to "Search Results" tab
        Then I can still see 4 notes found
        When I close "Search Results" tab
        Then I see that "Recent Activity" tab becomes active
        And "Search Results" tab disappears
        And I see all 5 notes
        """
        self._add_default_notes()
        self.notes_page.visit()

        # We're on Recent Activity tab.
        self.assertEqual(len(self.notes_page.tabs), 3)
        self.assertEqual([u"Recent Activity", u"Location in Course", u"Tags"], self.notes_page.tabs)
        self.notes_page.search("note")
        # We're on Search Results tab
        self.assertEqual(len(self.notes_page.tabs), 4)
        self.assertIn(u"Search Results", self.notes_page.tabs)
        self.assertEqual(len(self.notes_page.notes), 4)
        # We can switch on Recent Activity tab and back.
        self.notes_page.switch_to_tab("recent")
        self.assertEqual(len(self.notes_page.notes), 5)
        self.notes_page.switch_to_tab("structure")
        self.assertEqual(len(self.notes_page.chapter_groups), 2)
        self.assertEqual(len(self.notes_page.notes), 5)
        self.notes_page.switch_to_tab("search")
        self.assertEqual(len(self.notes_page.notes), 4)
        # Can close search results page
        self.notes_page.close_tab()
        self.assertEqual(len(self.notes_page.tabs), 3)
        self.assertNotIn(u"Search Results", self.notes_page.tabs)
        self.assertEqual(len(self.notes_page.notes), 5)

    def test_open_note_when_accessed_from_notes_page(self):
        """
        Scenario: Ensure that the link to the Unit opens a note only once.
        Given I have a course with 2 sequentials that contain respectively one note and two notes
        When I open Notes page
        And I click on the first unit link
        Then I see the note opened on the unit page
        When I switch to the second sequential
        I do not see any note opened
        When I switch back to first sequential
        I do not see any note opened
        """
        xblocks = self.course_fixture.get_nested_xblocks(category="html")
        self._add_notes([
            Note(
                usage_id=xblocks[1].locator,
                user=self.username,
                course_id=self.course_fixture._course_key,
                text="Third note",
                quote="Annotate this",
                updated=datetime(2012, 1, 1, 1, 1, 1, 1).isoformat(),
                ranges=[Range(startOffset=0, endOffset=14)],
            ),
            Note(
                usage_id=xblocks[2].locator,
                user=self.username,
                course_id=self.course_fixture._course_key,
                text="Second note",
                quote="Annotate this",
                updated=datetime(2013, 1, 1, 1, 1, 1, 1).isoformat(),
                ranges=[Range(startOffset=0, endOffset=14)],
            ),
            Note(
                usage_id=xblocks[0].locator,
                user=self.username,
                course_id=self.course_fixture._course_key,
                text="First note",
                quote="Annotate this",
                updated=datetime(2014, 1, 1, 1, 1, 1, 1).isoformat(),
                ranges=[Range(startOffset=0, endOffset=14)],
            ),
        ])
        self.notes_page.visit()
        item = self.notes_page.notes[0]
        item.go_to_unit()
        self.courseware_page.wait_for_page()
        note = self.note_unit_page.notes[0]
        self.assertTrue(note.is_visible)
        note = self.note_unit_page.notes[1]
        self.assertFalse(note.is_visible)
        self.courseware_page.go_to_sequential_position(2)
        note = self.note_unit_page.notes[0]
        self.assertFalse(note.is_visible)
        self.courseware_page.go_to_sequential_position(1)
        self.courseware_page.wait_for_ajax()
        note = self.note_unit_page.notes[0]
        self.assertFalse(note.is_visible)

    def test_page_size_limit(self):
        """
        Scenario: Verify that we can't get notes more than default page size.

        Given that I am a registered user
        And I have a course with 11 notes
        When I open Notes page
        Then I can see notes list contains 10 items
        And I should see paging header and footer with correct data
        And I should see disabled previous button
        And I should also see enabled next button
        """
        self._add_default_notes(extra_notes=21)
        self.notes_page.visit()

        self._verify_pagination_info(
            notes_count_on_current_page=25,
            header_text='Showing 1-25 out of 26 total',
            previous_button_enabled=False,
            next_button_enabled=True,
            current_page_number=1,
            total_pages=2
        )

    def test_pagination_with_single_page(self):
        """
        Scenario: Notes list pagination works as expected for single page
        Given that I am a registered user
        And I have a course with 5 notes
        When I open Notes page
        Then I can see notes list contains 5 items
        And I should see paging header and footer with correct data
        And I should see disabled previous and next buttons
        """
        self._add_default_notes()
        self.notes_page.visit()
        self._verify_pagination_info(
            notes_count_on_current_page=5,
            header_text='Showing 1-5 out of 5 total',
            previous_button_enabled=False,
            next_button_enabled=False,
            current_page_number=1,
            total_pages=1
        )

    def test_next_and_previous_page_button(self):
        """
        Scenario: Next & Previous buttons are working as expected for notes list pagination

        Given that I am a registered user
        And I have a course with 26 notes
        When I open Notes page
        Then I can see notes list contains 25 items
        And I should see paging header and footer with correct data
        And I should see disabled previous button
        And I should see enabled next button

        When I click on next page button in footer
        Then I should be navigated to second page
        And I should see a list with 1 item
        And I should see paging header and footer with correct info
        And I should see enabled previous button
        And I should also see disabled next button

        When I click on previous page button in footer
        Then I should be navigated to first page
        And I should see a list with 25 items
        And I should see paging header and footer with correct info
        And I should see disabled previous button
        And I should also see enabled next button
        """
        self._add_default_notes(extra_notes=21)
        self.notes_page.visit()

        self._verify_pagination_info(
            notes_count_on_current_page=25,
            header_text='Showing 1-25 out of 26 total',
            previous_button_enabled=False,
            next_button_enabled=True,
            current_page_number=1,
            total_pages=2
        )

        self.notes_page.press_next_page_button()
        self._verify_pagination_info(
            notes_count_on_current_page=1,
            header_text='Showing 26-26 out of 26 total',
            previous_button_enabled=True,
            next_button_enabled=False,
            current_page_number=2,
            total_pages=2
        )
        self.notes_page.press_previous_page_button()
        self._verify_pagination_info(
            notes_count_on_current_page=25,
            header_text='Showing 1-25 out of 26 total',
            previous_button_enabled=False,
            next_button_enabled=True,
            current_page_number=1,
            total_pages=2
        )

    def test_pagination_with_valid_and_invalid_page_number(self):
        """
        Scenario: Notes list pagination works as expected for valid & invalid page number

        Given that I am a registered user
        And I have a course with 26 notes
        When I open Notes page
        Then I can see notes list contains 25 items
        And I should see paging header and footer with correct data
        And I should see total page value is 2
        When I enter 2 in the page number input
        Then I should be navigated to page 2

        When I enter 3 in the page number input
        Then I should not be navigated away from page 2
        """
        self._add_default_notes(extra_notes=21)
        self.notes_page.visit()

        self.assertEqual(self.notes_page.get_total_pages, 2)

        # test pagination with valid page number
        self.notes_page.go_to_page(2)
        self._verify_pagination_info(
            notes_count_on_current_page=1,
            header_text='Showing 26-26 out of 26 total',
            previous_button_enabled=True,
            next_button_enabled=False,
            current_page_number=2,
            total_pages=2
        )

        # test pagination with invalid page number
        self.notes_page.go_to_page(3)
        self._verify_pagination_info(
            notes_count_on_current_page=1,
            header_text='Showing 26-26 out of 26 total',
            previous_button_enabled=True,
            next_button_enabled=False,
            current_page_number=2,
            total_pages=2
        )

    def test_search_behaves_correctly_with_pagination(self):
        """
        Scenario: Searching behaves correctly with pagination.

        Given that I am a registered user
        And I have a course with 27 notes
        When I open Notes page
        Then I can see notes list with 25 items
        And I should see paging header and footer with correct data
        And previous button is disabled
        And next button is enabled
        When I run the search with "note" query
        Then I see no error message
        And I see that "Search Results" tab appears with 26 notes found
        And an event has fired indicating that the Search Results view was selected
        And an event has fired recording the search that was performed
        """
        self.search_and_verify()
        self._verify_pagination_info(
            notes_count_on_current_page=25,
            header_text='Showing 1-25 out of 26 total',
            previous_button_enabled=False,
            next_button_enabled=True,
            current_page_number=1,
            total_pages=2
        )

        self.assert_viewed_event('Search Results')
        self.assert_search_event('note', 26)

    def test_search_with_next_and_prev_page_button(self):
        """
        Scenario: Next & Previous buttons are working as expected for search

        Given that I am a registered user
        And I have a course with 27 notes
        When I open Notes page
        Then I can see notes list with 25 items
        And I should see paging header and footer with correct data
        And previous button is disabled
        And next button is enabled

        When I run the search with "note" query
        Then I see that "Search Results" tab appears with 26 notes found
        And an event has fired indicating that the Search Results view was selected
        And an event has fired recording the search that was performed

        When I click on next page button in footer
        Then I should be navigated to second page
        And I should see a list with 1 item
        And I should see paging header and footer with correct info
        And I should see enabled previous button
        And I should also see disabled next button

        When I click on previous page button in footer
        Then I should be navigated to first page
        And I should see a list with 25 items
        And I should see paging header and footer with correct info
        And I should see disabled previous button
        And I should also see enabled next button
        """
        self.search_and_verify()

        self._verify_pagination_info(
            notes_count_on_current_page=25,
            header_text='Showing 1-25 out of 26 total',
            previous_button_enabled=False,
            next_button_enabled=True,
            current_page_number=1,
            total_pages=2
        )

        self.assert_viewed_event('Search Results')
        self.assert_search_event('note', 26)

        self.notes_page.press_next_page_button()
        self._verify_pagination_info(
            notes_count_on_current_page=1,
            header_text='Showing 26-26 out of 26 total',
            previous_button_enabled=True,
            next_button_enabled=False,
            current_page_number=2,
            total_pages=2
        )
        self.notes_page.press_previous_page_button()
        self._verify_pagination_info(
            notes_count_on_current_page=25,
            header_text='Showing 1-25 out of 26 total',
            previous_button_enabled=False,
            next_button_enabled=True,
            current_page_number=1,
            total_pages=2
        )

    def test_search_with_valid_and_invalid_page_number(self):
        """
        Scenario: Notes list pagination works as expected for valid & invalid page number

        Given that I am a registered user
        And I have a course with 27 notes
        When I open Notes page
        Then I can see notes list contains 25 items
        And I should see paging header and footer with correct data
        And I should see total page value is 2

        When I run the search with "note" query
        Then I see that "Search Results" tab appears with 26 notes found
        And an event has fired indicating that the Search Results view was selected
        And an event has fired recording the search that was performed

        When I enter 2 in the page number input
        Then I should be navigated to page 2

        When I enter 3 in the page number input
        Then I should not be navigated away from page 2
        """
        self.search_and_verify()

        # test pagination with valid page number
        self.notes_page.go_to_page(2)
        self._verify_pagination_info(
            notes_count_on_current_page=1,
            header_text='Showing 26-26 out of 26 total',
            previous_button_enabled=True,
            next_button_enabled=False,
            current_page_number=2,
            total_pages=2
        )

        # test pagination with invalid page number
        self.notes_page.go_to_page(3)
        self._verify_pagination_info(
            notes_count_on_current_page=1,
            header_text='Showing 26-26 out of 26 total',
            previous_button_enabled=True,
            next_button_enabled=False,
            current_page_number=2,
            total_pages=2
        )


@attr(shard=4)
class EdxNotesToggleSingleNoteTest(EdxNotesTestMixin):
    """
    Tests for toggling single annotation.
    """

    def setUp(self):
        super(EdxNotesToggleSingleNoteTest, self).setUp()
        self._add_notes()
        self.note_unit_page.visit()

    def test_can_toggle_by_clicking_on_highlighted_text(self):
        """
        Scenario: User can toggle a single note by clicking on highlighted text.
        Given I have a course with components with notes
        When I click on highlighted text
        And I move mouse out of the note
        Then I see that the note is still shown
        When I click outside the note
        Then I see the the note is closed
        """
        note = self.note_unit_page.notes[0]

        note.click_on_highlight()
        self.note_unit_page.move_mouse_to("body")
        self.assertTrue(note.is_visible)
        self.note_unit_page.click("body")
        self.assertFalse(note.is_visible)

    def test_can_toggle_by_clicking_on_the_note(self):
        """
        Scenario: User can toggle a single note by clicking on the note.
        Given I have a course with components with notes
        When I click on the note
        And I move mouse out of the note
        Then I see that the note is still shown
        When I click outside the note
        Then I see the the note is closed
        """
        note = self.note_unit_page.notes[0]

        note.show().click_on_viewer()
        self.note_unit_page.move_mouse_to("body")
        self.assertTrue(note.is_visible)
        self.note_unit_page.click("body")
        self.assertFalse(note.is_visible)

    def test_interaction_between_notes(self):
        """
        Scenario: Interactions between notes works well.
        Given I have a course with components with notes
        When I click on highlighted text in the first component
        And I move mouse out of the note
        Then I see that the note is still shown
        When I click on highlighted text in the second component
        Then I see that the new note is shown
        """
        note_1 = self.note_unit_page.notes[0]
        note_2 = self.note_unit_page.notes[1]

        note_1.click_on_highlight()
        self.note_unit_page.move_mouse_to("body")
        self.assertTrue(note_1.is_visible)

        note_2.click_on_highlight()
        self.assertFalse(note_1.is_visible)
        self.assertTrue(note_2.is_visible)


@attr(shard=4)
class EdxNotesToggleNotesTest(EdxNotesTestMixin):
    """
    Tests for toggling visibility of all notes.
    """

    def setUp(self):
        super(EdxNotesToggleNotesTest, self).setUp()
        self._add_notes()
        self.note_unit_page.visit()

    def test_can_disable_all_notes(self):
        """
        Scenario: User can disable all notes.
        Given I have a course with components with notes
        And I open the unit with annotatable components
        When I click on "Show notes" checkbox
        Then I do not see any notes on the sequential position
        When I change sequential position to "2"
        Then I still do not see any notes on the sequential position
        When I go to "Test Subsection 2" subsection
        Then I do not see any notes on the subsection
        """
        # Disable all notes
        self.note_unit_page.toggle_visibility()
        self.assertEqual(len(self.note_unit_page.notes), 0)
        self.courseware_page.go_to_sequential_position(2)
        self.assertEqual(len(self.note_unit_page.notes), 0)
        self.course_home_page.visit()
        self.course_home_page.outline.go_to_section(u"Test Section 1", u"Test Subsection 2")
        self.assertEqual(len(self.note_unit_page.notes), 0)

    def test_can_reenable_all_notes(self):
        """
        Scenario: User can toggle notes visibility.
        Given I have a course with components with notes
        And I open the unit with annotatable components
        When I click on "Show notes" checkbox
        Then I do not see any notes on the sequential position
        When I click on "Show notes" checkbox again
        Then I see that all notes appear
        When I change sequential position to "2"
        Then I still can see all notes on the sequential position
        When I go to "Test Subsection 2" subsection
        Then I can see all notes on the subsection
        """
        # Disable notes
        self.note_unit_page.toggle_visibility()
        self.assertEqual(len(self.note_unit_page.notes), 0)
        # Enable notes to make sure that I can enable notes without refreshing
        # the page.
        self.note_unit_page.toggle_visibility()
        self.assertGreater(len(self.note_unit_page.notes), 0)
        self.courseware_page.go_to_sequential_position(2)
        self.assertGreater(len(self.note_unit_page.notes), 0)
        self.course_home_page.visit()
        self.course_home_page.outline.go_to_section(u"Test Section 1", u"Test Subsection 2")
        self.assertGreater(len(self.note_unit_page.notes), 0)