import datetime

from io import BytesIO
from unittest import mock

from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.core import checks
from django.test import TestCase
from django.utils.timezone import make_aware
from openpyxl import load_workbook

from wagtail.admin.admin_url_finder import AdminURLFinder
from wagtail.contrib.images.models import Image
from wagtail.contrib.images.tests.utils import get_test_image_file
from wagtail.contrib.modeladmin.helpers.search import DjangoORMSearchHandler
from wagtail.edit_handlers import FieldPanel, TabbedInterface
from wagtail.models import ModelLogEntry, Page
from wagtail.test.modeladmintest.models import Author, Book, Publisher, RelatedLink, Token
from wagtail.test.modeladmintest.wagtail_hooks import BookModelAdmin
from wagtail.test.utils import WagtailTestUtils


class TestBookIndexView(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']

    def setUp(self):
        self.login()

        img = Image.objects.create(
            title="LOTR cover",
            file=get_test_image_file(),
        )
        book = Book.objects.get(title="The Lord of the Rings")
        book.cover_image = img
        book.save()

    def get(self, **params):
        return self.client.get('/admin/modeladmintest/book/', params)

    def test_simple(self):
        response = self.get()

        self.assertEqual(response.status_code, 200)

        # There are four books in the test data
        self.assertEqual(response.context['result_count'], 4)

        # The result count content is shown in the header
        self.assertContains(response, '<span class="result-count">4 out of 4</span>', html=True)

        # User has add permission
        self.assertEqual(response.context['user_can_create'], True)

    def test_csv_export(self):
        # Export the whole queryset
        response = self.get(export='csv')

        self.assertEqual(response.status_code, 200)
        # Check attachment is present and named correctly using the modeladmin export_filename
        self.assertEqual(response.get('content-disposition'), 'attachment; filename="books-export.csv"')

        # Check response - all books should be in it
        data_lines = response.getvalue().decode().split("\n")
        self.assertEqual(data_lines[0], 'Title,Author,Author Date Of Birth\r')
        self.assertEqual(data_lines[1], 'Charlie and the Chocolate Factory,Roald Dahl,1916-09-13\r')
        self.assertEqual(data_lines[2], 'The Chronicles of Narnia,Roald Dahl,1898-11-29\r')
        self.assertEqual(data_lines[3], 'The Hobbit,J. R. R. Tolkien,1892-01-03\r')
        self.assertEqual(data_lines[4], 'The Lord of the Rings,J. R. R. Tolkien,1892-01-03\r')

    def test_xlsx_export(self):
        # Export the whole queryset
        response = self.get(export='xlsx')

        self.assertEqual(response.status_code, 200)
        # Check attachment is present and named correctly using the modeladmin export_filename
        self.assertEqual(response.get('content-disposition'), 'attachment; filename="books-export.xlsx"')

        # Check response - all books should be in it
        workbook_data = response.getvalue()
        worksheet = load_workbook(filename=BytesIO(workbook_data))['Sheet1']
        cell_array = [[cell.value for cell in row] for row in worksheet.rows]
        self.assertEqual(cell_array[0], ['Title', 'Author', 'Author Date Of Birth'])
        self.assertEqual(cell_array[1], ['Charlie and the Chocolate Factory', 'Roald Dahl', '1916-09-13'])
        self.assertEqual(cell_array[2], ['The Chronicles of Narnia', 'Roald Dahl', '1898-11-29'])
        self.assertEqual(cell_array[3], ['The Hobbit', 'J. R. R. Tolkien', '1892-01-03'])
        self.assertEqual(cell_array[4], ['The Lord of the Rings', 'J. R. R. Tolkien', '1892-01-03'])
        self.assertEqual(len(cell_array), 5)

    def test_tr_attributes(self):
        response = self.get()

        # Charlie & The Chocolate factory should be in the list with the
        # `data-author_yob` and `data-object_pk` attributes added
        self.assertContains(response, 'data-author-yob="1916"')
        self.assertContains(response, 'data-object-pk="3"')

        # There should be two odd rows and two even ones, and 'book' should be
        # added to the `class` attribute for every one.
        self.assertContains(response, 'class="book odd"', count=2)
        self.assertContains(response, 'class="book even"', count=2)

    def test_filter(self):
        # Filter by author 1 (JRR Tolkien)
        response = self.get(author__id__exact=1)

        self.assertEqual(response.status_code, 200)

        # JRR Tolkien has two books in the test data
        self.assertEqual(response.context['result_count'], 2)

        # The result count content is shown in the header
        self.assertContains(response, '<span class="result-count">2 out of 4</span>', html=True)

        for book in response.context['object_list']:
            self.assertEqual(book.author_id, 1)

    def test_filtered_csv_export(self):
        # Filter by author 1 (JRR Tolkien) and export the current selection
        response = self.get(author__id__exact=1, export='csv')

        # Check response - only books by JRR Tolkien should be in it
        self.assertEqual(response.status_code, 200)
        data_lines = response.getvalue().decode().split("\n")
        self.assertEqual(data_lines[0], 'Title,Author,Author Date Of Birth\r')
        self.assertEqual(data_lines[1], 'The Hobbit,J. R. R. Tolkien,1892-01-03\r')
        self.assertEqual(data_lines[2], 'The Lord of the Rings,J. R. R. Tolkien,1892-01-03\r')
        self.assertEqual(data_lines[3], '')

    def test_search_indexed(self):
        response = self.get(q='of')

        self.assertEqual(response.status_code, 200)

        # There are two books where the title contains 'of'
        self.assertEqual(response.context['result_count'], 2)

        # The result count content is shown in the header
        self.assertContains(response, '<span class="result-count">2 out of 4</span>', html=True)

    def test_search_form_present(self):
        # Test the backend search handler allows the search form to render
        response = self.get()

        self.assertContains(response, '<input id="id_q"')

    def test_search_form_absent(self):
        # DjangoORMSearchHandler + no search_fields, search form should be absent
        with mock.patch.object(BookModelAdmin, 'search_handler_class', DjangoORMSearchHandler):
            response = self.get()

            self.assertNotContains(response, '<input id="id_q"')

    def test_ordering(self):
        response = self.get(o='0.1')

        self.assertEqual(response.status_code, 200)

        # There are four books in the test data
        self.assertEqual(response.context['result_count'], 4)

    def test_paging(self):
        # should be corrected to just the first page, as there aren't enough
        # objects to make up more than one page
        response = self.get(p=9)

        self.assertEqual(response.status_code, 200)

        # There are four books in the test data
        self.assertEqual(response.context['result_count'], 4)

        # Should raise a ValueError that gets caught during initialisation
        response = self.get(p='notaninteger')

        self.assertEqual(response.status_code, 200)

        # There are four books in the test data
        self.assertEqual(response.context['result_count'], 4)


class TestAuthorIndexView(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']

    def setUp(self):
        self.login()

    def get(self, **params):
        return self.client.get('/admin/modeladmintest/author/', params)

    def test_search(self):
        response = self.get(q='Roald Dahl')

        self.assertEqual(response.status_code, 200)

        self.assertEqual(response.context['result_count'], 2)

        # The result count content is shown in the header
        self.assertContains(response, '<span class="result-count">2 out of 5</span>', html=True)

    def test_col_extra_class_names(self):
        response = self.get()
        self.assertEqual(response.status_code, 200)
        test_html = """
            <td class="field-first_book for-author-1 title">The Lord of the Rings</td>
        """
        self.assertContains(response, test_html, html=True)

    def test_col_extra_attributes(self):
        response = self.get()
        self.assertEqual(response.status_code, 200)
        test_html = """
            <td class="field-last_book title" data-for_author="1">The Hobbit</td>
        """
        self.assertContains(response, test_html, html=True)


class TestCreateView(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']

    def setUp(self):
        self.login()

    def get(self):
        return self.client.get('/admin/modeladmintest/book/create/')

    def post(self, post_data):
        return self.client.post('/admin/modeladmintest/book/create/', post_data)

    def test_simple(self):
        response = self.get()

        self.assertEqual(response.status_code, 200)

    def test_create(self):
        response = self.post({
            'title': "George's Marvellous Medicine",
            'author': 2,
        })
        # Should redirect back to index
        self.assertRedirects(response, '/admin/modeladmintest/book/')

        # Check that the book was created
        self.assertEqual(Book.objects.filter(title="George's Marvellous Medicine").count(), 1)

        response = self.client.get('/admin/modeladmintest/publisher/create/')
        self.assertIn('name', response.content.decode('UTF-8'))
        self.assertNotIn('headquartered_in', response.content.decode('UTF-8'))
        self.assertEqual(
            [ii for ii in response.context['form'].fields],
            ['name']
        )
        self.client.post('/admin/modeladmintest/publisher/create/', {
            'name': 'Sharper Collins'
        })
        publisher = Publisher.objects.get(name='Sharper Collins')
        self.assertEqual(publisher.headquartered_in, None)

    def test_post_invalid(self):
        initial_book_count = Book.objects.count()

        response = self.post({
            'title': '',
            'author': 2,
        })
        final_book_count = Book.objects.count()

        self.assertEqual(response.status_code, 200)
        # Check that the book was not created
        self.assertEqual(initial_book_count, final_book_count)

        # Check that a form error was raised
        self.assertFormError(response, 'form', 'title', "This field is required.")
        self.assertContains(response, """<p class="error-message"><span>This field is required.</span></p>""",
                            count=1, html=True)

    def test_exclude_passed_to_extract_panel_definitions(self):
        path_to_form_fields_exclude_property = 'wagtail.contrib.modeladmin.options.ModelAdmin.form_fields_exclude'
        with mock.patch('wagtail.contrib.modeladmin.options.extract_panel_definitions_from_model_class') as m:
            with mock.patch(path_to_form_fields_exclude_property, new_callable=mock.PropertyMock) as mock_form_fields_exclude:
                mock_form_fields_exclude.return_value = ['123']

                self.get()
                self.assertTrue(mock_form_fields_exclude.called)
                m.assert_called_with(Book, exclude=mock_form_fields_exclude.return_value)

    def test_clean_form_once(self):
        with mock.patch('wagtail.test.modeladmintest.wagtail_hooks.PublisherModelAdminForm.clean') as mock_form_clean:
            response = self.client.post('/admin/modeladmintest/publisher/create/', {'name': ''})
            self.assertEqual(response.status_code, 200)

            mock_form_clean.assert_called_once()

    def test_create_view_with_multifieldpanel(self):
        # https://github.com/wagtail/wagtail/issues/6413
        response = self.client.get('/admin/modeladmintest/relatedlink/create/')
        self.assertEqual(response.status_code, 200)

        response = self.client.post('/admin/modeladmintest/relatedlink/create/', {
            'title': "Homepage",
            'link': Page.objects.filter(depth=2).first().id,
        })
        # Should redirect back to index
        self.assertRedirects(response, '/admin/modeladmintest/relatedlink/')

        # Check that the link was created
        self.assertEqual(RelatedLink.objects.filter(title="Homepage").count(), 1)

    def test_prepopulated_field_data_in_context(self):
        response = self.get()
        self.assertIn('data-prepopulated-fields="[{&quot;id&quot;: &quot;#id_title&quot;, &quot;name&quot;: &quot;title&quot;, &quot;dependency_ids&quot;: [&quot;#id_author&quot;], &quot;dependency_list&quot;: [&quot;author&quot;], &quot;maxLength&quot;: 255, &quot;allowUnicode&quot;: false}]"', response.content.decode('UTF-8'))


class TestInspectView(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']

    def setUp(self):
        self.login()

        img = Image.objects.create(
            title="LOTR cover",
            file=get_test_image_file(),
        )
        book = Book.objects.get(title="The Lord of the Rings")
        book.cover_image = img
        book.save()

    def get_for_author(self, author_id):
        return self.client.get('/admin/modeladmintest/author/inspect/%d/' % author_id)

    def get_for_book(self, book_id):
        return self.client.get('/admin/modeladmintest/book/inspect/%d/' % book_id)

    def test_author_simple(self):
        response = self.get_for_author(1)
        self.assertEqual(response.status_code, 200)

    def test_author_name_present(self):
        """
        The author name should appear twice. Once in the header, and once
        more in the field listing
        """
        response = self.get_for_author(1)
        self.assertContains(response, 'J. R. R. Tolkien', 2)

    def test_author_dob_not_present(self):
        """
        The date of birth shouldn't appear, because the field wasn't included
        in the `inspect_view_fields` list
        """
        response = self.get_for_author(1)
        self.assertNotContains(response, '1892')

    def test_book_simple(self):
        response = self.get_for_book(1)
        self.assertEqual(response.status_code, 200)

    def test_book_title_present(self):
        """
        The book title should appear once only, in the header, as 'title'
        was added to the `inspect_view_fields_ignore` list
        """
        response = self.get_for_book(1)
        self.assertContains(response, 'The Lord of the Rings', 1)

    def test_book_author_present(self):
        """
        The author name should appear, because 'author' is not in
        `inspect_view_fields_ignore` and should be returned by the
        `get_inspect_view_fields` method.
        """
        response = self.get_for_book(1)
        self.assertContains(response, 'J. R. R. Tolkien', 1)

    def test_non_existent(self):
        response = self.get_for_book(100)
        self.assertEqual(response.status_code, 404)


class TestEditView(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']

    def setUp(self):
        self.user = self.login()
        ModelLogEntry.objects.create(
            content_type=ContentType.objects.get_for_model(Book),
            label="The Lord of the Rings",
            action='wagtail.create',
            timestamp=make_aware(datetime.datetime(2021, 9, 30, 10, 1, 0)),
            object_id='1',
        )

    def get(self, book_id):
        return self.client.get('/admin/modeladmintest/book/edit/%d/' % book_id)

    def post(self, book_id, post_data):
        return self.client.post('/admin/modeladmintest/book/edit/%d/' % book_id, post_data)

    def test_simple(self):
        response = self.get(1)

        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'The Lord of the Rings')

        # "Last updated" timestamp should be present
        self.assertContains(response, 'data-wagtail-tooltip="Sept. 30, 2021, 10:01 a.m."')
        # History link should be present
        self.assertContains(response, 'href="/admin/modeladmintest/book/history/1/"')

        url_finder = AdminURLFinder(self.user)
        expected_url = '/admin/modeladmintest/book/edit/1/'
        self.assertEqual(url_finder.get_edit_url(Book.objects.get(id=1)), expected_url)

    def test_non_existent(self):
        response = self.get(100)

        self.assertEqual(response.status_code, 404)

    def test_edit(self):
        response = self.post(1, {
            'title': 'The Lady of the Rings',
            'author': 1,
        })

        # Should redirect back to index
        self.assertRedirects(response, '/admin/modeladmintest/book/')

        # Check that the book was updated
        self.assertEqual(Book.objects.get(id=1).title, 'The Lady of the Rings')

    def test_post_invalid(self):
        response = self.post(1, {
            'title': '',
            'author': 1,
        })

        self.assertEqual(response.status_code, 200)

        # Check that the title was not updated
        self.assertEqual(Book.objects.get(id=1).title, 'The Lord of the Rings')

        # Check that a form error was raised
        self.assertFormError(response, 'form', 'title', "This field is required.")
        self.assertContains(response, """<p class="error-message"><span>This field is required.</span></p>""",
                            count=1, html=True)

    def test_exclude_passed_to_extract_panel_definitions(self):
        path_to_form_fields_exclude_property = 'wagtail.contrib.modeladmin.options.ModelAdmin.form_fields_exclude'
        with mock.patch('wagtail.contrib.modeladmin.options.extract_panel_definitions_from_model_class') as m:
            with mock.patch(path_to_form_fields_exclude_property, new_callable=mock.PropertyMock) as mock_form_fields_exclude:
                mock_form_fields_exclude.return_value = ['123']

                self.get(1)
                self.assertTrue(mock_form_fields_exclude.called)
                m.assert_called_with(Book, exclude=mock_form_fields_exclude.return_value)

    def test_clean_form_once(self):
        with mock.patch('wagtail.test.modeladmintest.wagtail_hooks.PublisherModelAdminForm.clean') as mock_form_clean:
            publisher = Publisher.objects.create(name='Sharper Collins')

            response = self.client.post('/admin/modeladmintest/publisher/edit/%d/' % publisher.pk, {'name': ''})
            self.assertEqual(response.status_code, 200)

            mock_form_clean.assert_called_once()

    def test_edit_view_with_multifieldpanel(self):
        # https://github.com/wagtail/wagtail/issues/6413
        link = RelatedLink.objects.create(title='Homepage', link=Page.objects.filter(depth=2).first())
        response = self.client.get('/admin/modeladmintest/relatedlink/edit/%d/' % link.id)
        self.assertEqual(response.status_code, 200)

        response = self.client.post('/admin/modeladmintest/relatedlink/edit/%d/' % link.id, {
            'title': "Homepage edited",
            'link': Page.objects.filter(depth=2).first().id,
        })
        # Should redirect back to index
        self.assertRedirects(response, '/admin/modeladmintest/relatedlink/')

        # Check that the link was updated
        self.assertEqual(RelatedLink.objects.filter(title="Homepage edited").count(), 1)


class TestPageSpecificViews(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']
    expected_status_code = 404

    def setUp(self):
        self.login()

    def test_choose_parent(self):
        response = self.client.get('/admin/modeladmintest/book/choose_parent/')
        self.assertEqual(response.status_code, self.expected_status_code)


class TestConfirmDeleteView(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']

    def setUp(self):
        self.login()

    def get(self, book_id):
        return self.client.get('/admin/modeladmintest/book/delete/%d/' % book_id)

    def post(self, book_id):
        return self.client.post('/admin/modeladmintest/book/delete/%d/' % book_id)

    def test_simple(self):
        response = self.get(1)

        self.assertEqual(response.status_code, 200)

    def test_non_existent(self):
        response = self.get(100)

        self.assertEqual(response.status_code, 404)

    def test_post(self):
        response = self.post(1)

        # User redirected to index
        self.assertRedirects(response, '/admin/modeladmintest/book/')

        # Book deleted
        self.assertFalse(Book.objects.filter(id=1).exists())


class TestDeleteViewWithProtectedRelation(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']

    def setUp(self):
        self.login()

    def get(self, author_id):
        return self.client.get('/admin/modeladmintest/author/delete/%d/' % author_id)

    def post(self, author_id):
        return self.client.post('/admin/modeladmintest/author/delete/%d/' % author_id)

    def test_get_with_dependent_object(self):
        response = self.get(1)

        self.assertEqual(response.status_code, 200)

    def test_get_without_dependent_object(self):
        response = self.get(4)

        self.assertEqual(response.status_code, 200)

    def test_post_with_dependent_object(self):
        response = self.post(1)

        self.assertEqual(response.status_code, 200)
        self.assertContains(
            response,
            "'J. R. R. Tolkien' is currently referenced by other objects"
        )
        self.assertContains(
            response,
            "<li><b>Book:</b> The Lord of the Rings</li>"
        )

        # Author not deleted
        self.assertTrue(Author.objects.filter(id=1).exists())

    def test_post_without_dependent_object(self):
        response = self.post(4)

        # User redirected to index
        self.assertRedirects(response, '/admin/modeladmintest/author/')

        # Author deleted
        self.assertFalse(Author.objects.filter(id=4).exists())

    def test_post_with_1to1_dependent_object(self):
        response = self.post(5)

        self.assertEqual(response.status_code, 200)
        self.assertContains(
            response,
            "'Harper Lee' is currently referenced by other objects"
        )
        self.assertContains(
            response,
            "<li><b>Solo Book:</b> To Kill a Mockingbird</li>"
        )

        # Author not deleted
        self.assertTrue(Author.objects.filter(id=5).exists())


class TestDeleteViewModelReprPrimary(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']

    def setUp(self):
        self.login()

    def test_delete(self):
        response = self.client.post('/admin/modeladmintest/token/delete/boom/')
        self.assertEqual(response.status_code, 302)


class TestEditorAccess(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']

    def setUp(self):
        # Create a user
        user = self.create_user(username='test2', password='password')
        user.groups.add(Group.objects.get(pk=2))
        self.user = user
        # Login
        self.login(username='test2', password='password')

        return user

    def test_index_permitted(self):
        response = self.client.get('/admin/modeladmintest/book/')
        self.assertRedirects(response, '/admin/')

    def test_inpspect_permitted(self):
        response = self.client.get('/admin/modeladmintest/book/inspect/2/')
        self.assertRedirects(response, '/admin/')

    def test_create_permitted(self):
        response = self.client.get('/admin/modeladmintest/book/create/')
        self.assertRedirects(response, '/admin/')

    def test_edit_permitted(self):
        response = self.client.get('/admin/modeladmintest/book/edit/2/')
        self.assertRedirects(response, '/admin/')

        url_finder = AdminURLFinder(self.user)
        self.assertEqual(url_finder.get_edit_url(Book.objects.get(id=2)), None)

    def test_delete_get_permitted(self):
        response = self.client.get('/admin/modeladmintest/book/delete/2/')
        self.assertRedirects(response, '/admin/')

    def test_delete_post_permitted(self):
        response = self.client.post('/admin/modeladmintest/book/delete/2/')
        self.assertRedirects(response, '/admin/')


class TestHistoryView(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']

    def setUp(self):
        self.login()
        ModelLogEntry.objects.create(
            content_type=ContentType.objects.get_for_model(Book),
            label="The Lord of the Rings",
            action='wagtail.create',
            timestamp=make_aware(datetime.datetime(2021, 9, 30, 10, 1, 0)),
            object_id='1',
        )

    def test_simple(self):
        response = self.client.get('/admin/modeladmintest/book/history/1/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, '<td>Created</td>', html=True)
        self.assertContains(response, '<div class="human-readable-date" title="Sept. 30, 2021, 10:01 a.m.">')


class TestQuoting(TestCase, WagtailTestUtils):
    fixtures = ['modeladmintest_test.json']
    expected_status_code = 200

    def setUp(self):
        self.login()
        self.tok_reg = Token.objects.create(key="RegularName")
        self.tok_irr = Token.objects.create(key="Irregular_Name")

    def test_action_links(self):
        response = self.client.get('/admin/modeladmintest/token/')

        self.assertContains(response, 'href="/admin/modeladmintest/token/edit/RegularName/"')
        self.assertContains(response, 'href="/admin/modeladmintest/token/delete/RegularName/"')
        self.assertContains(response, 'href="/admin/modeladmintest/token/edit/Irregular_5FName/"')
        self.assertContains(response, 'href="/admin/modeladmintest/token/delete/Irregular_5FName/"')

        response = self.client.get('/admin/modeladmintest/token/edit/Irregular_5FName/')
        self.assertEqual(response.status_code, 200)
        response = self.client.get('/admin/modeladmintest/token/delete/Irregular_5FName/')
        self.assertEqual(response.status_code, 200)


class TestHeaderBreadcrumbs(TestCase, WagtailTestUtils):
    """
        Test that the <ul class="breadcrumbs">... is inserted within the
        <header> tag for potential future regression.
        See https://github.com/wagtail/wagtail/issues/3889
    """
    fixtures = ['modeladmintest_test.json']

    def setUp(self):
        self.login()

    def test_choose_inspect_model(self):
        response = self.client.get('/admin/modeladmintest/author/inspect/1/')

        # check correct templates were used
        self.assertTemplateUsed(response, 'modeladmin/includes/breadcrumb.html')
        self.assertTemplateUsed(response, 'wagtailadmin/shared/header.html')

        # check that home breadcrumb link exists
        expected = """
            <li class="home">
                <a href="/admin/">
                    <svg class="icon icon-home home_icon" aria-hidden="true" focusable="false">
                        <use href="#icon-home"></use>
                    </svg>
                    <span class="visuallyhidden">Home</span>
                    <svg class="icon icon-arrow-right arrow_right_icon" aria-hidden="true" focusable="false">
                        <use href="#icon-arrow-right"></use>
                    </svg>
                </a>
            </li>
        """
        self.assertContains(response, expected, html=True)

        # check that the breadcrumbs are before the first header closing tag
        content_str = str(response.content)
        position_of_header_close = content_str.index('</header>')
        position_of_breadcrumbs = content_str.index('<ul class="breadcrumb">')
        self.assertGreater(position_of_header_close, position_of_breadcrumbs)


class TestPanelConfigurationChecks(TestCase, WagtailTestUtils):

    def setUp(self):
        self.warning_id = 'wagtailadmin.W002'

        def get_checks_result():
            # run checks only with the 'panels' tag
            checks_result = checks.run_checks(tags=['panels'])
            return [
                warning for warning in
                checks_result if warning.id == self.warning_id]

        self.get_checks_result = get_checks_result

    def test_model_with_single_tabbed_panel_only(self):
        Publisher.content_panels = [FieldPanel('name'), FieldPanel('headquartered_in')]

        warning = checks.Warning(
            "Publisher.content_panels will have no effect on modeladmin editing",
            hint="""Ensure that Publisher uses `panels` instead of `content_panels`\
or set up an `edit_handler` if you want a tabbed editing interface.
There are no default tabs on non-Page models so there will be no\
 Content tab for the content_panels to render in.""",
            obj=Publisher,
            id='wagtailadmin.W002',
        )

        checks_results = self.get_checks_result()

        self.assertIn(warning, checks_results)

        # clean up for future checks
        delattr(Publisher, 'content_panels')

    def test_model_with_two_tabbed_panels_only(self):
        Publisher.settings_panels = [FieldPanel('name')]
        Publisher.promote_panels = [FieldPanel('headquartered_in')]

        warning_1 = checks.Warning(
            "Publisher.promote_panels will have no effect on modeladmin editing",
            hint="""Ensure that Publisher uses `panels` instead of `promote_panels`\
or set up an `edit_handler` if you want a tabbed editing interface.
There are no default tabs on non-Page models so there will be no\
 Promote tab for the promote_panels to render in.""",
            obj=Publisher,
            id='wagtailadmin.W002',
        )

        warning_2 = checks.Warning(
            "Publisher.settings_panels will have no effect on modeladmin editing",
            hint="""Ensure that Publisher uses `panels` instead of `settings_panels`\
or set up an `edit_handler` if you want a tabbed editing interface.
There are no default tabs on non-Page models so there will be no\
 Settings tab for the settings_panels to render in.""",
            obj=Publisher,
            id='wagtailadmin.W002',
        )

        checks_results = self.get_checks_result()

        self.assertIn(warning_1, checks_results)
        self.assertIn(warning_2, checks_results)

        # clean up for future checks
        delattr(Publisher, 'settings_panels')
        delattr(Publisher, 'promote_panels')

    def test_model_with_single_tabbed_panel_and_edit_handler(self):
        Publisher.content_panels = [FieldPanel('name'), FieldPanel('headquartered_in')]
        Publisher.edit_handler = TabbedInterface(Publisher.content_panels)

        # no errors should occur
        self.assertEqual(self.get_checks_result(), [])

        # clean up for future checks
        delattr(Publisher, 'content_panels')
        delattr(Publisher, 'edit_handler')
