import operator

from django.contrib.auth import get_user_model
from django.db.models import QuerySet
from django.test import TestCase
from django.utils import timezone

from bookmarks import queries
from bookmarks.models import Bookmark, BookmarkSearch, UserProfile
from bookmarks.tests.helpers import BookmarkFactoryMixin, random_sentence
from bookmarks.utils import unique

User = get_user_model()


class QueriesTestCase(TestCase, BookmarkFactoryMixin):
    def setUp(self):
        self.profile = self.get_or_create_test_user().profile

    def setup_bookmark_search_data(self) -> None:
        tag1 = self.setup_tag(name='tag1')
        tag2 = self.setup_tag(name='tag2')
        self.setup_tag(name='unused_tag1')

        self.other_bookmarks = [
            self.setup_bookmark(),
            self.setup_bookmark(),
            self.setup_bookmark(),
        ]
        self.term1_bookmarks = [
            self.setup_bookmark(url='http://example.com/term1'),
            self.setup_bookmark(title=random_sentence(including_word='term1')),
            self.setup_bookmark(title=random_sentence(including_word='TERM1')),
            self.setup_bookmark(description=random_sentence(including_word='term1')),
            self.setup_bookmark(description=random_sentence(including_word='TERM1')),
            self.setup_bookmark(notes=random_sentence(including_word='term1')),
            self.setup_bookmark(notes=random_sentence(including_word='TERM1')),
            self.setup_bookmark(website_title=random_sentence(including_word='term1')),
            self.setup_bookmark(website_title=random_sentence(including_word='TERM1')),
            self.setup_bookmark(website_description=random_sentence(including_word='term1')),
            self.setup_bookmark(website_description=random_sentence(including_word='TERM1')),
        ]
        self.term1_term2_bookmarks = [
            self.setup_bookmark(url='http://example.com/term1/term2'),
            self.setup_bookmark(title=random_sentence(including_word='term1'),
                                description=random_sentence(including_word='term2')),
            self.setup_bookmark(description=random_sentence(including_word='term1'),
                                title=random_sentence(including_word='term2')),
            self.setup_bookmark(website_title=random_sentence(including_word='term1'),
                                title=random_sentence(including_word='term2')),
            self.setup_bookmark(website_description=random_sentence(including_word='term1'),
                                title=random_sentence(including_word='term2')),
        ]
        self.tag1_bookmarks = [
            self.setup_bookmark(tags=[tag1]),
            self.setup_bookmark(title=random_sentence(), tags=[tag1]),
            self.setup_bookmark(description=random_sentence(), tags=[tag1]),
            self.setup_bookmark(website_title=random_sentence(), tags=[tag1]),
            self.setup_bookmark(website_description=random_sentence(), tags=[tag1]),
        ]
        self.tag1_as_term_bookmarks = [
            self.setup_bookmark(url='http://example.com/tag1'),
            self.setup_bookmark(title=random_sentence(including_word='tag1')),
            self.setup_bookmark(description=random_sentence(including_word='tag1')),
            self.setup_bookmark(website_title=random_sentence(including_word='tag1')),
            self.setup_bookmark(website_description=random_sentence(including_word='tag1')),
        ]
        self.term1_tag1_bookmarks = [
            self.setup_bookmark(url='http://example.com/term1', tags=[tag1]),
            self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[tag1]),
            self.setup_bookmark(description=random_sentence(including_word='term1'), tags=[tag1]),
            self.setup_bookmark(website_title=random_sentence(including_word='term1'), tags=[tag1]),
            self.setup_bookmark(website_description=random_sentence(including_word='term1'), tags=[tag1]),
        ]
        self.tag2_bookmarks = [
            self.setup_bookmark(tags=[tag2]),
        ]
        self.tag1_tag2_bookmarks = [
            self.setup_bookmark(tags=[tag1, tag2]),
        ]

    def setup_tag_search_data(self):
        tag1 = self.setup_tag(name='tag1')
        tag2 = self.setup_tag(name='tag2')
        self.setup_tag(name='unused_tag1')

        self.other_bookmarks = [
            self.setup_bookmark(tags=[self.setup_tag()]),
            self.setup_bookmark(tags=[self.setup_tag()]),
            self.setup_bookmark(tags=[self.setup_tag()]),
        ]
        self.term1_bookmarks = [
            self.setup_bookmark(url='http://example.com/term1', tags=[self.setup_tag()]),
            self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
            self.setup_bookmark(title=random_sentence(including_word='TERM1'), tags=[self.setup_tag()]),
            self.setup_bookmark(description=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
            self.setup_bookmark(description=random_sentence(including_word='TERM1'), tags=[self.setup_tag()]),
            self.setup_bookmark(notes=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
            self.setup_bookmark(notes=random_sentence(including_word='TERM1'), tags=[self.setup_tag()]),
            self.setup_bookmark(website_title=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
            self.setup_bookmark(website_title=random_sentence(including_word='TERM1'), tags=[self.setup_tag()]),
            self.setup_bookmark(website_description=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
            self.setup_bookmark(website_description=random_sentence(including_word='TERM1'), tags=[self.setup_tag()]),
        ]
        self.term1_term2_bookmarks = [
            self.setup_bookmark(url='http://example.com/term1/term2', tags=[self.setup_tag()]),
            self.setup_bookmark(title=random_sentence(including_word='term1'),
                                description=random_sentence(including_word='term2'),
                                tags=[self.setup_tag()]),
            self.setup_bookmark(description=random_sentence(including_word='term1'),
                                title=random_sentence(including_word='term2'),
                                tags=[self.setup_tag()]),
            self.setup_bookmark(website_title=random_sentence(including_word='term1'),
                                title=random_sentence(including_word='term2'),
                                tags=[self.setup_tag()]),
            self.setup_bookmark(website_description=random_sentence(including_word='term1'),
                                title=random_sentence(including_word='term2'),
                                tags=[self.setup_tag()]),
        ]
        self.tag1_bookmarks = [
            self.setup_bookmark(tags=[tag1, self.setup_tag()]),
            self.setup_bookmark(title=random_sentence(), tags=[tag1, self.setup_tag()]),
            self.setup_bookmark(description=random_sentence(), tags=[tag1, self.setup_tag()]),
            self.setup_bookmark(website_title=random_sentence(), tags=[tag1, self.setup_tag()]),
            self.setup_bookmark(website_description=random_sentence(), tags=[tag1, self.setup_tag()]),
        ]
        self.tag1_as_term_bookmarks = [
            self.setup_bookmark(url='http://example.com/tag1'),
            self.setup_bookmark(title=random_sentence(including_word='tag1')),
            self.setup_bookmark(description=random_sentence(including_word='tag1')),
            self.setup_bookmark(website_title=random_sentence(including_word='tag1')),
            self.setup_bookmark(website_description=random_sentence(including_word='tag1')),
        ]
        self.term1_tag1_bookmarks = [
            self.setup_bookmark(url='http://example.com/term1', tags=[tag1, self.setup_tag()]),
            self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[tag1, self.setup_tag()]),
            self.setup_bookmark(description=random_sentence(including_word='term1'), tags=[tag1, self.setup_tag()]),
            self.setup_bookmark(website_title=random_sentence(including_word='term1'), tags=[tag1, self.setup_tag()]),
            self.setup_bookmark(website_description=random_sentence(including_word='term1'),
                                tags=[tag1, self.setup_tag()]),
        ]
        self.tag2_bookmarks = [
            self.setup_bookmark(tags=[tag2, self.setup_tag()]),
        ]
        self.tag1_tag2_bookmarks = [
            self.setup_bookmark(tags=[tag1, tag2, self.setup_tag()]),
        ]

    def get_tags_from_bookmarks(self, bookmarks: [Bookmark]):
        all_tags = []
        for bookmark in bookmarks:
            all_tags = all_tags + list(bookmark.tags.all())
        return all_tags

    def assertQueryResult(self, query: QuerySet, item_lists: [[any]]):
        expected_items = []
        for item_list in item_lists:
            expected_items = expected_items + item_list

        expected_items = unique(expected_items, operator.attrgetter('id'))

        self.assertCountEqual(list(query), expected_items)

    def test_query_bookmarks_should_return_all_for_empty_query(self):
        self.setup_bookmark_search_data()

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query=''))
        self.assertQueryResult(query, [
            self.other_bookmarks,
            self.term1_bookmarks,
            self.term1_term2_bookmarks,
            self.tag1_bookmarks,
            self.tag1_as_term_bookmarks,
            self.term1_tag1_bookmarks,
            self.tag2_bookmarks,
            self.tag1_tag2_bookmarks
        ])

    def test_query_bookmarks_should_search_single_term(self):
        self.setup_bookmark_search_data()

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1'))
        self.assertQueryResult(query, [
            self.term1_bookmarks,
            self.term1_term2_bookmarks,
            self.term1_tag1_bookmarks
        ])

    def test_query_bookmarks_should_search_multiple_terms(self):
        self.setup_bookmark_search_data()

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term2 term1'))

        self.assertQueryResult(query, [self.term1_term2_bookmarks])

    def test_query_bookmarks_should_search_single_tag(self):
        self.setup_bookmark_search_data()

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag1'))

        self.assertQueryResult(query, [self.tag1_bookmarks, self.tag1_tag2_bookmarks, self.term1_tag1_bookmarks])

    def test_query_bookmarks_should_search_multiple_tags(self):
        self.setup_bookmark_search_data()

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag1 #tag2'))

        self.assertQueryResult(query, [self.tag1_tag2_bookmarks])

    def test_query_bookmarks_should_search_multiple_tags_ignoring_casing(self):
        self.setup_bookmark_search_data()

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#Tag1 #TAG2'))

        self.assertQueryResult(query, [self.tag1_tag2_bookmarks])

    def test_query_bookmarks_should_search_terms_and_tags_combined(self):
        self.setup_bookmark_search_data()

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 #tag1'))

        self.assertQueryResult(query, [self.term1_tag1_bookmarks])

    def test_query_bookmarks_in_strict_mode_should_not_search_tags_as_terms(self):
        self.setup_bookmark_search_data()

        self.profile.tag_search = UserProfile.TAG_SEARCH_STRICT
        self.profile.save()

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1'))
        self.assertQueryResult(query, [self.tag1_as_term_bookmarks])

    def test_query_bookmarks_in_lax_mode_should_search_tags_as_terms(self):
        self.setup_bookmark_search_data()

        self.profile.tag_search = UserProfile.TAG_SEARCH_LAX
        self.profile.save()

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1'))
        self.assertQueryResult(query, [
            self.tag1_bookmarks,
            self.tag1_as_term_bookmarks,
            self.tag1_tag2_bookmarks,
            self.term1_tag1_bookmarks
        ])

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1 term1'))
        self.assertQueryResult(query, [
            self.term1_tag1_bookmarks,
        ])

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1 tag2'))
        self.assertQueryResult(query, [
            self.tag1_tag2_bookmarks,
        ])

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1 #tag2'))
        self.assertQueryResult(query, [
            self.tag1_tag2_bookmarks,
        ])

    def test_query_bookmarks_should_return_no_matches(self):
        self.setup_bookmark_search_data()

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term3'))
        self.assertQueryResult(query, [])

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 term3'))
        self.assertQueryResult(query, [])

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 #tag2'))
        self.assertQueryResult(query, [])

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag3'))
        self.assertQueryResult(query, [])

        # Unused tag
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#unused_tag1'))
        self.assertQueryResult(query, [])

        # Unused tag combined with tag that is used
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag1 #unused_tag1'))
        self.assertQueryResult(query, [])

        # Unused tag combined with term that is used
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 #unused_tag1'))
        self.assertQueryResult(query, [])

    def test_query_bookmarks_should_not_return_archived_bookmarks(self):
        bookmark1 = self.setup_bookmark()
        bookmark2 = self.setup_bookmark()
        self.setup_bookmark(is_archived=True)
        self.setup_bookmark(is_archived=True)
        self.setup_bookmark(is_archived=True)

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query=''))

        self.assertQueryResult(query, [[bookmark1, bookmark2]])

    def test_query_archived_bookmarks_should_not_return_unarchived_bookmarks(self):
        bookmark1 = self.setup_bookmark(is_archived=True)
        bookmark2 = self.setup_bookmark(is_archived=True)
        self.setup_bookmark()
        self.setup_bookmark()
        self.setup_bookmark()

        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query=''))

        self.assertQueryResult(query, [[bookmark1, bookmark2]])

    def test_query_bookmarks_should_only_return_user_owned_bookmarks(self):
        other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
        owned_bookmarks = [
            self.setup_bookmark(),
            self.setup_bookmark(),
            self.setup_bookmark(),
        ]
        self.setup_bookmark(user=other_user)
        self.setup_bookmark(user=other_user)
        self.setup_bookmark(user=other_user)

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query=''))

        self.assertQueryResult(query, [owned_bookmarks])

    def test_query_archived_bookmarks_should_only_return_user_owned_bookmarks(self):
        other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
        owned_bookmarks = [
            self.setup_bookmark(is_archived=True),
            self.setup_bookmark(is_archived=True),
            self.setup_bookmark(is_archived=True),
        ]
        self.setup_bookmark(is_archived=True, user=other_user)
        self.setup_bookmark(is_archived=True, user=other_user)
        self.setup_bookmark(is_archived=True, user=other_user)

        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query=''))

        self.assertQueryResult(query, [owned_bookmarks])

    def test_query_bookmarks_untagged_should_return_untagged_bookmarks_only(self):
        tag = self.setup_tag()
        untagged_bookmark = self.setup_bookmark()
        self.setup_bookmark(tags=[tag])
        self.setup_bookmark(tags=[tag])

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged'))
        self.assertCountEqual(list(query), [untagged_bookmark])

    def test_query_bookmarks_untagged_should_be_combinable_with_search_terms(self):
        tag = self.setup_tag()
        untagged_bookmark = self.setup_bookmark(title='term1')
        self.setup_bookmark(title='term2')
        self.setup_bookmark(tags=[tag])

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged term1'))
        self.assertCountEqual(list(query), [untagged_bookmark])

    def test_query_bookmarks_untagged_should_not_be_combinable_with_tags(self):
        tag = self.setup_tag()
        self.setup_bookmark()
        self.setup_bookmark(tags=[tag])
        self.setup_bookmark(tags=[tag])

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query=f'!untagged #{tag.name}'))
        self.assertCountEqual(list(query), [])

    def test_query_archived_bookmarks_untagged_should_return_untagged_bookmarks_only(self):
        tag = self.setup_tag()
        untagged_bookmark = self.setup_bookmark(is_archived=True)
        self.setup_bookmark(is_archived=True, tags=[tag])
        self.setup_bookmark(is_archived=True, tags=[tag])

        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged'))
        self.assertCountEqual(list(query), [untagged_bookmark])

    def test_query_archived_bookmarks_untagged_should_be_combinable_with_search_terms(self):
        tag = self.setup_tag()
        untagged_bookmark = self.setup_bookmark(is_archived=True, title='term1')
        self.setup_bookmark(is_archived=True, title='term2')
        self.setup_bookmark(is_archived=True, tags=[tag])

        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged term1'))
        self.assertCountEqual(list(query), [untagged_bookmark])

    def test_query_archived_bookmarks_untagged_should_not_be_combinable_with_tags(self):
        tag = self.setup_tag()
        self.setup_bookmark(is_archived=True)
        self.setup_bookmark(is_archived=True, tags=[tag])
        self.setup_bookmark(is_archived=True, tags=[tag])

        query = queries.query_archived_bookmarks(self.user, self.profile,
                                                 BookmarkSearch(query=f'!untagged #{tag.name}'))
        self.assertCountEqual(list(query), [])

    def test_query_bookmarks_unread_should_return_unread_bookmarks_only(self):
        unread_bookmarks = [
            self.setup_bookmark(unread=True),
            self.setup_bookmark(unread=True),
            self.setup_bookmark(unread=True),
        ]
        self.setup_bookmark()
        self.setup_bookmark()
        self.setup_bookmark()

        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='!unread'))
        self.assertCountEqual(list(query), unread_bookmarks)

    def test_query_archived_bookmarks_unread_should_return_unread_bookmarks_only(self):
        unread_bookmarks = [
            self.setup_bookmark(is_archived=True, unread=True),
            self.setup_bookmark(is_archived=True, unread=True),
            self.setup_bookmark(is_archived=True, unread=True),
        ]
        self.setup_bookmark(is_archived=True)
        self.setup_bookmark(is_archived=True)
        self.setup_bookmark(is_archived=True)

        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='!unread'))
        self.assertCountEqual(list(query), unread_bookmarks)

    def test_query_bookmark_tags_should_return_all_tags_for_empty_query(self):
        self.setup_tag_search_data()

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))

        self.assertQueryResult(query, [
            self.get_tags_from_bookmarks(self.other_bookmarks),
            self.get_tags_from_bookmarks(self.term1_bookmarks),
            self.get_tags_from_bookmarks(self.term1_term2_bookmarks),
            self.get_tags_from_bookmarks(self.tag1_bookmarks),
            self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
            self.get_tags_from_bookmarks(self.tag2_bookmarks),
            self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
        ])

    def test_query_bookmark_tags_should_search_single_term(self):
        self.setup_tag_search_data()

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1'))

        self.assertQueryResult(query, [
            self.get_tags_from_bookmarks(self.term1_bookmarks),
            self.get_tags_from_bookmarks(self.term1_term2_bookmarks),
            self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
        ])

    def test_query_bookmark_tags_should_search_multiple_terms(self):
        self.setup_tag_search_data()

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term2 term1'))

        self.assertQueryResult(query, [
            self.get_tags_from_bookmarks(self.term1_term2_bookmarks),
        ])

    def test_query_bookmark_tags_should_search_single_tag(self):
        self.setup_tag_search_data()

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag1'))

        self.assertQueryResult(query, [
            self.get_tags_from_bookmarks(self.tag1_bookmarks),
            self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
            self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
        ])

    def test_query_bookmark_tags_should_search_multiple_tags(self):
        self.setup_tag_search_data()

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag1 #tag2'))

        self.assertQueryResult(query, [
            self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
        ])

    def test_query_bookmark_tags_should_search_multiple_tags_ignoring_casing(self):
        self.setup_tag_search_data()

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#Tag1 #TAG2'))

        self.assertQueryResult(query, [
            self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
        ])

    def test_query_bookmark_tags_should_search_term_and_tag_combined(self):
        self.setup_tag_search_data()

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 #tag1'))

        self.assertQueryResult(query, [
            self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
        ])

    def test_query_bookmark_tags_in_strict_mode_should_not_search_tags_as_terms(self):
        self.setup_tag_search_data()

        self.profile.tag_search = UserProfile.TAG_SEARCH_STRICT
        self.profile.save()

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1'))
        self.assertQueryResult(query, self.get_tags_from_bookmarks(self.tag1_as_term_bookmarks))

    def test_query_bookmark_tags_in_lax_mode_should_search_tags_as_terms(self):
        self.setup_tag_search_data()

        self.profile.tag_search = UserProfile.TAG_SEARCH_LAX
        self.profile.save()

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1'))
        self.assertQueryResult(query, [
            self.get_tags_from_bookmarks(self.tag1_bookmarks),
            self.get_tags_from_bookmarks(self.tag1_as_term_bookmarks),
            self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
            self.get_tags_from_bookmarks(self.term1_tag1_bookmarks)
        ])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1 term1'))
        self.assertQueryResult(query, [
            self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
        ])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1 tag2'))
        self.assertQueryResult(query, [
            self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
        ])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1 #tag2'))
        self.assertQueryResult(query, [
            self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
        ])

    def test_query_bookmark_tags_should_return_no_matches(self):
        self.setup_tag_search_data()

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term3'))
        self.assertQueryResult(query, [])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 term3'))
        self.assertQueryResult(query, [])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 #tag2'))
        self.assertQueryResult(query, [])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag3'))
        self.assertQueryResult(query, [])

        # Unused tag
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#unused_tag1'))
        self.assertQueryResult(query, [])

        # Unused tag combined with tag that is used
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag1 #unused_tag1'))
        self.assertQueryResult(query, [])

        # Unused tag combined with term that is used
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 #unused_tag1'))
        self.assertQueryResult(query, [])

    def test_query_bookmark_tags_should_return_tags_for_unarchived_bookmarks_only(self):
        tag1 = self.setup_tag()
        tag2 = self.setup_tag()
        self.setup_bookmark(tags=[tag1])
        self.setup_bookmark()
        self.setup_bookmark(is_archived=True, tags=[tag2])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))

        self.assertQueryResult(query, [[tag1]])

    def test_query_bookmark_tags_should_return_distinct_tags(self):
        tag = self.setup_tag()
        self.setup_bookmark(tags=[tag])
        self.setup_bookmark(tags=[tag])
        self.setup_bookmark(tags=[tag])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))

        self.assertQueryResult(query, [[tag]])

    def test_query_archived_bookmark_tags_should_return_tags_for_archived_bookmarks_only(self):
        tag1 = self.setup_tag()
        tag2 = self.setup_tag()
        self.setup_bookmark(tags=[tag1])
        self.setup_bookmark()
        self.setup_bookmark(is_archived=True, tags=[tag2])

        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))

        self.assertQueryResult(query, [[tag2]])

    def test_query_archived_bookmark_tags_should_return_distinct_tags(self):
        tag = self.setup_tag()
        self.setup_bookmark(is_archived=True, tags=[tag])
        self.setup_bookmark(is_archived=True, tags=[tag])
        self.setup_bookmark(is_archived=True, tags=[tag])

        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))

        self.assertQueryResult(query, [[tag]])

    def test_query_bookmark_tags_should_only_return_user_owned_tags(self):
        other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
        owned_bookmarks = [
            self.setup_bookmark(tags=[self.setup_tag()]),
            self.setup_bookmark(tags=[self.setup_tag()]),
            self.setup_bookmark(tags=[self.setup_tag()]),
        ]
        self.setup_bookmark(user=other_user, tags=[self.setup_tag(user=other_user)])
        self.setup_bookmark(user=other_user, tags=[self.setup_tag(user=other_user)])
        self.setup_bookmark(user=other_user, tags=[self.setup_tag(user=other_user)])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))

        self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)])

    def test_query_archived_bookmark_tags_should_only_return_user_owned_tags(self):
        other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
        owned_bookmarks = [
            self.setup_bookmark(is_archived=True, tags=[self.setup_tag()]),
            self.setup_bookmark(is_archived=True, tags=[self.setup_tag()]),
            self.setup_bookmark(is_archived=True, tags=[self.setup_tag()]),
        ]
        self.setup_bookmark(is_archived=True, user=other_user, tags=[self.setup_tag(user=other_user)])
        self.setup_bookmark(is_archived=True, user=other_user, tags=[self.setup_tag(user=other_user)])
        self.setup_bookmark(is_archived=True, user=other_user, tags=[self.setup_tag(user=other_user)])

        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))

        self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)])

    def test_query_bookmark_tags_untagged_should_never_return_any_tags(self):
        tag = self.setup_tag()
        self.setup_bookmark()
        self.setup_bookmark(title='term1')
        self.setup_bookmark(title='term1', tags=[tag])
        self.setup_bookmark(tags=[tag])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged'))
        self.assertCountEqual(list(query), [])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged term1'))
        self.assertCountEqual(list(query), [])

        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query=f'!untagged #{tag.name}'))
        self.assertCountEqual(list(query), [])

    def test_query_archived_bookmark_tags_untagged_should_never_return_any_tags(self):
        tag = self.setup_tag()
        self.setup_bookmark(is_archived=True)
        self.setup_bookmark(is_archived=True, title='term1')
        self.setup_bookmark(is_archived=True, title='term1', tags=[tag])
        self.setup_bookmark(is_archived=True, tags=[tag])

        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged'))
        self.assertCountEqual(list(query), [])

        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged term1'))
        self.assertCountEqual(list(query), [])

        query = queries.query_archived_bookmark_tags(self.user, self.profile,
                                                     BookmarkSearch(query=f'!untagged #{tag.name}'))
        self.assertCountEqual(list(query), [])

    def test_query_shared_bookmarks(self):
        user1 = self.setup_user(enable_sharing=True)
        user2 = self.setup_user(enable_sharing=True)
        user3 = self.setup_user(enable_sharing=True)
        user4 = self.setup_user(enable_sharing=False)
        tag = self.setup_tag()

        shared_bookmarks = [
            self.setup_bookmark(user=user1, shared=True, title='test title'),
            self.setup_bookmark(user=user2, shared=True),
            self.setup_bookmark(user=user3, shared=True, tags=[tag]),
        ]

        # Unshared bookmarks
        self.setup_bookmark(user=user1, shared=False, title='test title'),
        self.setup_bookmark(user=user2, shared=False),
        self.setup_bookmark(user=user3, shared=False, tags=[tag]),
        self.setup_bookmark(user=user4, shared=True, tags=[tag]),

        # Should return shared bookmarks from all users
        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query=''), False)
        self.assertQueryResult(query_set, [shared_bookmarks])

        # Should respect search query
        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query='test title'), False)
        self.assertQueryResult(query_set, [[shared_bookmarks[0]]])

        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query=f'#{tag.name}'), False)
        self.assertQueryResult(query_set, [[shared_bookmarks[2]]])

    def test_query_publicly_shared_bookmarks(self):
        user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
        user2 = self.setup_user(enable_sharing=True)

        bookmark1 = self.setup_bookmark(user=user1, shared=True)
        self.setup_bookmark(user=user2, shared=True)

        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query=''), True)
        self.assertQueryResult(query_set, [[bookmark1]])

    def test_query_shared_bookmark_tags(self):
        user1 = self.setup_user(enable_sharing=True)
        user2 = self.setup_user(enable_sharing=True)
        user3 = self.setup_user(enable_sharing=True)
        user4 = self.setup_user(enable_sharing=False)

        shared_tags = [
            self.setup_tag(user=user1),
            self.setup_tag(user=user2),
            self.setup_tag(user=user3),
        ]

        self.setup_bookmark(user=user1, shared=True, tags=[shared_tags[0]]),
        self.setup_bookmark(user=user2, shared=True, tags=[shared_tags[1]]),
        self.setup_bookmark(user=user3, shared=True, tags=[shared_tags[2]]),

        self.setup_bookmark(user=user1, shared=False, tags=[self.setup_tag(user=user1)]),
        self.setup_bookmark(user=user2, shared=False, tags=[self.setup_tag(user=user2)]),
        self.setup_bookmark(user=user3, shared=False, tags=[self.setup_tag(user=user3)]),
        self.setup_bookmark(user=user4, shared=True, tags=[self.setup_tag(user=user4)]),

        query_set = queries.query_shared_bookmark_tags(None, self.profile, BookmarkSearch(query=''), False)

        self.assertQueryResult(query_set, [shared_tags])

    def test_query_publicly_shared_bookmark_tags(self):
        user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
        user2 = self.setup_user(enable_sharing=True)

        tag1 = self.setup_tag(user=user1)
        tag2 = self.setup_tag(user=user2)

        self.setup_bookmark(user=user1, shared=True, tags=[tag1]),
        self.setup_bookmark(user=user2, shared=True, tags=[tag2]),

        query_set = queries.query_shared_bookmark_tags(None, self.profile, BookmarkSearch(query=''), True)

        self.assertQueryResult(query_set, [[tag1]])

    def test_query_shared_bookmark_users(self):
        users_with_shared_bookmarks = [
            self.setup_user(enable_sharing=True),
            self.setup_user(enable_sharing=True),
        ]
        users_without_shared_bookmarks = [
            self.setup_user(enable_sharing=True),
            self.setup_user(enable_sharing=True),
            self.setup_user(enable_sharing=False),
        ]

        # Shared bookmarks
        self.setup_bookmark(user=users_with_shared_bookmarks[0], shared=True, title='test title'),
        self.setup_bookmark(user=users_with_shared_bookmarks[1], shared=True),

        # Unshared bookmarks
        self.setup_bookmark(user=users_without_shared_bookmarks[0], shared=False, title='test title'),
        self.setup_bookmark(user=users_without_shared_bookmarks[1], shared=False),
        self.setup_bookmark(user=users_without_shared_bookmarks[2], shared=True),

        # Should return users with shared bookmarks
        query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(query=''), False)
        self.assertQueryResult(query_set, [users_with_shared_bookmarks])

        # Should respect search query
        query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(query='test title'), False)
        self.assertQueryResult(query_set, [[users_with_shared_bookmarks[0]]])

    def test_query_publicly_shared_bookmark_users(self):
        user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
        user2 = self.setup_user(enable_sharing=True)

        self.setup_bookmark(user=user1, shared=True)
        self.setup_bookmark(user=user2, shared=True)

        query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(query=''), True)
        self.assertQueryResult(query_set, [[user1]])

    def test_sorty_by_date_added_asc(self):
        search = BookmarkSearch(sort=BookmarkSearch.SORT_ADDED_ASC)

        bookmarks = [
            self.setup_bookmark(added=timezone.datetime(2020, 1, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2021, 2, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2022, 3, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2023, 4, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2022, 5, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2021, 6, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2020, 7, 1, tzinfo=timezone.utc)),
        ]
        sorted_bookmarks = sorted(bookmarks, key=lambda b: b.date_added)

        query = queries.query_bookmarks(self.user, self.profile, search)
        self.assertEqual(list(query), sorted_bookmarks)

    def test_sorty_by_date_added_desc(self):
        search = BookmarkSearch(sort=BookmarkSearch.SORT_ADDED_DESC)

        bookmarks = [
            self.setup_bookmark(added=timezone.datetime(2020, 1, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2021, 2, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2022, 3, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2023, 4, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2022, 5, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2021, 6, 1, tzinfo=timezone.utc)),
            self.setup_bookmark(added=timezone.datetime(2020, 7, 1, tzinfo=timezone.utc)),
        ]
        sorted_bookmarks = sorted(bookmarks, key=lambda b: b.date_added, reverse=True)

        query = queries.query_bookmarks(self.user, self.profile, search)
        self.assertEqual(list(query), sorted_bookmarks)

    def setup_title_sort_data(self):
        # lots of combinations to test effective title logic
        bookmarks = [
            self.setup_bookmark(title='a_1_1'),
            self.setup_bookmark(title='A_1_2'),
            self.setup_bookmark(title='b_1_1'),
            self.setup_bookmark(title='B_1_2'),
            self.setup_bookmark(title='', website_title='a_2_1'),
            self.setup_bookmark(title='', website_title='A_2_2'),
            self.setup_bookmark(title='', website_title='b_2_1'),
            self.setup_bookmark(title='', website_title='B_2_2'),
            self.setup_bookmark(title='', website_title='', url='a_3_1'),
            self.setup_bookmark(title='', website_title='', url='A_3_2'),
            self.setup_bookmark(title='', website_title='', url='b_3_1'),
            self.setup_bookmark(title='', website_title='', url='B_3_2'),
            self.setup_bookmark(title='a_4_1', website_title='0'),
            self.setup_bookmark(title='A_4_2', website_title='0'),
            self.setup_bookmark(title='b_4_1', website_title='0'),
            self.setup_bookmark(title='B_4_2', website_title='0'),
            self.setup_bookmark(title='a_5_1', url='0'),
            self.setup_bookmark(title='A_5_2', url='0'),
            self.setup_bookmark(title='b_5_1', url='0'),
            self.setup_bookmark(title='B_5_2', url='0'),
            self.setup_bookmark(title='', website_title='a_6_1', url='0'),
            self.setup_bookmark(title='', website_title='A_6_2', url='0'),
            self.setup_bookmark(title='', website_title='b_6_1', url='0'),
            self.setup_bookmark(title='', website_title='B_6_2', url='0'),
            self.setup_bookmark(title='a_7_1', website_title='0', url='0'),
            self.setup_bookmark(title='A_7_2', website_title='0', url='0'),
            self.setup_bookmark(title='b_7_1', website_title='0', url='0'),
            self.setup_bookmark(title='B_7_2', website_title='0', url='0'),
        ]
        return bookmarks

    def test_sort_by_title_asc(self):
        search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_ASC)

        bookmarks = self.setup_title_sort_data()
        sorted_bookmarks = sorted(bookmarks, key=lambda b: b.resolved_title.lower())

        query = queries.query_bookmarks(self.user, self.profile, search)
        self.assertEqual(list(query), sorted_bookmarks)

    def test_sort_by_title_desc(self):
        search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_DESC)

        bookmarks = self.setup_title_sort_data()
        sorted_bookmarks = sorted(bookmarks, key=lambda b: b.resolved_title.lower(), reverse=True)

        query = queries.query_bookmarks(self.user, self.profile, search)
        self.assertEqual(list(query), sorted_bookmarks)
