"""Unit tests for reviewboard.actions.base.BaseAction.

Version Added:
    8.0
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from django.template import Context
from django.utils.safestring import SafeString
from djblets.pagestate.state import PageState

from reviewboard.actions import ActionPlacement, AttachmentPoint
from reviewboard.actions.base import BaseAction
from reviewboard.actions.errors import MissingActionRendererError
from reviewboard.actions.renderers import (ButtonActionRenderer,
                                           DefaultActionRenderer)
from reviewboard.actions.tests.base import (SpecialButtonActionRenderer,
                                            TestAction)
from reviewboard.deprecation import RemovedInReviewBoard10_0Warning
from reviewboard.testing import TestCase

if TYPE_CHECKING:
    from djblets.testing.testcases import ExpectedWarning


class BaseActionTests(TestCase):
    """Unit tests for BaseAction.

    Version Added:
        8.0
    """

    def test_init(self) -> None:
        """Testing BaseAction.__init__"""
        class MyAction(BaseAction):
            action_id = 'test-action'

        with self.assertNoWarnings():
            MyAction()

    def test_init_with_deprecations(self) -> None:
        """Testing BaseAction.__init__ with deprecations"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            template_name = 'my-action.html'
            js_view_class = 'My.Action'

            def get_js_view_data(
                self,
                *,
                context: Context,
            ) -> dict:
                return {}

        warning_list: list[ExpectedWarning] = [
            {
                'cls': RemovedInReviewBoard10_0Warning,
                'message': (
                    'MyAction.template_name is deprecated and support will '
                    'be removed in Review Board 10. Please move any custom '
                    'rendering to a reviewboard.actions.renderers.'
                    'BaseActionRenderer subclass instead.'
                ),
            },
            {
                'cls': RemovedInReviewBoard10_0Warning,
                'message': (
                    'MyAction.js_view_class is deprecated and support will '
                    'be removed in Review Board 10. Please move any custom '
                    'rendering to a reviewboard.actions.renderers.'
                    'BaseActionRenderer subclass instead.'
                ),
            },
            {
                'cls': RemovedInReviewBoard10_0Warning,
                'message': (
                    'MyAction.get_js_view_data is deprecated and support will '
                    'be removed in Review Board 10. Please move any custom '
                    'rendering to a reviewboard.actions.renderers.'
                    'BaseActionRenderer subclass instead.'
                ),
            },
        ]

        with self.assertWarnings(warning_list):
            MyAction()

    def test_get_dom_element_id(self) -> None:
        """Testing BaseAction.get_dom_element_id"""
        action = TestAction()

        with self.assertWarns(RemovedInReviewBoard10_0Warning):
            dom_element_id = action.get_dom_element_id()

        # The ID matches what the renderer computes for the placement.
        self.assertEqual(dom_element_id, 'action-review-request-test')

    def test_get_dom_element_id_with_placement_dom_element_id(self) -> None:
        """Testing BaseAction.get_dom_element_id with placement.dom_element_id
        """
        class MyAction(BaseAction):
            action_id = 'placement-test'
            placements = [
                ActionPlacement(attachment=AttachmentPoint.REVIEW_REQUEST,
                                dom_element_id='placement-id'),
            ]

        action = MyAction()

        with self.assertWarns(RemovedInReviewBoard10_0Warning):
            dom_element_id = action.get_dom_element_id()

        self.assertEqual(dom_element_id, 'placement-id')

    def test_get_dom_element_id_with_multiple_placements(self) -> None:
        """Testing BaseAction.get_dom_element_id with multiple placements"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            placements = [
                ActionPlacement(attachment=AttachmentPoint.HEADER),
                ActionPlacement(attachment=AttachmentPoint.REVIEW_REQUEST),
            ]

        action = MyAction()

        # The method can only resolve against the first placement. Renderers
        # compute the ID per-placement instead, which is why this is
        # deprecated. This is here for backwards compatibility.
        with self.assertWarns(RemovedInReviewBoard10_0Warning):
            dom_element_id = action.get_dom_element_id()

        self.assertEqual(dom_element_id, 'action-header-test-action')

    def test_should_register_with_rendered(self) -> None:
        """Testing BaseAction.should_register with rendered"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'
            default_renderer_cls = DefaultActionRenderer

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        MyAction().render_js(request=request,
                             context=context)

        self.assertTrue(action.should_register(request=request,
                                               context=context))

    def test_should_register_with_not_rendered(self) -> None:
        """Testing BaseAction.should_register with not rendered"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'
            default_renderer_cls = DefaultActionRenderer

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        self.assertFalse(action.should_register(request=request,
                                                context=context))

    def test_should_register_with_should_render_true(self) -> None:
        """Testing BaseAction.should_register with should_render() == True"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'
            default_renderer_cls = DefaultActionRenderer

            def should_render(
                self,
                *,
                context: Context,
            ) -> bool:
                return True

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        MyAction().render_js(request=request,
                             context=context)

        self.assertTrue(action.should_register(request=request,
                                               context=context))

    def test_should_register_with_should_render_false(self) -> None:
        """Testing BaseAction.should_register with should_render() == False"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'
            default_renderer_cls = DefaultActionRenderer

            def should_render(
                self,
                *,
                context: Context,
            ) -> bool:
                return False

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        MyAction().render_js(request=request,
                             context=context)

        self.assertFalse(action.should_register(request=request,
                                                context=context))

    def test_render_with_default_renderer(self) -> None:
        """Testing BaseAction.render with default renderer"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'
            default_renderer_cls = DefaultActionRenderer

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        html = action.render(
            request=request,
            context=context,
            placement=action.get_placement('review-request'))

        self.assertIsInstance(html, SafeString)
        self.assertHTMLEqual(
            html,
            """
            <li class="rb-c-actions__action" role="presentation">
             <a href="#"
                id="action-review-request-test-action"
                role="button">
              My Label
             </a>
            </li>
            """)

    def test_render_with_custom_default_renderer(self) -> None:
        """Testing BaseAction.render with custom default_renderer_cls"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            default_renderer_cls = ButtonActionRenderer
            label = 'My Label'

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        html = action.render(
            request=request,
            context=context,
            placement=action.get_placement('review-request'))

        self.assertIsInstance(html, SafeString)
        self.assertHTMLEqual(
            html,
            """
            <li class="rb-c-actions__action" role="presentation">
             <button aria-label="My Label"
                     class="ink-c-button"
                     id="action-review-request-test-action"
                     type="button">
              <label class="ink-c-button__label">My Label</label>
             </button>
            </li>
            """)

    def test_render_with_renderer(self) -> None:
        """Testing BaseAction.render with provided renderer="""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        html = action.render(
            request=request,
            context=context,
            renderer=ButtonActionRenderer,
            placement=action.get_placement('review-request'))

        self.assertIsInstance(html, SafeString)
        self.assertHTMLEqual(
            html,
            """
            <li class="rb-c-actions__action" role="presentation">
             <button aria-label="My Label"
                     class="ink-c-button"
                     id="action-review-request-test-action"
                     type="button">
              <label class="ink-c-button__label">My Label</label>
             </button>
            </li>
            """)

    def test_render_with_no_renderer(self) -> None:
        """Testing BaseAction.render with no renderer"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'
            default_renderer_cls = None

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        message = (
            "A renderer must be explicitly provided when rendering action "
            "<class 'reviewboard.actions.tests.test_base_action."
            "BaseActionTests.test_render_with_no_renderer.<locals>."
            "MyAction'>."
        )

        with self.assertRaisesMessage(MissingActionRendererError, message):
            action.render(request=request,
                          context=context,
                          placement=action.get_placement('review-request'))

    def test_render_with_should_render_false(self) -> None:
        """Testing BaseAction.render with should_render() returning False"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'
            default_renderer_cls = ButtonActionRenderer

            def should_render(
                self,
                *,
                context: Context,
            ) -> bool:
                return False

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        html = MyAction().render(request=request,
                                 context=context)

        self.assertIsInstance(html, SafeString)
        self.assertEqual(html, '')

    def test_render_model_js(self) -> None:
        """Testing BaseAction.render_model_js"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        js = MyAction().render_model_js(request=request,
                                        context=context)

        self.assertIsInstance(js, SafeString)
        self.assertHTMLEqual(
            js,
            """
            page.addAction(new RB.Actions.Action(
                {"id": "test-action",
                 "visible": true,
                 "label": "My Label",
                 "url": "#"},
                { parse: true }
            ));
            """)

    def test_render_js_with_default_renderer(self) -> None:
        """Testing BaseAction.render_js with default renderer"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'
            default_renderer_cls = DefaultActionRenderer

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        js = action.render_js(
            request=request,
            context=context,
            placement=action.get_placement('review-request'))

        self.assertIsInstance(js, SafeString)
        self.assertHTMLEqual(
            js,
            """
            page.addActionView(new RB.Actions.ActionView({
                "attachmentPointID": "review-request",
                el: $('#action-review-request-test-action'),
                model: page.getAction("test-action"),
            }));
            """)

        page_state = PageState.for_request(request)
        self.assertIn('rb-actions-rendered', page_state.extra_data)
        self.assertEqual(
            page_state.extra_data['rb-actions-rendered'],
            {
                action.action_id,
            })

    def test_render_js_with_custom_default_renderer(self) -> None:
        """Testing BaseAction.render_js with custom default_renderer_cls"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            default_renderer_cls = ButtonActionRenderer
            label = 'My Label'

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        js = action.render_js(
            request=request,
            context=context,
            placement=action.get_placement('review-request'))

        self.assertIsInstance(js, SafeString)
        self.assertHTMLEqual(
            js,
            """
            page.addActionView(new RB.Actions.ButtonActionView({
                "attachmentPointID": "review-request",
                el: $('#action-review-request-test-action'),
                model: page.getAction("test-action"),
            }));
            """)

        page_state = PageState.for_request(request)
        self.assertIn('rb-actions-rendered', page_state.extra_data)
        self.assertEqual(
            page_state.extra_data['rb-actions-rendered'],
            {
                action.action_id,
            })

    def test_render_js_with_renderer(self) -> None:
        """Testing BaseAction.render_js with provided renderer="""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        js = action.render_js(
            request=request,
            context=context,
            renderer=SpecialButtonActionRenderer,
            placement=action.get_placement('review-request'))

        self.assertIsInstance(js, SafeString)
        self.assertHTMLEqual(
            js,
            """
            page.addActionView(new SpecialButtonActionView({
                "attachmentPointID": "review-request",
                "label": "~~My Label~~",
                "specialKey": [123, 456],
                el: $('#action-review-request-test-action'),
                model: page.getAction("test-action"),
            }));
            """)

        page_state = PageState.for_request(request)
        self.assertIn('rb-actions-rendered', page_state.extra_data)
        self.assertEqual(
            page_state.extra_data['rb-actions-rendered'],
            {
                action.action_id,
            })

    def test_render_js_with_no_renderer(self) -> None:
        """Testing BaseAction.render_js with no renderer"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'
            default_renderer_cls = None

        action = MyAction()

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        message = (
            "A renderer must be explicitly provided when rendering "
            "JavaScript for action <class 'reviewboard.actions.tests."
            "test_base_action.BaseActionTests.test_render_js_with_no_renderer."
            "<locals>.MyAction'>."
        )

        with self.assertRaisesMessage(MissingActionRendererError, message):
            action.render_js(request=request,
                             context=context,
                             placement=action.get_placement('review-request'))

        page_state = PageState.for_request(request)
        self.assertNotIn('rb-actions-rendered', page_state.extra_data)

    def test_render_js_with_should_render_false(self) -> None:
        """Testing BaseAction.render_js with should_render() returning False"""
        class MyAction(BaseAction):
            action_id = 'test-action'
            label = 'My Label'
            default_renderer_cls = DefaultActionRenderer

            def should_render(
                self,
                *,
                context: Context,
            ) -> bool:
                return False

        request = self.create_http_request()
        context = Context({
            'request': request,
        })

        js = MyAction().render_js(request=request,
                                  context=context)

        self.assertIsInstance(js, SafeString)
        self.assertEqual(js, '')

        page_state = PageState.for_request(request)
        self.assertNotIn('rb-actions-rendered', page_state.extra_data)
