"""Tests for the Nextcloud Talk plugin (Python)."""

from __future__ import annotations

import hashlib
import hmac
import os
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from elizaos_plugin_nextcloud_talk.actions.send_message import (
    SEND_MESSAGE_ACTION,
    SendMessageResult,
    handle_send_message,
    validate_send_message,
)
from elizaos_plugin_nextcloud_talk.client import (
    generate_signature,
    verify_signature,
)
from elizaos_plugin_nextcloud_talk.config import NextcloudTalkConfig
from elizaos_plugin_nextcloud_talk.error import (
    ApiError,
    AuthenticationError,
    ConfigError,
    MessageSendError,
    NextcloudTalkError,
    RoomNotAllowedError,
    ServiceNotInitializedError,
    SignatureVerificationError,
)
from elizaos_plugin_nextcloud_talk.providers.chat_state import (
    CHAT_STATE_PROVIDER,
    ChatStateResult,
    get_chat_state,
)
from elizaos_plugin_nextcloud_talk.service import NextcloudTalkService
from elizaos_plugin_nextcloud_talk.types import (
    NextcloudTalkActor,
    NextcloudTalkContent,
    NextcloudTalkEventType,
    NextcloudTalkInboundMessage,
    NextcloudTalkObject,
    NextcloudTalkRoom,
    NextcloudTalkRoomType,
    NextcloudTalkSendResult,
    NextcloudTalkTarget,
    NextcloudTalkUser,
    NextcloudTalkWebhookHeaders,
    NextcloudTalkWebhookPayload,
)


# ---------------------------------------------------------------------------
# Action spec tests
# ---------------------------------------------------------------------------


class TestSendMessageActionSpec:
    """Tests for the SEND_MESSAGE_ACTION specification."""

    def test_action_name(self) -> None:
        assert SEND_MESSAGE_ACTION["name"] == "SEND_NEXTCLOUD_TALK_MESSAGE"

    def test_action_description(self) -> None:
        assert "nextcloud talk" in SEND_MESSAGE_ACTION["description"].lower()

    def test_action_has_similes(self) -> None:
        similes = SEND_MESSAGE_ACTION["similes"]
        assert isinstance(similes, list)
        assert len(similes) > 0

    def test_action_similes_contain_key_aliases(self) -> None:
        similes = SEND_MESSAGE_ACTION["similes"]
        assert "NEXTCLOUD_TALK_SEND_MESSAGE" in similes
        assert "NC_TALK_SEND" in similes

    def test_action_has_examples(self) -> None:
        examples = SEND_MESSAGE_ACTION["examples"]
        assert isinstance(examples, list)
        assert len(examples) >= 1

    def test_action_examples_structure(self) -> None:
        first_example = SEND_MESSAGE_ACTION["examples"][0]
        assert isinstance(first_example, list)
        assert len(first_example) == 2
        assert "name" in first_example[0]
        assert "content" in first_example[0]


# ---------------------------------------------------------------------------
# Action validation tests
# ---------------------------------------------------------------------------


class TestSendMessageValidation:
    """Tests for validate_send_message."""

    def test_validates_nextcloud_talk_source(self) -> None:
        assert validate_send_message("nextcloud-talk") is True

    def test_rejects_discord_source(self) -> None:
        assert validate_send_message("discord") is False

    def test_rejects_mattermost_source(self) -> None:
        assert validate_send_message("mattermost") is False

    def test_rejects_none_source(self) -> None:
        assert validate_send_message(None) is False

    def test_rejects_empty_source(self) -> None:
        assert validate_send_message("") is False


# ---------------------------------------------------------------------------
# Action handler tests
# ---------------------------------------------------------------------------


class TestHandleSendMessage:
    """Tests for handle_send_message."""

    @pytest.mark.asyncio
    async def test_basic_send(self) -> None:
        result = await handle_send_message("room123", "Hello, world!")
        assert result.success is True
        assert result.text == "Hello, world!"
        assert result.room_token == "room123"

    @pytest.mark.asyncio
    async def test_send_with_reply(self) -> None:
        result = await handle_send_message("room1", "Reply", reply_to_message_id="msg42")
        assert result.success is True
        assert result.text == "Reply"

    @pytest.mark.asyncio
    async def test_result_fields(self) -> None:
        result = await handle_send_message("room1", "Test")
        assert isinstance(result, SendMessageResult)
        assert result.message_id is None
        assert result.error is None

    @pytest.mark.asyncio
    async def test_result_dataclass(self) -> None:
        result = SendMessageResult(success=False, text="err", error="boom")
        assert result.success is False
        assert result.error == "boom"
        assert result.room_token is None


# ---------------------------------------------------------------------------
# Provider tests
# ---------------------------------------------------------------------------


class TestChatStateProvider:
    """Tests for the chat_state provider."""

    def test_provider_spec_name(self) -> None:
        assert CHAT_STATE_PROVIDER["name"] == "nextcloud_talk_chat_state"

    def test_provider_spec_description(self) -> None:
        assert "Nextcloud Talk" in CHAT_STATE_PROVIDER["description"]

    def test_provider_spec_dynamic(self) -> None:
        assert CHAT_STATE_PROVIDER["dynamic"] is True

    def test_group_chat_state(self) -> None:
        result = get_chat_state(
            room_token="room1",
            sender_id="user1",
            is_group_chat=True,
        )
        assert result.data["is_group_chat"] is True
        assert result.data["is_private"] is False

    def test_private_chat_state(self) -> None:
        result = get_chat_state(
            room_token="room1",
            is_group_chat=False,
        )
        assert result.data["is_group_chat"] is False
        assert result.data["is_private"] is True

    def test_unspecified_chat_type(self) -> None:
        result = get_chat_state(room_token="room1")
        assert result.data["is_group_chat"] is False
        assert result.data["is_private"] is False

    def test_values_dict(self) -> None:
        result = get_chat_state(
            room_token="room1",
            sender_id="user1",
            room_name="General",
            room_id="rid1",
            base_url="https://nc.test.com",
        )
        assert result.values["room_token"] == "room1"
        assert result.values["sender_id"] == "user1"
        assert result.values["room_name"] == "General"
        assert result.values["room_id"] == "rid1"
        assert result.values["base_url"] == "https://nc.test.com"

    def test_text_output_contains_room(self) -> None:
        result = get_chat_state(room_token="room1", room_name="General")
        assert "Room Token: room1" in result.text
        assert "Room Name: General" in result.text

    def test_text_output_group_label(self) -> None:
        result = get_chat_state(is_group_chat=True)
        assert "Group" in result.text

    def test_text_output_private_label(self) -> None:
        result = get_chat_state(is_group_chat=False)
        assert "Private (1:1)" in result.text

    def test_text_output_base_url(self) -> None:
        result = get_chat_state(base_url="https://nc.test.com")
        assert "Nextcloud URL: https://nc.test.com" in result.text

    def test_empty_state(self) -> None:
        result = get_chat_state()
        assert isinstance(result, ChatStateResult)
        assert result.data["room_token"] is None
        assert result.values["room_token"] == ""
        assert "Nextcloud Talk Chat State:" in result.text


# ---------------------------------------------------------------------------
# Type construction tests
# ---------------------------------------------------------------------------


class TestTypeConstruction:
    """Tests for constructing Nextcloud Talk types."""

    def test_event_type_enum(self) -> None:
        assert NextcloudTalkEventType.MESSAGE_RECEIVED == "NEXTCLOUD_TALK_MESSAGE_RECEIVED"
        assert NextcloudTalkEventType.MESSAGE_SENT == "NEXTCLOUD_TALK_MESSAGE_SENT"
        assert NextcloudTalkEventType.WORLD_CONNECTED == "NEXTCLOUD_TALK_WORLD_CONNECTED"
        assert NextcloudTalkEventType.REACTION_SENT == "NEXTCLOUD_TALK_REACTION_SENT"

    def test_room_type_enum(self) -> None:
        assert NextcloudTalkRoomType.ONE_TO_ONE.value == "one-to-one"
        assert NextcloudTalkRoomType.GROUP.value == "group"
        assert NextcloudTalkRoomType.PUBLIC.value == "public"
        assert NextcloudTalkRoomType.CHANGELOG.value == "changelog"

    def test_room_is_group_chat(self) -> None:
        room = NextcloudTalkRoom(
            token="r1",
            name="General",
            display_name="General",
            type=NextcloudTalkRoomType.GROUP,
        )
        assert room.is_group_chat() is True

    def test_room_is_not_group_chat(self) -> None:
        room = NextcloudTalkRoom(
            token="r1",
            name="DM",
            display_name="DM",
            type=NextcloudTalkRoomType.ONE_TO_ONE,
        )
        assert room.is_group_chat() is False

    def test_room_public_is_group_chat(self) -> None:
        room = NextcloudTalkRoom(
            token="r1",
            name="Public",
            display_name="Public Room",
            type=NextcloudTalkRoomType.PUBLIC,
        )
        assert room.is_group_chat() is True

    def test_user_construction(self) -> None:
        user = NextcloudTalkUser(id="u1", display_name="Alice")
        assert user.id == "u1"
        assert user.display_name == "Alice"
        assert user.actor_type is None

    def test_inbound_message_construction(self) -> None:
        msg = NextcloudTalkInboundMessage(
            message_id="m1",
            room_token="r1",
            room_name="General",
            sender_id="u1",
            sender_name="Alice",
            text="Hello",
            media_type="text/plain",
            timestamp=1700000000,
        )
        assert msg.message_id == "m1"
        assert msg.is_group_chat is False

    def test_send_result_construction(self) -> None:
        result = NextcloudTalkSendResult(
            message_id="m1",
            room_token="r1",
            timestamp=1700000000,
        )
        assert result.message_id == "m1"
        assert result.timestamp == 1700000000

    def test_content_construction(self) -> None:
        content = NextcloudTalkContent(text="Hello", room_token="r1", reply_to="m2")
        assert content.text == "Hello"
        assert content.room_token == "r1"
        assert content.reply_to == "m2"
        assert content.reaction is None

    def test_webhook_payload_construction(self) -> None:
        payload = NextcloudTalkWebhookPayload(
            type="Create",
            actor=NextcloudTalkActor(type="Person", id="u1", name="Alice"),
            object=NextcloudTalkObject(
                type="Note", id="m1", name="Hello", content="Hello", media_type="text/plain"
            ),
            target=NextcloudTalkTarget(type="Collection", id="r1", name="General"),
        )
        assert payload.type == "Create"
        assert payload.actor.id == "u1"
        assert payload.object.content == "Hello"
        assert payload.target.id == "r1"

    def test_webhook_headers_construction(self) -> None:
        headers = NextcloudTalkWebhookHeaders(
            signature="sig", random="rand", backend="https://nc.test.com"
        )
        assert headers.signature == "sig"
        assert headers.random == "rand"
        assert headers.backend == "https://nc.test.com"


# ---------------------------------------------------------------------------
# Config validation tests
# ---------------------------------------------------------------------------


class TestNextcloudTalkConfig:
    """Tests for NextcloudTalkConfig."""

    def test_config_direct_construction(self) -> None:
        config = NextcloudTalkConfig(
            base_url="https://nc.test.com",
            bot_secret="secret123",
        )
        assert config.base_url == "https://nc.test.com"
        assert config.bot_secret == "secret123"
        assert config.enabled is True
        assert config.webhook_port == 8788

    def test_config_from_env(self) -> None:
        env = {
            "NEXTCLOUD_URL": "https://nc.test.com",
            "NEXTCLOUD_BOT_SECRET": "secret123",
            "NEXTCLOUD_ENABLED": "false",
            "NEXTCLOUD_WEBHOOK_PORT": "9999",
        }
        with patch.dict(os.environ, env, clear=False):
            config = NextcloudTalkConfig.from_env()
        assert config.base_url == "https://nc.test.com"
        assert config.bot_secret == "secret123"
        assert config.enabled is False
        assert config.webhook_port == 9999

    def test_config_from_env_missing_url(self) -> None:
        env = {"NEXTCLOUD_BOT_SECRET": "secret123"}
        with patch.dict(os.environ, env, clear=True):
            with pytest.raises(ValueError, match="NEXTCLOUD_URL"):
                NextcloudTalkConfig.from_env()

    def test_config_from_env_missing_secret(self) -> None:
        env = {"NEXTCLOUD_URL": "https://nc.test.com"}
        with patch.dict(os.environ, env, clear=True):
            with pytest.raises(ValueError, match="NEXTCLOUD_BOT_SECRET"):
                NextcloudTalkConfig.from_env()

    def test_is_room_allowed_empty_list(self) -> None:
        config = NextcloudTalkConfig(base_url="https://nc.test.com", bot_secret="s")
        assert config.is_room_allowed("any_room") is True

    def test_is_room_allowed_specific(self) -> None:
        config = NextcloudTalkConfig(
            base_url="https://nc.test.com",
            bot_secret="s",
            allowed_rooms=["room1", "room2"],
        )
        assert config.is_room_allowed("room1") is True
        assert config.is_room_allowed("room3") is False

    def test_validate_config_valid(self) -> None:
        config = NextcloudTalkConfig(
            base_url="https://nc.test.com",
            bot_secret="secret123",
        )
        config.validate_config()  # Should not raise

    def test_validate_config_empty_url(self) -> None:
        config = NextcloudTalkConfig(base_url="", bot_secret="secret123")
        with pytest.raises(ValueError, match="base_url"):
            config.validate_config()

    def test_validate_config_empty_secret(self) -> None:
        config = NextcloudTalkConfig(base_url="https://nc.test.com", bot_secret="")
        with pytest.raises(ValueError, match="bot_secret"):
            config.validate_config()

    def test_validate_config_invalid_url_scheme(self) -> None:
        config = NextcloudTalkConfig(base_url="ftp://nc.test.com", bot_secret="secret")
        with pytest.raises(ValueError, match="http"):
            config.validate_config()


# ---------------------------------------------------------------------------
# Client / signature tests
# ---------------------------------------------------------------------------


class TestSignature:
    """Tests for HMAC signature generation and verification."""

    def test_verify_valid_signature(self) -> None:
        secret = "test-secret"
        random_val = "abc123"
        body = '{"message":"hello"}'
        expected = hmac.new(
            secret.encode("utf-8"),
            (random_val + body).encode("utf-8"),
            hashlib.sha256,
        ).hexdigest()

        assert verify_signature(expected, random_val, body, secret) is True

    def test_verify_invalid_signature(self) -> None:
        assert verify_signature("wrong", "rand", "body", "secret") is False

    def test_verify_empty_params(self) -> None:
        assert verify_signature("", "rand", "body", "secret") is False
        assert verify_signature("sig", "", "body", "secret") is False
        assert verify_signature("sig", "rand", "body", "") is False

    def test_generate_signature_returns_pair(self) -> None:
        random_val, sig = generate_signature("body", "secret")
        assert isinstance(random_val, str)
        assert isinstance(sig, str)
        assert len(random_val) > 0
        assert len(sig) > 0

    def test_generated_signature_verifies(self) -> None:
        body = '{"message":"test"}'
        secret = "my-secret"
        random_val, sig = generate_signature(body, secret)
        assert verify_signature(sig, random_val, body, secret) is True


# ---------------------------------------------------------------------------
# Error classes tests
# ---------------------------------------------------------------------------


class TestErrorClasses:
    """Tests for custom error classes."""

    def test_base_error(self) -> None:
        err = NextcloudTalkError("test error")
        assert str(err) == "test error"
        assert err.cause is None

    def test_config_error(self) -> None:
        err = ConfigError("bad config")
        assert isinstance(err, NextcloudTalkError)

    def test_service_not_initialized(self) -> None:
        err = ServiceNotInitializedError()
        assert "not initialized" in str(err).lower()

    def test_auth_error(self) -> None:
        err = AuthenticationError()
        assert "authentication" in str(err).lower()

    def test_signature_verification_error(self) -> None:
        err = SignatureVerificationError()
        assert "signature" in str(err).lower()

    def test_room_not_allowed_error(self) -> None:
        err = RoomNotAllowedError("room99")
        assert err.room_token == "room99"
        assert "room99" in str(err)

    def test_message_send_error(self) -> None:
        cause = Exception("network error")
        err = MessageSendError("room1", cause)
        assert err.room_token == "room1"
        assert err.cause is cause

    def test_api_error(self) -> None:
        err = ApiError(404, "not found")
        assert err.status_code == 404
        assert "404" in str(err)
        assert "not found" in str(err)


# ---------------------------------------------------------------------------
# Service tests
# ---------------------------------------------------------------------------


class TestNextcloudTalkService:
    """Tests for NextcloudTalkService."""

    def test_service_init(self) -> None:
        config = NextcloudTalkConfig(base_url="https://nc.test.com", bot_secret="secret")
        service = NextcloudTalkService(config)
        assert service.is_running is False

    def test_base_url(self) -> None:
        config = NextcloudTalkConfig(base_url="https://nc.test.com", bot_secret="secret")
        service = NextcloudTalkService(config)
        assert service.base_url == "https://nc.test.com"

    def test_get_room_returns_none(self) -> None:
        config = NextcloudTalkConfig(base_url="https://nc.test.com", bot_secret="secret")
        service = NextcloudTalkService(config)
        assert service.get_room("nonexistent") is None

    def test_on_message_registers_handler(self) -> None:
        config = NextcloudTalkConfig(base_url="https://nc.test.com", bot_secret="secret")
        service = NextcloudTalkService(config)
        handler = AsyncMock()
        service.on_message(handler)
        assert handler in service._message_handlers

    def test_on_event_registers_handler(self) -> None:
        config = NextcloudTalkConfig(base_url="https://nc.test.com", bot_secret="secret")
        service = NextcloudTalkService(config)
        handler = MagicMock()
        service.on_event(NextcloudTalkEventType.MESSAGE_RECEIVED, handler)
        assert NextcloudTalkEventType.MESSAGE_RECEIVED in service._event_handlers


# ---------------------------------------------------------------------------
# Module exports test
# ---------------------------------------------------------------------------


class TestModuleExports:
    """Tests for the __init__ module exports."""

    def test_plugin_name(self) -> None:
        from elizaos_plugin_nextcloud_talk import PLUGIN_NAME
        assert PLUGIN_NAME == "nextcloud-talk"

    def test_plugin_description(self) -> None:
        from elizaos_plugin_nextcloud_talk import PLUGIN_DESCRIPTION
        assert "Nextcloud Talk" in PLUGIN_DESCRIPTION

    def test_version(self) -> None:
        from elizaos_plugin_nextcloud_talk import __version__
        assert __version__ == "2.0.0"
