class Card(_ReprMixin, metaclass=_CardMeta):
    """Represents a Card, which consists a Rank and a Suit."""

    __slots__ = ("rank", "suit")

    def __new__(cls, card):
        if isinstance(card, cls):
            return card

        if len(card) != 2:
            raise ValueError("length should be two in %r" % card)

        self = object.__new__(cls)
        self.rank = Rank(card[0])
        self.suit = Suit(card[1])
        return self

    def __hash__(self):
        return hash(self.rank) + hash(self.suit)

    def __eq__(self, other):
        if self.__class__ is other.__class__:
            return self.rank == other.rank and self.suit == other.suit
        return NotImplemented

    def __lt__(self, other):
        if self.__class__ is not other.__class__:
            return NotImplemented

        # with same ranks, suit counts
        if self.rank == other.rank:
            return self.suit < other.suit
        return self.rank < other.rank

    def __str__(self):
        return f"{self.rank}{self.suit}"

    @property
    def is_face(self):
        return self.rank in FACE_RANKS

    @property
    def is_broadway(self):
        return self.rank in BROADWAY_RANKS class Combo(_ReprMixin):
    """Hand combination."""

    __slots__ = ("first", "second")

    def __new__(cls, combo):
        if isinstance(combo, Combo):
            return combo

        if len(combo) != 4:
            raise ValueError("%r, should have a length of 4" % combo)
        elif combo[0] == combo[2] and combo[1] == combo[3]:
            raise ValueError(f"{combo!r}, Pair can't have the same suit: {combo[1]!r}")

        self = super().__new__(cls)
        self._set_cards_in_order(combo[:2], combo[2:])
        return self

    @classmethod
    def from_cards(cls, first, second):
        self = super().__new__(cls)
        first = first.rank.val + first.suit.val
        second = second.rank.val + second.suit.val
        self._set_cards_in_order(first, second)
        return self

    def __str__(self):
        return f"{self.first}{self.second}"

    def __hash__(self):
        return hash(self.first) + hash(self.second)

    def __eq__(self, other):
        if self.__class__ is other.__class__:
            return self.first == other.first and self.second == other.second
        return NotImplemented

    def __lt__(self, other):
        if self.__class__ is not other.__class__:
            return NotImplemented

        # lookup optimization
        self_is_pair, other_is_pair = self.is_pair, other.is_pair
        self_first, other_first = self.first, other.first

        if self_is_pair and other_is_pair:
            if self_first == other_first:
                return self.second < other.second
            return self_first < other_first

        elif self_is_pair or other_is_pair:
            # Pairs are better than non-pairs
            return self_is_pair < other_is_pair

        else:
            if self_first.rank == other_first.rank:
                if self.second.rank == other.second.rank:
                    # same ranks, suited go first in order by Suit rank
                    if self.is_suited or other.is_suited:
                        return self.is_suited < other.is_suited
                    # both are suited
                    return self_first.suit < other_first.suit
                return self.second < other.second
            return self_first < other_first

    def _set_cards_in_order(self, first, second):
        self.first, self.second = Card(first), Card(second)
        if self.first < self.second:
            self.first, self.second = self.second, self.first

    def to_hand(self):
        """Convert combo to :class:`Hand` object, losing suit information."""
        return Hand(f"{self.first.rank}{self.second.rank}{self.shape}")

    @property
    def is_suited_connector(self):
        return self.is_suited and self.is_connector

    @property
    def is_suited(self):
        return self.first.suit == self.second.suit

    @property
    def is_offsuit(self):
        return not self.is_suited and not self.is_pair

    @property
    def is_connector(self):
        return self.rank_difference == 1

    @property
    def is_one_gapper(self):
        return self.rank_difference == 2

    @property
    def is_two_gapper(self):
        return self.rank_difference == 3

    @property
    def rank_difference(self):
        """The difference between the first and second rank of the Combo."""
        # self.first >= self.second
        return Rank.difference(self.first.rank, self.second.rank)

    @property
    def is_pair(self):
        return self.first.rank == self.second.rank

    @property
    def is_broadway(self):
        return self.first.is_broadway and self.second.is_broadway

    @property
    def shape(self):
        if self.is_pair:
            return Shape.PAIR
        elif self.is_suited:
            return Shape.SUITED
        else:
            return Shape.OFFSUIT

    @shape.setter
    def shape(self, value):
        self._shape = Shape(value).val class Currency(PokerEnum):
    USD = "USD", "$"
    EUR = "EUR", "€"
    GBP = "GBP", "£"
    STARS_COIN = "SC", "StarsCoin" class GameType(PokerEnum):
    TOUR = "Tournament", "TOUR"
    CASH = "Cash game", "CASH", "RING"
    SNG = "Sit & Go", "SNG", "SIT AND GO", "Sit&go" class Game(PokerEnum):
    HOLDEM = "Hold'em", "HOLDEM"
    OMAHA = ("Omaha",)
    OHILO = ("Omaha Hi/Lo",)
    RAZZ = ("Razz",)
    STUD = ("Stud",) class Limit(PokerEnum):
    NL = "NL", "No limit"
    PL = "PL", "Pot limit"
    FL = "FL", "Fixed limit", "Limit" class Action(PokerEnum):
    BET = "bet", "bets"
    RAISE = "raise", "raises"
    CHECK = "check", "checks"
    FOLD = "fold", "folded", "folds"
    CALL = "call", "calls"
    RETURN = "return", "returned", "uncalled"
    WIN = "win", "won", "collected"
    SHOW = ("show",)
    MUCK = "don't show", "didn't show", "did not show", "mucks"
    THINK = ("seconds left to act",) class MoneyType(PokerEnum):
    REAL = ("Real money",)
    PLAY = ("Play money",) class _Player:
    """Player participating in the hand history."""

    name = attr.ib()
    stack = attr.ib()
    seat = attr.ib()
    combo = attr.ib() class _PlayerAction:
    """Player actions on the street."""

    name = attr.ib()
    action = attr.ib()
    amount = attr.ib() class PokerStarsHandHistory(hh._SplittableHandHistoryMixin, hh._BaseHandHistory):
    """Parses PokerStars Tournament hands."""

    _DATE_FORMAT = "%Y/%m/%d %H:%M:%S ET"
    _TZ = pytz.timezone("US/Eastern")  # ET
    _split_re = re.compile(r" ?\*\*\* ?\n?|\n")
    _header_re = re.compile(
        r"""
                        ^PokerStars\s+                                # Poker Room
                        Hand\s+\#(?P<ident>\d+):\s+                   # Hand history id
                        (Tournament\s+\#(?P<tournament_ident>\d+),\s+ # Tournament Number
                         ((?P<freeroll>Freeroll)|(                    # buyin is Freeroll
                          \$?(?P<buyin>\d+(\.\d+)?)                   # or buyin
                          (\+\$?(?P<rake>\d+(\.\d+)?))?               # and rake
                          (\s+(?P<currency>[A-Z]+))?                  # and currency
                         ))\s+
                        )?
                        (?P<game>.+?)\s+                              # game
                        (?P<limit>(?:Pot\s+|No\s+|)Limit)\s+          # limit
                        (-\s+Level\s+(?P<tournament_level>\S+)\s+)?   # Level (optional)
                        \(
                         (((?P<sb>\d+)/(?P<bb>\d+))|(                 # tournament blinds
                          \$(?P<cash_sb>\d+(\.\d+)?)/                 # cash small blind
                          \$(?P<cash_bb>\d+(\.\d+)?)                  # cash big blind
                          (\s+(?P<cash_currency>\S+))?                # cash currency
                         ))
                        \)\s+
                        -\s+.+?\s+                                    # localized date
                        \[(?P<date>.+?)\]                             # ET date
                        """,
        re.VERBOSE,
    )
    _table_re = re.compile(
        r"^Table '(.*)' (\d+)-max Seat #(?P<button>\d+) is the button"
    )
    _seat_re = re.compile(
        r"^Seat (?P<seat>\d+): (?P<name>.+?) \(\$?(?P<stack>\d+(\.\d+)?) in chips\)"
    )  # noqa
    _hero_re = re.compile(r"^Dealt to (?P<hero_name>.+?) \[(..) (..)\]")
    _pot_re = re.compile(r"^Total pot (\d+(?:\.\d+)?) .*\| Rake (\d+(?:\.\d+)?)")
    _winner_re = re.compile(r"^Seat (\d+): (.+?) collected \((\d+(?:\.\d+)?)\)")
    _showdown_re = re.compile(r"^Seat (\d+): (.+?) showed \[.+?\] and won")
    _ante_re = re.compile(r".*posts the ante (\d+(?:\.\d+)?)")
    _board_re = re.compile(r"(?<=[\[ ])(..)(?=[\] ])")

    def parse_header(self):
        # sections[0] is before HOLE CARDS
        # sections[-1] is before SUMMARY
        self._split_raw()

        match = self._header_re.match(self._splitted[0])

        self.extra = dict()
        self.ident = match.group("ident")

        # We cannot use the knowledege of the game type to pick between the blind
        # and cash blind captures because a cash game play money blind looks exactly
        # like a tournament blind

        self.sb = Decimal(match.group("sb") or match.group("cash_sb"))
        self.bb = Decimal(match.group("bb") or match.group("cash_bb"))

        if match.group("tournament_ident"):
            self.game_type = GameType.TOUR
            self.tournament_ident = match.group("tournament_ident")
            self.tournament_level = match.group("tournament_level")

            currency = match.group("currency")
            self.buyin = Decimal(match.group("buyin") or 0)
            self.rake = Decimal(match.group("rake") or 0)
        else:
            self.game_type = GameType.CASH
            self.tournament_ident = None
            self.tournament_level = None
            currency = match.group("cash_currency")
            self.buyin = None
            self.rake = None

        if match.group("freeroll") and not currency:
            currency = "USD"

        if not currency:
            self.extra["money_type"] = MoneyType.PLAY
            self.currency = None
        else:
            self.extra["money_type"] = MoneyType.REAL
            self.currency = Currency(currency)

        self.game = Game(match.group("game"))
        self.limit = Limit(match.group("limit"))

        self._parse_date(match.group("date"))

        self.header_parsed = True

    def parse(self):
        """Parses the body of the hand history, but first parse header if not yet parsed."""
        if not self.header_parsed:
            self.parse_header()

        self._parse_table()
        self._parse_players()
        self._parse_button()
        self._parse_hero()
        self._parse_preflop()
        self._parse_flop()
        self._parse_street("turn")
        self._parse_street("river")
        self._parse_showdown()
        self._parse_pot()
        self._parse_board()
        self._parse_winners()

        self._del_split_vars()
        self.parsed = True

    def _parse_table(self):
        self._table_match = self._table_re.match(self._splitted[1])
        self.table_name = self._table_match.group(1)
        self.max_players = int(self._table_match.group(2))

    def _parse_players(self):
        self.players = self._init_seats(self.max_players)
        for line in self._splitted[2:]:
            match = self._seat_re.match(line)
            # we reached the end of the players section
            if not match:
                break
            index = int(match.group("seat")) - 1
            self.players[index] = hh._Player(
                name=match.group("name"),
                stack=int(match.group("stack")),
                seat=int(match.group("seat")),
                combo=None,
            )

    def _parse_button(self):
        button_seat = int(self._table_match.group("button"))
        self.button = self.players[button_seat - 1]

    def _parse_hero(self):
        hole_cards_line = self._splitted[self._sections[0] + 2]
        match = self._hero_re.match(hole_cards_line)
        hero, hero_index = self._get_hero_from_players(match.group("hero_name"))
        hero.combo = Combo(match.group(2) + match.group(3))
        self.hero = self.players[hero_index] = hero
        if self.button.name == self.hero.name:
            self.button = hero

    def _parse_preflop(self):
        start = self._sections[0] + 3
        stop = self._sections[1]
        self.preflop_actions = tuple(self._splitted[start:stop])

    def _parse_flop(self):
        try:
            start = self._splitted.index("FLOP") + 1
        except ValueError:
            self.flop = None
            return
        stop = self._splitted.index("", start)
        floplines = self._splitted[start:stop]
        self.flop = _Street(floplines)

    def _parse_street(self, street):
        try:
            start = self._splitted.index(street.upper()) + 2
            stop = self._splitted.index("", start)
            street_actions = self._splitted[start:stop]
            setattr(
                self,
                f"{street.lower()}_actions",
                tuple(street_actions) if street_actions else None,
            )
        except ValueError:
            setattr(self, street, None)
            setattr(self, f"{street.lower()}_actions", None)

    def _parse_showdown(self):
        self.show_down = "SHOW DOWN" in self._splitted

    def _parse_pot(self):
        potline = self._splitted[self._sections[-1] + 2]
        match = self._pot_re.match(potline)
        self.total_pot = int(match.group(1))

    def _parse_board(self):
        boardline = self._splitted[self._sections[-1] + 3]
        if not boardline.startswith("Board"):
            return
        cards = self._board_re.findall(boardline)
        self.turn = Card(cards[3]) if len(cards) > 3 else None
        self.river = Card(cards[4]) if len(cards) > 4 else None

    def _parse_winners(self):
        winners = set()
        start = self._sections[-1] + 4
        for line in self._splitted[start:]:
            if not self.show_down and "collected" in line:
                match = self._winner_re.match(line)
                winners.add(match.group(2))
            elif self.show_down and "won" in line:
                match = self._showdown_re.match(line)
                winners.add(match.group(2))

        self.winners = tuple(winners) class _Street(hh._BaseStreet):
    def _parse_cards(self, boardline):
        self.cards = (Card(boardline[1:3]), Card(boardline[4:6]), Card(boardline[7:9]))

    def _parse_actions(self, actionlines):
        actions = []
        for line in actionlines:
            if line.startswith("Uncalled bet"):
                action = self._parse_uncalled(line)
            elif "collected" in line:
                action = self._parse_collected(line)
            elif "doesn't show hand" in line:
                action = self._parse_muck(line)
            elif ' said, "' in line:  # skip chat lines
                continue
            elif ":" in line:
                action = self._parse_player_action(line)
            else:
                raise RuntimeError("bad action line: " + line)

            actions.append(hh._PlayerAction(*action))
        self.actions = tuple(actions) if actions else None

    def _parse_uncalled(self, line):
        first_paren_index = line.find("(")
        second_paren_index = line.find(")")
        amount = line[first_paren_index + 1 : second_paren_index]
        name_start_index = line.find("to ") + 3
        name = line[name_start_index:]
        return name, Action.RETURN, Decimal(amount)

    def _parse_collected(self, line):
        first_space_index = line.find(" ")
        name = line[:first_space_index]
        second_space_index = line.find(" ", first_space_index + 1)
        third_space_index = line.find(" ", second_space_index + 1)
        amount = line[second_space_index + 1 : third_space_index]
        self.pot = Decimal(amount)
        return name, Action.WIN, self.pot

    def _parse_muck(self, line):
        colon_index = line.find(":")
        name = line[:colon_index]
        return name, Action.MUCK, None

    def _parse_player_action(self, line):
        name, _, action = line.partition(": ")
        action, _, amount = action.partition(" ")
        amount, _, _ = amount.partition(" ")

        if amount:
            return name, Action(action), Decimal(amount)
        else:
            return name, Action(action), None
from decimal import Decimal
from datetime import datetime
from poker.card import Card
from poker.hand import Combo
from poker.constants import Currency, GameType, Game, Limit, Action, MoneyType
from poker.handhistory import _Player, _PlayerAction
from poker.room.pokerstars import PokerStarsHandHistory, _Street
from . import stars_hands
import pytz
import pytest
                    "wrsport1015: folds",
                    "W2lkm2n: folds",
                    "fischero68: folds",
                    "snelle_jel: folds",
                    "Uncalled bet (600) returned to Theralion",
                    "Theralion collected 1900 from pot",
                    "Theralion: doesn't show hand",
                ),
            ),
            ("turn_actions", None),
            ("river_actions", None),
            ("total_pot", Decimal(1900)),
            ("show_down", False),
            ("winners", ("Theralion",)),
        ],
    )
    def test_body(self, hand, attribute, expected_value):
        assert getattr(hand, attribute) == expected_value

    def test_flop(self, hand):
        assert hand.flop is None


class TestBodyEveryStreet:
    hand_text = stars_hands.HAND4

    @pytest.mark.parametrize(
        ("attribute", "expected_value"),
        [
            ("ident", "105025168298"),
            ("game_type", GameType.TOUR),
            ("tournament_ident", "797469411"),
            ("tournament_level", "IV"),
            ("currency", Currency.USD),
            ("buyin", Decimal("3.19")),
            ("rake", Decimal("0.31")),
            ("game", Game.HOLDEM),
            ("limit", Limit.NL),
            ("sb", Decimal(50)),
            ("bb", Decimal(100)),
            ("date", ET.localize(datetime(2013, 10, 4, 14, 19, 17))),
        ],
    )
    def test_values_after_header_parsed(self, hand_header, attribute, expected_value):
        assert getattr(hand_header, attribute) == expected_value

    @pytest.mark.parametrize(
        ("attribute", "expected_value"),
        [
            ("table_name", "797469411 15"),
            ("max_players", 9),
            (
                "button",
                _Player(name="W2lkm2n", stack=5145, seat=5, combo=Combo("Jc5c")),
            ),
            ("hero", _Player(name="W2lkm2n", stack=5145, seat=5, combo=Combo("Jc5c"))),
            (
                "players",
                [
                    _Player(name="flettl2", stack=3000, seat=1, combo=None),
                    _Player(name="santy312", stack=5890, seat=2, combo=None),
                    _Player(name="flavio766", stack=11010, seat=3, combo=None),
                    _Player(name="strongi82", stack=2855, seat=4, combo=None),
                    _Player(name="W2lkm2n", stack=5145, seat=5, combo=Combo("Jc5c")),
                    _Player(name="MISTRPerfect", stack=2395, seat=6, combo=None),
                    _Player(name="blak_douglas", stack=3000, seat=7, combo=None),
                    _Player(name="sinus91", stack=3000, seat=8, combo=None),
                    _Player(name="STBIJUJA", stack=1205, seat=9, combo=None),
                ],
            ),
            ("turn", Card("8c")),
            ("river", Card("Kd")),
            ("board", (Card("6s"), Card("4d"), Card("3s"), Card("8c"), Card("Kd"))),
            (
                "preflop_actions",
                (
                    "sinus91: folds",
                    "STBIJUJA: folds",
                    "flettl2: raises 125 to 225",
                    "santy312: folds",
                    "flavio766: folds",
                    "strongi82: folds",
                    "W2lkm2n: folds",
                    "MISTRPerfect: folds",
                    "blak_douglas: calls 125",
                ),
            ),
            (
                "turn_actions",
                (
                    "blak_douglas: checks",
                    "flettl2: bets 250",
                    "blak_douglas: calls 250",
                ),
            ),
            (
                "river_actions",
                (
                    "blak_douglas: checks",
                    "flettl2: bets 1300",
                    "blak_douglas: folds",
                    "Uncalled bet (1300) returned to flettl2",
                    "flettl2 collected 1300 from pot",
                    "flettl2: doesn't show hand",
                ),
            ),
            ("total_pot", Decimal(1300)),
            ("show_down", False),
            ("winners", ("flettl2",)),
        ],
    )
    def test_body(self, hand, attribute, expected_value):
        assert getattr(hand, attribute) == expected_value

    @pytest.mark.parametrize(
        ("attribute", "expected_value"),
        [
            (
                "actions",
                (

                    _PlayerAction("blak_douglas", Action.CHECK, None),