import json
import unittest

from django.test import SimpleTestCase, TestCase
from django.urls import reverse
from django.utils import translation

from wagtail.blocks.field_block import FieldBlockAdapter
from wagtail.contrib.table_block.blocks import DEFAULT_TABLE_OPTIONS, TableBlock
from wagtail.models import Page
from wagtail.test.testapp.models import TableBlockStreamPage
from wagtail.test.utils import WagtailTestUtils

from .blocks import TableInput


class TestTableBlock(TestCase):
    def setUp(self):
        self.default_table_options = {
            "minSpareRows": 0,
            "startRows": 3,
            "startCols": 3,
            "colHeaders": False,
            "rowHeaders": False,
            "contextMenu": True,
            "editor": "text",
            "stretchH": "all",
            "height": 108,
            "language": "en",
            "renderer": "text",
            "autoColumnSize": False,
        }

    def test_table_block_render(self):
        """
        Test a generic render.
        """
        value = {
            "first_row_is_table_header": False,
            "first_col_is_header": False,
            "data": [
                ["Test 1", "Test 2", "Test 3"],
                [None, None, None],
                [None, None, None],
            ],
        }
        block = TableBlock()
        result = block.render(value)
        expected = """
            <table>
                <tbody>
                    <tr><td>Test 1</td><td>Test 2</td><td>Test 3</td></tr>
                    <tr><td></td><td></td><td></td></tr>
                    <tr><td></td><td></td><td></td></tr>
                </tbody>
            </table>
        """

        self.assertHTMLEqual(result, expected)
        self.assertIn("Test 2", result)

    def test_table_block_alignment_render(self):
        """
        Test a generic render with some cells aligned.
        """
        value = {
            "first_row_is_table_header": True,
            "first_col_is_header": False,
            "cell": [
                {"row": 0, "col": 1, "className": "htLeft"},
                {"row": 1, "col": 1, "className": "htRight"},
            ],
            "data": [
                ["Test 1", "Test 2", "Test 3"],
                [None, None, None],
                [None, None, None],
            ],
        }
        block = TableBlock()
        result = block.render(value)
        expected = """
            <table>
                <thead>
                    <tr><th scope="col">Test 1</th><th scope="col" class="htLeft">Test 2</th><th scope="col">Test 3</th></tr>
                </thead>
                <tbody>
                    <tr><td></td><td class="htRight"></td><td></td></tr>
                    <tr><td></td><td></td><td></td></tr>
                </tbody>
            </table>
        """

        self.assertHTMLEqual(result, expected)
        self.assertIn("Test 2", result)

    def test_render_empty_table(self):
        """
        An empty table should render okay.
        """
        block = TableBlock()
        result = block.render(
            {
                "first_row_is_table_header": False,
                "first_col_is_header": False,
                "data": [[None, None, None], [None, None, None], [None, None, None]],
            }
        )
        expected = """
            <table>
                <tbody>
                    <tr><td></td><td></td><td></td></tr>
                    <tr><td></td><td></td><td></td></tr>
                    <tr><td></td><td></td><td></td></tr>
                </tbody>
            </table>
        """
        self.assertHTMLEqual(result, expected)

    def test_do_not_render_html_by_default(self):
        """
        Ensure that raw html doesn't render
        by default.
        """
        value = {
            "first_row_is_table_header": False,
            "first_col_is_header": False,
            "data": [
                ["<p><strong>Test</strong></p>", None, None],
                [None, None, None],
                [None, None, None],
            ],
        }

        expected = """
            <table>
                <tbody>
                    <tr><td>&lt;p&gt;&lt;strong&gt;Test&lt;/strong&gt;&lt;/p&gt;</td><td></td><td></td></tr>
                    <tr><td></td><td></td><td></td></tr>
                    <tr><td></td><td></td><td></td></tr>
                </tbody>
            </table>
        """

        block = TableBlock()
        result = block.render(value)
        self.assertHTMLEqual(result, expected)

    def test_does_render_html_if_allowed(self):
        """
        Ensure html renders if table_options set renderer to allow html
        """
        value = {
            "first_row_is_table_header": False,
            "first_col_is_header": False,
            "data": [
                ["<p><strong>Test</strong></p>", None, None],
                [None, None, None],
                [None, None, None],
            ],
        }

        expected = """
            <table>
                <tbody>
                    <tr><td><p><strong>Test</strong></p></td><td></td><td></td></tr>
                    <tr><td></td><td></td><td></td></tr>
                    <tr><td></td><td></td><td></td></tr>
                </tbody>
            </table>
        """

        new_options = self.default_table_options.copy()
        new_options["renderer"] = "html"
        block = TableBlock(table_options=new_options)
        result = block.render(value)
        self.assertHTMLEqual(result, expected)

    def test_row_headers(self):
        """
        Ensure that row headers are properly rendered.
        """
        value = {
            "first_row_is_table_header": True,
            "first_col_is_header": False,
            "data": [["Foo", "Bar", "Baz"], [None, None, None], [None, None, None]],
        }

        expected = """
            <table>
                <thead>
                    <tr><th scope="col">Foo</th><th scope="col">Bar</th><th scope="col">Baz</th></tr>
                </thead>
                <tbody>
                    <tr><td></td><td></td><td></td></tr>
                    <tr><td></td><td></td><td></td></tr>
                </tbody>
            </table>
        """
        block = TableBlock()
        result = block.render(value)
        self.assertHTMLEqual(result, expected)

    def test_column_headers(self):
        """
        Ensure that column headers are properly rendered.
        """
        value = {
            "first_row_is_table_header": False,
            "first_col_is_header": True,
            "data": [
                ["Foo", "Bar", "Baz"],
                ["one", "two", "three"],
                ["four", "five", "six"],
            ],
        }

        expected = """
            <table>
                <tbody>
                    <tr><th scope="row">Foo</th><td>Bar</td><td>Baz</td></tr>
                    <tr><th scope="row">one</th><td>two</td><td>three</td></tr>
                    <tr><th scope="row">four</th><td>five</td><td>six</td></tr>
                </tbody>
            </table>
        """
        block = TableBlock()
        result = block.render(value)
        self.assertHTMLEqual(result, expected)

    def test_row_and_column_headers(self):
        """
        Test row and column headers at the same time.
        """
        value = {
            "first_row_is_table_header": True,
            "first_col_is_header": True,
            "data": [
                ["Foo", "Bar", "Baz"],
                ["one", "two", "three"],
                ["four", "five", "six"],
            ],
        }

        expected = """
            <table>
                <thead>
                    <tr><th scope="col">Foo</th><th scope="col">Bar</th><th scope="col">Baz</th></tr>
                </thead>
                <tbody>
                    <tr><th scope="row">one</th><td>two</td><td>three</td></tr>
                    <tr><th scope="row">four</th><td>five</td><td>six</td></tr>
                </tbody>
            </table>
        """
        block = TableBlock()
        result = block.render(value)
        self.assertHTMLEqual(result, expected)

    def test_no_headers(self):
        """
        Test table without headers.
        """
        value = {
            "first_row_is_table_header": False,
            "first_col_is_header": False,
            "data": [
                ["Foo", "Bar", "Baz"],
                ["one", "two", "three"],
                ["four", "five", "six"],
            ],
        }

        expected = """
            <table>
                <tbody>
                    <tr><td>Foo</td><td>Bar</td><td>Baz</td></tr>
                    <tr><td>one</td><td>two</td><td>three</td></tr>
                    <tr><td>four</td><td>five</td><td>six</td></tr>
                </tbody>
            </table>
        """
        block = TableBlock()
        result = block.render(value)
        self.assertHTMLEqual(result, expected)

    def test_value_for_and_from_form(self):
        """
        Make sure we get back good json and make
        sure it translates back to python.
        """
        value = {
            "first_row_is_table_header": False,
            "first_col_is_header": False,
            "data": [["Foo", 1, None], [3.5, "Bar", "Baz"]],
        }
        block = TableBlock()
        expected_json = '{"first_row_is_table_header": false, "first_col_is_header": false, "data": [["Foo", 1, null], [3.5, "Bar", "Baz"]]}'
        returned_json = block.value_for_form(value)

        self.assertJSONEqual(expected_json, returned_json)
        self.assertEqual(block.value_from_form(returned_json), value)

    def test_is_html_renderer(self):
        """
        Test that settings flow through correctly to
        the is_html_renderer method.
        """
        # TableBlock with default table_options
        block1 = TableBlock()
        self.assertIs(block1.is_html_renderer(), False)

        # TableBlock with altered table_options
        new_options = self.default_table_options.copy()
        new_options["renderer"] = "html"
        block2 = TableBlock(table_options=new_options)
        self.assertIs(block2.is_html_renderer(), True)

    def test_searchable_content(self):
        value = {
            "first_row_is_table_header": False,
            "first_col_is_header": False,
            "data": [
                ["Test 1", "Test 2", "Test 3"],
                [None, "Bar", None],
                [None, "Foo", None],
            ],
        }
        block = TableBlock()
        content = block.get_searchable_content(value)
        self.assertEqual(
            content,
            [
                "Test 1",
                "Test 2",
                "Test 3",
                "Bar",
                "Foo",
            ],
        )

    def test_searchable_content_for_null_block(self):
        value = None
        block = TableBlock()
        content = block.get_searchable_content(value)
        self.assertEqual(content, [])

    def test_render_with_extra_context(self):
        """
        Test that extra context variables passed in block.render are passed through
        to the template.
        """
        block = TableBlock(template="tests/blocks/table_block_with_caption.html")

        value = {
            "first_row_is_table_header": False,
            "first_col_is_header": False,
            "data": [
                ["Test 1", "Test 2", "Test 3"],
                [None, None, None],
                [None, None, None],
            ],
        }
        result = block.render(value, context={"caption": "A fascinating table."})
        self.assertIn("Test 1", result)
        self.assertIn("<div>A fascinating table.</div>", result)

    def test_table_block_caption_render(self):
        """
        Test a generic render with caption.
        """
        value = {
            "table_caption": "caption",
            "first_row_is_table_header": False,
            "first_col_is_header": False,
            "data": [
                ["Test 1", "Test 2", "Test 3"],
                [None, None, None],
                [None, None, None],
            ],
        }
        block = TableBlock()
        result = block.render(value)
        expected = """
            <table>
                <caption>caption</caption>
                <tbody>
                    <tr><td>Test 1</td><td>Test 2</td><td>Test 3</td></tr>
                    <tr><td></td><td></td><td></td></tr>
                    <tr><td></td><td></td><td></td></tr>
                </tbody>
            </table>
        """
        self.assertHTMLEqual(result, expected)
        self.assertIn("Test 2", result)

    def test_empty_table_block_is_not_rendered(self):
        """
        Test an empty table is not rendered.
        """
        value = None
        block = TableBlock()
        result = block.render(value)
        expected = ""

        self.assertHTMLEqual(result, expected)
        self.assertNotIn("None", result)

    def test_merge_cells_render(self):
        """
        Test that merged table cells are rendered.
        """
        value = {
            "first_row_is_table_header": False,
            "first_col_is_header": False,
            "data": [
                ["one", None, "two"],
                ["three", "four", "five"],
                ["six", "seven", None],
            ],
            "cell": [
                {"row": 0, "col": 1, "hidden": True},
                {"row": 2, "col": 2, "hidden": True},
            ],
            "mergeCells": [
                {"row": 0, "col": 0, "rowspan": 1, "colspan": 2},
                {"row": 1, "col": 2, "rowspan": 2, "colspan": 1},
            ],
        }
        block = TableBlock()
        result = block.render(value)
        expected = """
            <table>
                <tbody>
                    <tr><td rowspan="1" colspan="2">one</td><td>two</td></tr>
                    <tr><td>three</td><td>four</td><td rowspan="2" colspan="1">five</td></tr>
                    <tr><td>six</td><td>seven</td></tr>
                </tbody>
            </table>
        """
        self.assertHTMLEqual(result, expected)

    def test_empty_table_block_to_python(self):
        # Should not raise an error
        # See https://github.com/wagtail/wagtail/issues/11816
        block = TableBlock()
        self.assertIsNone(block.to_python(None))


class TestTableBlockForm(WagtailTestUtils, SimpleTestCase):
    def setUp(self):
        # test value for table data
        self.value = {
            "first_row_is_table_header": True,
            "first_col_is_header": True,
            "data": [
                ["Ship", "Type", "Status"],
                ["Galactica", "Battlestar", "Active"],
                ["Valkyrie", "Battlestar", "Destroyed"],
                ["Cylon Basestar", "Basestar", "Active"],
                ["Brenik", "Small Military Vessel", "Destroyed"],
            ],
        }
        # set language from testing environment
        language = translation.get_language()

        self.default_table_options = DEFAULT_TABLE_OPTIONS.copy()
        self.default_table_options["language"] = language

    def test_default_table_options(self):
        """
        Test options without any custom table_options provided.
        """
        block = TableBlock()
        # check that default_table_options created correctly
        self.assertEqual(block.table_options, block.get_table_options())
        # check that default_table_options used on self
        self.assertEqual(self.default_table_options, block.table_options)
        # check a few individual keys from DEFAULT_TABLE_OPTIONS
        self.assertEqual(
            DEFAULT_TABLE_OPTIONS["startRows"], block.table_options["startRows"]
        )
        self.assertEqual(
            DEFAULT_TABLE_OPTIONS["colHeaders"], block.table_options["colHeaders"]
        )
        self.assertEqual(
            DEFAULT_TABLE_OPTIONS["contextMenu"], block.table_options["contextMenu"]
        )
        self.assertEqual(DEFAULT_TABLE_OPTIONS["editor"], block.table_options["editor"])
        self.assertEqual(
            DEFAULT_TABLE_OPTIONS["stretchH"], block.table_options["stretchH"]
        )

    def test_table_options_language(self):
        """
        Test that the environment's language is used if no language provided.
        """
        # default must always contain a language value
        block = TableBlock()
        self.assertIn("language", block.table_options)
        # French
        translation.activate("fr-fr")
        block_fr = TableBlock()
        self.assertEqual("fr-fr", block_fr.table_options["language"])
        translation.activate("it")
        # Italian
        block_it = TableBlock()
        self.assertEqual("it", block_it.table_options["language"])
        # table_options with language provided, different to environment
        block_with_lang = TableBlock(table_options={"language": "ja"})
        self.assertNotEqual("it", block_with_lang.table_options["language"])
        self.assertEqual("ja", block_with_lang.table_options["language"])
        translation.activate("en")

    def test_table_options_context_menu(self):
        """
        Test how contextMenu is set to default.
        """
        default_context_menu = list(DEFAULT_TABLE_OPTIONS["contextMenu"])  # create copy
        # confirm the default is correct
        table_options = TableBlock().table_options
        self.assertEqual(table_options["contextMenu"], default_context_menu)
        # confirm that when custom option is True, default is still used
        table_options_menu_true = TableBlock(
            table_options={"contextMenu": True}
        ).table_options
        self.assertEqual(table_options_menu_true["contextMenu"], default_context_menu)
        # confirm menu is removed if False is passed in
        table_options_menu_false = TableBlock(
            table_options={"contextMenu": False}
        ).table_options
        self.assertIs(table_options_menu_false["contextMenu"], False)
        # confirm if list passed in, it is used
        table_options_menu_list = TableBlock(
            table_options={"contextMenu": ["undo", "redo"]}
        ).table_options
        self.assertEqual(table_options_menu_list["contextMenu"], ["undo", "redo"])
        # test if empty array passed in
        table_options_menu_list = TableBlock(
            table_options={"contextMenu": []}
        ).table_options
        self.assertEqual(table_options_menu_list["contextMenu"], [])

    def test_table_options_others(self):
        """
        Test simple options overrides get passed correctly.
        """
        block_1_opts = TableBlock(
            table_options={"startRows": 5, "startCols": 2}
        ).table_options
        self.assertEqual(block_1_opts["startRows"], 5)
        self.assertEqual(block_1_opts["startCols"], 2)

        block_2_opts = TableBlock(table_options={"stretchH": "none"}).table_options
        self.assertEqual(block_2_opts["stretchH"], "none")

        # check value that is not part of the defaults
        block_3_opts = TableBlock(table_options={"allowEmpty": False}).table_options
        self.assertIs(block_3_opts["allowEmpty"], False)

    def test_adapt(self):
        block = TableBlock(description="A table to display data.")

        block.set_name("test_tableblock")
        js_args = FieldBlockAdapter().js_args(block)

        self.assertEqual(js_args[0], "test_tableblock")
        self.assertIsInstance(js_args[1], TableInput)
        self.assertEqual(
            js_args[2],
            {
                "label": "Test tableblock",
                "description": "A table to display data.",
                "required": True,
                "icon": "table",
                "blockDefId": block.definition_prefix,
                "isPreviewable": block.is_previewable,
                "classname": "w-field w-field--char_field w-field--table_input",
                "showAddCommentButton": True,
                "strings": {"ADD_COMMENT": "Add Comment"},
            },
        )

    def test_searchable_content(self):
        """
        Test searchable content is created correctly.
        """
        block = TableBlock()
        search_content = block.get_searchable_content(value=self.value)
        self.assertIn("Galactica", search_content)
        self.assertIn("Brenik", search_content)


# TODO(telepath) replace this with a functional test
class TestTableBlockPageEdit(WagtailTestUtils, TestCase):
    def setUp(self):
        self.value = {
            "first_row_is_table_header": True,
            "first_col_is_header": True,
            "data": [
                ["Ship", "Type", "Status"],
                ["Galactica", "Battlestar", "Active"],
                ["Valkyrie", "Battlestar", "Destroyed"],
                ["Cylon Basestar", "Basestar", "Active"],
                ["Brenik", "Small Military Vessel", "Destroyed"],
            ],
        }
        self.root_page = Page.objects.get(id=2)
        table_block_page_instance = TableBlockStreamPage(
            title="Ships", table=json.dumps([{"type": "table", "value": self.value}])
        )
        self.table_block_page = self.root_page.add_child(
            instance=table_block_page_instance
        )
        self.user = self.login()

    @unittest.expectedFailure
    def test_page_edit_page_view(self):
        """
        Test that edit page loads with saved table data and correct init function.
        """
        response = self.client.get(
            reverse("wagtailadmin_pages:edit", args=(self.table_block_page.id,))
        )
        # check page + field renders
        self.assertContains(
            response,
            '<div data-contentpath="table" class="w-field w-field--char_field w-field--table_input">',
        )
        # check data
        self.assertContains(response, "Battlestar")
        self.assertContains(response, "Galactica")
        # check init
        self.assertContains(response, 'initTable("table\\u002D0\\u002Dvalue"')
        self.assertContains(response, "minSpareRows")
        self.assertContains(response, "startRows")
