#!/usr/bin/env python3

# Libervia: an XMPP client
# Copyright (C) 2009-2026 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 unittest.mock import MagicMock, AsyncMock
from pytest import fixture
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from twisted.words.xish import domish
from libervia.backend import G
from tx_xmpp.data_form import NS_X_DATA
from libervia.backend.core.main import LiberviaBackend
from libervia.backend.models.tor import TorMode
from libervia.backend.tools import async_trigger as trigger
from libervia.backend.core import xmpp


@fixture(scope="session")
def bridge():
    bridge = AsyncMock()
    bridge.add_signal = MagicMock()
    bridge.add_method = MagicMock()
    return bridge


@fixture(scope="session")
def storage():
    return AsyncMock()


class MockLiberviaBackend(LiberviaBackend):

    def __init__(self, bridge, storage):
        G.set_host(self)
        self._cb_map = {}
        self._menus = {}
        self._menus_paths = {}
        self._test_config = {}
        self.profiles = {}
        self.plugins = {}
        # map for short name to whole namespace,
        # extended by plugins with register_namespace
        self.ns_map = {
            "x-data": NS_X_DATA,
            "disco#info": xmpp.NS_DISCO_INFO,
        }
        self.memory = MagicMock()
        self.memory.storage = storage
        self.memory.config_get.side_effect = self.get_test_config

        self.trigger = trigger.TriggerManager()
        self._bridge = bridge
        defer.ensureDeferred(self._post_init())
        self.common_cache = AsyncMock()
        self._import_plugins()
        self._add_base_menus()
        self.initialised = defer.Deferred()
        self.initialised.callback(None)

    def get_local_path(self, *args, **kwargs):
        return MagicMock()

    def get_test_config(self, section, name, default=None):
        return self._test_config.get((section or None, name), default)

    def register_namespace(self, short_name, namespace):
        # Plugin classes may be instantiated several times in tests, so we do nothing in
        # this method to avoid ConflictError.
        pass

    def _init(self):
        pass

    async def _post_init(self):
        pass


@fixture(scope="session")
def host(bridge, storage):
    host = MockLiberviaBackend(bridge=bridge, storage=storage)
    return host


@fixture
def client_IQ():
    """Factory fixture to create a mock IQ method for testing.

    Usage:
        def test_something(client, client_IQ):
            response_elt = domish.Element((None, "iq"))
            response_elt["type"] = "result"
            client.IQ = client_IQ(response_elt=response_elt)

            # Later in test:
            assert len(client.IQ.sent_iqs) == 1

    @param response: Optional response to return from send(). If None, returns empty IQ.
    @return: The mock IQ method with attached sent_iqs list.
    """

    def factory(response_elt=None):
        sent_iqs = []

        def client_IQ(type_="set", timeout=60):
            iq_elt = domish.Element((None, "iq"))
            iq_elt["type"] = type_

            async def mock_send():
                sent_iqs.append(iq_elt)
                if response_elt is not None:
                    return response_elt
                # Return empty IQ by default
                empty_response = domish.Element((None, "iq"))
                empty_response["type"] = "result"
                return empty_response

            iq_elt.send = mock_send
            return iq_elt

        # Attach list for test assertions
        client_IQ.sent_iqs = sent_iqs
        return client_IQ

    return factory


@fixture
def client():
    client = MagicMock()
    client.jid = jid.JID("test_user@test.example/123")
    client.server_jid = jid.JID("test.example")
    client.pubsub_service = jid.JID("pubsub.test.example")
    client.pubsub_client = AsyncMock()
    client.tor = None
    client.tor_mode = TorMode.DISABLED
    return client


@fixture(autouse=True)
def reset_storage(host):
    """Replace storage with a fresh mock before each test.

    The host and storage fixtures use session scope, which means any assignments to
    storage attributes (e.g., storage.method = AsyncMock(...)) persist across tests. This
    fixture ensures each test starts with a clean slate.
    """
    host.memory.storage = AsyncMock()
