#!/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/>.

"""Tests for XEP-0115 Entity Capabilities implementation."""

from unittest.mock import MagicMock
from pytest import fixture
from twisted.words.protocols.jabber import jid
from tx_xmpp import data_form, disco

from libervia.backend.plugins.plugin_xep_0115 import (
    NS_ENTITY_CAPABILITY,
    NS_CAPS_OPTIMIZE,
    XEP_0115,
)
from libervia.backend.core.constants import Const as C
from libervia.backend.memory.disco import Discovery


@fixture
def xep_0115(host) -> XEP_0115:
    """Fixture for initializing XEP-0115 plugin."""
    return XEP_0115(host)


@fixture(autouse=True)
def setup_memory_disco(host):
    host.memory.disco = Discovery(host)
    host.memory.disco.hashes = {}
    return host


class TestXEP0115HashGeneration:
    """Tests for hash generation per XEP-0115 section 4.1."""

    def test_simple_hash_generation(self, xep_0115):
        """Test hash generation with simple identity and features (XEP-0115 example)."""
        disco_infos = disco.DiscoInfo()
        disco_infos.append(disco.DiscoIdentity("client", "pc", "Exodus 0.9.1"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/caps"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/disco#info"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/disco#items"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/muc"))

        cap_hash = xep_0115.generate_hash(disco_infos)

        assert cap_hash == "QgayPKawpkPSDYmwT/WM94uAlu0="

    def test_hash_generation_with_xml_lang(self, xep_0115):
        """Test hash generation with xml:lang attribute on identities."""
        disco_infos = disco.DiscoInfo()
        disco_infos.append(disco.DiscoIdentity("client", "pc", "Psi 0.11", "en"))
        disco_infos.append(disco.DiscoIdentity("client", "pc", "Ψ 0.11", "el"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/caps"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/disco#info"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/disco#items"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/muc"))

        cap_hash = xep_0115.generate_hash(disco_infos)
        cap_hash2 = xep_0115.generate_hash(disco_infos)
        # Verify that the hash is deterministic
        assert cap_hash == cap_hash2

        # Verify identities with lang are properly included in hash
        # The hash should be different from one without lang
        disco_infos_no_lang = disco.DiscoInfo()
        disco_infos_no_lang.append(disco.DiscoIdentity("client", "pc", "Psi 0.11"))
        disco_infos_no_lang.append(disco.DiscoIdentity("client", "pc", "Ψ 0.11"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/caps"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/disco#info"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/disco#items"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/muc"))

        cap_hash_no_lang = xep_0115.generate_hash(disco_infos_no_lang)
        assert cap_hash != cap_hash_no_lang

    def test_hash_generation_spec_complex_example(self, xep_0115):
        """Test hash generation against the XEP-0115 §5.3 Complex Generation Example."""
        disco_infos = disco.DiscoInfo()
        disco_infos.append(disco.DiscoIdentity("client", "pc", "\u03a8 0.11", "el"))
        disco_infos.append(disco.DiscoIdentity("client", "pc", "Psi 0.11", "en"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/caps"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/disco#info"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/disco#items"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/muc"))

        form = data_form.Form("result")
        form.formNamespace = "urn:xmpp:dataforms:softwareinfo"

        form_type_field = data_form.Field("hidden", "FORM_TYPE")
        form_type_field.values = ["urn:xmpp:dataforms:softwareinfo"]
        form.addField(form_type_field)

        ip_version_field = data_form.Field("text-multi", "ip_version")
        ip_version_field.values = ["ipv4", "ipv6"]
        form.addField(ip_version_field)

        os_field = data_form.Field("text", "os")
        os_field.values = ["Mac"]
        form.addField(os_field)

        os_version_field = data_form.Field("text", "os_version")
        os_version_field.values = ["10.5.1"]
        form.addField(os_version_field)

        software_field = data_form.Field("text", "software")
        software_field.values = ["Psi"]
        form.addField(software_field)

        software_version_field = data_form.Field("text", "software_version")
        software_version_field.values = ["0.11"]
        form.addField(software_version_field)

        disco_infos.append(form)

        cap_hash = xep_0115.generate_hash(disco_infos)

        # Expected hash from XEP-0115 §5.3
        assert cap_hash == "q07IKJEyjvHSyhy//CH0CxmKi8w="


class TestXEP0115Handler:
    """Tests for XEP-0115 handler."""

    def test_getDiscoInfo_returns_caps_features(self, xep_0115):
        """getDiscoInfo returns caps features for non-caps queries."""
        handler = xep_0115.get_handler(MagicMock())

        result = handler.getDiscoInfo(MagicMock(), MagicMock(), "")

        assert disco.DiscoFeature(NS_ENTITY_CAPABILITY) in result
        assert disco.DiscoFeature(NS_CAPS_OPTIMIZE) in result

    def test_getDiscoInfo_caps_node_query(self, host, xep_0115):
        """getDiscoInfo returns cached disco info for caps node queries."""
        handler = xep_0115.get_handler(MagicMock())

        # Create and cache some disco info
        disco_infos = disco.DiscoInfo()
        disco_infos.append(disco.DiscoIdentity("client", "pc", "Test Client"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/caps"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/disco#info"))

        cap_hash = xep_0115.generate_hash(disco_infos)
        host.memory.disco.hashes[cap_hash] = disco_infos

        # Query with caps node format
        node_identifier = f"{C.APP_URL}#{cap_hash}"
        result = handler.getDiscoInfo(MagicMock(), MagicMock(), node_identifier)

        # Should return the cached disco info items
        assert len(result) > 0
        assert any(isinstance(item, disco.DiscoIdentity) for item in result)
        assert any(isinstance(item, disco.DiscoFeature) for item in result)

        # Verify that the returned items match the cached disco info
        identities = [item for item in result if isinstance(item, disco.DiscoIdentity)]
        features = [item for item in result if isinstance(item, disco.DiscoFeature)]
        assert len(identities) == len(disco_infos.identities)
        assert len(features) == len(disco_infos.features)
        assert identities[0].name == "Test Client"
        assert disco.DiscoFeature("http://jabber.org/protocol/caps") in features
        assert disco.DiscoFeature("http://jabber.org/protocol/disco#info") in features

    def test_getDiscoInfo_unknown_caps_node(self, xep_0115):
        """getDiscoInfo returns caps features for unknown caps nodes."""
        handler = xep_0115.get_handler(MagicMock())

        # Query with unknown hash
        node_identifier = f"{C.APP_URL}#unknownhash123"
        result = handler.getDiscoInfo(MagicMock(), MagicMock(), node_identifier)

        # Should return just the caps features, not cached info
        assert disco.DiscoFeature(NS_ENTITY_CAPABILITY) in result
        assert disco.DiscoFeature(NS_CAPS_OPTIMIZE) in result

    def test_getDiscoItems_empty(self, xep_0115):
        """getDiscoItems returns empty list (caps nodes are virtual)."""
        handler = xep_0115.get_handler(MagicMock())

        result = handler.getDiscoItems(MagicMock(), MagicMock(), "")

        assert result == []


class TestXEP0115Validation:
    """Tests for caps validation."""

    def test_validate_disco_info_well_formed(self, xep_0115):
        """Validation passes for well-formed disco info."""
        disco_infos = disco.DiscoInfo()
        disco_infos.append(disco.DiscoIdentity("client", "pc", "Test"))
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/caps"))

        assert (
            xep_0115._validate_caps(
                jid.JID("test@example.com"),
                "testhash",
                "http://test.com",
                disco_infos,
            )
            is True
        )

    def test_validate_disco_info_duplicate_identity(self, xep_0115, capsys):
        """Validation fails for duplicate identities."""
        disco_infos = disco.DiscoInfo()
        disco_infos.append(disco.DiscoIdentity("client", "pc", "Test", "en"))
        disco_infos.append(
            disco.DiscoIdentity("client", "pc", "Test2", "en")
        )  # Duplicate category/type/lang

        assert (
            xep_0115._validate_caps(
                jid.JID("test@example.com"),
                "testhash",
                "http://test.com",
                disco_infos,
            )
            is False
        )

        captured = capsys.readouterr()
        assert "Duplicate identity found" in captured.out

    def test_validate_disco_info_duplicate_feature(self, xep_0115, capsys):
        """Validation fails for duplicate features."""
        disco_infos = disco.DiscoInfo()
        disco_infos.append(disco.DiscoFeature("http://jabber.org/protocol/caps"))
        disco_infos.append(
            disco.DiscoFeature("http://jabber.org/protocol/caps")
        )  # Duplicate

        assert (
            xep_0115._validate_caps(
                jid.JID("test@example.com"),
                "testhash",
                "http://test.com",
                disco_infos,
            )
            is False
        )

        captured = capsys.readouterr()
        assert "Duplicate feature found" in captured.out
