#!/usr/bin/env python3
"""Unit tests for monop_players.py — test bot responses to monop messages."""

import sys
import unittest
from unittest.mock import MagicMock, patch
import time

# Import PlayerBot directly
sys.path.insert(0, "/tmp/monop-state")
from monop_players import PlayerBot


class FakePlayerBot(PlayerBot):
    """PlayerBot that captures messages instead of sending to IRC."""

    def __init__(self, nick, player_names, player_index):
        # Don't call super().__init__ — skip socket stuff
        self.nick = nick
        self.channel = "#monop"
        self.host = "127.0.0.1"
        self.port = 6667
        self.player_names = player_names
        self.player_index = player_index
        self.num_players = len(player_names)

        self.sock = None
        self.buffer = ""
        self.lock = __import__("threading").Lock()

        # Game state
        self.setup_phase = True
        self.setup_registrations_seen = 0
        self.current_player = None
        self.my_money = 1500
        self.in_jail = False
        self.jail_turns = 0
        self.in_debt = False
        self.in_auction = False
        self.auction_bid = 0
        self.awaiting_prompt = None
        self.game_started = False
        self.game_over = False
        self.rolled_this_turn = False
        self.my_properties = []
        self.mortgaged = set()
        self._prompt_answered = False
        self._first_player_announced = False
        self.in_trade = False
        self.trade_props_offered = 0
        self.turns_played = 0

        # Capture sent messages instead of IRC
        self.sent_messages = []

    def say(self, msg):
        self.sent_messages.append(msg)

    def say_delayed(self, msg, delay=None, force=False):
        """Immediate version — no threading, no delay."""
        if force or self.is_my_turn():
            self.sent_messages.append(msg)

    def feed(self, msg):
        """Simulate receiving a message from the monop bot."""
        self.sent_messages.clear()
        self._handle_bot_msg(msg)
        return list(self.sent_messages)

    def feed_setup(self, msg):
        """Feed a setup-phase message."""
        self.sent_messages.clear()
        result = self._handle_setup(msg)
        return list(self.sent_messages), result


class TestSetup(unittest.TestCase):
    def test_player_count(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        msgs, handled = bot.feed_setup("How many players?")
        self.assertTrue(handled)
        self.assertEqual(msgs, ["2"])

    def test_player_count_only_first(self):
        bot = FakePlayerBot("bob", ["alice", "bob"], 1)
        msgs, handled = bot.feed_setup("How many players?")
        self.assertTrue(handled)
        self.assertEqual(msgs, [])  # bob doesn't send count

    def test_register_correct_player(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        msgs, handled = bot.feed_setup("Player 1, say ''me'' please.")
        self.assertTrue(handled)
        self.assertEqual(msgs, ["alice"])

    def test_register_wrong_player(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        msgs, handled = bot.feed_setup("Player 2, say ''me'' please.")
        self.assertTrue(handled)
        self.assertEqual(msgs, [])  # alice doesn't respond to player 2

    def test_goes_first(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        msgs, handled = bot.feed_setup("alice (1) goes first")
        self.assertTrue(handled)
        self.assertFalse(bot.setup_phase)
        self.assertEqual(bot.current_player, "alice")


class TestTurns(unittest.TestCase):
    def _make_bot(self, nick="alice", current_player=None):
        bot = FakePlayerBot(nick, ["alice", "bob"], 0 if nick == "alice" else 1)
        bot.setup_phase = False
        bot.current_player = current_player
        return bot

    def test_checkpoint_triggers_roll(self):
        """Checkpoint sets up turn, -- Command: triggers the roll."""
        bot = self._make_bot("alice")
        msgs = bot.feed("alice (1) (cash $1500) on === GO ===")
        self.assertEqual(msgs, [], "Checkpoint should not send commands")
        # Roll happens at -- Command:
        with patch("random.random", return_value=0.99):  # no trade
            msgs = bot.feed("-- Command:")
        self.assertIn("roll", msgs)
        self.assertTrue(bot.rolled_this_turn)

    def test_checkpoint_other_player_no_roll(self):
        bot = self._make_bot("alice")
        msgs = bot.feed("bob (2) (cash $1500) on === GO ===")
        self.assertEqual(msgs, [])
        self.assertEqual(bot.current_player, "bob")

    def test_doubles_roll_again(self):
        bot = self._make_bot("alice", "alice")
        msgs = bot.feed("alice rolled doubles.  Goes again")
        self.assertIn("roll", msgs)

    def test_buy_prompt(self):
        bot = self._make_bot("alice", "alice")
        msgs = bot.feed("Do you want to buy?")
        self.assertIn("yes", msgs)

    def test_buy_prompt_not_my_turn(self):
        bot = self._make_bot("alice", "bob")
        msgs = bot.feed("Do you want to buy?")
        self.assertEqual(msgs, [])

    def test_command_prompt_rolls_if_needed(self):
        bot = self._make_bot("alice", "alice")
        bot.rolled_this_turn = False
        msgs = bot.feed("-- Command:")
        self.assertIn("roll", msgs)

    def test_command_prompt_no_double_roll(self):
        bot = self._make_bot("alice", "alice")
        bot.rolled_this_turn = True
        msgs = bot.feed("-- Command:")
        self.assertEqual(msgs, [])


class TestJail(unittest.TestCase):
    def test_jail_turn_rolls(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        bot.setup_phase = False
        bot.current_player = "alice"
        msgs = bot.feed("(This is your 1st turn in JAIL)")
        self.assertIn("roll", msgs)
        self.assertTrue(bot.in_jail)


class TestDebt(unittest.TestCase):
    def test_debt_mortgages(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        bot.setup_phase = False
        bot.current_player = "alice"
        msgs = bot.feed("How are you going to fix it up?")
        self.assertIn("mortgage", msgs)
        self.assertTrue(bot.in_debt)

    def test_mortgage_prompt_sends_question(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        bot.setup_phase = False
        bot.current_player = "alice"
        bot.in_debt = True
        msgs = bot.feed("Which property do you want to mortgage?")
        self.assertIn("?", msgs)

    def test_mortgage_prompt_done_when_solvent(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        bot.setup_phase = False
        bot.current_player = "alice"
        bot.in_debt = False
        msgs = bot.feed("Which property do you want to mortgage?")
        self.assertIn("done", msgs)

    def test_no_property_sells_houses(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        bot.setup_phase = False
        bot.current_player = "alice"
        bot.in_debt = True
        msgs = bot.feed("You don't have any un-mortgaged property.")
        self.assertIn("sell houses", msgs)

    def test_no_houses_resigns(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        bot.setup_phase = False
        bot.current_player = "alice"
        bot.in_debt = True
        msgs = bot.feed("You don't have any houses to sell!!")
        self.assertIn("resign", msgs)


class TestTax(unittest.TestCase):
    def test_tax_choice(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        bot.setup_phase = False
        bot.current_player = "alice"
        msgs = bot.feed("Do you wish to lose 10%% of your total worth or $200? ")
        self.assertIn("10%", msgs)


class TestTrading(unittest.TestCase):
    def _make_bot(self, nick="alice"):
        bot = FakePlayerBot(nick, ["alice", "bob"], 0 if nick == "alice" else 1)
        bot.setup_phase = False
        bot.current_player = "alice"
        return bot

    @patch("monop_players.random")
    def test_trade_initiated(self, mock_random):
        """With random < 0.10, bot initiates trade at -- Command:."""
        mock_random.random.return_value = 0.05  # < 0.10
        bot = self._make_bot("alice")
        bot.turns_played = 10  # past turn 5
        bot.feed("alice (1) (cash $1500) on === GO ===")
        msgs = bot.feed("-- Command:")
        self.assertIn("trade", msgs)
        self.assertTrue(bot.in_trade)
        self.assertNotIn("roll", msgs)

    @patch("monop_players.random")
    def test_trade_not_initiated_early(self, mock_random):
        """No trades before turn 5."""
        mock_random.random.return_value = 0.05
        bot = self._make_bot("alice")
        bot.turns_played = 2
        bot.feed("alice (1) (cash $1500) on === GO ===")
        msgs = bot.feed("-- Command:")
        self.assertIn("roll", msgs)
        self.assertFalse(bot.in_trade)

    def test_trade_property_prompt_sends_done(self):
        bot = self._make_bot("alice")
        bot.in_trade = True
        bot.trade_props_offered = 1  # already offered one
        msgs = bot.feed("Which property do you wish to trade?")
        self.assertIn("done", msgs)

    def test_trade_cash_prompt(self):
        bot = self._make_bot("alice")
        bot.in_trade = True
        msgs = bot.feed("You have $1500.  How much are you trading?")
        self.assertEqual(len(msgs), 1)
        amount = int(msgs[0])
        self.assertGreaterEqual(amount, 0)
        self.assertLessEqual(amount, 375)  # max 25% of 1500

    def test_trade_gojf_prompt(self):
        bot = self._make_bot("alice")
        bot.in_trade = True
        msgs = bot.feed("You have 2 get-out-of-jail-free cards. How many are you trading?")
        self.assertEqual(msgs, ["0"])

    @patch("monop_players.random")
    def test_trade_accepted(self, mock_random):
        mock_random.random.return_value = 0.3  # < 0.5 → accept
        bot = self._make_bot("alice")
        bot.in_trade = True
        # alice is asked to confirm (she's the tradee)
        bot.current_player = "bob"  # bob initiated
        msgs = bot.feed("alice, is the trade ok?")
        self.assertIn("yes", msgs)

    @patch("monop_players.random")
    def test_trade_rejected(self, mock_random):
        mock_random.random.return_value = 0.7  # >= 0.5 → reject
        bot = self._make_bot("alice")
        bot.in_trade = True
        bot.current_player = "bob"
        msgs = bot.feed("alice, is the trade ok?")
        self.assertIn("no", msgs)

    def test_trade_done_resets(self):
        bot = self._make_bot("alice")
        bot.in_trade = True
        msgs = bot.feed("Trade is done!")
        self.assertFalse(bot.in_trade)

    def test_trade_nobody_around(self):
        bot = self._make_bot("alice")
        bot.in_trade = True
        msgs = bot.feed("There ain't no-one around to trade WITH!!")
        self.assertFalse(bot.in_trade)
        self.assertIn("roll", msgs)

    def test_command_prompt_resets_trade(self):
        bot = self._make_bot("alice")
        bot.in_trade = True
        bot.rolled_this_turn = False
        msgs = bot.feed("-- Command:")
        self.assertFalse(bot.in_trade)
        self.assertIn("roll", msgs)


class TestTradeInJail(unittest.TestCase):
    """Regression tests for trade-while-in-jail bug.

    Sequence that caused the hang:
      1. Checkpoint "charlie (3) (cash $985) on JAIL" → trade initiated
      2. "(This is your 2nd turn in JAIL)" → roll queued (ignoring in_trade)
      3. ".trade" sent, then ".roll" sent into trade prompt → hang
    """

    def _make_bot(self, nick="charlie"):
        bot = FakePlayerBot(nick, ["alice", "bob", "charlie"], 2)
        bot.setup_phase = False
        bot.game_started = True
        bot.turns_played = 10  # past trade threshold
        return bot

    def test_jail_handler_no_roll_during_trade(self):
        """Jail turn handler must not queue a roll when in_trade is True."""
        bot = self._make_bot()
        bot.current_player = "charlie"
        bot.in_trade = True
        bot.in_jail = True
        msgs = bot.feed("(This is your 2nd turn in JAIL)")
        self.assertNotIn("roll", msgs,
                         "Should not roll while in_trade is True")

    def test_no_trade_from_jail(self):
        """Can't trade while in jail — must roll first."""
        bot = self._make_bot()
        msgs = bot.feed("charlie (3) (cash $985) on JAIL")
        msgs = bot.feed("(This is your 2nd turn in JAIL)")
        self.assertIn("roll", msgs, "Should roll in jail")

        # Even if random triggers, no trade while in_jail
        with patch("random.random", return_value=0.01):
            msgs = bot.feed("-- Command:")
        self.assertFalse(bot.in_trade, "Should not trade while in jail")
        self.assertIn("roll", msgs)

    def test_trade_which_player_prompt(self):
        """Bot should pick a trade partner when asked."""
        bot = self._make_bot()
        bot.current_player = "charlie"
        bot.in_trade = True
        msgs = bot.feed("Which player do you wish to trade with?")
        self.assertTrue(len(msgs) > 0, "Should respond to trade partner prompt")
        self.assertNotIn("roll", msgs)
        # Should pick one of the other players
        self.assertTrue(
            msgs[0] in ["alice", "bob"],
            f"Expected a player name, got: {msgs[0]}"
        )

    def test_no_trade_in_jail_still_rolls(self):
        """When NOT in a trade, jail handler should still queue a roll."""
        bot = self._make_bot()
        bot.current_player = "charlie"
        bot.in_trade = False
        bot.in_jail = True
        msgs = bot.feed("(This is your 2nd turn in JAIL)")
        self.assertIn("roll", msgs, "Should roll when not trading")

    def test_trade_done_triggers_roll(self):
        """Trade is done! resets in_trade and triggers roll."""
        bot = self._make_bot()
        bot.current_player = "charlie"
        bot.in_trade = True
        bot.rolled_this_turn = False
        msgs = bot.feed("Trade is done!")
        self.assertFalse(bot.in_trade)
        self.assertIn("roll", msgs)

    def test_trade_decision_at_command_prompt(self):
        """Trade/roll decision happens at -- Command:, not at checkpoint."""
        bot = self._make_bot()
        # Checkpoint just records the turn
        msgs = bot.feed("charlie (3) (cash $1500) on === GO ===")
        self.assertNotIn("trade", msgs, "Checkpoint must not send trade")
        self.assertNotIn("roll", msgs, "Checkpoint must not send roll")

        # -- Command: is where the decision happens
        with patch("random.random", return_value=0.01):  # triggers trade
            msgs = bot.feed("-- Command:")
        self.assertTrue(bot.in_trade)
        self.assertIn("trade", msgs)
        self.assertNotIn("roll", msgs)


class TestValidInputs(unittest.TestCase):
    def test_picks_first_option(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        bot.setup_phase = False
        bot.current_player = "alice"
        msgs = bot.feed("Valid inputs are: Mediterranean ave. (P), Baltic ave. (P), done")
        self.assertIn("Mediterranean ave. (P)", msgs)

    def test_picks_done(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        bot.setup_phase = False
        bot.current_player = "alice"
        msgs = bot.feed("Valid inputs are: done")
        self.assertIn("done", msgs)


class TestBadPlayer(unittest.TestCase):
    def test_turn_correction(self):
        bot = FakePlayerBot("alice", ["alice", "bob"], 0)
        bot.setup_phase = False
        bot.current_player = "bob"
        msgs = bot.feed("Illegal action: bad player (alice's turn, not bob)")
        self.assertEqual(bot.current_player, "alice")
        # alice should roll
        self.assertIn("roll", msgs)


if __name__ == "__main__":
    unittest.main(verbosity=2)
