"""Tests for src/adapters/telegram_bot.py — Telegram bot adapter."""

import json
import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from src.config import Config
from src.adapters import telegram_bot
from src.adapters.telegram_bot import (
    create_telegram_bot,
    is_admin,
    is_owner,
    is_registered_chat,
    split_message,
    cmd_start,
    cmd_help,
    cmd_clear,
    cmd_status,
    cmd_model,
    cmd_register,
    callback_model,
    handle_message,
)


# --- Fixtures ---


@pytest.fixture
def tmp_config(tmp_path):
    """Create a Config backed by a temp file with default data."""
    data = {
        "bot": {
            "name": "Echo",
            "default_model": "sonnet",
            "owner": None,
            "admins": [],
        },
        "channels": {},
        "telegram_channels": {},
    }
    config_file = tmp_path / "config.json"
    config_file.write_text(json.dumps(data, indent=2))
    return Config(config_file)


@pytest.fixture
def owned_config(tmp_path):
    """Config with owner and telegram channels set."""
    data = {
        "bot": {
            "name": "Echo",
            "default_model": "sonnet",
            "owner": "111",
            "admins": ["222"],
        },
        "channels": {},
        "telegram_channels": {
            "general": {"id": "900", "default_model": "sonnet"},
        },
    }
    config_file = tmp_path / "config.json"
    config_file.write_text(json.dumps(data, indent=2))
    return Config(config_file)


@pytest.fixture(autouse=True)
def _set_config(tmp_config):
    """Ensure module config is set for each test."""
    telegram_bot._config = tmp_config
    yield
    telegram_bot._config = None


@pytest.fixture
def _set_owned(owned_config):
    """Set owned config for specific tests."""
    telegram_bot._config = owned_config
    yield
    telegram_bot._config = None


def _mock_update(user_id=123, chat_id=456, text="hello", chat_type="private", username="testuser"):
    """Create a mock telegram Update."""
    update = MagicMock()
    update.effective_user = MagicMock()
    update.effective_user.id = user_id
    update.effective_user.username = username
    update.effective_chat = MagicMock()
    update.effective_chat.id = chat_id
    update.effective_chat.type = chat_type
    update.message = MagicMock()
    update.message.text = text
    update.message.reply_text = AsyncMock()
    update.message.reply_to_message = None
    return update


def _mock_context(bot_id=999, bot_username="echo_bot"):
    """Create a mock context."""
    context = MagicMock()
    context.args = []
    context.bot = MagicMock()
    context.bot.id = bot_id
    context.bot.username = bot_username
    context.bot.send_chat_action = AsyncMock()
    return context


# --- Authorization helpers ---


class TestIsOwner:
    def test_is_owner_true(self, _set_owned):
        assert is_owner(111) is True

    def test_is_owner_false(self, _set_owned):
        assert is_owner(999) is False

    def test_is_owner_none_owner(self):
        assert is_owner(123) is False


class TestIsAdmin:
    def test_is_admin_owner_is_admin(self, _set_owned):
        assert is_admin(111) is True

    def test_is_admin_listed(self, _set_owned):
        assert is_admin(222) is True

    def test_is_admin_not_listed(self, _set_owned):
        assert is_admin(999) is False


class TestIsRegisteredChat:
    def test_is_registered_true(self, _set_owned):
        assert is_registered_chat(900) is True

    def test_is_registered_false(self, _set_owned):
        assert is_registered_chat(000) is False

    def test_is_registered_empty(self):
        assert is_registered_chat(900) is False


# --- split_message ---


class TestSplitMessage:
    def test_short_message_not_split(self):
        assert split_message("hello") == ["hello"]

    def test_long_message_split(self):
        text = "a" * 8192
        chunks = split_message(text, limit=4096)
        assert len(chunks) == 2
        assert all(len(c) <= 4096 for c in chunks)
        assert "".join(chunks) == text

    def test_split_at_newline(self):
        text = "line1\n" * 1000
        chunks = split_message(text, limit=100)
        assert all(len(c) <= 100 for c in chunks)

    def test_empty_string(self):
        assert split_message("") == [""]


# --- Command handlers ---


class TestCmdStart:
    @pytest.mark.asyncio
    async def test_start_responds(self):
        update = _mock_update()
        context = _mock_context()
        await cmd_start(update, context)
        update.message.reply_text.assert_called_once()
        msg = update.message.reply_text.call_args[0][0]
        assert "Echo Core" in msg


class TestCmdHelp:
    @pytest.mark.asyncio
    async def test_help_responds(self):
        update = _mock_update()
        context = _mock_context()
        await cmd_help(update, context)
        update.message.reply_text.assert_called_once()
        msg = update.message.reply_text.call_args[0][0]
        assert "/clear" in msg
        assert "/model" in msg


class TestCmdClear:
    @pytest.mark.asyncio
    async def test_clear_no_session(self):
        update = _mock_update()
        context = _mock_context()
        with patch("src.adapters.telegram_bot.clear_session", return_value=False):
            await cmd_clear(update, context)
        msg = update.message.reply_text.call_args[0][0]
        assert "No active session" in msg

    @pytest.mark.asyncio
    async def test_clear_with_session(self):
        update = _mock_update()
        context = _mock_context()
        with patch("src.adapters.telegram_bot.clear_session", return_value=True):
            await cmd_clear(update, context)
        msg = update.message.reply_text.call_args[0][0]
        assert "Session cleared" in msg


class TestCmdStatus:
    @pytest.mark.asyncio
    async def test_status_no_session(self):
        update = _mock_update()
        context = _mock_context()
        with patch("src.adapters.telegram_bot.get_active_session", return_value=None):
            await cmd_status(update, context)
        msg = update.message.reply_text.call_args[0][0]
        assert "No active session" in msg

    @pytest.mark.asyncio
    async def test_status_with_session(self):
        update = _mock_update()
        context = _mock_context()
        session = {
            "model": "opus",
            "session_id": "sess-abc-12345678",
            "message_count": 5,
            "total_input_tokens": 1000,
            "total_output_tokens": 500,
        }
        with patch("src.adapters.telegram_bot.get_active_session", return_value=session):
            await cmd_status(update, context)
        msg = update.message.reply_text.call_args[0][0]
        assert "opus" in msg
        assert "5" in msg


class TestCmdModel:
    @pytest.mark.asyncio
    async def test_model_with_arg(self, tmp_path, monkeypatch):
        update = _mock_update()
        context = _mock_context()
        context.args = ["opus"]

        # Need session dir for _save_sessions
        sessions_dir = tmp_path / "sessions"
        sessions_dir.mkdir()
        from src import claude_session
        monkeypatch.setattr(claude_session, "SESSIONS_DIR", sessions_dir)
        monkeypatch.setattr(claude_session, "_SESSIONS_FILE", sessions_dir / "active.json")

        with patch("src.adapters.telegram_bot.get_active_session", return_value=None):
            await cmd_model(update, context)
        msg = update.message.reply_text.call_args[0][0]
        assert "opus" in msg

    @pytest.mark.asyncio
    async def test_model_invalid(self):
        update = _mock_update()
        context = _mock_context()
        context.args = ["gpt4"]
        await cmd_model(update, context)
        msg = update.message.reply_text.call_args[0][0]
        assert "Invalid model" in msg

    @pytest.mark.asyncio
    async def test_model_keyboard(self):
        update = _mock_update()
        context = _mock_context()
        context.args = []
        with patch("src.adapters.telegram_bot.get_active_session", return_value=None):
            await cmd_model(update, context)
        call_kwargs = update.message.reply_text.call_args[1]
        assert "reply_markup" in call_kwargs


class TestCmdRegister:
    @pytest.mark.asyncio
    async def test_register_not_owner(self):
        update = _mock_update(user_id=999)
        context = _mock_context()
        context.args = ["test"]
        await cmd_register(update, context)
        msg = update.message.reply_text.call_args[0][0]
        assert "Owner only" in msg

    @pytest.mark.asyncio
    async def test_register_owner(self, _set_owned):
        update = _mock_update(user_id=111, chat_id=777)
        context = _mock_context()
        context.args = ["mychat"]
        await cmd_register(update, context)
        msg = update.message.reply_text.call_args[0][0]
        assert "registered" in msg
        assert "mychat" in msg

    @pytest.mark.asyncio
    async def test_register_no_args(self, _set_owned):
        update = _mock_update(user_id=111)
        context = _mock_context()
        context.args = []
        await cmd_register(update, context)
        msg = update.message.reply_text.call_args[0][0]
        assert "Usage" in msg


# --- Message handler ---


class TestHandleMessage:
    @pytest.mark.asyncio
    async def test_private_chat_admin(self, _set_owned):
        update = _mock_update(user_id=111, chat_type="private", text="Hello Claude")
        context = _mock_context()
        with patch("src.adapters.telegram_bot.route_message", return_value=("Hi!", False)) as mock_route:
            await handle_message(update, context)
        mock_route.assert_called_once()
        update.message.reply_text.assert_called_with("Hi!")

    @pytest.mark.asyncio
    async def test_private_chat_unauthorized(self, _set_owned):
        update = _mock_update(user_id=999, chat_type="private", text="Hello")
        context = _mock_context()
        with patch("src.adapters.telegram_bot.route_message") as mock_route:
            await handle_message(update, context)
        mock_route.assert_not_called()
        update.message.reply_text.assert_not_called()

    @pytest.mark.asyncio
    async def test_group_chat_unregistered(self, _set_owned):
        update = _mock_update(user_id=111, chat_id=999, chat_type="supergroup", text="Hello")
        context = _mock_context()
        with patch("src.adapters.telegram_bot.route_message") as mock_route:
            await handle_message(update, context)
        mock_route.assert_not_called()

    @pytest.mark.asyncio
    async def test_group_chat_registered_mention(self, _set_owned):
        update = _mock_update(
            user_id=111, chat_id=900, chat_type="supergroup",
            text="@echo_bot what is the weather?"
        )
        context = _mock_context(bot_username="echo_bot")
        with patch("src.adapters.telegram_bot.route_message", return_value=("Sunny!", False)):
            await handle_message(update, context)
        update.message.reply_text.assert_called_with("Sunny!")

    @pytest.mark.asyncio
    async def test_group_chat_registered_no_mention(self, _set_owned):
        update = _mock_update(
            user_id=111, chat_id=900, chat_type="supergroup",
            text="just chatting"
        )
        context = _mock_context(bot_username="echo_bot")
        with patch("src.adapters.telegram_bot.route_message") as mock_route:
            await handle_message(update, context)
        mock_route.assert_not_called()

    @pytest.mark.asyncio
    async def test_group_chat_reply_to_bot(self, _set_owned):
        update = _mock_update(
            user_id=111, chat_id=900, chat_type="supergroup",
            text="follow up"
        )
        # Set up reply-to-bot
        update.message.reply_to_message = MagicMock()
        update.message.reply_to_message.from_user = MagicMock()
        update.message.reply_to_message.from_user.id = 999  # bot id
        context = _mock_context(bot_id=999)
        with patch("src.adapters.telegram_bot.route_message", return_value=("Response", False)):
            await handle_message(update, context)
        update.message.reply_text.assert_called_with("Response")

    @pytest.mark.asyncio
    async def test_long_response_split(self, _set_owned):
        update = _mock_update(user_id=111, chat_type="private", text="Hello")
        context = _mock_context()
        long_response = "x" * 8000
        with patch("src.adapters.telegram_bot.route_message", return_value=(long_response, False)):
            await handle_message(update, context)
        assert update.message.reply_text.call_count == 2

    @pytest.mark.asyncio
    async def test_error_handling(self, _set_owned):
        update = _mock_update(user_id=111, chat_type="private", text="Hello")
        context = _mock_context()
        with patch("src.adapters.telegram_bot.route_message", side_effect=Exception("boom")):
            await handle_message(update, context)
        msg = update.message.reply_text.call_args[0][0]
        assert "Sorry" in msg


# --- Security logging ---


class TestSecurityLogging:
    @pytest.mark.asyncio
    async def test_unauthorized_dm_logged(self, _set_owned):
        update = _mock_update(user_id=999, chat_type="private", text="hack attempt")
        context = _mock_context()
        with patch.object(telegram_bot._security_log, "warning") as mock_log:
            await handle_message(update, context)
            mock_log.assert_called_once()
            assert "Unauthorized" in mock_log.call_args[0][0]

    @pytest.mark.asyncio
    async def test_unauthorized_register_logged(self):
        update = _mock_update(user_id=999)
        context = _mock_context()
        context.args = ["test"]
        with patch.object(telegram_bot._security_log, "warning") as mock_log:
            await cmd_register(update, context)
            mock_log.assert_called_once()


# --- Factory ---


class TestCreateTelegramBot:
    def test_creates_application(self, tmp_config):
        from telegram.ext import Application
        app = create_telegram_bot(tmp_config, "fake-token-123")
        assert isinstance(app, Application)

    def test_sets_config(self, tmp_config):
        create_telegram_bot(tmp_config, "fake-token-123")
        assert telegram_bot._config is tmp_config
