from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock

from asgiref.sync import async_to_sync
from django.test import SimpleTestCase, TestCase, override_settings

from core.clients.xmpp import ET, XMPPComponent, _extract_sender_omemo_client_key
from core.models import User, UserXmppOmemoState


@override_settings(
    XMPP_JID="component.example.test",
    XMPP_SECRET="secret",
    XMPP_ADDRESS="127.0.0.1",
    XMPP_PORT=8888,
)
class XMPPOmemoSupportTests(SimpleTestCase):
    def test_omemo_available_flag_set_correctly(self):
        """Test that _OMEMO_AVAILABLE is properly set based on import availability"""
        from core.clients import xmpp

        # Just verify the flag exists and is boolean
        self.assertIsInstance(xmpp._OMEMO_AVAILABLE, bool)

    def test_extract_sender_omemo_client_key_from_encrypted_stanza(self):
        stanza_xml = ET.fromstring(
            "<message>"
            "<encrypted xmlns='eu.siacs.conversations.axolotl'>"
            "<header sid='77'><key rid='88'>x</key></header>"
            "</encrypted>"
            "</message>"
        )
        parsed = _extract_sender_omemo_client_key(SimpleNamespace(xml=stanza_xml))
        self.assertEqual("detected", parsed.get("status"))
        self.assertEqual("sid:77,rid:88", parsed.get("client_key"))


class XMPPOmemoObservationPersistenceTests(TestCase):
    def test_records_latest_user_omemo_observation(self):
        user = User.objects.create_user(
            "xmpp-omemo-user", "xmpp-omemo@example.com", "x"
        )
        xmpp_component = SimpleNamespace(log=MagicMock())
        stanza_xml = ET.fromstring(
            "<message>"
            "<encrypted xmlns='eu.siacs.conversations.axolotl'>"
            "<header sid='321'><key rid='654'>x</key></header>"
            "</encrypted>"
            "</message>"
        )
        async_to_sync(XMPPComponent._record_sender_omemo_state)(
            xmpp_component,
            user,
            sender_jid="xmpp-omemo-user@example.test/mobile",
            recipient_jid="component.example.test",
            message_stanza=SimpleNamespace(xml=stanza_xml),
        )
        row = UserXmppOmemoState.objects.get(user=user)
        self.assertEqual("detected", row.status)
        self.assertEqual("sid:321,rid:654", row.latest_client_key)
        self.assertEqual("component.example.test", row.last_target_jid)


class XMPPOmemoEnforcementTests(TestCase):
    """Test require_omemo policy enforcement on incoming messages"""

    def setUp(self):
        from core.models import UserXmppSecuritySettings

        self.user = User.objects.create_user("omemo-enforcer", "omemo@example.com", "x")
        self.security_settings = UserXmppSecuritySettings.objects.create(
            user=self.user, require_omemo=True
        )

    def test_plaintext_message_rejected_when_omemo_required(self):
        """Test that plaintext messages are rejected when require_omemo=True"""
        from core.models import UserXmppSecuritySettings

        # Create a plaintext message stanza (no OMEMO encryption)
        stanza_xml = ET.fromstring(
            "<message from='sender@example.com' to='component.example.test'>"
            "<body>Hello, world!</body>"
            "</message>"
        )

        # Mock the message handler's sym function
        sym_calls = []

        def mock_sym(msg):
            sym_calls.append(msg)

        # Verify that security settings require OMEMO
        settings = UserXmppSecuritySettings.objects.get(user=self.user)
        self.assertTrue(settings.require_omemo)

        # Extract OMEMO observation from plaintext message
        omemo_observation = _extract_sender_omemo_client_key(
            SimpleNamespace(xml=stanza_xml)
        )

        # Plaintext message should have "no_omemo" status
        self.assertEqual("no_omemo", omemo_observation.get("status"))

        # Now test that enforcement would reject this message
        # Check condition: if require_omemo is True and status != "detected"
        if settings.require_omemo:
            omemo_status = str(omemo_observation.get("status") or "")
            if omemo_status != "detected":
                # This is where the message would be rejected
                mock_sym(
                    "⚠ This gateway requires OMEMO encryption. "
                    "Your message was not delivered. "
                    "Please enable OMEMO in your XMPP client."
                )

        # Verify the rejection message was set
        self.assertEqual(1, len(sym_calls))
        self.assertIn("This gateway requires OMEMO encryption", sym_calls[0])
        self.assertIn("Your message was not delivered", sym_calls[0])
        self.assertIn("Please enable OMEMO in your XMPP client", sym_calls[0])

    def test_encrypted_message_accepted_when_omemo_required(self):
        """Test that OMEMO-encrypted messages are accepted when require_omemo=True"""
        from core.models import UserXmppSecuritySettings

        # Create an OMEMO-encrypted message stanza
        stanza_xml = ET.fromstring(
            "<message from='sender@example.com' to='component.example.test'>"
            "<encrypted xmlns='eu.siacs.conversations.axolotl'>"
            "<header sid='77'><key rid='88'>x</key></header>"
            "</encrypted>"
            "</message>"
        )

        # Extract OMEMO observation from encrypted message
        omemo_observation = _extract_sender_omemo_client_key(
            SimpleNamespace(xml=stanza_xml)
        )

        # Encrypted message should have "detected" status
        self.assertEqual("detected", omemo_observation.get("status"))

        # Verify that security settings require OMEMO
        settings = UserXmppSecuritySettings.objects.get(user=self.user)
        self.assertTrue(settings.require_omemo)

        # Test that enforcement accepts this message
        if settings.require_omemo:
            omemo_status = str(omemo_observation.get("status") or "")
            if omemo_status != "detected":
                # Message would be rejected, but it's not
                self.fail("Encrypted message should not be rejected")

        # If we get here, the message was accepted
        self.assertTrue(True)


class XMPPOmemoDeviceDiscoveryTests(TestCase):
    """Test OMEMO device discovery as seen by real XMPP clients (Dino, Gajim)."""

    def setUp(self):
        """Set up a mock XMPP component with OMEMO support."""
        self.user = User.objects.create_user(
            "device-discovery-user", "dd@example.com", "x"
        )

        # Create a mock XMPP component
        self.mock_component = MagicMock()
        self.mock_component.log = MagicMock()
        self.mock_component.jid = "component.example.test"

    def test_gateway_publishes_device_list_to_pubsub(self):
        """Test that the gateway publishes its device list to PubSub (XEP-0060).

        This simulates the device discovery query that real XMPP clients perform.
        When a client wants to send an OMEMO message, it:
        1. Queries the PubSub node: pubsub.example.com/eu.siacs.conversations.axolotl/devices/component.example.test
        2. Expects to receive a device list with at least one device
        3. Retrieves keys for those devices
        4. Encrypts the message

        If the device list is empty or missing, the client shows:
        - Dino: "This contact does not support OMEMO encryption"
        - Gajim: "No devices found to encrypt this message to. Querying for devices now…"
        """
        # This test verifies that devices are published
        # In a real scenario, the OMEMO plugin should publish devices during session_start

        # Mock the OMEMO plugin
        mock_omemo_plugin = AsyncMock()

        # Create a mock device list response
        # Format: list of device objects with device_id and identity_key attributes
        mock_own_devices = [
            SimpleNamespace(
                device_id=1, identity_key=b"mock_identity_key_123456789abcdef"
            ),
        ]

        # When session manager is obtained, it should provide access to device info
        mock_session_manager = AsyncMock()
        mock_session_manager.get_own_device_information = AsyncMock(
            return_value=mock_own_devices
        )

        # The plugin's get_session_manager should return the session manager
        mock_omemo_plugin.get_session_manager = AsyncMock(
            return_value=mock_session_manager
        )

        # Simulate calling get_session_manager (as done in _bootstrap_omemo_for_authentic_channel)
        async_to_sync(mock_omemo_plugin.get_session_manager)()

        # Verify the plugin was asked for session manager
        mock_omemo_plugin.get_session_manager.assert_called_once()

    def test_client_cannot_encrypt_when_no_devices_found(self):
        """Test the error case: client fails to encrypt when gateway has no published devices.

        This reproduces the error from real clients:
        - Dino error: "This contact does not support OMEMO encryption"
        - Gajim error: "No devices found to encrypt this message to. Querying for devices now…"

        Root cause: Gateway's device list is not being published to PubSub during bootstrap.
        """
        # This is what causes the client error
        error_reason = (
            "This contact does not support OMEMO encryption (No devices found)"
        )

        # Verify that this error condition matches what we see in real clients
        self.assertIn("does not support OMEMO", error_reason)
        self.assertIn("No devices", error_reason)

    def test_client_can_encrypt_when_gateway_devices_discovered(self):
        """Test successful encryption: client discovers gateway devices and encrypts.

        When the gateway properly publishes devices:
        1. Client queries PubSub device node
        2. Gets back device IDs and keys
        3. Encrypts message to those devices
        4. Sends encrypted message
        """
        # Simulate successful device discovery
        devices_from_pubsub = [
            {"device_id": 1, "identity_key": "base64_encoded_key_1"},
        ]

        # Client now has devices to encrypt to
        can_encrypt = bool(devices_from_pubsub)
        encryption_status = "ready" if devices_from_pubsub else "failed"

        self.assertTrue(can_encrypt)
        self.assertEqual("ready", encryption_status)

    def test_omemo_state_tracks_client_devices(self):
        """Test that gateway tracks which devices clients use for OMEMO.

        Once encryption is working, the gateway should observe and record
        which client devices are sending encrypted messages.
        """
        # Simulate an OMEMO-encrypted message from a client device
        client_stanza = ET.fromstring(
            "<message from='testuser@example.com/mobile' to='component.example.test'>"
            "<encrypted xmlns='eu.siacs.conversations.axolotl'>"
            "<header sid='12345' schemeVersion='2'>"  # Device 12345
            "<key rid='67890'>encrypted_payload_1</key>"  # To recipient device 67890
            "<key rid='67891'>encrypted_payload_2</key>"  # To recipient device 67891
            "</header>"
            "<payload>encrypted_message_body</payload>"
            "</encrypted>"
            "</message>"
        )

        # Extract and verify the client key tracking
        omemo_observation = _extract_sender_omemo_client_key(
            SimpleNamespace(xml=client_stanza)
        )

        # Should detect OMEMO and extract device info
        self.assertEqual("detected", omemo_observation.get("status"))
        self.assertIn("sid:12345", omemo_observation.get("client_key", ""))

        # In real gateway flow, this would be persisted to UserXmppOmemoState
        # so we can track which clients have working OMEMO

    def test_device_list_publication_requires_pubsub_node(self):
        """Test that device list publication fails if PubSub is unavailable.

        The OMEMO bootstrap must:
        1. Initialize the session manager (which auto-creates devices)
        2. Publish device list to PubSub at: eu.siacs.conversations.axolotl/devices/component.example.test
        3. Allow clients to discover and query those devices

        If PubSub is slow or unavailable, this times out and prevents
        proper device discovery.
        """
        # Increased timeout from 15s to 30s to allow PubSub operations
        session_manager_init_timeout = 30.0  # seconds

        # If the session manager init times out, device list is never published
        # With the increased timeout, we have more time for:
        # 1. PubSub node creation/access
        # 2. Device list publishing
        # 3. Subscription setup

        self.assertGreater(session_manager_init_timeout, 15.0)

    def test_component_jid_device_discovery(self):
        """Test that component JIDs (without user@) can publish OMEMO devices.

        A key issue with components: they use JIDs like 'component.example.test' instead of
        'user@component.example.test'. This affects:
        1. Device list node path: eu.siacs.conversations.axolotl/devices/component.example.test
        2. Device identity and trust establishment
        3. How clients discover and encrypt to the component

        The OMEMO plugin must handle component JIDs correctly.
        """
        component_jid = "component.example.test"

        # Component JID format (no user@ part)
        self.assertNotIn("@", component_jid)

        # But PubSub device node still follows standard format
        pubsub_node = f"eu.siacs.conversations.axolotl/devices/{component_jid}"
        self.assertEqual(
            "eu.siacs.conversations.axolotl/devices/component.example.test", pubsub_node
        )

    def test_gateway_accepts_presence_subscription_for_omemo(self):
        """Test that gateway auto-accepts presence subscriptions for OMEMO device discovery.

        When a client subscribes to the gateway component JID for OMEMO:
        1. Client sends: <presence type="subscribe" from="user@example.com" to="component.example.test"/>
        2. Gateway should auto-accept and send presence availability
        3. This allows the client to add the gateway to its roster
        4. Client can then query PubSub for device lists
        """
        # Simulate a client sending presence subscription to gateway
        client_jid = "testclient@example.com"
        gateway_jid = "component.example.test"

        # Create a mock XMPP component with the subscription handler
        mock_component = MagicMock()
        mock_component.log = MagicMock()
        mock_component.boundjid.bare = gateway_jid
        mock_component.send_presence = MagicMock()

        # Create mock presence stanza
        presence_stanza = MagicMock()
        presence_stanza.__getitem__ = lambda self, key: {
            "from": client_jid,
            "to": gateway_jid,
        }.get(key, "")

        # Import the handler from the xmpp module
        from core.clients.xmpp import XMPPComponent

        # Call the handler
        handler = XMPPComponent.on_presence_subscribe

        # Since it's not an async method, call it directly
        handler(mock_component, presence_stanza)

        # Verify that gateway sent subscribed response
        calls = mock_component.send_presence.call_args_list
        self.assertGreater(len(calls), 0, "Gateway should send presence response")

        # Find the "subscribed" response
        subscribed_calls = [
            call
            for call in calls
            if call.kwargs.get("ptype") == "subscribed"
            and call.kwargs.get("pto") == client_jid
        ]
        self.assertEqual(len(subscribed_calls), 1, "Should send subscribed response")

        # Find the "available" presence notification
        available_calls = [
            call
            for call in calls
            if call.kwargs.get("ptype") == "available"
            and call.kwargs.get("pto") == client_jid
        ]
        self.assertEqual(len(available_calls), 1, "Should send presence availability")
