import base64
import random
import time
from typing import Optional, Dict, List, Tuple, Any

from bemani.client.base import BaseClient
from bemani.common import ID, Time
from bemani.protocol import Node


def b64str(string: str) -> str:
    return base64.b64encode(string.encode()).decode('ascii')


class DDRAceClient(BaseClient):
    NAME = 'TEST'

    def verify_eventlog_write(self, location: str) -> None:
        call = self.call_node()

        # Construct node
        eventlog = Node.void('eventlog')
        call.add_child(eventlog)
        eventlog.set_attribute('method', 'write')
        eventlog.add_child(Node.u32('retrycnt', 0))
        data = Node.void('data')
        eventlog.add_child(data)
        data.add_child(Node.string('eventid', 'S_PWRON'))
        data.add_child(Node.s32('eventorder', 0))
        data.add_child(Node.u64('pcbtime', int(time.time() * 1000)))
        data.add_child(Node.s64('gamesession', -1))
        data.add_child(Node.string('strdata1', b64str('2.4.0')))
        data.add_child(Node.string('strdata2', ''))
        data.add_child(Node.s64('numdata1', 1))
        data.add_child(Node.s64('numdata2', 0))
        data.add_child(Node.string('locationid', location))

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

        # Verify that response is correct
        self.assert_path(resp, "response/eventlog/gamesession")
        self.assert_path(resp, "response/eventlog/logsendflg")
        self.assert_path(resp, "response/eventlog/logerrlevel")
        self.assert_path(resp, "response/eventlog/evtidnosendflg")

    def verify_system_convcardnumber(self, cardno: str) -> None:
        call = self.call_node()

        # Construct node
        system = Node.void('system')
        call.add_child(system)
        system.set_attribute('method', 'convcardnumber')
        info = Node.void('info')
        system.add_child(info)
        info.add_child(Node.s32('version', 1))
        data = Node.void('data')
        system.add_child(data)
        data.add_child(Node.string('card_id', cardno))
        data.add_child(Node.s32('card_type', 1))

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

        # Verify that response is correct
        self.assert_path(resp, "response/system/data/card_number")
        self.assert_path(resp, "response/system/result")

    def verify_playerdata_usergamedata_advanced_usernew(self, refid: str) -> int:
        call = self.call_node()

        # Construct node
        playerdata = Node.void('playerdata')
        call.add_child(playerdata)
        playerdata.set_attribute('method', 'usergamedata_advanced')
        playerdata.add_child(Node.u32('retrycnt', 0))
        info = Node.void('info')
        playerdata.add_child(info)
        info.add_child(Node.s32('version', 1))
        data = Node.void('data')
        playerdata.add_child(data)
        data.add_child(Node.string('mode', 'usernew'))
        data.add_child(Node.string('shoparea', '.'))
        data.add_child(Node.s64('gamesession', 123456))
        data.add_child(Node.string('refid', refid))
        data.add_child(Node.string('dataid', refid))
        data.add_child(Node.string('gamekind', 'MDX'))
        data.add_child(Node.string('pcbid', self.pcbid))
        data.add_child(Node.void('record'))

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

        # Verify that response is correct
        self.assert_path(resp, "response/playerdata/seq")
        self.assert_path(resp, "response/playerdata/code")
        self.assert_path(resp, "response/playerdata/shoparea")
        self.assert_path(resp, "response/playerdata/result")

        return resp.child_value('playerdata/code')

    def verify_playerdata_usergamedata_advanced_ghostload(self, refid: str, ghostid: int) -> Dict[str, Any]:
        call = self.call_node()

        # Construct node
        playerdata = Node.void('playerdata')
        call.add_child(playerdata)
        playerdata.set_attribute('method', 'usergamedata_advanced')
        playerdata.add_child(Node.u32('retrycnt', 0))
        info = Node.void('info')
        playerdata.add_child(info)
        info.add_child(Node.s32('version', 1))
        data = Node.void('data')
        playerdata.add_child(data)
        data.add_child(Node.string('mode', 'ghostload'))
        data.add_child(Node.s32('ghostid', ghostid))
        data.add_child(Node.s64('gamesession', 123456))
        data.add_child(Node.string('refid', refid))
        data.add_child(Node.string('dataid', refid))
        data.add_child(Node.string('gamekind', 'MDX'))
        data.add_child(Node.string('pcbid', self.pcbid))
        data.add_child(Node.void('record'))

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

        # Verify that response is correct
        self.assert_path(resp, "response/playerdata/ghostdata/code")
        self.assert_path(resp, "response/playerdata/ghostdata/mcode")
        self.assert_path(resp, "response/playerdata/ghostdata/notetype")
        self.assert_path(resp, "response/playerdata/ghostdata/ghostsize")
        self.assert_path(resp, "response/playerdata/ghostdata/ghost")

        return {
            'extid': resp.child_value('playerdata/ghostdata/code'),
            'id': resp.child_value('playerdata/ghostdata/mcode'),
            'chart': resp.child_value('playerdata/ghostdata/notetype'),
            'ghost': resp.child_value('playerdata/ghostdata/ghost'),
        }

    def verify_playerdata_usergamedata_advanced_rivalload(self, refid: str, loadflag: int) -> None:
        call = self.call_node()

        # Construct node
        playerdata = Node.void('playerdata')
        call.add_child(playerdata)
        playerdata.set_attribute('method', 'usergamedata_advanced')
        playerdata.add_child(Node.u32('retrycnt', 0))
        info = Node.void('info')
        playerdata.add_child(info)
        info.add_child(Node.s32('version', 1))
        data = Node.void('data')
        playerdata.add_child(data)
        data.add_child(Node.string('mode', 'rivalload'))
        data.add_child(Node.u64('targettime', Time.now() * 1000))
        data.add_child(Node.string('shoparea', '.'))
        data.add_child(Node.bool('isdouble', False))
        data.add_child(Node.s32('loadflag', loadflag))
        data.add_child(Node.s32('ddrcode', 0))
        data.add_child(Node.s64('gamesession', 123456))
        data.add_child(Node.string('refid', refid))
        data.add_child(Node.string('dataid', refid))
        data.add_child(Node.string('gamekind', 'MDX'))
        data.add_child(Node.string('pcbid', self.pcbid))
        data.add_child(Node.void('record'))

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

        # Verify that response is correct
        self.assert_path(resp, "response/playerdata/data/recordtype")
        if loadflag != 2:
            # As implemented, its possible for a machine not in an arcade to have scores.
            # So, if the test PCBID we're using isn't in an arcade, we won't fetch scores
            # for area records (flag 2), so don't check for these in that case.
            self.assert_path(resp, "response/playerdata/data/record/mcode")
            self.assert_path(resp, "response/playerdata/data/record/notetype")
            self.assert_path(resp, "response/playerdata/data/record/rank")
            self.assert_path(resp, "response/playerdata/data/record/clearkind")
            self.assert_path(resp, "response/playerdata/data/record/flagdata")
            self.assert_path(resp, "response/playerdata/data/record/name")
            self.assert_path(resp, "response/playerdata/data/record/area")
            self.assert_path(resp, "response/playerdata/data/record/code")
            self.assert_path(resp, "response/playerdata/data/record/score")
            self.assert_path(resp, "response/playerdata/data/record/ghostid")

        if resp.child_value('playerdata/data/recordtype') != loadflag:
            raise Exception('Invalid record type returned!')

    def verify_playerdata_usergamedata_advanced_userload(self, refid: str) -> Tuple[bool, List[Dict[str, Any]]]:
        call = self.call_node()

        # Construct node
        playerdata = Node.void('playerdata')
        call.add_child(playerdata)
        playerdata.set_attribute('method', 'usergamedata_advanced')
        playerdata.add_child(Node.u32('retrycnt', 0))
        info = Node.void('info')
        playerdata.add_child(info)
        info.add_child(Node.s32('version', 1))
        data = Node.void('data')
        playerdata.add_child(data)
        data.add_child(Node.string('mode', 'userload'))
        data.add_child(Node.s64('gamesession', 123456))
        data.add_child(Node.string('refid', refid))
        data.add_child(Node.string('dataid', refid))
        data.add_child(Node.string('gamekind', 'MDX'))
        data.add_child(Node.string('pcbid', self.pcbid))
        data.add_child(Node.void('record'))

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

        # Verify that response is correct
        self.assert_path(resp, "response/playerdata/result")
        self.assert_path(resp, "response/playerdata/is_new")

        music = []
        for child in resp.child('playerdata').children:
            if child.name != 'music':
                continue

            songid = child.child_value('mcode')
            chart = 0
            for note in child.children:
                if note.name != 'note':
                    continue

                if note.child_value('count') != 0:
                    # Actual song
                    music.append({
                        'id': songid,
                        'chart': chart,
                        'rank': note.child_value('rank'),
                        'halo': note.child_value('clearkind'),
                        'score': note.child_value('score'),
                        'ghostid': note.child_value('ghostid'),
                    })

                chart = chart + 1

        return (
            resp.child_value('playerdata/is_new'),
            music,
        )

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

        # Construct node
        playerdata = Node.void('playerdata')
        call.add_child(playerdata)
        playerdata.set_attribute('method', 'usergamedata_advanced')
        playerdata.add_child(Node.u32('retrycnt', 0))
        info = Node.void('info')
        playerdata.add_child(info)
        info.add_child(Node.s32('version', 1))
        data = Node.void('data')
        playerdata.add_child(data)
        data.add_child(Node.string('mode', 'inheritance'))
        data.add_child(Node.string('locid', locid))
        data.add_child(Node.s64('gamesession', 123456))
        data.add_child(Node.string('refid', refid))
        data.add_child(Node.string('dataid', refid))
        data.add_child(Node.string('gamekind', 'MDX'))
        data.add_child(Node.string('pcbid', self.pcbid))
        data.add_child(Node.void('record'))

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

        # Verify that response is correct
        self.assert_path(resp, "response/playerdata/InheritanceStatus")
        self.assert_path(resp, "response/playerdata/result")

    def verify_playerdata_usergamedata_advanced_usersave(self, refid: str, extid: int, locid: str, score: Dict[str, Any], scorepos: int=0) -> None:
        call = self.call_node()

        # Construct node
        playerdata = Node.void('playerdata')
        call.add_child(playerdata)
        playerdata.set_attribute('method', 'usergamedata_advanced')
        playerdata.add_child(Node.u32('retrycnt', 0))
        info = Node.void('info')
        playerdata.add_child(info)
        info.add_child(Node.s32('version', 1))
        data = Node.void('data')
        playerdata.add_child(data)
        data.add_child(Node.string('mode', 'usersave'))
        data.add_child(Node.string('name', self.NAME))
        data.add_child(Node.s32('ddrcode', extid))
        data.add_child(Node.s32('playside', 1))
        data.add_child(Node.s32('playstyle', 0))
        data.add_child(Node.s32('area', 58))
        data.add_child(Node.s32('weight100', 0))
        data.add_child(Node.string('shopname', 'gmw='))
        data.add_child(Node.bool('ispremium', False))
        data.add_child(Node.bool('iseapass', True))
        data.add_child(Node.bool('istakeover', False))
        data.add_child(Node.bool('isrepeater', False))
        data.add_child(Node.bool('isgameover', scorepos < 0))
        data.add_child(Node.string('locid', locid))
        data.add_child(Node.string('shoparea', '.'))
        data.add_child(Node.s64('gamesession', 123456))
        data.add_child(Node.string('refid', refid))
        data.add_child(Node.string('dataid', refid))
        data.add_child(Node.string('gamekind', 'MDX'))
        data.add_child(Node.string('pcbid', self.pcbid))
        data.add_child(Node.void('record'))

        for i in range(5):
            if i == scorepos:
                # Fill in score here
                note = Node.void('note')
                data.add_child(note)
                note.add_child(Node.u8('stagenum', i + 1))
                note.add_child(Node.u32('mcode', score['id']))
                note.add_child(Node.u8('notetype', score['chart']))
                note.add_child(Node.u8('rank', score['rank']))
                note.add_child(Node.u8('clearkind', score['halo']))
                note.add_child(Node.s32('score', score['score']))
                note.add_child(Node.s32('exscore', 0))
                note.add_child(Node.s32('maxcombo', 0))
                note.add_child(Node.s32('life', 0))
                note.add_child(Node.s32('fastcount', 0))
                note.add_child(Node.s32('slowcount', 0))
                note.add_child(Node.s32('judge_marvelous', 0))
                note.add_child(Node.s32('judge_perfect', 0))
                note.add_child(Node.s32('judge_great', 0))
                note.add_child(Node.s32('judge_good', 0))
                note.add_child(Node.s32('judge_boo', 0))
                note.add_child(Node.s32('judge_miss', 0))
                note.add_child(Node.s32('judge_ok', 0))
                note.add_child(Node.s32('judge_ng', 0))
                note.add_child(Node.s32('calorie', 0))
                note.add_child(Node.s32('ghostsize', len(score['ghost'])))
                note.add_child(Node.string('ghost', score['ghost']))
                note.add_child(Node.u8('opt_speed', 0))
                note.add_child(Node.u8('opt_boost', 0))
                note.add_child(Node.u8('opt_appearance', 0))
                note.add_child(Node.u8('opt_turn', 0))
                note.add_child(Node.u8('opt_dark', 0))
                note.add_child(Node.u8('opt_scroll', 0))
                note.add_child(Node.u8('opt_arrowcolor', 0))
                note.add_child(Node.u8('opt_cut', 0))
                note.add_child(Node.u8('opt_freeze', 0))
                note.add_child(Node.u8('opt_jump', 0))
                note.add_child(Node.u8('opt_arrowshape', 0))
                note.add_child(Node.u8('opt_filter', 0))
                note.add_child(Node.u8('opt_guideline', 0))
                note.add_child(Node.u8('opt_gauge', 0))
                note.add_child(Node.u8('opt_judgepriority', 0))
                note.add_child(Node.u8('opt_timing', 0))
                note.add_child(Node.string('basename', ''))
                note.add_child(Node.string('title_b64', ''))
                note.add_child(Node.string('artist_b64', ''))
                note.add_child(Node.u16('bpmMax', 0))
                note.add_child(Node.u16('bpmMin', 0))
                note.add_child(Node.u8('level', 0))
                note.add_child(Node.u8('series', 0))
                note.add_child(Node.u32('bemaniFlag', 0))
                note.add_child(Node.u32('genreFlag', 0))
                note.add_child(Node.u8('limited', 0))
                note.add_child(Node.u8('region', 0))
                note.add_child(Node.s32('gr_voltage', 0))
                note.add_child(Node.s32('gr_stream', 0))
                note.add_child(Node.s32('gr_chaos', 0))
                note.add_child(Node.s32('gr_freeze', 0))
                note.add_child(Node.s32('gr_air', 0))
                note.add_child(Node.bool('share', False))
                note.add_child(Node.u64('endtime', 0))
                note.add_child(Node.s32('folder', 0))
            else:
                note = Node.void('note')
                data.add_child(note)
                note.add_child(Node.u8('stagenum', 0))
                note.add_child(Node.u32('mcode', 0))
                note.add_child(Node.u8('notetype', 0))
                note.add_child(Node.u8('rank', 0))
                note.add_child(Node.u8('clearkind', 0))
                note.add_child(Node.s32('score', 0))
                note.add_child(Node.s32('exscore', 0))
                note.add_child(Node.s32('maxcombo', 0))
                note.add_child(Node.s32('life', 0))
                note.add_child(Node.s32('fastcount', 0))
                note.add_child(Node.s32('slowcount', 0))
                note.add_child(Node.s32('judge_marvelous', 0))
                note.add_child(Node.s32('judge_perfect', 0))
                note.add_child(Node.s32('judge_great', 0))
                note.add_child(Node.s32('judge_good', 0))
                note.add_child(Node.s32('judge_boo', 0))
                note.add_child(Node.s32('judge_miss', 0))
                note.add_child(Node.s32('judge_ok', 0))
                note.add_child(Node.s32('judge_ng', 0))
                note.add_child(Node.s32('calorie', 0))
                note.add_child(Node.s32('ghostsize', 0))
                note.add_child(Node.string('ghost', ''))
                note.add_child(Node.u8('opt_speed', 0))
                note.add_child(Node.u8('opt_boost', 0))
                note.add_child(Node.u8('opt_appearance', 0))
                note.add_child(Node.u8('opt_turn', 0))
                note.add_child(Node.u8('opt_dark', 0))
                note.add_child(Node.u8('opt_scroll', 0))
                note.add_child(Node.u8('opt_arrowcolor', 0))
                note.add_child(Node.u8('opt_cut', 0))
                note.add_child(Node.u8('opt_freeze', 0))
                note.add_child(Node.u8('opt_jump', 0))
                note.add_child(Node.u8('opt_arrowshape', 0))
                note.add_child(Node.u8('opt_filter', 0))
                note.add_child(Node.u8('opt_guideline', 0))
                note.add_child(Node.u8('opt_gauge', 0))
                note.add_child(Node.u8('opt_judgepriority', 0))
                note.add_child(Node.u8('opt_timing', 0))
                note.add_child(Node.string('basename', ''))
                note.add_child(Node.string('title_b64', ''))
                note.add_child(Node.string('artist_b64', ''))
                note.add_child(Node.u16('bpmMax', 0))
                note.add_child(Node.u16('bpmMin', 0))
                note.add_child(Node.u8('level', 0))
                note.add_child(Node.u8('series', 0))
                note.add_child(Node.u32('bemaniFlag', 0))
                note.add_child(Node.u32('genreFlag', 0))
                note.add_child(Node.u8('limited', 0))
                note.add_child(Node.u8('region', 0))
                note.add_child(Node.s32('gr_voltage', 0))
                note.add_child(Node.s32('gr_stream', 0))
                note.add_child(Node.s32('gr_chaos', 0))
                note.add_child(Node.s32('gr_freeze', 0))
                note.add_child(Node.s32('gr_air', 0))
                note.add_child(Node.bool('share', False))
                note.add_child(Node.u64('endtime', 0))
                note.add_child(Node.s32('folder', 0))

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

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

    def verify_usergamedata_send(self, ref_id: str, ext_id: int, msg_type: str, send_only_common: bool=False) -> None:
        call = self.call_node()

        # Set up profile write
        profiledata = {
            'COMMON': [
                b'1',
                b'0',  # shoparea spot, filled in below
                b'3c880f8',
                b'1',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'ffffffffffffffff',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'',  # Name spot, filled in below
                ID.format_extid(ext_id).encode('ascii'),
                b'',
                b'',
                b'',
                b'',
                b'',
                b'',
            ],
            'OPTION': [
                b'0',
                b'3',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'3',
                b'0',
                b'0',
                b'0',
                b'0',
                b'1',
                b'2',
                b'0',
                b'0',
                b'0',
                b'10.000000',
                b'10.000000',
                b'10.000000',
                b'10.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'',
                b'',
                b'',
                b'',
                b'',
                b'',
                b'',
                b'',
            ],
            'LAST': [
                b'1',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'',
                b'',
                b'',
                b'',
                b'',
                b'',
                b'',
                b'',
            ],
            'RIVAL': [
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'0.000000',
                b'',
                b'',
                b'',
                b'',
                b'',
                b'',
                b'',
                b'',
            ]
        }

        if msg_type == 'new':
            # New profile gets blank name, because we save over it at the end of the round.
            profiledata['COMMON'][1] = b'0'
            profiledata['COMMON'][25] = b''

        elif msg_type == 'existing':
            # Exiting profile gets our hardcoded name saved.
            profiledata['COMMON'][1] = b'3a'
            profiledata['COMMON'][25] = self.NAME.encode('shift-jis')

        else:
            raise Exception(f'Unknown message type {msg_type}!')

        if send_only_common:
            profiledata = {'COMMON': profiledata['COMMON']}

        # Construct node
        playerdata = Node.void('playerdata')
        call.add_child(playerdata)
        playerdata.set_attribute('method', 'usergamedata_send')
        playerdata.add_child(Node.u32('retrycnt', 0))
        info = Node.void('info')
        playerdata.add_child(info)
        info.add_child(Node.s32('version', 1))
        data = Node.void('data')
        playerdata.add_child(data)
        data.add_child(Node.string('refid', ref_id))
        data.add_child(Node.string('dataid', ref_id))
        data.add_child(Node.string('gamekind', 'MDX'))
        data.add_child(Node.u32('datanum', len(profiledata.keys())))
        record = Node.void('record')
        data.add_child(record)
        for ptype in profiledata:
            profile = [b'ffffffff', ptype.encode('ascii')] + profiledata[ptype]
            d = Node.string('d', base64.b64encode(b','.join(profile)).decode('ascii'))
            record.add_child(d)
            d.add_child(Node.string('bin1', ''))

        # Swap with server
        resp = self.exchange('', call)
        self.assert_path(resp, "response/playerdata/result")

    def verify_usergamedata_recv(self, ref_id: str) -> str:
        call = self.call_node()

        # Construct node
        playerdata = Node.void('playerdata')
        call.add_child(playerdata)
        playerdata.set_attribute('method', 'usergamedata_recv')
        info = Node.void('info')
        playerdata.add_child(info)
        info.add_child(Node.s32('version', 1))
        data = Node.void('data')
        playerdata.add_child(data)
        data.add_child(Node.string('refid', ref_id))
        data.add_child(Node.string('dataid', ref_id))
        data.add_child(Node.string('gamekind', 'MDX'))
        data.add_child(Node.u32('recv_num', 4))
        data.add_child(Node.string('recv_csv', 'COMMON,3fffffffff,OPTION,3fffffffff,LAST,3fffffffff,RIVAL,3fffffffff'))

        # Swap with server
        resp = self.exchange('', call)
        self.assert_path(resp, "response/playerdata/result")
        self.assert_path(resp, "response/playerdata/player/record/d/bin1")
        self.assert_path(resp, "response/playerdata/player/record_num")

        profiles = 0
        name = ''
        for child in resp.child('playerdata/player/record').children:
            if child.name != 'd':
                continue

            if profiles == 0:
                bindata = child.value
                profiledata = base64.b64decode(bindata).split(b',')
                name = profiledata[25].decode('ascii')

            profiles = profiles + 1

        if profiles != 4:
            raise Exception('Didn\'t receive all four profiles in the right order!')

        return name

    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()
        location = self.verify_facility_get()
        self.verify_pcbevent_put()
        self.verify_eventlog_write(location)

        # Verify the game-wide packets Ace insists on sending before profile load
        is_new, music = self.verify_playerdata_usergamedata_advanced_userload('X0000000000000000000000000123456')
        if not is_new:
            raise Exception('Fake profiles should be new!')
        if len(music) > 0:
            raise Exception('Fake profiles should have no scores associated!')

        # 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)
            self.verify_system_convcardnumber(card)
            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')
            extid = self.verify_playerdata_usergamedata_advanced_usernew(ref_id)
            self.verify_usergamedata_send(ref_id, extid, 'new')
            self.verify_playerdata_usergamedata_advanced_inheritance(ref_id, location)
            name = self.verify_usergamedata_recv(ref_id)
            if name != '':
                raise Exception('Name stored on profile we just created!')
            self.verify_usergamedata_send(ref_id, extid, 'existing', send_only_common=True)
            name = self.verify_usergamedata_recv(ref_id)
            if name != self.NAME:
                raise Exception('Name stored on profile is incorrect!')
        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')

        if cardid is None:
            is_new, music = self.verify_playerdata_usergamedata_advanced_userload(ref_id)
            if is_new:
                raise Exception('Profile should not be new!')
            if len(music) > 0:
                raise Exception('Created profile should have no scores associated!')

            # Verify score saving and updating
            for phase in [1, 2]:
                if phase == 1:
                    dummyscores = [
                        # An okay score on a chart
                        {
                            'id': 10,
                            'chart': 3,
                            'rank': 5,
                            'halo': 6,
                            'score': 765432,
                            'ghost': '765432',
                        },
                        # A good score on an easier chart of the same song
                        {
                            'id': 10,
                            'chart': 2,
                            'rank': 2,
                            'halo': 8,
                            'score': 876543,
                            'ghost': '876543',
                        },
                        # A bad score on a hard chart
                        {
                            'id': 479,
                            'chart': 2,
                            'rank': 11,
                            'halo': 6,
                            'score': 654321,
                            'ghost': '654321',
                        },
                        # A terrible score on an easy chart
                        {
                            'id': 479,
                            'chart': 1,
                            'rank': 15,
                            'halo': 6,
                            'score': 123456,
                            'ghost': '123456',
                        },
                    ]
                if phase == 2:
                    dummyscores = [
                        # A better score on the same chart
                        {
                            'id': 10,
                            'chart': 3,
                            'rank': 4,
                            'halo': 7,
                            'score': 888888,
                            'ghost': '888888',
                        },
                        # A worse score on another same chart
                        {
                            'id': 10,
                            'chart': 2,
                            'rank': 3,
                            'halo': 7,
                            'score': 654321,
                            'ghost': '654321',
                            'expected_score': 876543,
                            'expected_halo': 8,
                            'expected_rank': 2,
                            'expected_ghost': '876543',
                        },
                    ]

                pos = 0
                for dummyscore in dummyscores:
                    self.verify_playerdata_usergamedata_advanced_usersave(
                        ref_id,
                        extid,
                        location,
                        dummyscore,
                        pos,
                    )
                    pos = pos + 1

                is_new, scores = self.verify_playerdata_usergamedata_advanced_userload(ref_id)
                if is_new:
                    raise Exception('Profile should not be new!')
                if len(scores) == 0:
                    raise Exception('Expected some scores after saving!')

                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_rank' in expected:
                        expected_rank = expected['expected_rank']
                    else:
                        expected_rank = expected['rank']
                    if 'expected_halo' in expected:
                        expected_halo = expected['expected_halo']
                    else:
                        expected_halo = expected['halo']

                    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['rank'] != expected_rank:
                        raise Exception(f'Expected a rank of \'{expected_rank}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got rank \'{actual["rank"]}\'')
                    if actual['halo'] != expected_halo:
                        raise Exception(f'Expected a halo of \'{expected_halo}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got halo \'{actual["halo"]}\'')

                    # Now verify that the ghost for this score is what we saved
                    ghost = self.verify_playerdata_usergamedata_advanced_ghostload(ref_id, received['ghostid'])
                    if 'expected_ghost' in expected:
                        expected_ghost = expected['expected_ghost']
                    else:
                        expected_ghost = expected['ghost']

                    if ghost['id'] != received['id']:
                        raise Exception(f'Wrong song ID \'{ghost["id"]}\' returned for ghost, expected ID \'{received["id"]}\'')
                    if ghost['chart'] != received['chart']:
                        raise Exception(f'Wrong song chart \'{ghost["chart"]}\' returned for ghost, expected chart \'{received["chart"]}\'')
                    if ghost['ghost'] != expected_ghost:
                        raise Exception(f'Wrong ghost data \'{ghost["ghost"]}\' returned for ghost, expected \'{expected_ghost}\'')
                    if ghost['extid'] != extid:
                        raise Exception(f'Wrong extid \'{ghost["extid"]}\' returned for ghost, expected \'{extid}\'')

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

            # Simulate game over conditions
            self.verify_playerdata_usergamedata_advanced_usersave(
                ref_id,
                extid,
                location,
                {},
                -1,
            )
        else:
            print("Skipping score checks for existing card")

        # Verify global scores now that we've inserted some
        self.verify_playerdata_usergamedata_advanced_rivalload('X0000000000000000000000000123456', 1)
        self.verify_playerdata_usergamedata_advanced_rivalload('X0000000000000000000000000123456', 2)
        self.verify_playerdata_usergamedata_advanced_rivalload('X0000000000000000000000000123456', 4)

        # 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)
