import random
import time
from typing import Any, Dict, List, Optional, cast

from bemani.client.base import BaseClient
from bemani.protocol import Node


class ReflecBeatGroovinUpper(BaseClient):
    NAME = 'ＴＥＳＴ'

    def verify_pcb_rb4boot(self, loc: str) -> None:
        call = self.call_node()

        pcb = Node.void('pcb')
        pcb.set_attribute('method', 'rb4boot')
        pcb.add_child(Node.string('lid', loc))
        pcb.add_child(Node.string('rno', 'Unknown'))
        call.add_child(pcb)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/pcb/sinfo/nm")
        self.assert_path(resp, "response/pcb/sinfo/cl_enbl")
        self.assert_path(resp, "response/pcb/sinfo/cl_h")
        self.assert_path(resp, "response/pcb/sinfo/cl_m")
        self.assert_path(resp, "response/pcb/sinfo/shop_flag")

    def verify_pcb_rb4error(self, loc: str) -> None:
        call = self.call_node()

        pcb = Node.void('pcb')
        call.add_child(pcb)
        pcb.set_attribute('method', 'rb4error')
        pcb.add_child(Node.string('lid', loc))
        pcb.add_child(Node.string('code', 'exception'))
        pcb.add_child(Node.string('msg', 'exceptionstring'))

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/pcb/@status")

    def verify_info_rb4common(self, loc: str) -> None:
        call = self.call_node()

        info = Node.void('info')
        call.add_child(info)
        info.set_attribute('method', 'rb4common')
        info.add_child(Node.string('lid', loc))

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/info/event_ctrl")
        self.assert_path(resp, "response/info/item_lock_ctrl")
        self.assert_path(resp, "response/info/shop_score/today")
        self.assert_path(resp, "response/info/shop_score/yesterday")

    def verify_info_rb4shop_score_ranking(self, loc: str) -> None:
        call = self.call_node()

        info = Node.void('info')
        call.add_child(info)
        info.set_attribute('method', 'rb4shop_score_ranking')
        # Arbitrarily chosen based on the song IDs we send in the
        # score section below.
        info.add_child(Node.s16('min', 1))
        info.add_child(Node.s16('max', 3))
        info.add_child(Node.string('lid', loc))

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/info/shop_score/time")
        self.assert_path(resp, "response/info/shop_score/data/rank")
        self.assert_path(resp, "response/info/shop_score/data/music_id")
        self.assert_path(resp, "response/info/shop_score/data/note_grade")
        self.assert_path(resp, "response/info/shop_score/data/clear_type")
        self.assert_path(resp, "response/info/shop_score/data/user_id")
        self.assert_path(resp, "response/info/shop_score/data/icon_id")
        self.assert_path(resp, "response/info/shop_score/data/score")
        self.assert_path(resp, "response/info/shop_score/data/time")
        self.assert_path(resp, "response/info/shop_score/data/name")

    def verify_info_rb4ranking(self) -> None:
        call = self.call_node()

        info = Node.void('info')
        info.set_attribute('method', 'rb4ranking')
        info.add_child(Node.s32('ver', 0))
        call.add_child(info)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/info/ver")
        self.assert_path(resp, "response/info/ranking/weekly/bt")
        self.assert_path(resp, "response/info/ranking/weekly/et")
        self.assert_path(resp, "response/info/ranking/weekly/new/d/mid")
        self.assert_path(resp, "response/info/ranking/weekly/new/d/cnt")
        self.assert_path(resp, "response/info/ranking/monthly/bt")
        self.assert_path(resp, "response/info/ranking/monthly/et")
        self.assert_path(resp, "response/info/ranking/monthly/new/d/mid")
        self.assert_path(resp, "response/info/ranking/monthly/new/d/cnt")
        self.assert_path(resp, "response/info/ranking/total/bt")
        self.assert_path(resp, "response/info/ranking/total/et")
        self.assert_path(resp, "response/info/ranking/total/new/d/mid")
        self.assert_path(resp, "response/info/ranking/total/new/d/cnt")

    def verify_player_rb4start(self, refid: str) -> None:
        call = self.call_node()

        player = Node.void('player')
        player.set_attribute('method', 'rb4start')
        player.add_child(Node.string('rid', refid))
        player.add_child(Node.u8_array('ga', [127, 0, 0, 1]))
        player.add_child(Node.u16('gp', 10573))
        player.add_child(Node.u8_array('la', [16, 0, 0, 0]))
        player.add_child(Node.u8_array('pnid', [39, 16, 0, 0, 0, 23, 62, 60, 39, 127, 0, 0, 1, 23, 62, 60]))

        call.add_child(player)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/player/plyid")
        self.assert_path(resp, "response/player/start_time")
        self.assert_path(resp, "response/player/event_ctrl")
        self.assert_path(resp, "response/player/item_lock_ctrl")

    def verify_player_rb4end(self, refid: str) -> None:
        call = self.call_node()

        player = Node.void('player')
        player.set_attribute('method', 'rb4end')
        player.add_child(Node.string('rid', refid))
        call.add_child(player)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/player")

    def verify_player_rb4total_bestallrank_read(self) -> None:
        call = self.call_node()

        player = Node.void('player')
        player.set_attribute('method', 'rb4total_bestallrank_read')
        player.add_child(Node.s32('uid', 0))
        player.add_child(Node.s32_array('score', [897, 897, 0, 0, 0, 284]))
        call.add_child(player)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/player/score/rank")
        self.assert_path(resp, "response/player/score/score")
        self.assert_path(resp, "response/player/score/allrank")

    def verify_player_rb4selectscore(self, extid: int) -> None:
        call = self.call_node()

        player = Node.void('player')
        player.set_attribute('method', 'rb4selectscore')
        player.add_child(Node.s32('uid', extid))
        player.add_child(Node.s32('music_id', 1))
        player.add_child(Node.s32('note_grade', 0))
        call.add_child(player)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/player/@status")

        # Verify that we got a score if the extid is nonzero
        if extid != 0:
            self.assert_path(resp, "response/player/player_select_score/user_id")
            self.assert_path(resp, "response/player/player_select_score/name")
            self.assert_path(resp, "response/player/player_select_score/m_score")
            self.assert_path(resp, "response/player/player_select_score/m_scoreTime")
            self.assert_path(resp, "response/player/player_select_score/m_iconID")

            if resp.child_value('player/player_select_score/name') != self.NAME:
                raise Exception(
                    f'Invalid name {resp.child_value("player/player_select_score/name")} returned on score read!'
                )
            if resp.child_value('player/player_select_score/user_id') != extid:
                raise Exception(
                    f'Invalid name {resp.child_value("player/player_select_score/user_id")} returned on score read!'
                )

    def verify_player_rb4succeed(self, refid: str) -> None:
        call = self.call_node()

        player = Node.void('player')
        player.set_attribute('method', 'rb4succeed')
        player.add_child(Node.string('rid', refid))
        call.add_child(player)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/player/name")
        self.assert_path(resp, "response/player/lv")
        self.assert_path(resp, "response/player/exp")
        self.assert_path(resp, "response/player/grd")
        self.assert_path(resp, "response/player/ap")
        self.assert_path(resp, "response/player/money")
        self.assert_path(resp, "response/player/released")
        self.assert_path(resp, "response/player/mrecord")

    def verify_player_rb4read(self, refid: str, cardid: str, location: str) -> None:
        call = self.call_node()

        player = Node.void('player')
        player.set_attribute('method', 'rb4read')
        player.add_child(Node.string('rid', refid))
        player.add_child(Node.string('lid', location))
        player.add_child(Node.s16('ver', 1))
        player.add_child(Node.string('card_id', cardid))
        player.add_child(Node.s16('card_type', 1))
        call.add_child(player)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/player/pdata/account/usrid")
        self.assert_path(resp, "response/player/pdata/account/tpc")
        self.assert_path(resp, "response/player/pdata/account/dpc")
        self.assert_path(resp, "response/player/pdata/account/crd")
        self.assert_path(resp, "response/player/pdata/account/brd")
        self.assert_path(resp, "response/player/pdata/account/tdc")
        self.assert_path(resp, "response/player/pdata/account/intrvld")
        self.assert_path(resp, "response/player/pdata/account/ver")
        self.assert_path(resp, "response/player/pdata/account/pst")
        self.assert_path(resp, "response/player/pdata/account/st")
        self.assert_path(resp, "response/player/pdata/account/debutVer")
        self.assert_path(resp, "response/player/pdata/base/name")
        self.assert_path(resp, "response/player/pdata/base/exp")
        self.assert_path(resp, "response/player/pdata/base/lv")
        self.assert_path(resp, "response/player/pdata/base/mg")
        self.assert_path(resp, "response/player/pdata/base/ap")
        self.assert_path(resp, "response/player/pdata/base/cmnt")
        self.assert_path(resp, "response/player/pdata/base/uattr")
        self.assert_path(resp, "response/player/pdata/base/money")
        self.assert_path(resp, "response/player/pdata/base/tbs")
        self.assert_path(resp, "response/player/pdata/base/tbs_r")
        self.assert_path(resp, "response/player/pdata/base/tbgs")
        self.assert_path(resp, "response/player/pdata/base/tbgs_r")
        self.assert_path(resp, "response/player/pdata/base/tbms")
        self.assert_path(resp, "response/player/pdata/base/tbms_r")
        self.assert_path(resp, "response/player/pdata/base/qe_win")
        self.assert_path(resp, "response/player/pdata/base/qe_legend")
        self.assert_path(resp, "response/player/pdata/base/qe2_win")
        self.assert_path(resp, "response/player/pdata/base/qe2_legend")
        self.assert_path(resp, "response/player/pdata/base/qe3_win")
        self.assert_path(resp, "response/player/pdata/base/qe3_legend")
        self.assert_path(resp, "response/player/pdata/base/mlog")
        self.assert_path(resp, "response/player/pdata/base/class")
        self.assert_path(resp, "response/player/pdata/base/class_ar")
        self.assert_path(resp, "response/player/pdata/base/getrfl")
        self.assert_path(resp, "response/player/pdata/base/upper_pt")
        self.assert_path(resp, "response/player/pdata/rival")
        self.assert_path(resp, "response/player/pdata/stamp")
        self.assert_path(resp, "response/player/pdata/config")
        self.assert_path(resp, "response/player/pdata/custom")
        self.assert_path(resp, "response/player/pdata/released")
        self.assert_path(resp, "response/player/pdata/announce")
        self.assert_path(resp, "response/player/pdata/dojo")
        self.assert_path(resp, "response/player/pdata/player_param")
        self.assert_path(resp, "response/player/pdata/shop_score")
        self.assert_path(resp, "response/player/pdata/quest")
        self.assert_path(resp, "response/player/pdata/derby/is_open")
        self.assert_path(resp, "response/player/pdata/codebreaking")
        self.assert_path(resp, "response/player/pdata/iidx_linkage")
        self.assert_path(resp, "response/player/pdata/pue")

        if resp.child_value('player/pdata/base/name') != self.NAME:
            raise Exception(f'Invalid name {resp.child_value("player/pdata/base/name")} returned on profile read!')

    def verify_player_rb4readscore(self, refid: str, location: str) -> List[Dict[str, int]]:
        call = self.call_node()

        player = Node.void('player')
        call.add_child(player)
        player.set_attribute('method', 'rb4readscore')
        player.add_child(Node.string('rid', refid))
        player.add_child(Node.string('lid', location))
        player.add_child(Node.s16('ver', 1))

        # Swap with server
        resp = self.exchange('', call)

        scores = []
        for child in resp.child('player/pdata/record').children:
            if child.name != 'rec':
                continue

            score = {
                'id': child.child_value('mid'),
                'chart': child.child_value('ntgrd'),
                'clear_type': child.child_value('ct'),
                'combo_type': child.child_value('param'),
                'achievement_rate': child.child_value('ar'),
                'score': child.child_value('scr'),
                'miss_count': child.child_value('ms'),
            }
            scores.append(score)
        return scores

    def verify_player_rb4readepisode(self, extid: int) -> List[Dict[str, int]]:
        call = self.call_node()

        player = Node.void('player')
        call.add_child(player)
        player.set_attribute('method', 'rb4readepisode')
        player.add_child(Node.s32('user_id', extid))
        player.add_child(Node.s32('limit', 20))

        # Swap with server
        resp = self.exchange('', call)

        episodes = []
        for child in resp.child('player/pdata/episode').children:
            if child.name != 'info':
                continue

            if child.child_value('user_id') != extid:
                raise Exception(f'Invalid user ID returned {child.child_value("user_id")}')

            episode = {
                'id': child.child_value('type'),
                'user': child.child_value('user_id'),
                'values': [
                    child.child_value('value0'),
                    child.child_value('value1'),
                ],
                'text': child.child_value('text'),
                'time': child.child_value('time'),
            }
            episodes.append(episode)
        return episodes

    def verify_player_rb4write(
        self,
        refid: str,
        loc: str,
        scores: List[Dict[str, int]]=[],
        episodes: List[Dict[str, Any]]=[],
    ) -> int:
        call = self.call_node()

        player = Node.void('player')
        call.add_child(player)
        player.set_attribute('method', 'rb4write')
        pdata = Node.void('pdata')
        player.add_child(pdata)
        account = Node.void('account')
        pdata.add_child(account)
        account.add_child(Node.s32('usrid', 0))
        account.add_child(Node.s32('plyid', 0))
        account.add_child(Node.s32('tpc', 1))
        account.add_child(Node.s32('dpc', 1))
        account.add_child(Node.s32('crd', 1))
        account.add_child(Node.s32('brd', 1))
        account.add_child(Node.s32('tdc', 1))
        account.add_child(Node.string('rid', refid))
        account.add_child(Node.string('lid', loc))
        account.add_child(Node.u8('wmode', 0))
        account.add_child(Node.u8('gmode', 0))
        account.add_child(Node.s16('ver', 1))
        account.add_child(Node.bool('pp', False))
        account.add_child(Node.bool('ps', False))
        account.add_child(Node.s16('pay', 0))
        account.add_child(Node.s16('pay_pc', 0))
        account.add_child(Node.u64('st', int(time.time() * 1000)))
        account.add_child(Node.u8('debutVer', 3))
        account.add_child(Node.s32('upper_pt', 0))
        account.add_child(Node.s32('upper_op', -1))
        base = Node.void('base')
        pdata.add_child(base)
        base.add_child(Node.string('name', self.NAME))
        base.add_child(Node.s32('exp', 0))
        base.add_child(Node.s32('lv', 1))
        base.add_child(Node.s32('mg', -1))
        base.add_child(Node.s32('ap', -1))
        base.add_child(Node.s32('money', 0))
        base.add_child(Node.bool('is_tut', False))
        base.add_child(Node.s32('class', -1))
        base.add_child(Node.s32('class_ar', 0))
        base.add_child(Node.s32('upper_pt', 0))
        stglog = Node.void('stglog')
        pdata.add_child(stglog)

        index = 0
        for score in scores:
            log = Node.void('log')
            stglog.add_child(log)
            log.add_child(Node.s8('stg', index))
            log.add_child(Node.s16('mid', score['id']))
            log.add_child(Node.s8('ng', score['chart']))
            log.add_child(Node.s8('col', 1))
            log.add_child(Node.s8('mt', 0))
            log.add_child(Node.s8('rt', 0))
            log.add_child(Node.s8('ct', score['clear_type']))
            log.add_child(Node.s16('param', score['combo_type']))
            log.add_child(Node.s16('grd', 0))
            log.add_child(Node.s16('ar', score['achievement_rate']))
            log.add_child(Node.s16('sc', score['score']))
            log.add_child(Node.s16('jt_jst', 0))
            log.add_child(Node.s16('jt_grt', 0))
            log.add_child(Node.s16('jt_gd', 0))
            log.add_child(Node.s16('jt_ms', score['miss_count']))
            log.add_child(Node.s16('jt_jr', 0))
            log.add_child(Node.s32('r_uid', 0))
            log.add_child(Node.s32('r_plyid', 0))
            log.add_child(Node.s8('r_stg', 0))
            log.add_child(Node.s8('r_ct', -1))
            log.add_child(Node.s16('r_sc', 0))
            log.add_child(Node.s16('r_grd', 0))
            log.add_child(Node.s16('r_ar', 0))
            log.add_child(Node.s8('r_cpuid', -1))
            log.add_child(Node.s32('time', int(time.time())))
            log.add_child(Node.s8('decide', 0))
            log.add_child(Node.s8('hazard', 0))
            index = index + 1

        episode = Node.void('episode')
        pdata.add_child(episode)
        for ep in episodes:
            info = Node.void('info')
            episode.add_child(info)
            info.add_child(Node.s32('user_id', ep['user']))
            info.add_child(Node.u8('type', ep['id']))
            info.add_child(Node.u16('value0', ep['values'][0]))
            info.add_child(Node.u16('value1', ep['values'][1]))
            info.add_child(Node.string('text', ep['text']))
            info.add_child(Node.s32('time', ep['time']))

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/player/uid")
        return resp.child_value('player/uid')

    def verify_lobby_rb4read(self, location: str, extid: int) -> None:
        call = self.call_node()

        lobby = Node.void('lobby')
        lobby.set_attribute('method', 'rb4read')
        lobby.add_child(Node.s32('uid', extid))
        lobby.add_child(Node.s32('plyid', 0))
        lobby.add_child(Node.u8('m_grade', 255))
        lobby.add_child(Node.string('lid', location))
        lobby.add_child(Node.s32('max', 128))
        lobby.add_child(Node.s32_array('friend', []))
        lobby.add_child(Node.u8('var', 2))
        call.add_child(lobby)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/lobby/interval")
        self.assert_path(resp, "response/lobby/interval_p")

    def verify_lobby_rb4entry(self, location: str, extid: int) -> int:
        call = self.call_node()

        lobby = Node.void('lobby')
        lobby.set_attribute('method', 'rb4entry')
        e = Node.void('e')
        lobby.add_child(e)
        e.add_child(Node.s32('eid', 0))
        e.add_child(Node.u16('mid', 79))
        e.add_child(Node.u8('ng', 0))
        e.add_child(Node.s32('uid', extid))
        e.add_child(Node.s32('uattr', 0))
        e.add_child(Node.string('pn', self.NAME))
        e.add_child(Node.s32('plyid', 0))
        e.add_child(Node.s16('mg', 255))
        e.add_child(Node.s32('mopt', 0))
        e.add_child(Node.string('lid', location))
        e.add_child(Node.string('sn', ''))
        e.add_child(Node.u8('pref', 51))
        e.add_child(Node.s8('stg', 4))
        e.add_child(Node.s8('pside', 0))
        e.add_child(Node.s16('eatime', 30))
        e.add_child(Node.u8_array('ga', [127, 0, 0, 1]))
        e.add_child(Node.u16('gp', 10007))
        e.add_child(Node.u8_array('la', [16, 0, 0, 0]))
        e.add_child(Node.u8('ver', 2))
        e.add_child(Node.s8('tension', 0))
        lobby.add_child(Node.s32_array('friend', []))
        call.add_child(lobby)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/lobby/interval")
        self.assert_path(resp, "response/lobby/interval_p")
        self.assert_path(resp, "response/lobby/eid")
        self.assert_path(resp, "response/lobby/e/eid")
        self.assert_path(resp, "response/lobby/e/mid")
        self.assert_path(resp, "response/lobby/e/ng")
        self.assert_path(resp, "response/lobby/e/uid")
        self.assert_path(resp, "response/lobby/e/uattr")
        self.assert_path(resp, "response/lobby/e/pn")
        self.assert_path(resp, "response/lobby/e/plyid")
        self.assert_path(resp, "response/lobby/e/mg")
        self.assert_path(resp, "response/lobby/e/mopt")
        self.assert_path(resp, "response/lobby/e/lid")
        self.assert_path(resp, "response/lobby/e/sn")
        self.assert_path(resp, "response/lobby/e/pref")
        self.assert_path(resp, "response/lobby/e/stg")
        self.assert_path(resp, "response/lobby/e/pside")
        self.assert_path(resp, "response/lobby/e/eatime")
        self.assert_path(resp, "response/lobby/e/ga")
        self.assert_path(resp, "response/lobby/e/gp")
        self.assert_path(resp, "response/lobby/e/la")
        self.assert_path(resp, "response/lobby/e/ver")
        self.assert_path(resp, "response/lobby/e/tension")
        return resp.child_value('lobby/eid')

    def verify_lobby_rb4delete(self, eid: int) -> None:
        call = self.call_node()

        lobby = Node.void('lobby')
        lobby.set_attribute('method', 'rb4delete')
        lobby.add_child(Node.s32('eid', eid))
        call.add_child(lobby)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/lobby")

    def verify_rb4pzlcmt_read(self, loc: str, extid: int) -> None:
        call = self.call_node()

        info = Node.void('info')
        info.set_attribute('method', 'rb4pzlcmt_read')
        info.add_child(Node.s32('uid', extid))
        info.add_child(Node.string('lid', loc))
        info.add_child(Node.s32('time', 0))
        info.add_child(Node.s32('limit', 30))
        call.add_child(info)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/info/comment/time")
        self.assert_path(resp, "response/info/c/uid")
        self.assert_path(resp, "response/info/c/name")
        self.assert_path(resp, "response/info/c/icon")
        self.assert_path(resp, "response/info/c/bln")
        self.assert_path(resp, "response/info/c/lid")
        self.assert_path(resp, "response/info/c/pref")
        self.assert_path(resp, "response/info/c/time")
        self.assert_path(resp, "response/info/c/comment")
        self.assert_path(resp, "response/info/c/is_tweet")

        # Verify we posted our comment earlier
        found = False
        for child in resp.child('info').children:
            if child.name != 'c':
                continue
            if child.child_value('uid') == extid:
                name = child.child_value('name')
                comment = child.child_value('comment')
                if name != self.NAME:
                    raise Exception(f'Invalid name \'{name}\' returned for comment!')
                if comment != 'アメ〜〜！':
                    raise Exception(f'Invalid comment \'{comment}\' returned for comment!')
                found = True

        if not found:
            raise Exception('Comment we posted was not found!')

    def verify_rb4pzlcmt_write(self, loc: str, extid: int) -> None:
        call = self.call_node()

        info = Node.void('info')
        info.set_attribute('method', 'rb4pzlcmt_write')
        info.add_child(Node.s32('uid', extid))
        info.add_child(Node.string('name', self.NAME))
        info.add_child(Node.s16('icon', 0))
        info.add_child(Node.s8('bln', 0))
        info.add_child(Node.string('lid', loc))
        info.add_child(Node.s8('pref', 51))
        info.add_child(Node.s32('time', int(time.time())))
        info.add_child(Node.string('comment', 'アメ〜〜！'))
        info.add_child(Node.bool('is_tweet', False))
        call.add_child(info)

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/info/@status")

    def verify_player_rbsvLinkageSave(self, refid: str) -> None:
        call = self.call_node()

        player = Node.void('player')
        call.add_child(player)
        player.set_attribute('method', 'rbsvLinkageSave')
        player.add_child(Node.string('rid', refid))

        # Swap with server
        resp = self.exchange('', call)

        # Verify that response is correct
        self.assert_path(resp, "response/player/before_pk_value")
        self.assert_path(resp, "response/player/after_pk_value")
        self.assert_path(resp, "response/player/before_bn_value")
        self.assert_path(resp, "response/player/after_bn_value")

    def verify(self, cardid: Optional[str]) -> None:
        # Verify boot sequence is okay
        self.verify_services_get(
            expected_services=[
                'pcbtracker',
                'pcbevent',
                'local',
                'message',
                'facility',
                'cardmng',
                'package',
                'posevent',
                'pkglist',
                'dlstatus',
                'eacoin',
                'lobby',
                'ntp',
                'keepalive'
            ]
        )
        paseli_enabled = self.verify_pcbtracker_alive()
        self.verify_message_get()
        self.verify_package_list()
        self.verify_dlstatus_progress()
        location = self.verify_facility_get()
        self.verify_pcbevent_put()
        self.verify_info_rb4common(location)
        self.verify_pcb_rb4error(location)
        self.verify_pcb_rb4boot(location)

        # Verify card registration and profile lookup
        if cardid is not None:
            card = cardid
        else:
            card = self.random_card()
            print(f"Generated random card ID {card} for use.")

        if cardid is None:
            self.verify_cardmng_inquire(card, msg_type='unregistered', paseli_enabled=paseli_enabled)
            ref_id = self.verify_cardmng_getrefid(card)
            if len(ref_id) != 16:
                raise Exception(f'Invalid refid \'{ref_id}\' returned when registering card')
            if ref_id != self.verify_cardmng_inquire(card, msg_type='new', paseli_enabled=paseli_enabled):
                raise Exception(f'Invalid refid \'{ref_id}\' returned when querying card')

            # Always get a player start, regardless of new profile or not
            self.verify_player_rb4start(ref_id)
            self.verify_player_rb4succeed(ref_id)
            extid = self.verify_player_rb4write(
                ref_id,
                location,
                [],
            )
        else:
            print("Skipping new card checks for existing card")
            ref_id = self.verify_cardmng_inquire(card, msg_type='query', paseli_enabled=paseli_enabled)

        # Verify pin handling and return card handling
        self.verify_cardmng_authpass(ref_id, correct=True)
        self.verify_cardmng_authpass(ref_id, correct=False)
        if ref_id != self.verify_cardmng_inquire(card, msg_type='query', paseli_enabled=paseli_enabled):
            raise Exception(f'Invalid refid \'{ref_id}\' returned when querying card')

        # Verify lobby functionality
        self.verify_lobby_rb4read(location, extid)
        eid = self.verify_lobby_rb4entry(location, extid)
        self.verify_lobby_rb4delete(eid)

        # Verify puzzle comment read and write
        self.verify_rb4pzlcmt_write(location, extid)
        self.verify_rb4pzlcmt_read(location, extid)

        # Verify Sound Voltex/ReflecBeat collabo save
        self.verify_player_rbsvLinkageSave(ref_id)

        # Verify user episode functionalty
        episodes = self.verify_player_rb4readepisode(extid)
        if len(episodes) > 0:
            raise Exception('Existing episodes returned on new card?')
        dummyepisodes = sorted(
            [
                {
                    'id': 1,
                    'user': extid,
                    'values': [5, 10],
                    'text': 'test1',
                    'time': 12345,
                },
                {
                    'id': 2,
                    'user': extid,
                    'values': [6, 11],
                    'text': 'test2',
                    'time': 54321,
                },
            ],
            key=lambda ep: cast(int, ep['id']),
        )
        self.verify_player_rb4write(ref_id, location, episodes=dummyepisodes)
        episodes = sorted(
            self.verify_player_rb4readepisode(extid),
            key=lambda ep: ep['id'],
        )
        if len(episodes) != len(dummyepisodes):
            raise Exception('Unexpected number of episodes returned!')
        for i in range(len(dummyepisodes)):
            for key in dummyepisodes[i]:
                if dummyepisodes[i][key] != episodes[i][key]:
                    raise Exception(f'Invalid value {episodes[i][key]} returned for episode {dummyepisodes[i]["id"]} key {key}')

        # Verify we start with empty scores
        scores = self.verify_player_rb4readscore(ref_id, location)
        if len(scores) > 0:
            raise Exception('Existing scores returned on new card?')

        if cardid is None:
            # Verify score saving and updating
            for phase in [1, 2]:
                if phase == 1:
                    dummyscores = [
                        # An okay score on a chart
                        {
                            'id': 1,
                            'chart': 1,
                            'clear_type': 9,
                            'combo_type': 0,
                            'achievement_rate': 7543,
                            'score': 432,
                            'miss_count': 5,
                        },
                        # A good score on an easier chart of the same song
                        {
                            'id': 1,
                            'chart': 0,
                            'clear_type': 9,
                            'combo_type': 1,
                            'achievement_rate': 9876,
                            'score': 543,
                            'miss_count': 0,
                        },
                        # A bad score on a hard chart
                        {
                            'id': 3,
                            'chart': 2,
                            'clear_type': 9,
                            'combo_type': 0,
                            'achievement_rate': 1234,
                            'score': 123,
                            'miss_count': 54,
                        },
                        # A terrible score on an easy chart
                        {
                            'id': 3,
                            'chart': 0,
                            'clear_type': 9,
                            'combo_type': 0,
                            'achievement_rate': 1024,
                            'score': 50,
                            'miss_count': 90,
                        },
                    ]
                if phase == 2:
                    dummyscores = [
                        # A better score on the same chart
                        {
                            'id': 1,
                            'chart': 1,
                            'clear_type': 9,
                            'combo_type': 0,
                            'achievement_rate': 8765,
                            'score': 469,
                            'miss_count': 1,
                        },
                        # A worse score on another same chart
                        {
                            'id': 1,
                            'chart': 0,
                            'clear_type': 9,
                            'combo_type': 0,
                            'achievement_rate': 8765,
                            'score': 432,
                            'miss_count': 15,
                            'expected_score': 543,
                            'expected_clear_type': 9,
                            'expected_combo_type': 1,
                            'expected_achievement_rate': 9876,
                            'expected_miss_count': 0,
                        },
                    ]
                self.verify_player_rb4write(ref_id, location, scores=dummyscores)

                self.verify_player_rb4read(ref_id, card, location)
                scores = self.verify_player_rb4readscore(ref_id, location)
                for expected in dummyscores:
                    actual = None
                    for received in scores:
                        if received['id'] == expected['id'] and received['chart'] == expected['chart']:
                            actual = received
                            break

                    if actual is None:
                        raise Exception(f"Didn't find song {expected['id']} chart {expected['chart']} in response!")

                    if 'expected_score' in expected:
                        expected_score = expected['expected_score']
                    else:
                        expected_score = expected['score']
                    if 'expected_achievement_rate' in expected:
                        expected_achievement_rate = expected['expected_achievement_rate']
                    else:
                        expected_achievement_rate = expected['achievement_rate']
                    if 'expected_clear_type' in expected:
                        expected_clear_type = expected['expected_clear_type']
                    else:
                        expected_clear_type = expected['clear_type']
                    if 'expected_combo_type' in expected:
                        expected_combo_type = expected['expected_combo_type']
                    else:
                        expected_combo_type = expected['combo_type']
                    if 'expected_miss_count' in expected:
                        expected_miss_count = expected['expected_miss_count']
                    else:
                        expected_miss_count = expected['miss_count']

                    if actual['score'] != expected_score:
                        raise Exception(f'Expected a score of \'{expected_score}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got score \'{actual["score"]}\'')
                    if actual['achievement_rate'] != expected_achievement_rate:
                        raise Exception(f'Expected an achievement rate of \'{expected_achievement_rate}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got achievement rate \'{actual["achievement_rate"]}\'')
                    if actual['clear_type'] != expected_clear_type:
                        raise Exception(f'Expected a clear_type of \'{expected_clear_type}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got clear_type \'{actual["clear_type"]}\'')
                    if actual['combo_type'] != expected_combo_type:
                        raise Exception(f'Expected a combo_type of \'{expected_combo_type}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got combo_type \'{actual["combo_type"]}\'')
                    if actual['miss_count'] != expected_miss_count:
                        raise Exception(f'Expected a miss count of \'{expected_miss_count}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got miss count \'{actual["miss_count"]}\'')

                # Sleep so we don't end up putting in score history on the same second
                time.sleep(1)

        else:
            print("Skipping score checks for existing card")

        # Verify ending game
        self.verify_player_rb4end(ref_id)

        # Verify empty and non-empty select score
        self.verify_player_rb4selectscore(0)
        self.verify_player_rb4selectscore(extid)

        # Verify high score tables and shop rank
        self.verify_info_rb4ranking()
        self.verify_info_rb4shop_score_ranking(location)
        self.verify_player_rb4total_bestallrank_read()

        # Verify paseli handling
        if paseli_enabled:
            print("PASELI enabled for this PCBID, executing PASELI checks")
        else:
            print("PASELI disabled for this PCBID, skipping PASELI checks")
            return

        sessid, balance = self.verify_eacoin_checkin(card)
        if balance == 0:
            print("Skipping PASELI consume check because card has 0 balance")
        else:
            self.verify_eacoin_consume(sessid, balance, random.randint(0, balance))
        self.verify_eacoin_checkout(sessid)
