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 Game(PokerEnum):
    HOLDEM = "Hold'em", "HOLDEM"
    OMAHA = ("Omaha",)
    OHILO = ("Omaha Hi/Lo",)
    RAZZ = ("Razz",)
    STUD = ("Stud",) class Currency(PokerEnum):
    USD = "USD", "$"
    EUR = "EUR", "€"
    GBP = "GBP", "£"
    STARS_COIN = "SC", "StarsCoin" class Limit(PokerEnum):
    NL = "NL", "No limit"
    PL = "PL", "Pot limit"
    FL = "FL", "Fixed limit", "Limit" class GameType(PokerEnum):
    TOUR = "Tournament", "TOUR"
    CASH = "Cash game", "CASH", "RING"
    SNG = "Sit & Go", "SNG", "SIT AND GO", "Sit&go" 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 _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 FullTiltPokerHandHistory(hh._SplittableHandHistoryMixin, hh._BaseHandHistory):
    """Parses Full Tilt Poker hands the same way as PokerStarsHandHistory class."""

    rake = None
    tournament_level = None

    _DATE_FORMAT = "%H:%M:%S ET - %Y/%m/%d"
    _TZ = pytz.timezone("US/Eastern")  # ET
    _split_re = re.compile(r" ?\*\*\* ?\n?|\n")
    _header_re = re.compile(
        r"""
        ^Full[ ]Tilt[ ]Poker[ ]                                 # Poker Room
        Game[ ]\#(?P<ident>\d*):[ ]                             # Hand history id
        (?P<tournament_name>                                    # Tournament name
            \$?(?P<buyin>\d*)?                                  # buyin, not always there,
                                                                # part of tournament_name
        .*)[ ]                                                  # end of tournament_name
        \((?P<tournament_ident>\d*)\),[ ]                       # Tournament Number
        Table[ ](?P<table_name>\d*)[ ]-[ ]                      # Table name
        (?P<limit>NL|PL|FL|No Limit|Pot Limit|Fix Limit)[ ]     # limit
        (?P<game>.*?)[ ]-[ ]                                    # game
        (?P<sb>\d*)/(?P<bb>\d*)[ ]-[ ].*                        # blinds
        \[(?P<date>.*)\]$                                       # date in ET
        """,
        re.VERBOSE,
    )
    _seat_re = re.compile(r"^Seat (\d): (.*) \(([\d,]*)\)$")
    _button_re = re.compile(r"^The button is in seat #(\d)$")
    _hero_re = re.compile(r"^Dealt to (?P<hero_name>.*) \[(..) (..)\]$")
    _street_re = re.compile(r"\[([^\]]*)\] \(Total Pot: (\d*)\, (\d) Players")
    _pot_re = re.compile(r"^Total pot ([\d,]*) .*\| Rake (\d*)$")
    _winner_re = re.compile(r"^Seat (?P<seat>\d): (?P<name>.*?) .*collected \((\d*)\),")
    _showdown_re = re.compile(r"^Seat (\d): (.*) showed .* and won")
    _board_re = re.compile(r"(?<=[\[ ])(..)(?=[\] ])")

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

        header_match = self._header_re.match(self._splitted[0])
        self.sb = Decimal(header_match.group("sb"))
        self.bb = Decimal(header_match.group("bb"))
        self._parse_date(header_match.group("date"))
        self.ident = header_match.group("ident")
        tournament_name = header_match.group("tournament_name")
        self.game_type = (
            GameType.SNG if "Sit & Go" in tournament_name else GameType.TOUR
        )
        self.currency = Currency.USD if "$" in tournament_name else None
        self.tournament_ident = header_match.group("tournament_ident")
        self.table_name = header_match.group("table_name")
        self.limit = Limit(header_match.group("limit"))
        self.game = Game(header_match.group("game"))
        buyin = header_match.group("buyin")
        self.buyin = Decimal(buyin) if buyin else None

        self.extra = dict()
        self.extra["tournament_name"] = tournament_name

        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_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._parse_extra()

        self._del_split_vars()
        self.parsed = True

    def _parse_players(self):
        # In hh there is no indication of max_players, so init for 9.
        players = self._init_seats(9)
        for line in self._splitted[1:]:
            match = self._seat_re.match(line)
            if not match:
                break
            seat = int(match.group(1))
            players[seat - 1] = hh._Player(
                name=match.group(2),
                seat=seat,
                stack=_make_int(match.group(3)),
                combo=None,
            )
        self.max_players = seat
        self.players = players[:seat]  # cut off unneccesary seats

    def _parse_button(self):
        # one line before the first split.
        button_line = self._splitted[self._sections[0] - 1]
        button_seat = int(self._button_re.match(button_line).group(1))
        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 = self.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")
        except ValueError:
            self.flop = None
            return
        stop = next(v for v in self._sections if v > start)
        floplines = self._splitted[start + 1 : stop]
        self.flop = _Street(floplines)

    def _parse_street(self, street):
        try:
            start = self._splitted.index(street.upper()) + 1
            self._parse_streetline(start, street)
            stop = next(v for v in self._sections if v > start)
            street_actions = self._splitted[start + 1 : stop]
            setattr(
                self,
                f"{street}_actions",
                tuple(street_actions) if street_actions else None,
            )
        except ValueError:
            setattr(self, street, None)
            setattr(self, f"{street}_actions", None)
            setattr(self, f"{street}_pot", None)
            setattr(self, f"{street}_num_players", 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.replace(",", ""))
        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("name"))
            elif self.show_down and "won" in line:
                match = self._showdown_re.match(line)
                winners.add(match.group("name"))

        self.winners = tuple(winners)

    def _parse_extra(self):
        # tournament name already parsed in header
        for street in ("turn", "river"):
            try:
                start = self._splitted.index(street.upper()) + 1
                self._parse_streetline(start, street)
            except ValueError:
                self.extra[f"{street}_pot"] = None
                self.extra[f"{street}_num_players"] = None

    def _parse_streetline(self, start, street):
        """Parse pot, num players."""

        # Exceptions caught in _parse_street.
        board_line = self._splitted[start]
        match = self._street_re.search(board_line)
        pot = match.group(2)
        self.extra[f"{street}_pot"] = Decimal(pot)

        num_players = int(match.group(3))
        self.extra[f"{street}_num_players"] = num_players 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 "raises to" in line:
                action = self._parse_raise(line)
            elif "wins the pot" in line:
                action = self._parse_win(line)
            elif "mucks" in line:
                action = self._parse_muck(line)
            elif "seconds left to act" in line:
                action = self._parse_think(line)
            elif " " in line:
                action = self._parse_player_action(line)
            else:
                raise
            actions.append(hh._PlayerAction(*action))
        self.actions = tuple(actions) if actions else None

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

    def _parse_raise(self, line):
        first_space_index = line.find(" ")
        name = line[:first_space_index]
        amount_start_index = line.find("to ") + 3
        amount = line[amount_start_index:]
        return name, Action.RAISE, Decimal(amount)

    def _parse_win(self, line):
        first_space_index = line.find(" ")
        name = line[:first_space_index]
        first_paren_index = line.find("(")
        last_paren_index = -1
        amount = line[first_paren_index + 1 : last_paren_index]
        self.pot = Decimal(amount)
        return name, Action.WIN, self.pot

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

    def _parse_think(self, line):
        space_index = line.find(" ")
        name = line[:space_index]
        return name, Action.THINK, None

    def _parse_player_action(self, line):
        space_index = line.find(" ")
        name = line[:space_index]
        end_action_index = line.find(" ", space_index + 1)
        # -1 means not found
        if end_action_index == -1:
            end_action_index = None  # until the end
        action = Action(line[space_index + 1 : end_action_index])
        if end_action_index:
            amount = line[end_action_index + 1 :]
            return name, action, Decimal(amount)
        else:
            return name, action, None
from datetime import datetime
from decimal import Decimal
from poker.card import Card
from poker.hand import Combo
from poker.constants import Game, Currency, Limit, GameType, Action
from poker.handhistory import _Player, _PlayerAction
from poker.room.fulltiltpoker import FullTiltPokerHandHistory, _Street
from . import ftp_hands
import pytz
import pytest
def hand(request):
    """Parse handhistory defined in hand_text class attribute
    and returns a FullTiltPokerHandHistory instance.
    """
    hh = FullTiltPokerHandHistory(request.instance.hand_text)
    hh.parse()
    return hh


@pytest.fixture
def flop(scope="module"):
    return _Street(
        [
            "[8h 4h Tc] (Total Pot: 230, 2 Players)",
            "JohnyyR checks",
            "FatalRevange has 15 seconds left to act",
            "FatalRevange bets 120",
            "JohnyyR folds",
            "Uncalled bet of 120 returned to FatalRevange",
            "FatalRevange mucks",
            "FatalRevange wins the pot (230)",
        ],
        0,
    )


class TestHandWithFlopOnly:
    hand_text = ftp_hands.HAND1

    @pytest.mark.parametrize(
        ("attribute", "expected_value"),
        [
            ("game_type", GameType.TOUR),
            ("sb", Decimal(10)),
            ("bb", Decimal(20)),
            ("date", ET.localize(datetime(2013, 9, 22, 13, 26, 50))),
            ("game", Game.HOLDEM),
            ("limit", Limit.NL),
            ("ident", "33286946295"),
            ("tournament_ident", "255707037"),
            ("table_name", "179"),
            ("tournament_level", None),
            ("buyin", None),
            ("rake", None),
            ("currency", None),
        ],
    )
    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",
        [
            (
                "players",
                [
                    _Player(name="Popp1987", stack=13587, seat=1, combo=None),
                    _Player(name="Luckytobgood", stack=10110, seat=2, combo=None),
                    _Player(name="FatalRevange", stack=9970, seat=3, combo=None),
                    _Player(
                        name="IgaziFerfi", stack=10000, seat=4, combo=Combo("Ks9d")
                    ),
                    _Player(name="egis25", stack=6873, seat=5, combo=None),
                    _Player(name="gamblie", stack=9880, seat=6, combo=None),
                    _Player(name="idanuTz1", stack=10180, seat=7, combo=None),
                    _Player(name="PtheProphet", stack=9930, seat=8, combo=None),
                    _Player(name="JohnyyR", stack=9840, seat=9, combo=None),
                ],
            ),
            ("button", _Player(name="egis25", stack=6873, seat=5, combo=None)),
            ("max_players", 9),
            (
                "hero",
                _Player(name="IgaziFerfi", stack=10000, seat=4, combo=Combo("Ks9d")),
            ),
            (
                "preflop_actions",
                (
                    "PtheProphet has 15 seconds left to act",
                    "PtheProphet folds",
                    "JohnyyR raises to 40",
                    "Popp1987 has 15 seconds left to act",
                    "Popp1987 folds",
                    "Luckytobgood folds",
                    "FatalRevange raises to 100",
                    "IgaziFerfi folds",
                    "egis25 folds",
                    "gamblie folds",
                    "idanuTz1 folds",
                    "JohnyyR has 15 seconds left to act",
                    "JohnyyR calls 60",
                ),
            ),
            ("turn", None),
            ("river", None),
            ("total_pot", Decimal(230)),
            ("show_down", False),
            ("winners", ("FatalRevange",)),
            ("board", (Card("8h"), Card("4h"), Card("Tc"))),
            (
                "extra",
                dict(
                    tournament_name="MiniFTOPS Main Event",
                    turn_pot=None,
                    turn_num_players=None,
                    river_pot=None,
                    river_num_players=None,
                ),
            ),
        ],
    )
    def test_body(self, hand, attribute, expected_value):
        assert getattr(hand, attribute) == expected_value

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

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