#!/usr/bin/env python3

# Libervia Web
# Copyright (C) 2009-2025 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from collections import defaultdict
import sys
from libervia.backend.tools.common import data_format
import pytest
from unittest.mock import MagicMock, AsyncMock, patch

# We mock all non-stdlib modules, so the module can be imported.
sys.modules["bridge"] = MagicMock(AsyncBridge=AsyncMock)
sys.modules["cache"] = MagicMock(cache=MagicMock())
sys.modules["dialog"] = MagicMock()
sys.modules["editor"] = MagicMock(TagsEditor=MagicMock())
sys.modules["file_uploader"] = MagicMock(FileUploader=MagicMock())
sys.modules["javascript"] = MagicMock()
sys.modules["jid"] = MagicMock(JID=MagicMock())
sys.modules["loading"] = MagicMock()
sys.modules["popup"] = MagicMock(create_popup=MagicMock())
sys.modules["js_modules"] = MagicMock()
sys.modules["js_modules.quill"] = MagicMock(Quill=MagicMock())
sys.modules["js_modules.quill_mention"] = MagicMock()
sys.modules["template"] = MagicMock()
sys.modules["identities"] = MagicMock()


class FakeElement:

    def __init__(self):
        self.attrs = defaultdict(MagicMock)

    def __getattr__(self, attr) -> MagicMock:
        return self.attrs[attr]


class FakeDocument:

    def __init__(self):
        self.attrs = defaultdict(MagicMock)
        self.items = defaultdict(FakeElement)

    def __getattr__(self, attr) -> MagicMock:
        return self.attrs[attr]

    def __getitem__(self, item) -> FakeElement:
        return self.items[item]

    def __setitem__(self, key, item) -> None:
        self.items[key] = item


class FakeEvent:
    """Fake event object with preventDefault and stopPropagation methods."""

    def __init__(self):
        self.defaultPrevented = False
        self.currentTarget = None

    def preventDefault(self):
        self.defaultPrevented = True

    def stopPropagation(self):
        pass


document = FakeDocument()
browser = sys.modules["browser"] = MagicMock(
    aio=MagicMock(),
    console=MagicMock(),
    document=document,
    window=MagicMock(
        service="test_service",
        node="test_node",
        subscribed=True,
        location=MagicMock(href="http://example.com/test"),
        URL=MagicMock(
            new=MagicMock(
                return_value=MagicMock(href="http://example.com/test?search=test")
            )
        ),
        RegExp=MagicMock(),
    ),
)


class FakeTemplate(MagicMock):

    def get_elt(self, *args, **kwargs):
        return FakeElement()


sys.modules["template"] = MagicMock(Template=FakeTemplate)

# We can now import the actual module
from libervia.web.pages.forums.view import _browser as forums_module


class TestLiberviaForumView:
    @pytest.fixture
    def forum_view(self) -> forums_module.LiberviaForumView:
        """Fixture to create a LiberviaForumView instance with mocks."""
        # Create a mock for the Quill editor
        quill_mock = MagicMock()
        quill_mock.getSemanticHTML.return_value = "<p>test content</p>"
        quill_mock.getContents.return_value = {"ops": []}
        quill_mock.getModule.return_value = MagicMock(container=MagicMock())

        # Mock the Quill constructor
        with patch("js_modules.quill.Quill.new", return_value=quill_mock):
            forum_view = forums_module.LiberviaForumView()

        # Mock the tag editor
        forum_view.tag_editor = MagicMock(current_tags=set())

        return forum_view

    @pytest.fixture
    def bridge(self, monkeypatch) -> AsyncMock:
        bridge = AsyncMock()
        monkeypatch.setattr(forums_module, "bridge", bridge)
        return bridge

    @pytest.mark.asyncio
    async def test_toggle_subscription_subscribed(self, forum_view, bridge) -> None:
        """Toggle subscription calls unsubscribe when already subscribed."""
        with patch.object(browser.window, "subscribed", True):
            button = FakeElement()
            document["subscribe-button"] = button

            await forum_view.toggle_subscription()

            bridge.ps_unsubscribe.assert_awaited_once_with("test_service", "test_node")

    @pytest.mark.asyncio
    async def test_toggle_subscription_not_subscribed(self, forum_view, bridge) -> None:
        """Toggle subscription calls subscribe when not subscribed."""
        with patch.object(browser.window, "subscribed", False):
            button = FakeElement()
            document["subscribe-button"] = button

            await forum_view.toggle_subscription()

            bridge.ps_subscribe.assert_awaited_once_with("test_service", "test_node", "")

    @pytest.mark.asyncio
    async def test_send_message_with_mentions(self, forum_view, bridge) -> None:
        """Sending message with mentions updates ``mb_data``."""
        # Mock Quill content with mentions
        quill_mock = MagicMock()
        quill_mock.getSemanticHTML.return_value = "<p>test message</p>"
        quill_mock.getContents.return_value = {
            "ops": [{"insert": {"mention": {"id": "user@example.com"}}}]
        }
        forum_view.message_editor = quill_mock

        forum_view.tag_editor.current_tags = set()

        await forum_view.send_message()

        bridge.mb_send.assert_awaited()
        mb_data = data_format.deserialise(bridge.mb_send.call_args[0][0])
        assert mb_data["mentions"] == ["user@example.com"]

    @pytest.mark.asyncio
    async def test_send_message_error_handling(self, forum_view, bridge) -> None:
        """Error in ``bridge.mb_send`` is reported."""
        bridge.mb_send.side_effect = Exception("Send failed")

        quill_mock = MagicMock()
        quill_mock.getSemanticHTML.return_value = "<p>test message</p>"
        quill_mock.getContents.return_value = {"ops": []}
        forum_view.message_editor = quill_mock

        forum_view.tag_editor.current_tags = set()

        with patch("dialog.notification.show") as mock_show:
            await forum_view.send_message()

            # Verify that error notification was shown
            mock_show.assert_called_with("Can't send message: Send failed.", "error")

    def test_on_add_tag(self, forum_view) -> None:
        """``tags_container`` become visible when ``on_add_tag``is called."""
        tags_container = FakeElement()
        tags_container.classList = MagicMock()
        document["tags_container"] = tags_container

        forum_view.on_add_tag(FakeEvent())

        # Verify that the class list was modified
        tags_container.classList.remove.assert_called_once_with("is-hidden")

    def test_on_add_attachment(self, forum_view) -> None:
        """``file_input`` click is called from ``on_add_attachment``."""
        file_input = FakeElement()
        document["forum-file-input"] = file_input

        file_input.click = MagicMock()

        forum_view.on_add_attachment(FakeEvent())

        file_input.click.assert_called_once()
