import urllib.parse
from collections import OrderedDict
from unittest.mock import patch

from django.contrib.auth.models import User
from django.urls import reverse
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.response import Response

from bookmarks.models import Bookmark, BookmarkSearch, UserProfile
from bookmarks.services import website_loader
from bookmarks.services.website_loader import WebsiteMetadata
from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin


class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):

    def authenticate(self):
        self.api_token = Token.objects.get_or_create(user=self.get_or_create_test_user())[0]
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key)

    def assertBookmarkListEqual(self, data_list, bookmarks):
        expectations = []
        for bookmark in bookmarks:
            tag_names = [tag.name for tag in bookmark.tags.all()]
            tag_names.sort(key=str.lower)
            expectation = OrderedDict()
            expectation['id'] = bookmark.id
            expectation['url'] = bookmark.url
            expectation['title'] = bookmark.title
            expectation['description'] = bookmark.description
            expectation['notes'] = bookmark.notes
            expectation['website_title'] = bookmark.website_title
            expectation['website_description'] = bookmark.website_description
            expectation['is_archived'] = bookmark.is_archived
            expectation['unread'] = bookmark.unread
            expectation['shared'] = bookmark.shared
            expectation['tag_names'] = tag_names
            expectation['date_added'] = bookmark.date_added.isoformat().replace('+00:00', 'Z')
            expectation['date_modified'] = bookmark.date_modified.isoformat().replace('+00:00', 'Z')
            expectations.append(expectation)

        for data in data_list:
            data['tag_names'].sort(key=str.lower)

        self.assertCountEqual(data_list, expectations)

    def test_list_bookmarks(self):
        self.authenticate()
        bookmarks = self.setup_numbered_bookmarks(5)

        response = self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], bookmarks)

    def test_list_bookmarks_does_not_return_archived_bookmarks(self):
        self.authenticate()
        bookmarks = self.setup_numbered_bookmarks(5)
        self.setup_numbered_bookmarks(5, archived=True)

        response = self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], bookmarks)

    def test_list_bookmarks_should_filter_by_query(self):
        self.authenticate()
        search_value = self.get_random_string()
        bookmarks = self.setup_numbered_bookmarks(5, prefix=search_value)
        self.setup_numbered_bookmarks(5)

        response = self.get(reverse('bookmarks:bookmark-list') + '?q=' + search_value,
                            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], bookmarks)

    def test_list_bookmarks_filter_unread(self):
        self.authenticate()
        unread_bookmarks = self.setup_numbered_bookmarks(5, unread=True)
        read_bookmarks = self.setup_numbered_bookmarks(5, unread=False)

        # Filter off
        response = self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], unread_bookmarks + read_bookmarks)

        # Filter shared
        response = self.get(reverse('bookmarks:bookmark-list') + '?unread=yes',
                            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], unread_bookmarks)

        # Filter unshared
        response = self.get(reverse('bookmarks:bookmark-list') + '?unread=no',
                            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], read_bookmarks)

    def test_list_bookmarks_filter_shared(self):
        self.authenticate()
        unshared_bookmarks = self.setup_numbered_bookmarks(5)
        shared_bookmarks = self.setup_numbered_bookmarks(5, shared=True)

        # Filter off
        response = self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], unshared_bookmarks + shared_bookmarks)

        # Filter shared
        response = self.get(reverse('bookmarks:bookmark-list') + '?shared=yes',
                            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], shared_bookmarks)

        # Filter unshared
        response = self.get(reverse('bookmarks:bookmark-list') + '?shared=no',
                            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], unshared_bookmarks)

    def test_list_bookmarks_should_respect_sort(self):
        self.authenticate()
        bookmarks = self.setup_numbered_bookmarks(5)
        bookmarks.reverse()

        response = self.get(reverse('bookmarks:bookmark-list') + '?sort=title_desc',
                            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], bookmarks)

    def test_list_archived_bookmarks_does_not_return_unarchived_bookmarks(self):
        self.authenticate()
        self.setup_numbered_bookmarks(5)
        archived_bookmarks = self.setup_numbered_bookmarks(5, archived=True)

        response = self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], archived_bookmarks)

    def test_list_archived_bookmarks_should_filter_by_query(self):
        self.authenticate()
        search_value = self.get_random_string()
        archived_bookmarks = self.setup_numbered_bookmarks(5, archived=True, prefix=search_value)
        self.setup_numbered_bookmarks(5, archived=True)

        response = self.get(reverse('bookmarks:bookmark-archived') + '?q=' + search_value,
                            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], archived_bookmarks)

    def test_list_archived_bookmarks_should_respect_sort(self):
        self.authenticate()
        bookmarks = self.setup_numbered_bookmarks(5, archived=True)
        bookmarks.reverse()

        response = self.get(reverse('bookmarks:bookmark-archived') + '?sort=title_desc',
                            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], bookmarks)

    def test_list_shared_bookmarks(self):
        self.authenticate()

        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_bookmarks = [
            self.setup_bookmark(shared=True, user=user1),
            self.setup_bookmark(shared=True, user=user2),
            self.setup_bookmark(shared=True, user=user3),
        ]
        # Unshared bookmarks
        self.setup_bookmark(shared=False, user=user1)
        self.setup_bookmark(shared=False, user=user2)
        self.setup_bookmark(shared=False, user=user3)
        self.setup_bookmark(shared=True, user=user4)

        response = self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], shared_bookmarks)

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

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

        response = self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], shared_bookmarks)

    def test_list_shared_bookmarks_should_filter_by_query_and_user(self):
        self.authenticate()

        # Search by query
        user1 = self.setup_user(enable_sharing=True)
        user2 = self.setup_user(enable_sharing=True)
        user3 = self.setup_user(enable_sharing=True)
        expected_bookmarks = [
            self.setup_bookmark(title='searchvalue', shared=True, user=user1),
            self.setup_bookmark(title='searchvalue', shared=True, user=user2),
            self.setup_bookmark(title='searchvalue', shared=True, user=user3),
        ]
        self.setup_bookmark(shared=True, user=user1),
        self.setup_bookmark(shared=True, user=user2),
        self.setup_bookmark(shared=True, user=user3),

        response = self.get(reverse('bookmarks:bookmark-shared') + '?q=searchvalue',
                            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], expected_bookmarks)

        # Search by user
        user_search_user = self.setup_user(enable_sharing=True)
        expected_bookmarks = [
            self.setup_bookmark(shared=True, user=user_search_user),
            self.setup_bookmark(shared=True, user=user_search_user),
            self.setup_bookmark(shared=True, user=user_search_user),
        ]
        response = self.get(reverse('bookmarks:bookmark-shared') + '?user=' + user_search_user.username,
                            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], expected_bookmarks)

        # Search by query and user
        combined_search_user = self.setup_user(enable_sharing=True)
        expected_bookmarks = [
            self.setup_bookmark(title='searchvalue', shared=True, user=combined_search_user),
            self.setup_bookmark(title='searchvalue', shared=True, user=combined_search_user),
            self.setup_bookmark(title='searchvalue', shared=True, user=combined_search_user),
        ]
        response = self.get(
            reverse('bookmarks:bookmark-shared') + '?q=searchvalue&user=' + combined_search_user.username,
            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], expected_bookmarks)

    def test_list_shared_bookmarks_should_respect_sort(self):
        self.authenticate()
        user = self.setup_user(enable_sharing=True)
        bookmarks = self.setup_numbered_bookmarks(5, shared=True, user=user)
        bookmarks.reverse()

        response = self.get(reverse('bookmarks:bookmark-shared') + '?sort=title_desc',
                            expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual(response.data['results'], bookmarks)

    def test_create_bookmark(self):
        self.authenticate()

        data = {
            'url': 'https://example.com/',
            'title': 'Test title',
            'description': 'Test description',
            'notes': 'Test notes',
            'is_archived': False,
            'unread': False,
            'shared': False,
            'tag_names': ['tag1', 'tag2']
        }
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
        bookmark = Bookmark.objects.get(url=data['url'])
        self.assertEqual(bookmark.url, data['url'])
        self.assertEqual(bookmark.title, data['title'])
        self.assertEqual(bookmark.description, data['description'])
        self.assertEqual(bookmark.notes, data['notes'])
        self.assertFalse(bookmark.is_archived, data['is_archived'])
        self.assertFalse(bookmark.unread, data['unread'])
        self.assertFalse(bookmark.shared, data['shared'])
        self.assertEqual(bookmark.tags.count(), 2)
        self.assertEqual(bookmark.tags.filter(name=data['tag_names'][0]).count(), 1)
        self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)

    def test_create_bookmark_with_same_url_updates_existing_bookmark(self):
        self.authenticate()

        original_bookmark = self.setup_bookmark()
        data = {
            'url': original_bookmark.url,
            'title': 'Updated title',
            'description': 'Updated description',
            'notes': 'Updated notes',
            'unread': True,
            'shared': True,
            'is_archived': True,
            'tag_names': ['tag1', 'tag2']
        }
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
        bookmark = Bookmark.objects.get(url=data['url'])
        self.assertEqual(bookmark.id, original_bookmark.id)
        self.assertEqual(bookmark.url, data['url'])
        self.assertEqual(bookmark.title, data['title'])
        self.assertEqual(bookmark.description, data['description'])
        self.assertEqual(bookmark.notes, data['notes'])
        # Saving a duplicate bookmark should not modify archive flag - right?
        self.assertFalse(bookmark.is_archived)
        self.assertEqual(bookmark.unread, data['unread'])
        self.assertEqual(bookmark.shared, data['shared'])
        self.assertEqual(bookmark.tags.count(), 2)
        self.assertEqual(bookmark.tags.filter(name=data['tag_names'][0]).count(), 1)
        self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)

    def test_create_bookmark_replaces_whitespace_in_tag_names(self):
        self.authenticate()

        data = {
            'url': 'https://example.com/',
            'title': 'Test title',
            'description': 'Test description',
            'tag_names': ['tag 1', 'tag 2']
        }
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
        bookmark = Bookmark.objects.get(url=data['url'])
        tag_names = [tag.name for tag in bookmark.tags.all()]
        self.assertListEqual(tag_names, ['tag-1', 'tag-2'])

    def test_create_bookmark_minimal_payload(self):
        self.authenticate()

        data = {'url': 'https://example.com/'}
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)

    def test_create_archived_bookmark(self):
        self.authenticate()

        data = {
            'url': 'https://example.com/',
            'title': 'Test title',
            'description': 'Test description',
            'is_archived': True,
            'tag_names': ['tag1', 'tag2']
        }
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
        bookmark = Bookmark.objects.get(url=data['url'])
        self.assertEqual(bookmark.url, data['url'])
        self.assertEqual(bookmark.title, data['title'])
        self.assertEqual(bookmark.description, data['description'])
        self.assertTrue(bookmark.is_archived)
        self.assertEqual(bookmark.tags.count(), 2)
        self.assertEqual(bookmark.tags.filter(name=data['tag_names'][0]).count(), 1)
        self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)

    def test_create_bookmark_is_not_archived_by_default(self):
        self.authenticate()

        data = {'url': 'https://example.com/'}
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
        bookmark = Bookmark.objects.get(url=data['url'])
        self.assertFalse(bookmark.is_archived)

    def test_create_unread_bookmark(self):
        self.authenticate()

        data = {'url': 'https://example.com/', 'unread': True}
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
        bookmark = Bookmark.objects.get(url=data['url'])
        self.assertTrue(bookmark.unread)

    def test_create_bookmark_is_not_unread_by_default(self):
        self.authenticate()

        data = {'url': 'https://example.com/'}
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
        bookmark = Bookmark.objects.get(url=data['url'])
        self.assertFalse(bookmark.unread)

    def test_create_shared_bookmark(self):
        self.authenticate()

        data = {'url': 'https://example.com/', 'shared': True}
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
        bookmark = Bookmark.objects.get(url=data['url'])
        self.assertTrue(bookmark.shared)

    def test_create_bookmark_is_not_shared_by_default(self):
        self.authenticate()

        data = {'url': 'https://example.com/'}
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
        bookmark = Bookmark.objects.get(url=data['url'])
        self.assertFalse(bookmark.shared)

    def test_get_bookmark(self):
        self.authenticate()
        bookmark = self.setup_bookmark()

        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        response = self.get(url, expected_status_code=status.HTTP_200_OK)
        self.assertBookmarkListEqual([response.data], [bookmark])

    def test_update_bookmark(self):
        self.authenticate()
        bookmark = self.setup_bookmark()

        data = {'url': 'https://example.com/updated'}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.put(url, data, expected_status_code=status.HTTP_200_OK)
        updated_bookmark = Bookmark.objects.get(id=bookmark.id)
        self.assertEqual(updated_bookmark.url, data['url'])

    def test_update_bookmark_fails_without_required_fields(self):
        self.authenticate()
        bookmark = self.setup_bookmark()

        data = {'title': 'https://example.com/'}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.put(url, data, expected_status_code=status.HTTP_400_BAD_REQUEST)

    def test_update_bookmark_with_minimal_payload_clears_all_fields(self):
        self.authenticate()
        bookmark = self.setup_bookmark()

        data = {'url': 'https://example.com/'}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.put(url, data, expected_status_code=status.HTTP_200_OK)
        updated_bookmark = Bookmark.objects.get(id=bookmark.id)
        self.assertEqual(updated_bookmark.url, data['url'])
        self.assertEqual(updated_bookmark.title, '')
        self.assertEqual(updated_bookmark.description, '')
        self.assertEqual(updated_bookmark.notes, '')
        self.assertEqual(updated_bookmark.tag_names, [])

    def test_update_bookmark_unread_flag(self):
        self.authenticate()
        bookmark = self.setup_bookmark()

        data = {'url': 'https://example.com/', 'unread': True}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.put(url, data, expected_status_code=status.HTTP_200_OK)
        updated_bookmark = Bookmark.objects.get(id=bookmark.id)
        self.assertEqual(updated_bookmark.unread, True)

    def test_update_bookmark_shared_flag(self):
        self.authenticate()
        bookmark = self.setup_bookmark()

        data = {'url': 'https://example.com/', 'shared': True}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.put(url, data, expected_status_code=status.HTTP_200_OK)
        updated_bookmark = Bookmark.objects.get(id=bookmark.id)
        self.assertEqual(updated_bookmark.shared, True)

    def test_patch_bookmark(self):
        self.authenticate()
        bookmark = self.setup_bookmark()

        data = {'url': 'https://example.com'}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.patch(url, data, expected_status_code=status.HTTP_200_OK)
        bookmark.refresh_from_db()
        self.assertEqual(bookmark.url, data['url'])

        data = {'title': 'Updated title'}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.patch(url, data, expected_status_code=status.HTTP_200_OK)
        bookmark.refresh_from_db()
        self.assertEqual(bookmark.title, data['title'])

        data = {'description': 'Updated description'}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.patch(url, data, expected_status_code=status.HTTP_200_OK)
        bookmark.refresh_from_db()
        self.assertEqual(bookmark.description, data['description'])

        data = {'notes': 'Updated notes'}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.patch(url, data, expected_status_code=status.HTTP_200_OK)
        bookmark.refresh_from_db()
        self.assertEqual(bookmark.notes, data['notes'])

        data = {'unread': True}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.patch(url, data, expected_status_code=status.HTTP_200_OK)
        bookmark.refresh_from_db()
        self.assertTrue(bookmark.unread)

        data = {'unread': False}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.patch(url, data, expected_status_code=status.HTTP_200_OK)
        bookmark.refresh_from_db()
        self.assertFalse(bookmark.unread)

        data = {'shared': True}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.patch(url, data, expected_status_code=status.HTTP_200_OK)
        bookmark.refresh_from_db()
        self.assertTrue(bookmark.shared)

        data = {'shared': False}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.patch(url, data, expected_status_code=status.HTTP_200_OK)
        bookmark.refresh_from_db()
        self.assertFalse(bookmark.shared)

        data = {'tag_names': ['updated-tag-1', 'updated-tag-2']}
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.patch(url, data, expected_status_code=status.HTTP_200_OK)
        bookmark.refresh_from_db()
        tag_names = [tag.name for tag in bookmark.tags.all()]
        self.assertListEqual(tag_names, ['updated-tag-1', 'updated-tag-2'])

    def test_patch_with_empty_payload_does_not_modify_bookmark(self):
        self.authenticate()
        bookmark = self.setup_bookmark()

        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.patch(url, {}, expected_status_code=status.HTTP_200_OK)
        updated_bookmark = Bookmark.objects.get(id=bookmark.id)
        self.assertEqual(updated_bookmark.url, bookmark.url)
        self.assertEqual(updated_bookmark.title, bookmark.title)
        self.assertEqual(updated_bookmark.description, bookmark.description)
        self.assertListEqual(updated_bookmark.tag_names, bookmark.tag_names)

    def test_delete_bookmark(self):
        self.authenticate()
        bookmark = self.setup_bookmark()

        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
        self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT)
        self.assertEqual(len(Bookmark.objects.filter(id=bookmark.id)), 0)

    def test_archive(self):
        self.authenticate()
        bookmark = self.setup_bookmark()

        url = reverse('bookmarks:bookmark-archive', args=[bookmark.id])
        self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
        bookmark = Bookmark.objects.get(id=bookmark.id)
        self.assertTrue(bookmark.is_archived)

    def test_unarchive(self):
        self.authenticate()
        bookmark = self.setup_bookmark(is_archived=True)

        url = reverse('bookmarks:bookmark-unarchive', args=[bookmark.id])
        self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
        bookmark = Bookmark.objects.get(id=bookmark.id)
        self.assertFalse(bookmark.is_archived)

    def test_check_returns_no_bookmark_if_url_is_not_bookmarked(self):
        self.authenticate()

        url = reverse('bookmarks:bookmark-check')
        check_url = urllib.parse.quote_plus('https://example.com')
        response = self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK)
        bookmark_data = response.data['bookmark']

        self.assertIsNone(bookmark_data)

    def test_check_returns_scraped_metadata_if_url_is_not_bookmarked(self):
        self.authenticate()

        with patch.object(website_loader, 'load_website_metadata') as mock_load_website_metadata:
            expected_metadata = WebsiteMetadata(
                'https://example.com',
                'Scraped metadata',
                'Scraped description'
            )
            mock_load_website_metadata.return_value = expected_metadata

            url = reverse('bookmarks:bookmark-check')
            check_url = urllib.parse.quote_plus('https://example.com')
            response = self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK)
            metadata = response.data['metadata']

            self.assertIsNotNone(metadata)
            self.assertIsNotNone(expected_metadata.url, metadata['url'])
            self.assertIsNotNone(expected_metadata.title, metadata['title'])
            self.assertIsNotNone(expected_metadata.description, metadata['description'])

    def test_check_returns_bookmark_if_url_is_bookmarked(self):
        self.authenticate()

        bookmark = self.setup_bookmark(url='https://example.com',
                                       title='Example title',
                                       description='Example description')

        url = reverse('bookmarks:bookmark-check')
        check_url = urllib.parse.quote_plus('https://example.com')
        response = self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK)
        bookmark_data = response.data['bookmark']

        self.assertIsNotNone(bookmark_data)
        self.assertEqual(bookmark.id, bookmark_data['id'])
        self.assertEqual(bookmark.url, bookmark_data['url'])
        self.assertEqual(bookmark.title, bookmark_data['title'])
        self.assertEqual(bookmark.description, bookmark_data['description'])

    def test_check_returns_existing_metadata_if_url_is_bookmarked(self):
        self.authenticate()

        bookmark = self.setup_bookmark(url='https://example.com',
                                       website_title='Existing title',
                                       website_description='Existing description')

        with patch.object(website_loader, 'load_website_metadata') as mock_load_website_metadata:
            url = reverse('bookmarks:bookmark-check')
            check_url = urllib.parse.quote_plus('https://example.com')
            response = self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK)
            metadata = response.data['metadata']

            mock_load_website_metadata.assert_not_called()
            self.assertIsNotNone(metadata)
            self.assertIsNotNone(bookmark.url, metadata['url'])
            self.assertIsNotNone(bookmark.website_title, metadata['title'])
            self.assertIsNotNone(bookmark.website_description, metadata['description'])

    def test_can_only_access_own_bookmarks(self):
        self.authenticate()
        self.setup_bookmark()
        self.setup_bookmark(is_archived=True)

        other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
        inaccessible_bookmark = self.setup_bookmark(user=other_user)
        inaccessible_shared_bookmark = self.setup_bookmark(user=other_user, shared=True)
        self.setup_bookmark(user=other_user, is_archived=True)

        url = reverse('bookmarks:bookmark-list')
        response = self.get(url, expected_status_code=status.HTTP_200_OK)
        self.assertEqual(len(response.data['results']), 1)

        url = reverse('bookmarks:bookmark-archived')
        response = self.get(url, expected_status_code=status.HTTP_200_OK)
        self.assertEqual(len(response.data['results']), 1)

        url = reverse('bookmarks:bookmark-detail', args=[inaccessible_bookmark.id])
        self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND)

        url = reverse('bookmarks:bookmark-detail', args=[inaccessible_shared_bookmark.id])
        self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND)

        url = reverse('bookmarks:bookmark-detail', args=[inaccessible_bookmark.id])
        self.put(url, {url: 'https://example.com/'}, expected_status_code=status.HTTP_404_NOT_FOUND)

        url = reverse('bookmarks:bookmark-detail', args=[inaccessible_shared_bookmark.id])
        self.put(url, {url: 'https://example.com/'}, expected_status_code=status.HTTP_404_NOT_FOUND)

        url = reverse('bookmarks:bookmark-detail', args=[inaccessible_bookmark.id])
        self.delete(url, expected_status_code=status.HTTP_404_NOT_FOUND)

        url = reverse('bookmarks:bookmark-detail', args=[inaccessible_shared_bookmark.id])
        self.delete(url, expected_status_code=status.HTTP_404_NOT_FOUND)

        url = reverse('bookmarks:bookmark-archive', args=[inaccessible_bookmark.id])
        self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)

        url = reverse('bookmarks:bookmark-archive', args=[inaccessible_shared_bookmark.id])
        self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)

        url = reverse('bookmarks:bookmark-unarchive', args=[inaccessible_bookmark.id])
        self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)

        url = reverse('bookmarks:bookmark-unarchive', args=[inaccessible_shared_bookmark.id])
        self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)

        url = reverse('bookmarks:bookmark-check')
        check_url = urllib.parse.quote_plus(inaccessible_bookmark.url)
        response = self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK)
        self.assertIsNone(response.data['bookmark'])

    def assertUserProfile(self, response: Response, profile: UserProfile):
        self.assertEqual(response.data['theme'], profile.theme)
        self.assertEqual(response.data['bookmark_date_display'], profile.bookmark_date_display)
        self.assertEqual(response.data['bookmark_link_target'], profile.bookmark_link_target)
        self.assertEqual(response.data['web_archive_integration'], profile.web_archive_integration)
        self.assertEqual(response.data['tag_search'], profile.tag_search)
        self.assertEqual(response.data['enable_sharing'], profile.enable_sharing)
        self.assertEqual(response.data['enable_public_sharing'], profile.enable_public_sharing)
        self.assertEqual(response.data['enable_favicons'], profile.enable_favicons)
        self.assertEqual(response.data['display_url'], profile.display_url)
        self.assertEqual(response.data['permanent_notes'], profile.permanent_notes)
        self.assertEqual(response.data['search_preferences'], profile.search_preferences)

    def test_user_profile(self):
        self.authenticate()

        # default profile
        profile = self.user.profile
        url = reverse('bookmarks:user-profile')
        response = self.get(url, expected_status_code=status.HTTP_200_OK)

        self.assertUserProfile(response, profile)

        # update profile
        profile.theme = 'dark'
        profile.bookmark_date_display = 'absolute'
        profile.bookmark_link_target = '_self'
        profile.web_archive_integration = 'enabled'
        profile.tag_search = 'lax'
        profile.enable_sharing = True
        profile.enable_public_sharing = True
        profile.enable_favicons = True
        profile.display_url = True
        profile.permanent_notes = True
        profile.search_preferences = {
            'sort': BookmarkSearch.SORT_TITLE_ASC,
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
        }
        profile.save()

        url = reverse('bookmarks:user-profile')
        response = self.get(url, expected_status_code=status.HTTP_200_OK)

        self.assertUserProfile(response, profile)
