#!/usr/bin/env python3

# Tests for Libervia's Pubsub Contacts plugin
# 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 twisted.words.xish import domish
from tx_xmpp.pubsub import NS_PUBSUB

from libervia.backend.plugins.plugin_misc_pubsub_contacts import (
    Contact,
    Contacts,
    ContactsData,
    Group,
    Groups,
    NS_CONTACTS,
)


class TestGroup:
    """Tests for Group model."""

    def test_from_element(self):
        """Group is correctly parsed from element."""
        element = domish.Element((NS_CONTACTS, "group"))
        element["id"] = "group123"
        element["name"] = "My Group"
        desc_elt = element.addElement((NS_CONTACTS, "description"))
        desc_elt.addContent("A test group")

        group = Group.from_element(element)

        assert group.id == "group123"
        assert group.name == "My Group"
        assert group.description == "A test group"

    def test_from_element_nested_in_parent(self):
        """Group is correctly parsed from <group> element's parent."""
        parent = domish.Element((None, "parent"))
        group_elt = parent.addElement((NS_CONTACTS, "group"))
        group_elt["id"] = "group123"
        group_elt["name"] = "Test Group"

        group = Group.from_element(parent)

        assert group.id == "group123"
        assert group.name == "Test Group"

    def test_from_element_set_encrypted(self):
        """ "encrypted" flag is correctly set when present in metadata."""
        element = domish.Element((NS_CONTACTS, "group"))
        element["id"] = "group123"
        metadata = {"encrypted": {"group123": True}}

        group = Group.from_element(element, metadata)

        assert group.encrypted is True

    def test_from_element_invalid_type_raises_error(self):
        """ValueError is raised when element is not a group element."""
        element = domish.Element((NS_CONTACTS, "contact"))

        try:
            Group.from_element(element)
            assert False, "Expected ValueError"
        except ValueError as e:
            assert "Invalid element" in str(e)

    def test_to_element(self):
        """Group element is correctly created with all fields."""
        group = Group(
            id="group123", name="My Group", description="A test group", encrypted=True
        )
        element = group.to_element()

        assert element.uri == NS_CONTACTS
        assert element.name == "group"
        assert element["id"] == "group123"
        assert element["name"] == "My Group"

        desc_elements = list(element.elements(NS_CONTACTS, "description"))
        assert len(desc_elements) == 1
        assert str(desc_elements[0]) == "A test group"

    def test_to_element_missing_name_raises_error(self):
        """ValueError is raised when name is missing and id_only is False."""
        group = Group(id="group123")

        try:
            group.to_element(id_only=False)
            assert False, "Expected ValueError"
        except ValueError as e:
            assert "name must be set" in str(e)


class TestGroups:
    """Tests for Groups model."""

    def test_append_and_iterate(self):
        """Groups can be appended and iterated."""
        groups = Groups([])
        groups.append(Group(id="g1", name="Group 1"))
        groups.append(Group(id="g2", name="Group 2"))

        group_list = list(groups)
        assert len(group_list) == 2
        assert group_list[0].id == "g1"
        assert group_list[1].id == "g2"

    def test_len_and_remove(self):
        """Groups len and remove work correctly."""
        groups = Groups([])
        groups.append(Group(id="g1", name="Group 1"))
        groups.append(Group(id="g2", name="Group 2"))

        assert len(groups) == 2
        groups.remove(groups[0])
        assert len(groups) == 1
        assert groups[0].id == "g2"

    def test_extend(self):
        """Groups extend works correctly."""
        groups = Groups([])
        groups.extend([Group(id="g1"), Group(id="g2")])

        assert len(groups) == 2

    def test_clear(self):
        """Groups clear works correctly."""
        groups = Groups([Group(id="g1"), Group(id="g2")])
        groups.clear()

        assert len(groups) == 0

    def test_sort(self):
        """Groups sort works correctly."""
        groups = Groups([Group(id="g2"), Group(id="g1")])
        groups.sort(key=lambda g: g.id)

        assert groups[0].id == "g1"
        assert groups[1].id == "g2"

    def test_getitem(self):
        """Groups __getitem__ works correctly."""
        groups = Groups([Group(id="g1"), Group(id="g2")])
        assert groups[0].id == "g1"
        assert groups[1].id == "g2"


class TestContact:
    """Tests for Contact model."""

    def test_from_element_with_jid_id(self):
        """Contact is correctly parsed from element with jid identity."""
        contact_elt = domish.Element((NS_CONTACTS, "contact"))
        contact_elt["name"] = "Test Name"
        identity_elt = contact_elt.addElement((NS_CONTACTS, "identity"))
        identity_elt["type"] = "jid"
        identity_elt.addContent("user@example.com")

        contact = Contact.from_element(contact_elt, ContactsData())

        assert contact.identities == {"jid": "user@example.com"}
        assert contact.name == "Test Name"
        assert len(contact.groups) == 0

    def test_from_element_with_group(self):
        """Contact is correctly parsed with group reference."""
        parent = domish.Element((NS_PUBSUB, "items"))
        item_elt = parent.addElement((NS_PUBSUB, "item"))
        item_elt["id"] = "contact123"
        contact_elt = item_elt.addElement((NS_CONTACTS, "contact"))
        identity_elt = contact_elt.addElement((NS_CONTACTS, "identity"))
        identity_elt["type"] = "jid"
        identity_elt.addContent("user@example.com")
        group_elt = contact_elt.addElement((NS_CONTACTS, "group"))
        group_elt["id"] = "g1"

        contacts_data = ContactsData()
        contacts_data.groups = Groups([Group(id="g1", name="Friends")])

        contact = Contact.from_element(contact_elt, contacts_data)

        assert contact.id == "contact123"
        assert len(contact.groups) == 1
        assert contact.groups[0].id == "g1"
        assert contact.groups[0].name == "Friends"

    def test_from_element_with_ancestor_item_id(self):
        """Contact id is extracted from ancestor item element."""
        parent = domish.Element((NS_PUBSUB, "items"))
        item_elt = parent.addElement((NS_PUBSUB, "item"))
        item_elt["id"] = "contact123"
        contact_elt = item_elt.addElement((NS_CONTACTS, "contact"))
        identity_elt = contact_elt.addElement((NS_CONTACTS, "identity"))
        identity_elt["type"] = "jid"
        identity_elt.addContent("user@example.com")

        contact = Contact.from_element(contact_elt, ContactsData())

        assert contact.id == "contact123"

    def test_from_element_with_ancestor_item_encrypted(self):
        """Contact encrypted flag is set from item metadata."""
        parent = domish.Element((NS_PUBSUB, "items"))
        item_elt = parent.addElement((NS_PUBSUB, "item"))
        item_elt["id"] = "contact123"
        item_elt["encrypted"] = "true"
        contact_elt = item_elt.addElement((NS_CONTACTS, "contact"))
        identity_elt = contact_elt.addElement((NS_CONTACTS, "identity"))
        identity_elt["type"] = "jid"
        identity_elt.addContent("user@example.com")

        metadata = {"encrypted": {"contact123": True}}
        contact = Contact.from_element(contact_elt, ContactsData(), metadata)

        assert contact.encrypted is True

    def test_to_element(self):
        """Contact element is correctly created."""
        contact = Contact(
            id="contact123",
            name="Test Name",
            identities={"jid": "user@example.com"},
            groups=Groups([Group(id="g1", name="Friends")]),
        )
        element = contact.to_element()

        assert element.uri == NS_CONTACTS
        assert element.name == "contact"
        assert element["name"] == "Test Name"

        identity_elements = list(element.elements(NS_CONTACTS, "identity"))
        assert len(identity_elements) == 1
        assert identity_elements[0]["type"] == "jid"
        assert str(identity_elements[0]) == "user@example.com"

        group_elements = list(element.elements(NS_CONTACTS, "group"))
        assert len(group_elements) == 1
        group_element = group_elements[0]
        assert group_element["id"] == "g1"
        assert not group_element.hasAttribute(
            "name"
        ), "name attribute should not be present in group with id_only"


class TestContacts:
    """Tests for Contacts root model."""

    def test_append_and_iterate(self):
        """Contacts can be appended and iterated."""
        contacts = Contacts([])
        contacts.append(Contact(identities={"jid": "user1@example.com"}, id="c1"))
        contacts.append(Contact(identities={"jid": "user2@example.com"}, id="c2"))

        contact_list = list(contacts)
        assert len(contact_list) == 2
        assert contact_list[0].id == "c1"
        assert contact_list[1].id == "c2"

    def test_len_and_remove(self):
        """Contacts len and remove work correctly."""
        contacts = Contacts([])
        contacts.append(Contact(identities={"jid": "user1@example.com"}, id="c1"))
        contacts.append(Contact(identities={"jid": "user2@example.com"}, id="c2"))

        assert len(contacts) == 2
        contacts.remove(contacts[0])
        assert len(contacts) == 1
        assert contacts[0].id == "c2"

    def test_extend(self):
        """Contacts extend works correctly."""
        contacts = Contacts([])
        contacts.extend(
            [
                Contact(identities={"jid": "user1@example.com"}),
                Contact(identities={"jid": "user2@example.com"}),
            ]
        )

        assert len(contacts) == 2

    def test_clear(self):
        """Contacts clear works correctly."""
        contacts = Contacts(
            [
                Contact(identities={"jid": "user1@example.com"}),
                Contact(identities={"jid": "user2@example.com"}),
            ]
        )
        contacts.clear()

        assert len(contacts) == 0

    def test_sort(self):
        """Contacts sort works correctly."""
        contacts = Contacts(
            [
                Contact(identities={"jid": "user2@example.com"}, id="c2"),
                Contact(identities={"jid": "user1@example.com"}, id="c1"),
            ]
        )
        contacts.sort(key=lambda c: c.id)

        assert contacts[0].id == "c1"
        assert contacts[1].id == "c2"

    def test_getitem(self):
        """Contacts __getitem__ works correctly."""
        contacts = Contacts(
            [
                Contact(identities={"jid": "user1@example.com"}, id="c1"),
                Contact(identities={"jid": "user2@example.com"}, id="c2"),
            ]
        )
        assert contacts[0].id == "c1"
        assert contacts[1].id == "c2"


class TestContactsData:
    """Tests for ContactsData class."""

    def test_jid_to_contacts_empty(self):
        """jid_to_contacts returns empty dict when contacts is empty."""
        data = ContactsData()
        assert data.jid_to_contacts == {}

    def test_jid_to_contacts(self):
        """jid_to_contacts correctly maps JIDs to contacts."""
        contact1 = Contact(
            id="c1",
            identities={"jid": "user1@example.com"},
        )
        contact2 = Contact(
            id="c2",
            identities={"jid": "user2@example.com"},
        )
        data = ContactsData(contacts=Contacts([contact1, contact2]))

        jid_map = data.jid_to_contacts

        assert len(jid_map) == 2
        assert jid_map["user1@example.com"].id == "c1"
        assert jid_map["user2@example.com"].id == "c2"

    def test_jid_to_contacts_skips_contact_without_id(self):
        """jid_to_contacts skips contacts without id."""
        contact1 = Contact(
            id="c1",
            identities={"jid": "user1@example.com"},
        )
        contact2 = Contact(
            id="c2",
            identities={"jid": "user2@example.com"},
        )
        contact_no_id = Contact(
            id=None,
            identities={"jid": "user3@example.com"},
        )
        data = ContactsData(contacts=Contacts([contact1, contact2, contact_no_id]))

        jid_map = data.jid_to_contacts

        assert len(jid_map) == 2
        assert "user1@example.com" in jid_map
        assert "user2@example.com" in jid_map
        assert "user3@example.com" not in jid_map

    def test_ids_to_groups(self):
        """ids_to_groups correctly maps group IDs to groups."""
        group1 = Group(id="g1", name="Group 1")
        group2 = Group(id="g2", name="Group 2")
        data = ContactsData(groups=Groups([group1, group2]))

        id_map = data.ids_to_groups

        assert len(id_map) == 2
        assert id_map["g1"].name == "Group 1"
        assert id_map["g2"].name == "Group 2"

    def test_ids_to_groups_skips_missing_id(self):
        """ids_to_groups skips groups without ID."""
        group = Group(id="g1", name="Group 1")
        group_no_id = Group(name="No ID Group")
        data = ContactsData(groups=Groups([group, group_no_id]))

        id_map = data.ids_to_groups

        assert len(id_map) == 1
        assert "g1" in id_map

    def test_reset(self):
        """Reset clears cached mappings."""
        contact = Contact(
            id="c1",
            identities={"jid": "user1@example.com"},
        )
        group = Group(id="g1", name="Group 1")
        data = ContactsData(contacts=Contacts([contact]), groups=Groups([group]))

        # Access the mappings to populate caches, and check that they are not empty.
        assert len(data.jid_to_contacts) == 1
        assert len(data.ids_to_groups) == 1

        data.reset()

        assert data._jid_to_contacts is None
        assert data._ids_to_groups is None
