import unittest.mock
from collections.abc import Callable, Iterator

import pytest
from slixmpp import JID, Iq
from slixmpp.exceptions import XMPPError

from slidge import (  # type:ignore[attr-defined]
    BaseGateway,
    BaseSession,
    LegacyBookmarks,
    LegacyContact,
    LegacyRoster,
)
from slidge.command import FormField
from slidge.command.base import (
    ConfirmationRecipient,
    ContactCommand,
    FormRecipient,
    MUCCommand,
)
from slidge.group import LegacyMUC, LegacyParticipant
from slidge.util.test import SlidgeTest  # type:ignore[attr-defined]


class Session(BaseSession["Contact | LegacyMUC[LegacyParticipant]"]):
    async def login(self) -> None:
        pass


class Gateway(BaseGateway):
    COMPONENT_NAME = "Megacorp Chat XMPP Gateway"
    GROUPS = True


class Contact(LegacyContact):
    pass


class Roster(LegacyRoster[Contact]):
    async def jid_username_to_legacy_id(self, jid_username: str) -> str:
        if jid_username.startswith("room-"):
            raise XMPPError("item-not-found", "This is a MUC JID")
        return jid_username

    async def legacy_id_to_jid_username(self, legacy_id: str) -> str:
        if legacy_id.startswith("room-"):
            raise XMPPError("item-not-found", "This is a MUC JID")
        return legacy_id


class Bookmarks(LegacyBookmarks[LegacyMUC[LegacyParticipant]]):
    async def jid_username_to_legacy_id(self, jid_username: str) -> str:
        if not jid_username.startswith("room-"):
            raise XMPPError("item-not-found", "This is a contact JID")
        return jid_username

    async def legacy_id_to_jid_username(self, legacy_id: str) -> str:
        if not legacy_id.startswith("room-"):
            raise XMPPError("item-not-found", "This is a contact JID")
        return legacy_id

    async def fill(self) -> None:
        pass


class ContactCommand1(ContactCommand[Contact]):
    NAME = "contact command #1"
    NODE = CHAT_COMMAND = "command1"
    HELP = "Do stuff"

    @staticmethod
    async def run(contact: Contact, *args: str) -> str:
        return f"it worked 1! {contact.legacy_id}"


class ContactCommand2(ContactCommand[Contact]):
    NAME = "contact command #2"
    NODE = CHAT_COMMAND = "command2"
    HELP = "Do some stuff"

    @staticmethod
    async def run(contact: Contact, *args: str) -> FormRecipient[Contact] | str:
        if args:
            return f"Single arg: {args[0]}"

        return FormRecipient[Contact](
            title="a title",
            instructions="some instructions",
            fields=[FormField(var="a var", label="a label", value=contact.legacy_id)],
            handler=ContactCommand2.confirm,
        )

    @staticmethod
    async def confirm(
        contact: Contact, form: dict[str, str | JID | bool]
    ) -> ConfirmationRecipient[Contact]:
        return ConfirmationRecipient(
            prompt=str(form["a var"]), handler=ContactCommand2.finish
        )

    @staticmethod
    async def finish(contact: Contact) -> str:
        return "yay!"


class MUCCommand1(MUCCommand[LegacyMUC[LegacyParticipant]]):
    NAME = "room command #1"
    NODE = CHAT_COMMAND = "command1"
    HELP = "Do stuff"

    @staticmethod
    async def run(muc: LegacyMUC[LegacyParticipant], *args: str) -> str:
        return f"it worked 1! {muc.legacy_id}"


class MUCCommand2(MUCCommand[LegacyMUC[LegacyParticipant]]):
    NAME = "room command #2"
    NODE = CHAT_COMMAND = "command2"
    HELP = "Do some stuff"

    @staticmethod
    async def run(
        muc: LegacyMUC[LegacyParticipant], *args: str
    ) -> FormRecipient[LegacyMUC[LegacyParticipant]] | str:
        if args:
            return f"Single arg: {args[0]}"

        return FormRecipient(
            title="a title",
            instructions="some instructions",
            fields=[FormField(var="a var", label="a label", value=muc.legacy_id)],
            handler=MUCCommand2.confirm,
        )

    @staticmethod
    async def confirm(
        muc: LegacyMUC[LegacyParticipant],
        form: dict[str, str | JID | bool],
    ) -> ConfirmationRecipient[LegacyMUC[LegacyParticipant]]:
        return ConfirmationRecipient(
            prompt=str(form["a var"]), handler=MUCCommand2.finish
        )

    @staticmethod
    async def finish(muc: LegacyMUC[LegacyParticipant]) -> str:
        return "yay!"


class _AdhocSetup(SlidgeTest):  # type:ignore[misc]
    plugin = globals()

    def setUp(self) -> None:
        super().setUp()
        self._add_slidge_user()
        self.__patch = unittest.mock.patch(
            "slixmpp.plugins.xep_0050.adhoc.XEP_0050.new_session", return_value="sid"
        )
        self.__patch.start()

    def tearDown(self) -> None:
        super().tearDown()
        self.__patch.stop()


class _AdhocTests:
    _test_command_jid: str
    _test_command_kind: str
    _test_command_legacy_id: str

    recv: Callable[[str], None]
    send: Callable[[str], None]
    next_sent: Callable[[], Iq]

    def test_info(self) -> None:
        self.recv(  # language=XML
            f"""
            <iq type='get'
                to='{self._test_command_jid}'
                from='romeo@shakespeare.lit/client'>
              <query xmlns='http://jabber.org/protocol/disco#info' />
            </iq>
            """
        )
        iq = self.next_sent()
        assert "http://jabber.org/protocol/commands" in iq["disco_info"]["features"]

    def test_items(self) -> None:
        self.recv(  # language=XML
            f"""
            <iq type='get'
                to='{self._test_command_jid}'
                from='romeo@shakespeare.lit/client'>
              <query xmlns='http://jabber.org/protocol/disco#items'
                     node='http://jabber.org/protocol/commands' />
            </iq>
            """
        )
        self.send(  # language=XML
            f"""
            <iq type="result"
                to="romeo@shakespeare.lit/client"
                from="{self._test_command_jid}"
                id="1">
              <query xmlns="http://jabber.org/protocol/disco#items">
                <item jid="{self._test_command_jid}"
                      node="command1"
                      name="{self._test_command_kind} command #1" />
                <item jid="{self._test_command_jid}"
                      node="command2"
                      name="{self._test_command_kind} command #2" />
              </query>
            </iq>
            """
        )

    def test_command_info(self) -> None:
        self.recv(  # language=XML
            f"""
            <iq type='get'
                to='{self._test_command_jid}'
                from='romeo@shakespeare.lit/client'>
              <query xmlns='http://jabber.org/protocol/disco#info'
                     node='command1'
                     name='{self._test_command_kind} command #1' />
            </iq>
            """
        )
        self.send(  # language=XML
            f"""
            <iq type="result"
                to="romeo@shakespeare.lit/client"
                from="{self._test_command_jid}"
                id="1">
              <query xmlns='http://jabber.org/protocol/disco#info'
                     node='command1'>
                <identity name='{self._test_command_kind} command #1'
                          category='automation'
                          type='command-node' />
                <feature var='http://jabber.org/protocol/commands' />
                <feature var='jabber:x:data' />
              </query>
            </iq>
            """
        )

    def test_execute_basic(self) -> None:
        self.recv(  # language=XML
            f"""
            <iq type='set'
                from='romeo@shakespeare.lit/client'
                to='{self._test_command_jid}'
                id='exec1'>
              <command xmlns='http://jabber.org/protocol/commands'
                       node='command1'
                       action='execute' />
            </iq>
            """
        )
        self.send(  # language=XML
            f"""
            <iq type="result"
                from="{self._test_command_jid}"
                to="romeo@shakespeare.lit/client"
                id="exec1">
              <command xmlns="http://jabber.org/protocol/commands"
                       node="command1"
                       sessionid="sid"
                       status="completed">
                <note type="info">it worked 1! {self._test_command_legacy_id}</note>
              </command>
            </iq>
            """
        )

    def test_execute_multistage(self) -> None:
        self.recv(  # language=XML
            f"""
            <iq type='set'
                from='romeo@shakespeare.lit/client'
                to='{self._test_command_jid}'
                id='exec1'>
              <command xmlns='http://jabber.org/protocol/commands'
                       node='command2'
                       action='execute' />
            </iq>
            """
        )
        self.send(  # language=XML
            f"""
            <iq type="result"
                from="{self._test_command_jid}"
                to="romeo@shakespeare.lit/client"
                id="exec1">
              <command xmlns="http://jabber.org/protocol/commands"
                       node="command2"
                       sessionid="sid"
                       status="executing">
                <actions>
                  <next />
                </actions>
                <x xmlns="jabber:x:data"
                   type="form">
                  <title>a title</title>
                  <instructions>some instructions</instructions>
                  <field var="a var"
                         label="a label"
                         type="text-single">
                    <value>{self._test_command_legacy_id}</value>
                  </field>
                </x>
              </command>
            </iq>
            """
        )
        self.recv(  # language=XML
            f"""
            <iq type="set"
                to="{self._test_command_jid}"
                from="romeo@shakespeare.lit/client"
                id="exec1">
              <command xmlns="http://jabber.org/protocol/commands"
                       node="command2"
                       sessionid="sid">
                <x xmlns="jabber:x:data"
                   type="submit">
                  <field var="a var">
                    <value>YAYAYA</value>
                  </field>
                </x>
              </command>
            </iq>
            """
        )
        self.send(  # language=XML
            f"""
            <iq type="result"
                to="romeo@shakespeare.lit/client"
                from="{self._test_command_jid}"
                id="exec1">
              <command xmlns="http://jabber.org/protocol/commands"
                       node="command2"
                       sessionid="sid"
                       status="executing">
                <actions>
                  <next />
                </actions>
                <x xmlns="jabber:x:data"
                   type="form">
                  <title>YAYAYA</title>
                  <field var="confirm"
                         label="Confirm"
                         type="boolean">
                    <value>1</value>
                  </field>
                </x>
              </command>
            </iq>
            """
        )
        self.recv(  # language=XML
            f"""
            <iq type="set"
                to="{self._test_command_jid}"
                from="romeo@shakespeare.lit/client"
                id="exec1">
              <command xmlns="http://jabber.org/protocol/commands"
                       node="command2"
                       sessionid="sid">
                <x xmlns="jabber:x:data"
                   type="submit">
                  <field var="confirm">
                    <value>1</value>
                  </field>
                </x>
              </command>
            </iq>
            """
        )
        self.send(  # language=XML
            f"""
            <iq type="result"
                from="{self._test_command_jid}"
                to="romeo@shakespeare.lit/client"
                id="exec1">
              <command xmlns="http://jabber.org/protocol/commands"
                       node="command2"
                       sessionid="sid"
                       status="completed">
                <note type="info">yay!</note>
              </command>
            </iq>
            """
        )


class TestContactAdhoc(_AdhocSetup, _AdhocTests):
    _test_command_jid = "juliet@aim.shakespeare.lit/slidge"
    _test_command_kind = "contact"
    _test_command_legacy_id = "juliet"


class TestMUCAdhoc(_AdhocSetup, _AdhocTests):
    _test_command_jid = "room-command@aim.shakespeare.lit"
    _test_command_kind = "room"
    _test_command_legacy_id = "room-command"


class _ChatMixin(SlidgeTest):  # type:ignore[misc]
    plugin = globals()

    def setUp(self) -> None:
        super().setUp()
        self._add_slidge_user()

    def _recv(self, body: str) -> None:
        self.recv(  # language=XML
            f"""
            <message from='romeo@shakespeare.lit/slidge'
                     to='aim.shakespeare.lit'
                     type='chat'>
              <body>{body}</body>
            </message>
            """
        )

    def _send(self, body: str) -> None:
        self.send(  # language=XML
            f"""
            <message from="aim.shakespeare.lit"
                     to="romeo@shakespeare.lit/slidge"
                     type='chat'>
              <body>{body}</body>
            </message>
            """
        )

    def _error(self, condition: str, type: str, text: str) -> None:
        el = f"<{condition} xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' />"
        self.send(  # language=XML
            f"""
            <message from="aim.shakespeare.lit"
                     to="romeo@shakespeare.lit/slidge"
                     type="error">
              <error type="{type}">{el};
              <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">{text}</text></error>
            </message>
            """
        )


class TestContactChat(_ChatMixin):
    def test_execute(self) -> None:
        self._recv("contact juliet command2")
        self._send("a title")
        self._send("some instructions")
        self._send("Default: juliet")
        self._send("a label? (or 'abort')")
        self._recv("echome")
        self._send("echome")
        self._recv("yes")
        self._send("yay!")

    def test_execute_with_args(self) -> None:
        self._recv("contact juliet command2 some-arg")
        self._send("Single arg: some-arg")

    def test_execute_with_invalid_contact(self) -> None:
        self._recv("contact room-prout command2")
        self._error("item-not-found", "cancel", "This is a MUC JID")

    def test_help(self) -> None:
        self._recv("contact")
        self._send(_HELP.format(recipient="contact"))

    def test_single(self) -> None:
        self._recv("contact bad-syntax")
        self._send(_HELP.format(recipient="contact"))
        self._error(
            "bad-request",
            "modify",
            "Contact commands require at least two parameters: contact_jid_username and command_name",
        )


class TestMUCChat(_ChatMixin):
    def test_execute(self) -> None:
        self._recv("room room-command command2")
        self._send("a title")
        self._send("some instructions")
        self._send("Default: room-command")
        self._send("a label? (or 'abort')")
        self._recv("echome")
        self._send("echome")
        self._recv("yes")
        self._send("yay!")

    def test_execute_with_args(self) -> None:
        self._recv("room room-command command2 some-arg")
        self._send("Single arg: some-arg")

    def test_execute_with_invalid_contact(self) -> None:
        self._recv("room juliet command2")
        self._error("item-not-found", "cancel", "This is a contact JID")

    def test_help(self) -> None:
        self._recv("room")
        self._send(_HELP.format(recipient="room"))

    def test_single(self) -> None:
        self._recv("room bad-syntax")
        self._send(_HELP.format(recipient="room"))
        self._error(
            "bad-request",
            "modify",
            "Contact commands require at least two parameters: room_jid_username and command_name",
        )


_HELP = """Available commands:
command1 ({recipient} command #1): Do stuff
command2 ({recipient} command #2): Do some stuff"""


@pytest.fixture(scope="module", autouse=True)
def _reset_commands() -> Iterator[None]:
    yield
    LegacyContact.commands.clear()
    LegacyMUC.commands.clear()
