"""
Behavioural tests: verify routing decisions and exact message text for the
price-lookup / amount-calculation / referral-link features — all with the
network layer stubbed out.
"""

import os
import sys

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from financegpt_bot import messages              # noqa: E402
from financegpt_bot.core import handlers, router  # noqa: E402


# --- capture outgoing calls ----------------------------------------------------

class FakeTelegram:
    def __init__(self):
        self.sent = []
        self.blocked_dm_chat_ids = set()
        self.member_status_by_chat = {}  # (chat_id, user_id) -> status
        self.reject_reply_to_message_ids = set()

    def send_message(self, chat_id, text, reply_markup=None, reply_to_message_id=None,
                      parse_mode=None, retry=True):
        if chat_id in self.blocked_dm_chat_ids:
            raise Exception("Forbidden: bot can't initiate conversation with a user")
        if reply_to_message_id in self.reject_reply_to_message_ids:
            raise Exception("Bad Request: message to be replied not found")
        self.sent.append({
            "chat_id": chat_id,
            "text": text,
            "reply_markup": reply_markup,
            "reply_to_message_id": reply_to_message_id,
            "parse_mode": parse_mode,
        })
        return {"result": {"chat": {"id": chat_id}}}

    def get_chat_member(self, chat_id, user_id):
        status = self.member_status_by_chat.get((chat_id, user_id), "member")
        return {"result": {"user": {"id": user_id}, "status": status}}


class FakePriceService:
    """Stands in for services.price_service — no real HTTP calls in tests."""

    def extract_symbol(self, text):
        if not text:
            return None
        upper = text.upper()
        if "BTC" in upper or "بیت کوین" in text or "بیتکوین" in text:
            return "BTC"
        if "USDT" in upper or "تتر" in text:
            return "USDT"
        return None

    def build_price_message(self, symbol):
        return f"PRICE[{symbol}]"

    def extract_amount(self, text):
        if not text:
            return None
        import re
        match = re.search(r"\d+(?:\.\d+)?", text)
        return float(match.group(0)) if match else None

    def build_amount_message(self, amount, symbol):
        return f"AMOUNT[{amount}][{symbol}]"

    def display_name(self, symbol):
        return {"BTC": "بیت کوین", "USDT": "تتر"}.get(symbol, symbol)


class FakeGroupLinks:
    """In-memory stand-in for services.group_links (avoids touching disk)."""

    def __init__(self):
        self._links = {}

    def get_link(self, chat_id):
        return self._links.get(chat_id)

    def set_link(self, chat_id, url):
        self._links[chat_id] = url


class FakeReferralState:
    """In-memory stand-in for services.referral_state (avoids touching disk)."""

    def __init__(self):
        self._pending = {}

    def mark_awaiting_link(self, reply_chat_id, admin_user_id, group_chat_id, group_title):
        self._pending[reply_chat_id] = {
            "admin_user_id": admin_user_id,
            "group_chat_id": group_chat_id,
            "group_title": group_title,
        }

    def get_pending(self, reply_chat_id, sender_user_id):
        entry = self._pending.get(reply_chat_id)
        if entry is None or entry["admin_user_id"] != sender_user_id:
            return None
        return entry

    def clear(self, reply_chat_id):
        self._pending.pop(reply_chat_id, None)


class FakeExplorerState:
    """In-memory stand-in for services.explorer_state (avoids touching disk)."""

    def __init__(self):
        self._pending = {}

    def mark_awaiting_hash(self, chat_id, network):
        self._pending[chat_id] = network

    def consume_awaiting_hash(self, chat_id):
        return self._pending.pop(chat_id, None)


class FakeExplorerService:
    """Stands in for services.explorer_service — no real HTTP calls in tests."""

    NETWORKS = ("BEP20", "TRC20", "ERC20", "BTC", "SOL")

    class TransactionNotFound(Exception):
        pass

    def __init__(self):
        self.results = {}  # (network, tx_hash) -> result dict/list, or an Exception instance

    def fetch_transaction(self, network, tx_hash):
        outcome = self.results.get((network, tx_hash))
        if isinstance(outcome, Exception):
            raise outcome
        if outcome is None:
            raise self.TransactionNotFound()
        return outcome


class FakeUsageLog:
    """In-memory stand-in for services.usage_log (avoids touching disk)."""

    def __init__(self):
        self.entries = []

    def log_action(self, chat_id, first_name, last_name, action, detail=None):
        self.entries.append({
            "chat_id": chat_id, "first_name": first_name, "last_name": last_name,
            "action": action, "detail": detail,
        })


def setup_function(_):
    handlers.tg = FakeTelegram()
    handlers.price_service = FakePriceService()
    handlers.group_links = FakeGroupLinks()
    handlers.referral_state = FakeReferralState()
    handlers.explorer_state = FakeExplorerState()
    handlers.explorer_service = FakeExplorerService()
    handlers.usage_log = FakeUsageLog()


def last_sent():
    return handlers.tg.sent[-1]


# --- routing -------------------------------------------------------------------

def _with_sig(text: str) -> str:
    # All price/amount replies use parse_mode="Markdown", so the signature
    # comes back with legacy-Markdown special characters escaped.
    from financegpt_bot import config as cfg
    from financegpt_bot.core.handlers import _escape_markdown
    return f"{text}\n{_escape_markdown(cfg.BOT_SIGNATURE)}"


def _with_plain_sig(text: str) -> str:
    # Plain-text replies have no parse_mode, so the signature is unescaped.
    from financegpt_bot import config as cfg
    return f"{text}\n{cfg.BOT_SIGNATURE}"


def test_coin_text_is_a_price_request():
    router.dispatch({"message": {"text": "usdt", "chat": {"type": "private", "id": 700},
                                 "from": {"id": 700}}})
    assert last_sent()["text"] == _with_sig("PRICE[USDT]")


def test_amount_plus_coin_text_is_an_amount_request():
    router.dispatch({"message": {"text": "1.4 بیت کوین میخوام", "chat": {"type": "private", "id": 705},
                                 "from": {"id": 705}}})
    assert last_sent()["text"] == _with_sig("AMOUNT[1.4][BTC]")
    assert last_sent()["reply_markup"] == messages.buy_coin_keyboard("بیت کوین")


def test_group_amount_plus_coin_text_is_an_amount_request():
    router.dispatch({
        "message": {
            "text": "2 بیت کوین چقدر میشه",
            "chat": {"type": "supergroup", "id": -1010},
            "from": {"id": 56},
            "message_id": 322,
        }
    })
    sent = last_sent()
    assert sent["chat_id"] == -1010
    assert sent["text"] == _with_sig("AMOUNT[2.0][BTC]")
    assert sent["reply_markup"] == messages.buy_coin_keyboard("بیت کوین")


def test_price_lookup_no_match_does_nothing():
    before = len(handlers.tg.sent)
    router.dispatch({"message": {"text": "سلام دوستان امروز بازار چطور است؟",
                                 "chat": {"type": "private", "id": 702},
                                 "from": {"id": 702}}})
    assert len(handlers.tg.sent) == before


def test_group_price_feature_disabled_is_ignored(monkeypatch):
    from financegpt_bot import config
    monkeypatch.setattr(config, "GROUP_PRICE_ENABLED", False)
    before = len(handlers.tg.sent)
    router.dispatch({"message": {"text": "BTC چند شده؟", "chat": {"type": "group", "id": 999},
                                 "from": {"id": 1}, "message_id": 1}})
    assert len(handlers.tg.sent) == before


def test_group_coin_text_gets_price_reply():
    router.dispatch({
        "message": {
            "text": "BTC چند شده؟",
            "chat": {"type": "supergroup", "id": -1009},
            "from": {"id": 55},
            "message_id": 321,
        }
    })
    sent = last_sent()
    assert sent["chat_id"] == -1009
    assert sent["reply_to_message_id"] == 321
    assert sent["text"] == _with_sig("PRICE[BTC]")
    assert sent["reply_markup"] == messages.buy_coin_keyboard("بیت کوین")


def test_group_non_price_text_is_ignored():
    before = len(handlers.tg.sent)
    router.dispatch({
        "message": {
            "text": "سلام دوستان امروز بازار چطور است؟",
            "chat": {"type": "group", "id": -1010},
            "from": {"id": 56},
            "message_id": 322,
        }
    })
    assert len(handlers.tg.sent) == before


def test_group_price_uses_registered_referral_link():
    handlers.group_links.set_link(-1020, "https://geevex.com/?ref=MYCODE")
    router.dispatch({
        "message": {
            "text": "BTC چند شده؟",
            "chat": {"type": "supergroup", "id": -1020},
            "from": {"id": 55},
            "message_id": 321,
        }
    })
    assert last_sent()["reply_markup"] == messages.buy_coin_keyboard(
        "بیت کوین", "https://geevex.com/?ref=MYCODE"
    )


def test_group_reply_falls_back_when_original_message_is_gone():
    # Telegram rejects a reply-to a deleted/inaccessible message; the bot
    # should still deliver the price instead of dropping the whole update.
    handlers.tg.reject_reply_to_message_ids.add(999)
    router.dispatch({
        "message": {
            "text": "BTC چند شده؟",
            "chat": {"type": "supergroup", "id": -1030},
            "from": {"id": 55},
            "message_id": 999,
        }
    })
    sent = last_sent()
    assert sent["chat_id"] == -1030
    assert sent["reply_to_message_id"] is None
    assert sent["text"] == _with_sig("PRICE[BTC]")


# --- channel membership gate ----------------------------------------------------

def test_start_triggers_membership_activation():
    router.dispatch({"message": {"text": "/start", "chat": {"type": "private"},
                                 "from": {"id": 888, "first_name": "A", "last_name": "B"}}})
    assert last_sent()["text"] == _with_plain_sig(messages.MEMBERSHIP_CONFIRMED_TEXT)
    assert last_sent()["reply_markup"] == messages.MAIN_MENU_KEYBOARD


def test_start_prompts_join_when_not_a_member():
    handlers.tg.get_chat_member = lambda chat_id, user_id: {"result": {"status": "left"}}
    router.dispatch({"message": {"text": "/start", "chat": {"type": "private"},
                                 "from": {"id": 889, "first_name": "A", "last_name": "B"}}})
    assert last_sent()["text"] == _with_plain_sig(messages.MEMBERSHIP_REQUIRED_TEXT)
    assert last_sent()["reply_markup"] == messages.MEMBERSHIP_REQUIRED_KEYBOARD


def test_membership_callback_activates_on_telegram():
    # "عضو شدم ✔️" — Telegram callback payload uses callback_query.from.id.
    router.dispatch({"callback_query": {"data": "check_membership",
                                        "message": {"chat": {"type": "private"}},
                                        "from": {"id": 1001}}})
    assert last_sent()["text"] == _with_plain_sig(messages.MEMBERSHIP_CONFIRMED_TEXT)
    assert last_sent()["chat_id"] == 1001


# --- main menu ---------------------------------------------------------------------

def test_price_menu_button_asks_for_coin():
    router.dispatch({"message": {"text": messages.MENU_PRICE_BUTTON_TEXT,
                                 "chat": {"type": "private"}, "from": {"id": 41}}})
    assert last_sent()["chat_id"] == 41
    assert last_sent()["text"] == _with_plain_sig(messages.ASK_COIN_TEXT)


def test_price_menu_button_then_coin_text_gets_a_price_reply():
    router.dispatch({"message": {"text": messages.MENU_PRICE_BUTTON_TEXT,
                                 "chat": {"type": "private", "id": 41}, "from": {"id": 41}}})
    router.dispatch({"message": {"text": "usdt", "chat": {"type": "private", "id": 41},
                                 "from": {"id": 41}}})
    assert last_sent()["text"] == _with_sig("PRICE[USDT]")


def test_earn_button_sends_add_to_group_deeplink():
    router.dispatch({"message": {"text": messages.MENU_EARN_BUTTON_TEXT,
                                 "chat": {"type": "private"}, "from": {"id": 42}}})
    sent = last_sent()
    assert sent["chat_id"] == 42
    assert sent["reply_markup"] == messages.add_to_group_keyboard()


def test_price_alert_button_is_a_no_op():
    before = len(handlers.tg.sent)
    router.dispatch({"message": {"text": messages.MENU_PRICE_ALERT_BUTTON_TEXT,
                                 "chat": {"type": "private"}, "from": {"id": 43}}})
    assert len(handlers.tg.sent) == before


def test_explorer_button_asks_for_network():
    router.dispatch({"message": {"text": messages.MENU_EXPLORER_BUTTON_TEXT,
                                 "chat": {"type": "private"}, "from": {"id": 43}}})
    sent = last_sent()
    assert sent["chat_id"] == 43
    assert sent["reply_markup"] == messages.explorer_network_keyboard()


# --- bot added to a group -> referral link request --------------------------------

def _my_chat_member_update(group_id, group_title, adder_id):
    return {
        "my_chat_member": {
            "chat": {"id": group_id, "type": "supergroup", "title": group_title},
            "from": {"id": adder_id},
            "old_chat_member": {"status": "left"},
            "new_chat_member": {"status": "member"},
        }
    }


def test_bot_added_to_group_by_admin_asks_for_link_via_dm():
    handlers.tg.member_status_by_chat[(-2001, 71)] = "administrator"
    router.dispatch(_my_chat_member_update(-2001, "My Group", 71))
    sent = last_sent()
    assert sent["chat_id"] == 71  # DM to the admin, not the group
    assert "My Group" in sent["text"]

    pending = handlers.referral_state.get_pending(71, 71)
    assert pending == {"admin_user_id": 71, "group_chat_id": -2001, "group_title": "My Group"}


def test_bot_added_to_group_by_non_admin_gets_not_admin_message():
    handlers.tg.member_status_by_chat[(-2002, 72)] = "member"  # not an admin
    router.dispatch(_my_chat_member_update(-2002, "My Group", 72))
    sent = last_sent()
    assert sent["chat_id"] == 72
    assert sent["text"] == _with_plain_sig(messages.NOT_GROUP_ADMIN_TEXT)
    assert handlers.referral_state.get_pending(72, 72) is None


def test_bot_added_to_group_dm_blocked_falls_back_to_group_message():
    handlers.tg.member_status_by_chat[(-2003, 73)] = "administrator"
    handlers.tg.blocked_dm_chat_ids.add(73)  # admin never DM'd the bot before
    router.dispatch(_my_chat_member_update(-2003, "My Group", 73))
    sent = last_sent()
    assert sent["chat_id"] == -2003  # fell back to posting in the group
    pending = handlers.referral_state.get_pending(-2003, 73)
    assert pending["group_chat_id"] == -2003


def test_referral_link_reply_valid_link_is_stored():
    handlers.tg.member_status_by_chat[(-2004, 74)] = "administrator"
    router.dispatch(_my_chat_member_update(-2004, "My Group", 74))
    router.dispatch({"message": {"text": "https://geevex.com/?ref=ABC",
                                 "chat": {"type": "private", "id": 74}, "from": {"id": 74}}})
    assert last_sent()["text"] == _with_plain_sig(messages.REFERRAL_LINK_SAVED_TEXT)
    assert handlers.group_links.get_link(-2004) == "https://geevex.com/?ref=ABC"
    assert handlers.referral_state.get_pending(74, 74) is None


def test_referral_link_reply_invalid_format_is_rejected():
    handlers.tg.member_status_by_chat[(-2005, 75)] = "administrator"
    router.dispatch(_my_chat_member_update(-2005, "My Group", 75))
    router.dispatch({"message": {"text": "not a link",
                                 "chat": {"type": "private", "id": 75}, "from": {"id": 75}}})
    assert last_sent()["text"] == _with_plain_sig(messages.REFERRAL_LINK_INVALID_TEXT)
    assert handlers.group_links.get_link(-2005) is None
    # still pending, since no valid link was received yet
    assert handlers.referral_state.get_pending(75, 75) is not None


def test_referral_link_reply_rejects_url_with_space():
    # A stray space (e.g. "https:// example.com") passes a naive prefix check
    # but Telegram rejects it as an inline-button url (BUTTON_URL_INVALID),
    # which would break every future price reply in that group.
    handlers.tg.member_status_by_chat[(-2006, 76)] = "administrator"
    router.dispatch(_my_chat_member_update(-2006, "My Group", 76))
    router.dispatch({"message": {"text": "https:// broken.example.com",
                                 "chat": {"type": "private", "id": 76}, "from": {"id": 76}}})
    assert last_sent()["text"] == _with_plain_sig(messages.REFERRAL_LINK_INVALID_TEXT)
    assert handlers.group_links.get_link(-2006) is None


# --- blockchain explorer -----------------------------------------------------------

def test_explorer_network_selection_asks_for_hash():
    router.dispatch({"callback_query": {"data": "explorer_network:BTC",
                                        "message": {"chat": {"type": "private"}},
                                        "from": {"id": 81}}})
    sent = last_sent()
    assert sent["chat_id"] == 81
    assert sent["text"] == _with_plain_sig(messages.explorer_ask_hash_text("BTC"))
    assert handlers.explorer_state._pending[81] == "BTC"


def test_explorer_hash_reply_looks_up_and_formats_result():
    handlers.explorer_state.mark_awaiting_hash(82, "BTC")
    handlers.explorer_service.results[("BTC", "abc123")] = {
        "source_address": "bc1qsrc", "destination_address": "bc1qdst",
        "asset": "BTC", "amount": "0.00100000", "date": "1403-01-01 00:00:00",
        "status": "تایید شده", "network": "BTC", "source": "https://mempool.space/tx/abc123",
    }
    router.dispatch({"message": {"text": "abc123",
                                 "chat": {"type": "private", "id": 82}, "from": {"id": 82}}})
    assert last_sent()["text"] == _with_sig(messages.explorer_result_text(
        handlers.explorer_service.results[("BTC", "abc123")]
    ))


def test_explorer_hash_reply_not_found():
    handlers.explorer_state.mark_awaiting_hash(83, "ERC20")
    router.dispatch({"message": {"text": "deadbeef",
                                 "chat": {"type": "private", "id": 83}, "from": {"id": 83}}})
    assert last_sent()["text"] == _with_plain_sig(messages.EXPLORER_NOT_FOUND_TEXT)


def test_explorer_hash_reply_upstream_error():
    handlers.explorer_state.mark_awaiting_hash(84, "SOL")
    handlers.explorer_service.results[("SOL", "badhash")] = Exception("network blew up")
    router.dispatch({"message": {"text": "badhash",
                                 "chat": {"type": "private", "id": 84}, "from": {"id": 84}}})
    assert last_sent()["text"] == _with_plain_sig(messages.EXPLORER_ERROR_TEXT)


def test_explorer_hash_reply_trc20_multi_transfer_combines_into_one_message():
    handlers.explorer_state.mark_awaiting_hash(85, "TRC20")
    handlers.explorer_service.results[("TRC20", "trxhash")] = [
        {"source_address": "TA", "destination_address": "TB", "asset": "USDT",
         "amount": "1.000000", "date": "d", "status": "تایید شده", "network": "TRC-20", "source": "s"},
        {"source_address": "TA", "destination_address": "TC", "asset": "USDC",
         "amount": "2.000000", "date": "d", "status": "تایید شده", "network": "TRC-20", "source": "s"},
    ]
    router.dispatch({"message": {"text": "trxhash",
                                 "chat": {"type": "private", "id": 85}, "from": {"id": 85}}})
    body = last_sent()["text"]
    assert "USDT" in body and "USDC" in body


def test_explorer_hash_reply_without_pending_state_falls_through_to_price_lookup():
    router.dispatch({"message": {"text": "usdt",
                                 "chat": {"type": "private", "id": 86}, "from": {"id": 86}}})
    assert last_sent()["text"] == _with_sig("PRICE[USDT]")


# --- usage report log ---------------------------------------------------------------

def _last_log():
    return handlers.usage_log.entries[-1]


def test_start_is_logged():
    router.dispatch({"message": {"text": "/start", "chat": {"type": "private"},
                                 "from": {"id": 900, "first_name": "Ali", "last_name": "Rezaei"}}})
    entry = handlers.usage_log.entries[0]
    assert entry == {"chat_id": 900, "first_name": "Ali", "last_name": "Rezaei",
                      "action": "start", "detail": None}


def test_price_menu_button_is_logged():
    router.dispatch({"message": {"text": messages.MENU_PRICE_BUTTON_TEXT,
                                 "chat": {"type": "private"},
                                 "from": {"id": 901, "first_name": "Sara", "last_name": "Ahmadi"}}})
    assert _last_log() == {"chat_id": 901, "first_name": "Sara", "last_name": "Ahmadi",
                            "action": "price_menu_button", "detail": None}


def test_explorer_menu_button_is_logged():
    router.dispatch({"message": {"text": messages.MENU_EXPLORER_BUTTON_TEXT,
                                 "chat": {"type": "private"},
                                 "from": {"id": 902, "first_name": "Reza", "last_name": None}}})
    assert _last_log()["action"] == "explorer_menu_button"


def test_add_to_group_is_logged():
    router.dispatch(_my_chat_member_update(-3001, "Some Group", 903))
    assert _last_log() == {"chat_id": 903, "first_name": None, "last_name": None,
                            "action": "add_to_group", "detail": "Some Group"}


def test_price_lookup_is_logged_with_symbol():
    router.dispatch({"message": {"text": "usdt", "chat": {"type": "private", "id": 904},
                                 "from": {"id": 904, "first_name": "Nima"}}})
    assert _last_log() == {"chat_id": 904, "first_name": "Nima", "last_name": None,
                            "action": "price_lookup", "detail": "USDT"}


def test_group_price_lookup_is_logged():
    router.dispatch({"message": {"text": "BTC چند شده؟",
                                 "chat": {"type": "supergroup", "id": -3002},
                                 "from": {"id": 905, "first_name": "Mina"}, "message_id": 1}})
    assert _last_log() == {"chat_id": 905, "first_name": "Mina", "last_name": None,
                            "action": "group_price_lookup", "detail": "BTC"}
