import unittest
from random import Random

from bridzik import cards, Card, Card_colors, Card_values, Round
from rl.encoding import card_index, index_card, legal_cards
from rl.env import Decision, PHASE_GUESS, PHASE_PLAY, RoundEnv
from rl.evaluate import evaluate, play_round
from rl.players import (
    HeuristicPlayer, McPlayer, RandomPlayer,
    _beats, _current_best, deal_consistent, deduce_voids,
    mc_guess_distribution, simulate_tricks,
)


class RoundEnvCase(unittest.TestCase):
    def test_episode_structure(self):
        env = RoundEnv(Random(42))
        decision = env.reset(round_number=6, first_player=1)
        rng = Random(0)

        # prve 4 rozhodnutia su tipy, v poradi od first_player
        expected_guessers = [1, 2, 3, 0]
        for expected in expected_guessers:
            self.assertIsInstance(decision, Decision)
            self.assertEqual(decision.phase, PHASE_GUESS)
            self.assertEqual(decision.player, expected)
            action = rng.choice([g for g in range(9) if decision.mask[g]])
            decision, rewards, done = env.step(action)
            self.assertIsNone(rewards)
            self.assertFalse(done)

        # potom hracie rozhodnutia az po terminal: 2 karty x 4 hraci
        steps = 0
        while True:
            self.assertEqual(decision.phase, PHASE_PLAY)
            self.assertEqual(decision.player, env.round.get_active_player())
            action = rng.choice([i for i in range(32) if decision.mask[i]])
            decision, rewards, done = env.step(action)
            steps += 1
            if done:
                break
        self.assertEqual(steps, 8)
        self.assertIsNone(decision)
        self.assertEqual(rewards, env.round.get_points_summary())
        self.assertEqual(len(rewards), 4)

        # po done sa step neda volat, reset zacne novu epizodu
        self.assertRaises(RuntimeError, env.step, 0)
        self.assertIsInstance(env.reset(), Decision)

    def test_reset_samples_round_and_seat(self):
        env = RoundEnv(Random(7))
        seen_rounds, seen_seats = set(), set()
        for _ in range(100):
            env.reset()
            seen_rounds.add(env.round.round_number)
            seen_seats.add(env.round.first_player)
        self.assertEqual(seen_rounds, set(range(8)))
        self.assertEqual(seen_seats, set(range(4)))

    def test_deterministic_with_seed(self):
        rewards = []
        for _ in range(2):
            env = RoundEnv(Random(123))
            players = [RandomPlayer(Random(5)) for _ in range(4)]
            rewards.append(play_round(players, env, round_number=0))
        self.assertEqual(rewards[0], rewards[1])


class SimulationHelpersCase(unittest.TestCase):
    def test_beats(self):
        heart_7 = Card(Card_colors['HEARTS'], Card_values['C7'])
        heart_8 = Card(Card_colors['HEARTS'], Card_values['C8'])
        leaves_ace = Card(Card_colors['LEAVES'], Card_values['ACE'])
        leaves_king = Card(Card_colors['LEAVES'], Card_values['KING'])
        bells_ace = Card(Card_colors['BELLS'], Card_values['ACE'])

        self.assertTrue(_beats(leaves_ace, leaves_king))   # vyssia vo farbe
        self.assertFalse(_beats(leaves_king, leaves_ace))
        self.assertTrue(_beats(heart_7, leaves_ace))       # tromf bije farbu
        self.assertFalse(_beats(bells_ace, leaves_king))   # cudzia farba neberie
        self.assertTrue(_beats(heart_8, heart_7))          # tromfy medzi sebou
        self.assertFalse(_beats(leaves_ace, heart_7))      # farba nebije tromf

    def test_current_best_tracks_stash(self):
        from bridzik import Stash
        leaves_7 = Card(Card_colors['LEAVES'], Card_values['C7'])
        leaves_ace = Card(Card_colors['LEAVES'], Card_values['ACE'])
        heart_7 = Card(Card_colors['HEARTS'], Card_values['C7'])

        self.assertIsNone(_current_best(None))
        s = Stash(0)
        self.assertIsNone(_current_best(s))
        s.add_card(0, leaves_7)
        self.assertEqual(_current_best(s), leaves_7)
        s.add_card(1, leaves_ace)
        self.assertEqual(_current_best(s), leaves_ace)
        s.add_card(2, heart_7)
        self.assertEqual(_current_best(s), heart_7)

    def test_simulate_tricks_consumes_hands(self):
        rng = Random(3)
        deck = list(cards)
        rng.shuffle(deck)
        hands = {seat: deck[seat * 8:(seat + 1) * 8] for seat in range(4)}
        tricks = simulate_tricks(hands, leader=2, rng=rng)
        self.assertEqual(sum(tricks), 8)
        for seat in range(4):
            self.assertEqual(hands[seat], [])


class HeuristicPlayerCase(unittest.TestCase):
    @staticmethod
    def _round_with_hand(player0_hand):
        # deterministicke rozdanie: player0_hand ide hracovi 0, zvysok dalej;
        # deal_starting_cards najprv zahodi 4*round_number kariet, preto
        # treba ruku umiestnit az ZA odkladaciu kopu
        round_number = 8 - len(player0_hand)
        rest = [c for c in cards if c not in player0_hand]
        skip = 4 * round_number
        deck = rest[:skip] + list(player0_hand) + rest[skip:]
        return Round(round_number, 0, deck, shuffler=lambda l: None)

    def test_mc_guess_all_hearts_is_certain(self):
        # 8 cerveni = tromfy beru kazdu kopku bez ohladu na rozdanie a hru
        all_hearts = [Card(Card_colors['HEARTS'], v) for v in Card_values]
        r = self._round_with_hand(all_hearts)
        counts = mc_guess_distribution(r, 0, n_samples=30, rng=Random(1))
        self.assertEqual(counts, {8: 30})
        self.assertEqual(HeuristicPlayer(Random(1), n_samples=30).guess(r, 0), 8)

    def test_mc_guess_weak_hand_low(self):
        # dve najnizsie necervene karty -> tip 0 s prehladom
        weak = [Card(Card_colors['LEAVES'], Card_values['C7']),
                Card(Card_colors['BELLS'], Card_values['C7'])]
        r = self._round_with_hand(weak)
        self.assertEqual(HeuristicPlayer(Random(2), n_samples=60).guess(r, 0), 0)

    def test_mc_guess_respects_mask(self):
        # posledny tipujuci: zakazana hodnota nesmie byt vratena, ani ked
        # je modom rozdelenia
        all_hearts = [Card(Card_colors['HEARTS'], v) for v in Card_values]
        r = self._round_with_hand(all_hearts)
        r.add_player_guess(0, 0)
        r.add_player_guess(1, 0)
        r.add_player_guess(2, 0)
        # zakazany tip pre hraca 3 je 8; jeho ruka je nahodna, ale nech by
        # simulacia vratila cokolvek, vysledok musi byt legalny
        guess = HeuristicPlayer(Random(3), n_samples=20).guess(r, 3)
        self.assertNotEqual(guess, 8)
        self.assertIn(guess, range(8))

    def test_play_takes_trick_when_needed(self):
        hand = [Card(Card_colors['LEAVES'], Card_values['ACE']),
                Card(Card_colors['LEAVES'], Card_values['C7']),
                Card(Card_colors['BELLS'], Card_values['C7'])]
        r = self._round_with_hand(hand)
        r.add_player_guess(0, 3)   # najvyssi tip -> hrac 0 vynasa
        r.add_player_guess(1, 0)
        r.add_player_guess(2, 0)
        r.add_player_guess(3, 1)
        # hrac 0 potrebuje kopky -> vynasa najsilnejsiu kartu (LEAVES ACE)
        action = HeuristicPlayer(Random(4)).play(r, 0)
        self.assertEqual(index_card(action), hand[0])

    def test_play_ducks_when_satisfied(self):
        hand = [Card(Card_colors['LEAVES'], Card_values['ACE']),
                Card(Card_colors['LEAVES'], Card_values['C7']),
                Card(Card_colors['BELLS'], Card_values['C7'])]
        r = self._round_with_hand(hand)
        r.add_player_guess(0, 0)   # hrac 0 nechce ziadnu kopku
        r.add_player_guess(1, 2)   # najvyssi tip -> vynasa hrac 1
        r.add_player_guess(2, 0)
        r.add_player_guess(3, 0)
        first_card = legal_cards(r.player_cards[1], None)[0]
        r.play_card(1, first_card)
        action = HeuristicPlayer(Random(5)).play(r, 2)
        # legalnost staci overit enginom; strategiu netestujeme natvrdo,
        # lebo zavisi od nahodnej ruky hraca 2
        r.play_card(2, index_card(action))

    def test_play_duck_scenario_deterministic(self):
        # hrac 0 tipol 0, ma na ruke LEAVES ACE aj C7; kopku vedie LEAVES C8
        # -> musi priznat farbu a spravne je podliezt (C7), nie zobrat esom
        hand0 = [Card(Card_colors['LEAVES'], Card_values['ACE']),
                 Card(Card_colors['LEAVES'], Card_values['C7'])]
        hand1 = [Card(Card_colors['LEAVES'], Card_values['C8']),
                 Card(Card_colors['LEAVES'], Card_values['C9'])]
        rest = [c for c in cards if c not in hand0 + hand1]
        deck = rest[:24] + hand0 + hand1 + rest[24:]   # 24 = odkladacia kopa
        r = Round(6, 0, deck, shuffler=lambda l: None)
        r.add_player_guess(0, 0)
        r.add_player_guess(1, 2)   # vynasa hrac 1
        r.add_player_guess(2, 0)
        r.add_player_guess(3, 1)   # 0+2+0+0 by bol zakazany sucet (2 kopky)
        r.play_card(1, hand1[0])
        action = HeuristicPlayer(Random(6)).play(r, 0)
        self.assertEqual(index_card(action), hand0[1])


class VoidDeductionCase(unittest.TestCase):
    def test_deduce_voids_from_stash(self):
        # hrac 0 vynasa zelen; 1 prizna farbu (nic), 2 tromfne cervenou
        # (void zelen), 3 hodi gulu (void zelen AJ cerven)
        hand0 = [Card(Card_colors['LEAVES'], Card_values['C7']),
                 Card(Card_colors['LEAVES'], Card_values['C8'])]
        hand1 = [Card(Card_colors['LEAVES'], Card_values['C9']),
                 Card(Card_colors['LEAVES'], Card_values['C10'])]
        hand2 = [Card(Card_colors['HEARTS'], Card_values['C7']),
                 Card(Card_colors['ACORNS'], Card_values['C7'])]
        hand3 = [Card(Card_colors['BELLS'], Card_values['C7']),
                 Card(Card_colors['BELLS'], Card_values['C8'])]
        rest = [c for c in cards if c not in hand0 + hand1 + hand2 + hand3]
        deck = rest[:24] + hand0 + hand1 + hand2 + hand3
        r = Round(6, 0, deck, shuffler=lambda l: None)
        r.add_player_guess(0, 2)   # najvyssi tip -> vynasa 0
        r.add_player_guess(1, 0)
        r.add_player_guess(2, 0)
        r.add_player_guess(3, 1)

        self.assertEqual(deduce_voids(r), {0: set(), 1: set(), 2: set(), 3: set()})
        r.play_card(0, hand0[0])
        r.play_card(1, hand1[0])   # priznal farbu -> nic
        r.play_card(2, hand2[0])   # cerven -> void zelen
        r.play_card(3, hand3[0])   # gula -> void zelen aj cerven
        voids = deduce_voids(r)
        self.assertEqual(voids[0], set())    # vynasajuci neprezradza nic
        self.assertEqual(voids[1], set())
        self.assertEqual(voids[2], {Card_colors['LEAVES']})
        self.assertEqual(voids[3], {Card_colors['LEAVES'], Card_colors['HEARTS']})

    def test_deduced_voids_never_contradict_hands(self):
        # fuzz: dedukovany void NIKDY neprotireci realnej ruke hraca
        rng = Random(21)
        for _ in range(30):
            r = Round(rng.randrange(4), rng.randrange(4))
            players = [RandomPlayer(Random(rng.random())) for _ in range(4)]
            for _ in range(4):
                seat = r.get_active_player()
                r.add_player_guess(seat, players[seat].guess(r, seat))
            while not r.is_completed():
                seat = r.get_active_player()
                r.play_card(seat, index_card(players[seat].play(r, seat)))
                for other, banned in deduce_voids(r).items():
                    held = {c.color for c in r.player_cards[other]}
                    self.assertFalse(held & banned,
                                     f'void {banned} vs ruka {held}')

    def test_deal_consistent_respects_voids(self):
        rng = Random(22)
        unknown = [c for c in cards][:20]
        voids = {1: {Card_colors['HEARTS']}, 2: set(),
                 3: {Card_colors['LEAVES'], Card_colors['BELLS']}}
        for _ in range(20):
            hands = deal_consistent(unknown, {1: 4, 2: 4, 3: 4}, voids, rng)
            self.assertTrue(all(len(h) == 4 for h in hands.values()))
            for seat, banned in voids.items():
                self.assertFalse({c.color for c in hands[seat]} & banned)


class McPlayerCase(unittest.TestCase):
    def test_plays_legal_full_rounds(self):
        rng = Random(23)
        env = RoundEnv(rng)
        players = [McPlayer(Random(24), n_samples=20, play_samples=8),
                   McPlayer(Random(25), n_samples=20, play_samples=8,
                            use_voids=False)] \
            + [RandomPlayer(Random(s)) for s in (26, 27)]
        for round_number in range(8):
            rewards = play_round(players, env, round_number)
            self.assertEqual(len(rewards), 4)

    def test_duck_scenario(self):
        # tip 0, kopku vedie sused LEAVES C8 a ja som HNED na tahu (MC hrac
        # stavia kopku poctivo, takze na rozdiel od pravidlovej heuristiky
        # vyzaduje konzistentne poradie): mam ACE aj C7 -> podlezt sedmickou
        hand0 = [Card(Card_colors['LEAVES'], Card_values['ACE']),
                 Card(Card_colors['LEAVES'], Card_values['C7'])]
        hand3 = [Card(Card_colors['LEAVES'], Card_values['C8']),
                 Card(Card_colors['LEAVES'], Card_values['C9'])]
        rest = [c for c in cards if c not in hand0 + hand3]
        deck = rest[:24] + hand0 + rest[24:26] + rest[26:28] + hand3
        r = Round(6, 0, deck, shuffler=lambda l: None)
        r.add_player_guess(0, 0)
        r.add_player_guess(1, 0)
        r.add_player_guess(2, 0)
        r.add_player_guess(3, 1)   # najvyssi tip -> vynasa hrac 3, po nom ja
        r.play_card(3, hand3[0])
        self.assertEqual(r.get_active_player(), 0)
        action = McPlayer(Random(28), play_samples=30).play(r, 0)
        self.assertEqual(index_card(action), hand0[1])


class EvaluateCase(unittest.TestCase):
    def test_full_random_matchup_runs(self):
        rng = Random(11)
        stats = evaluate([RandomPlayer(rng) for _ in range(4)], 40, rng)
        for s in stats:
            self.assertEqual(s['rounds'], 40)
            self.assertGreaterEqual(s['avg_points'], 0)
            self.assertLessEqual(s['hit_rate'], 1)

    def test_heuristic_beats_random(self):
        rng = Random(13)
        players = [HeuristicPlayer(rng, n_samples=40)] \
            + [RandomPlayer(rng) for _ in range(3)]
        stats = evaluate(players, 120, rng)
        heuristic, randoms = stats[0], stats[1:]
        best_random = max(s['avg_points'] for s in randoms)
        self.assertGreater(heuristic['avg_points'], best_random)
        self.assertGreater(heuristic['hit_rate'],
                           max(s['hit_rate'] for s in randoms))


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