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

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


class JubeatFestoClient(BaseClient):
    NAME = "TEST"

    def __verify_info(self, resp: Node, base: str) -> None:
        # Verify that response is correct
        self.assert_path(resp, f"response/{base}/data/info/event_info")
        self.assert_path(resp, f"response/{base}/data/info/genre_def_music")
        self.assert_path(resp, f"response/{base}/data/info/black_jacket_list")
        self.assert_path(resp, f"response/{base}/data/info/white_music_list")
        self.assert_path(resp, f"response/{base}/data/info/white_marker_list")
        self.assert_path(resp, f"response/{base}/data/info/white_theme_list")
        self.assert_path(resp, f"response/{base}/data/info/open_music_list")
        self.assert_path(resp, f"response/{base}/data/info/shareable_music_list")
        self.assert_path(resp, f"response/{base}/data/info/hot_music_list")
        self.assert_path(resp, f"response/{base}/data/info/jbox/point")
        self.assert_path(resp, f"response/{base}/data/info/jbox/emblem/normal/index")
        self.assert_path(resp, f"response/{base}/data/info/jbox/emblem/premium/index")
        self.assert_path(resp, f"response/{base}/data/info/born/status")
        self.assert_path(resp, f"response/{base}/data/info/born/year")
        self.assert_path(resp, f"response/{base}/data/info/konami_logo_50th/is_available")
        self.assert_path(resp, f"response/{base}/data/info/expert_option/is_available")
        self.assert_path(resp, f"response/{base}/data/info/all_music_matching/is_available")
        self.assert_path(resp, f"response/{base}/data/info/department/shop_list")
        self.assert_path(resp, f"response/{base}/data/info/question_list")
        # Don't bother asserting on actual courses, this is highly specific.
        self.assert_path(resp, f"response/{base}/data/info/course_list")
        self.assert_path(resp, f"response/{base}/data/info/share_music")
        self.assert_path(resp, f"response/{base}/data/info/weekly_music/value")
        self.assert_path(resp, f"response/{base}/data/info/weekly_music/music_list")
        self.assert_path(resp, f"response/{base}/data/info/add_default_music_list")

        # These below I'm not sure are needed, and I think some of them mess with stone tablet.
        self.assert_path(resp, f"response/{base}/data/info/team_battle")
        self.assert_path(resp, f"response/{base}/data/info/emo_list")
        self.assert_path(resp, f"response/{base}/data/info/hike_event")
        self.assert_path(resp, f"response/{base}/data/info/tip_list")
        self.assert_path(resp, f"response/{base}/data/info/travel")
        self.assert_path(resp, f"response/{base}/data/info/stamp")

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

        # Construct node
        shopinfo = Node.void("shopinfo")
        shopinfo.set_attribute("method", "regist")
        call.add_child(shopinfo)
        shop = Node.void("shop")
        shopinfo.add_child(shop)
        shop.add_child(Node.string("name", ""))
        shop.add_child(Node.string("pref", "JP-14"))
        shop.add_child(Node.string("softwareid", ""))
        shop.add_child(Node.string("systemid", self.pcbid))
        shop.add_child(Node.string("hardwareid", "01020304050607080900"))
        shop.add_child(Node.string("locationid", "US-1"))
        shop.add_child(Node.string("monitor", "D26L155             6252     151"))
        testmode = Node.void("testmode")
        shop.add_child(testmode)
        sound = Node.void("sound")
        testmode.add_child(sound)
        sound.add_child(Node.u8("volume_in_attract", 0))
        game = Node.void("game")
        testmode.add_child(game)
        play_settings = Node.void("play_settings")
        game.add_child(play_settings)
        play_settings.add_child(Node.u8("max_member", 1))
        game_settings = Node.void("game_settings")
        game.add_child(game_settings)
        game_settings.add_child(Node.u8("close_set", 0))
        game_settings.add_child(Node.s32("close_time", 0))
        display_type_settings = Node.void("display_type_settings")
        game.add_child(display_type_settings)
        display_type_settings.add_child(Node.u8("display_type", 2))
        coin = Node.void("coin")
        testmode.add_child(coin)
        coin.add_child(Node.u8("free_play", 0))
        coin.add_child(Node.u8("free_first_play", 1))
        coin.add_child(Node.u8("coin_slot", 8))
        coin.add_child(Node.u8("start", 1))
        network = Node.void("network")
        testmode.add_child(network)
        network.add_child(Node.u8("cabinet_id", 1))
        bookkeeping = Node.void("bookkeeping")
        testmode.add_child(bookkeeping)
        bookkeeping.add_child(Node.u8("enable", 0))
        clock = Node.void("clock")
        testmode.add_child(clock)
        clock.add_child(Node.u8("enable", 1))
        clock.add_child(Node.s32("offset", 0))
        virtual_coin = Node.void("virtual_coin")
        testmode.add_child(virtual_coin)
        pattern1 = Node.void("pattern1")
        virtual_coin.add_child(pattern1)
        pattern1.add_child(Node.u16("basic_rate", 1000))
        pattern1.add_child(Node.u8("balance_of_credit", 0))
        pattern1.add_child(Node.u8("is_premium_start", 0))
        pattern1.add_child(Node.u8("service_value", 10))
        pattern1.add_child(Node.u8("service_limit", 10))
        pattern1.add_child(Node.u8("service_time_start_h", 7))
        pattern1.add_child(Node.u8("service_time_start_m", 0))
        pattern1.add_child(Node.u8("service_time_end_h", 11))
        pattern1.add_child(Node.u8("service_time_end_m", 0))
        pattern2 = Node.void("pattern2")
        virtual_coin.add_child(pattern2)
        pattern2.add_child(Node.u16("basic_rate", 1000))
        pattern2.add_child(Node.u8("balance_of_credit", 0))
        pattern2.add_child(Node.u8("is_premium_start", 0))
        pattern2.add_child(Node.u8("service_value", 10))
        pattern2.add_child(Node.u8("service_limit", 10))
        pattern2.add_child(Node.u8("service_time_start_h", 7))
        pattern2.add_child(Node.u8("service_time_start_m", 0))
        pattern2.add_child(Node.u8("service_time_end_h", 11))
        pattern2.add_child(Node.u8("service_time_end_m", 0))
        pattern3 = Node.void("pattern3")
        virtual_coin.add_child(pattern3)
        pattern3.add_child(Node.u16("basic_rate", 1000))
        pattern3.add_child(Node.u8("balance_of_credit", 0))
        pattern3.add_child(Node.u8("is_premium_start", 0))
        pattern3.add_child(Node.u8("service_value", 10))
        pattern3.add_child(Node.u8("service_limit", 10))
        pattern3.add_child(Node.u8("service_time_start_h", 7))
        pattern3.add_child(Node.u8("service_time_start_m", 0))
        pattern3.add_child(Node.u8("service_time_end_h", 11))
        pattern3.add_child(Node.u8("service_time_end_m", 0))
        schedule = Node.void("schedule")
        virtual_coin.add_child(schedule)
        schedule.add_child(Node.u8("mon", 0))
        schedule.add_child(Node.u8("tue", 0))
        schedule.add_child(Node.u8("wed", 0))
        schedule.add_child(Node.u8("thu", 0))
        schedule.add_child(Node.u8("fri", 0))
        schedule.add_child(Node.u8("sat", 0))
        schedule.add_child(Node.u8("sun", 0))
        schedule.add_child(Node.u8("holi", 0))
        tax = Node.void("tax")
        testmode.add_child(tax)
        tax.add_child(Node.u8("tax_phase", 0))
        tax.add_child(Node.u8("tax_mode", 0))

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

        self.assert_path(resp, "response/shopinfo/data/cabid")
        self.assert_path(resp, "response/shopinfo/data/locationid")
        self.assert_path(resp, "response/shopinfo/data/tax_phase")
        self.assert_path(resp, "response/shopinfo/data/facility/exist")

        # Verify server flags for events and stuff.
        self.__verify_info(resp, "shopinfo")

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

        # Construct node
        logger = Node.void("logger")
        call.add_child(logger)
        logger.set_attribute("method", "report")
        logger.add_child(Node.s32("retry", 0))
        data = Node.void("data")
        logger.add_child(data)
        data.add_child(Node.string("code", "pcbinfo_01"))
        data.add_child(Node.string("information", "u can literally put anything here lmao"))

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

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

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

        # Construct node
        demodata = Node.void("demodata")
        call.add_child(demodata)
        demodata.set_attribute("method", "get_info")
        pcbinfo = Node.void("pcbinfo")
        demodata.add_child(pcbinfo)
        pcbinfo.set_attribute("client_data_version", "0")

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

        # Verify that response is correct
        self.assert_path(resp, "response/demodata/data/info/black_jacket_list")

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

        # Construct node
        demodata = Node.void("demodata")
        call.add_child(demodata)
        demodata.set_attribute("method", "get_news")

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

        # Verify that response is correct
        self.assert_path(resp, "response/demodata/data/officialnews/@count")

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

        # Construct node
        demodata = Node.void("demodata")
        call.add_child(demodata)
        demodata.set_attribute("method", "get_jbox_list")

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

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

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

        # Construct node
        lab = Node.void("lab")
        call.add_child(lab)
        lab.set_attribute("method", "get_ranking")
        lab.add_child(Node.s32("retry", 0))
        lab.add_child(Node.s8("category", 1))

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

        # Verify that response is correct
        self.assert_path(resp, "response/lab/category")
        self.assert_path(resp, "response/lab/entries/@count")

        # Category should be the same as when we requested it.
        if resp.child_value("lab/category") != 1:
            raise Exception("Lab category did not get round-tripped!")

    def __verify_profile(self, resp: Node, should_inherit: bool) -> int:
        for item in [
            "tune_cnt",
            "save_cnt",
            "saved_cnt",
            "fc_cnt",
            "ex_cnt",
            "clear_cnt",
            "match_cnt",
            "beat_cnt",
            "mynews_cnt",
            "bonus_tune_points",
            "is_bonus_tune_played",
            "inherit",
            "mtg_entry_cnt",
            "mtg_hold_cnt",
            "mtg_result",
        ]:
            self.assert_path(resp, f"response/gametop/data/player/info/{item}")

        # Technically we could use this to check profile succession doesn't show the
        # tutorial, but we don't go that far yet.
        if resp.child_value("gametop/data/player/info/inherit") != should_inherit:
            raise Exception("Inherit flag wrong for profile!")

        for item in [
            "music_list",
            "secret_list",
            "theme_list",
            "marker_list",
            "title_list",
            "parts_list",
            "emblem_list",
            "commu_list",
            "new/secret_list",
            "new/theme_list",
            "new/marker_list",
        ]:
            self.assert_path(resp, f"response/gametop/data/player/item/{item}")

        for item in [
            "play_time",
            "shopname",
            "areaname",
            "music_id",
            "seq_id",
            "sort",
            "category",
            "expert_option",
        ]:
            self.assert_path(resp, f"response/gametop/data/player/last/{item}")

        for item in [
            "marker",
            "theme",
            "title",
            "parts",
            "rank_sort",
            "combo_disp",
            "emblem",
            "matching",
            "hard",
            "hazard",
        ]:
            self.assert_path(resp, f"response/gametop/data/player/last/settings/{item}")

        # Misc stuff
        self.assert_path(resp, "response/gametop/data/player/session_id")
        self.assert_path(resp, "response/gametop/data/player/event_flag")

        # Profile settings
        self.assert_path(resp, "response/gametop/data/player/name")
        self.assert_path(resp, "response/gametop/data/player/jid")
        if resp.child_value("gametop/data/player/name") != self.NAME:
            raise Exception("Unexpected name received from server!")

        # Required nodes for events and stuff
        self.assert_path(resp, "response/gametop/data/player/rivallist")
        self.assert_path(resp, "response/gametop/data/player/lab_edit_seq")
        self.assert_path(resp, "response/gametop/data/player/fc_challenge/today/music_id")
        self.assert_path(resp, "response/gametop/data/player/fc_challenge/today/state")
        self.assert_path(resp, "response/gametop/data/player/fc_challenge/whim/music_id")
        self.assert_path(resp, "response/gametop/data/player/fc_challenge/whim/state")
        self.assert_path(resp, "response/gametop/data/player/official_news/news_list")
        self.assert_path(resp, "response/gametop/data/player/history/@count")
        self.assert_path(resp, "response/gametop/data/player/free_first_play/is_available")
        self.assert_path(resp, "response/gametop/data/player/event_info")
        self.assert_path(resp, "response/gametop/data/player/jbox/point")
        self.assert_path(resp, "response/gametop/data/player/jbox/emblem/normal/index")
        self.assert_path(resp, "response/gametop/data/player/jbox/emblem/premium/index")
        self.assert_path(resp, "response/gametop/data/player/new_music")
        self.assert_path(resp, "response/gametop/data/player/navi/flag")
        self.assert_path(resp, "response/gametop/data/player/gift_list")
        self.assert_path(resp, "response/gametop/data/player/born/status")
        self.assert_path(resp, "response/gametop/data/player/born/year")
        self.assert_path(resp, "response/gametop/data/player/question_list")
        self.assert_path(resp, "response/gametop/data/player/emo_list")
        self.assert_path(resp, "response/gametop/data/player/server")
        self.assert_path(resp, "response/gametop/data/player/course_list")
        self.assert_path(resp, "response/gametop/data/player/course_list/category_list")
        self.assert_path(
            resp,
            "response/gametop/data/player/fill_in_category/normal/no_gray_flag_list",
        )
        self.assert_path(
            resp,
            "response/gametop/data/player/fill_in_category/normal/all_yellow_flag_list",
        )
        self.assert_path(
            resp,
            "response/gametop/data/player/fill_in_category/normal/full_combo_flag_list",
        )
        self.assert_path(
            resp,
            "response/gametop/data/player/fill_in_category/normal/excellent_flag_list",
        )
        self.assert_path(resp, "response/gametop/data/player/fill_in_category/hard/no_gray_flag_list")
        self.assert_path(
            resp,
            "response/gametop/data/player/fill_in_category/hard/all_yellow_flag_list",
        )
        self.assert_path(
            resp,
            "response/gametop/data/player/fill_in_category/hard/full_combo_flag_list",
        )
        self.assert_path(
            resp,
            "response/gametop/data/player/fill_in_category/hard/excellent_flag_list",
        )
        self.assert_path(resp, "response/gametop/data/player/department/shop_list")
        self.assert_path(resp, "response/gametop/data/player/stamp/sheet_list")
        self.assert_path(resp, "response/gametop/data/player/festo_dungeon/phase")
        self.assert_path(resp, "response/gametop/data/player/festo_dungeon/clear_flag")

        # Return the jid
        return resp.child_value("gametop/data/player/jid")

    def verify_gameend_regist(
        self,
        ref_id: str,
        jid: int,
        scores: List[Dict[str, Any]],
    ) -> None:
        call = self.call_node()

        # Construct node
        gameend = Node.void("gameend")
        call.add_child(gameend)
        gameend.set_attribute("method", "regist")
        gameend.add_child(Node.s32("retry", 0))
        pcbinfo = Node.void("pcbinfo")
        gameend.add_child(pcbinfo)
        pcbinfo.set_attribute("client_data_version", "0")
        data = Node.void("data")
        gameend.add_child(data)
        player = Node.void("player")
        data.add_child(player)
        player.add_child(Node.string("refid", ref_id))
        player.add_child(Node.s32("jid", jid))
        player.add_child(Node.string("name", self.NAME))
        result = Node.void("result")
        data.add_child(result)
        result.set_attribute("count", str(len(scores)))

        # Send scores
        scoreid = 0
        for score in scores:
            # Always played
            bits = 0x1
            if score["clear"]:
                bits |= 0x2
            if score["fc"]:
                bits |= 0x4
            if score["ex"]:
                bits |= 0x8

            # Intentionally starting at 1 because that's what the game does
            scoreid = scoreid + 1
            tune = Node.void("tune")
            result.add_child(tune)
            tune.set_attribute("id", str(scoreid))
            tune.add_child(Node.s32("music", score["id"]))
            tune.add_child(Node.s64("timestamp", Time.now() * 1000))
            player_1 = Node.void("player")
            tune.add_child(player_1)
            player_1.set_attribute("rank", "1")
            scorenode = Node.s32("score", score["score"])
            player_1.add_child(scorenode)
            scorenode.set_attribute("seq", str(score["chart"]))
            scorenode.set_attribute("clear", str(bits))
            scorenode.set_attribute("combo", "69")
            player_1.add_child(
                Node.u8_array(
                    "mbar",
                    [
                        239,
                        175,
                        170,
                        170,
                        190,
                        234,
                        187,
                        158,
                        153,
                        230,
                        170,
                        90,
                        102,
                        170,
                        85,
                        150,
                        150,
                        102,
                        85,
                        234,
                        171,
                        169,
                        157,
                        150,
                        170,
                        101,
                        230,
                        90,
                        214,
                        255,
                    ],
                )
            )
            player_1.add_child(Node.bool("is_hard_mode", score["hard"]))
            player_1.add_child(Node.s32("music_rate", score["rate"]))

        # Swap with server
        resp = self.exchange("", call)
        self.assert_path(resp, "response/gameend/data/player/session_id")
        self.assert_path(resp, "response/gameend/data/player/end_final_session_id")

    def verify_gameend_final(
        self,
        ref_id: str,
        jid: int,
    ) -> None:
        call = self.call_node()

        # Construct node
        gameend = Node.void("gameend")
        call.add_child(gameend)
        gameend.set_attribute("method", "final")
        gameend.add_child(Node.s32("retry", 0))
        pcbinfo = Node.void("pcbinfo")
        gameend.add_child(pcbinfo)
        pcbinfo.set_attribute("client_data_version", "0")
        data = Node.void("data")
        gameend.add_child(data)
        info = Node.void("info")
        data.add_child(info)
        born = Node.void("born")
        info.add_child(born)
        born.add_child(Node.s8("status", 3))
        born.add_child(Node.s16("year", 0))
        info.add_child(Node.void("question_list"))
        player = Node.void("player")
        data.add_child(player)
        player.add_child(Node.string("refid", ref_id))
        player.add_child(Node.s32("jid", jid))
        jbox = Node.void("jbox")
        player.add_child(jbox)
        jbox.add_child(Node.s32("point", 0))
        emblem = Node.void("emblem")
        jbox.add_child(emblem)
        emblem.add_child(Node.u8("type", 0))
        emblem.add_child(Node.s16("index", 0))

        # Swap with server
        resp = self.exchange("", call)
        self.assert_path(resp, "response/gameend/@status")

    def verify_gametop_regist(self, card_id: str, ref_id: str) -> int:
        call = self.call_node()

        # Construct node
        gametop = Node.void("gametop")
        call.add_child(gametop)
        gametop.set_attribute("method", "regist")
        data = Node.void("data")
        gametop.add_child(data)
        player = Node.void("player")
        data.add_child(player)
        player.add_child(Node.string("refid", ref_id))
        player.add_child(Node.string("datid", ref_id))
        player.add_child(Node.string("uid", card_id))
        player.add_child(Node.bool("inherit", True))
        player.add_child(Node.string("name", self.NAME))

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

        # Verify server flags for events and stuff.
        self.__verify_info(resp, "gametop")

        # Verify nodes that cause crashes or failed card-ins if they don't exist
        return self.__verify_profile(resp, False)

    def verify_gametop_get_pdata(self, card_id: str, ref_id: str) -> int:
        call = self.call_node()

        # Construct node
        gametop = Node.void("gametop")
        call.add_child(gametop)
        gametop.set_attribute("method", "get_pdata")
        retry = Node.s32("retry", 0)
        gametop.add_child(retry)
        data = Node.void("data")
        gametop.add_child(data)
        player = Node.void("player")
        data.add_child(player)
        player.add_child(Node.string("refid", ref_id))
        player.add_child(Node.string("datid", ref_id))
        player.add_child(Node.string("uid", card_id))
        player.add_child(Node.string("card_no", CardCipher.encode(card_id)))

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

        # Verify nodes that cause crashes if they don't exist
        return self.__verify_profile(resp, False)

    def verify_gametop_get_mdata(self, jid: int) -> Dict[str, List[Dict[str, Any]]]:
        ret = {}
        for ver in [1, 2, 3]:
            # Construct node
            call = self.call_node()
            gametop = Node.void("gametop")
            call.add_child(gametop)
            gametop.set_attribute("method", "get_mdata")
            retry = Node.s32("retry", 0)
            gametop.add_child(retry)
            data = Node.void("data")
            gametop.add_child(data)
            player = Node.void("player")
            data.add_child(player)
            player.add_child(Node.s32("jid", jid))
            player.add_child(Node.s8("mdata_ver", ver))
            player.add_child(Node.bool("rival", False))

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

            # Parse out scores
            self.assert_path(resp, "response/gametop/data/player/jid")
            self.assert_path(resp, "response/gametop/data/player/mdata_list")
            if resp.child_value("gametop/data/player/jid") != jid:
                raise Exception("Unexpected jid received from server!")

            for musicdata in resp.child("gametop/data/player/mdata_list").children:
                if musicdata.name != "musicdata":
                    raise Exception("Unexpected node in playdata!")

                music_id = musicdata.attribute("music_id")
                scores_by_chart: List[Dict[str, int]] = [{}, {}, {}, {}, {}, {}]

                def extract_cnts(name: str, offset: int, val: List[int]) -> None:
                    scores_by_chart[offset + 0][name] = val[0]
                    scores_by_chart[offset + 1][name] = val[1]
                    scores_by_chart[offset + 2][name] = val[2]

                for subdata in musicdata.children:
                    if subdata.name == "normal":
                        offset = 0
                    elif subdata.name == "hard":
                        offset = 3
                    else:
                        raise Exception(f"Unexpected chart type {subdata.name}!")

                    extract_cnts("plays", offset, subdata.child_value("play_cnt"))
                    extract_cnts("clears", offset, subdata.child_value("clear_cnt"))
                    extract_cnts("full_combos", offset, subdata.child_value("fc_cnt"))
                    extract_cnts("excellents", offset, subdata.child_value("ex_cnt"))
                    extract_cnts("score", offset, subdata.child_value("score"))
                    extract_cnts("medal", offset, subdata.child_value("clear"))
                    extract_cnts("rate", offset, subdata.child_value("music_rate"))

                ret[music_id] = scores_by_chart

        return ret

    def verify_gametop_get_meeting(self, jid: int) -> None:
        call = self.call_node()

        # Construct node
        gametop = Node.void("gametop")
        call.add_child(gametop)
        gametop.set_attribute("method", "get_meeting")
        gametop.add_child(Node.s32("retry", 0))
        data = Node.void("data")
        gametop.add_child(data)
        player = Node.void("player")
        data.add_child(player)
        player.add_child(Node.s32("jid", jid))
        pcbinfo = Node.void("pcbinfo")
        gametop.add_child(pcbinfo)
        pcbinfo.set_attribute("client_data_version", "0")

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

        # Verify expected nodes
        self.assert_path(resp, "response/gametop/data/meeting/single/@count")
        self.assert_path(resp, "response/gametop/data/meeting/tag/@count")
        self.assert_path(resp, "response/gametop/data/reward/total")
        self.assert_path(resp, "response/gametop/data/reward/point")

    def verify_recommend_get_recommend(self, jid: int) -> None:
        call = self.call_node()

        # Construct node
        recommend = Node.void("recommend")
        call.add_child(recommend)
        recommend.set_attribute("method", "get_recommend")
        recommend.add_child(Node.s32("retry", 0))
        player = Node.void("player")
        recommend.add_child(player)
        player.add_child(Node.s32("jid", jid))
        player.add_child(Node.void("music_list"))

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

        # Verify expected nodes
        self.assert_path(resp, "response/recommend/data/player/music_list")

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

        # Construct node
        gametop = Node.void("demodata")
        call.add_child(gametop)
        gametop.set_attribute("method", "get_hitchart")

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

        # Verify expected nodes
        self.assert_path(resp, "response/demodata/data/update")
        self.assert_path(resp, "response/demodata/data/hitchart_lic/@count")
        self.assert_path(resp, "response/demodata/data/hitchart_org/@count")

    def verify_jbox_get_list(self, jid: int) -> None:
        call = self.call_node()

        # Construct node
        jbox = Node.void("jbox")
        call.add_child(jbox)
        jbox.set_attribute("method", "get_list")
        data = Node.void("data")
        jbox.add_child(data)
        player = Node.void("player")
        data.add_child(player)
        player.add_child(Node.s32("jid", jid))

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

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

    def verify_jbox_get_agreement(self, jid: int) -> None:
        call = self.call_node()

        # Construct node
        jbox = Node.void("jbox")
        call.add_child(jbox)
        jbox.set_attribute("method", "get_agreement")
        data = Node.void("data")
        jbox.add_child(data)
        player = Node.void("player")
        data.add_child(player)
        player.add_child(Node.s32("jid", jid))

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

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

    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",
                "netlog",
                "slocal",
                "ntp",
                "keepalive",
            ],
            include_net=True,
        )
        paseli_enabled = self.verify_pcbtracker_alive(ecflag=3)
        self.verify_package_list()
        self.verify_message_get()
        self.verify_facility_get(encoding="Shift-JIS")
        self.verify_pcbevent_put()
        self.verify_logger_report()
        self.verify_shopinfo_regist()
        self.verify_demodata_get_info()
        self.verify_demodata_get_jbox_list()
        self.verify_demodata_get_news()
        self.verify_demodata_get_hitchart()
        self.verify_lab_get_ranking()

        # 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")
            self.verify_gametop_regist(card, ref_id)
        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:
            # Verify score handling
            jid = self.verify_gametop_get_pdata(card, ref_id)
            self.verify_recommend_get_recommend(jid)
            scores = self.verify_gametop_get_mdata(jid)
            self.verify_gametop_get_meeting(jid)
            if scores is None:
                raise Exception("Expected to get scores back, didn't get anything!")
            if len(scores) > 0:
                raise Exception("Got nonzero score count on a new card!")

            # Verify end of game behavior
            self.verify_jbox_get_list(jid)
            self.verify_jbox_get_agreement(jid)
            self.verify_gameend_final(ref_id, jid)

            for phase in [1, 2]:
                if phase == 1:
                    dummyscores = [
                        # An okay score on a chart
                        {
                            "id": 40000059,
                            "chart": 2,
                            "hard": False,
                            "clear": True,
                            "fc": False,
                            "ex": False,
                            "score": 800000,
                            "rate": 567,
                            "expected_medal": 0x3,
                        },
                        # A good score on an easier chart of the same song
                        {
                            "id": 40000059,
                            "chart": 1,
                            "hard": False,
                            "clear": True,
                            "fc": True,
                            "ex": False,
                            "score": 990000,
                            "rate": 456,
                            "expected_medal": 0x5,
                        },
                        # A perfect score on an easiest chart of the same song
                        {
                            "id": 40000059,
                            "chart": 0,
                            "hard": False,
                            "clear": True,
                            "fc": True,
                            "ex": True,
                            "score": 1000000,
                            "rate": 678,
                            "expected_medal": 0x9,
                        },
                        # A bad score on a hard chart
                        {
                            "id": 30000027,
                            "chart": 2,
                            "hard": False,
                            "clear": False,
                            "fc": False,
                            "ex": False,
                            "score": 400000,
                            "rate": 123,
                            "expected_medal": 0x1,
                        },
                        # A terrible score on an easy chart
                        {
                            "id": 50000045,
                            "chart": 0,
                            "hard": False,
                            "clear": False,
                            "fc": False,
                            "ex": False,
                            "score": 100000,
                            "rate": 69,
                            "expected_medal": 0x1,
                        },
                        # A good score on a hard chart to make sure
                        # it doesn't pollute regular charts.
                        {
                            "id": 40000059,
                            "chart": 2,
                            "hard": True,
                            "clear": True,
                            "fc": False,
                            "ex": False,
                            "score": 812300,
                            "rate": 666,
                            "expected_medal": 0x3,
                        },
                    ]
                if phase == 2:
                    dummyscores = [
                        # A better score on the same chart
                        {
                            "id": 50000045,
                            "chart": 0,
                            "hard": False,
                            "clear": True,
                            "fc": False,
                            "ex": False,
                            "score": 850000,
                            "rate": 555,
                            "expected_medal": 0x3,
                        },
                        # A worse score on another same chart
                        {
                            "id": 40000059,
                            "chart": 1,
                            "hard": False,
                            "clear": True,
                            "fc": False,
                            "ex": False,
                            "score": 925000,
                            "rate": 432,
                            "expected_score": 990000,
                            "expected_rate": 456,
                            "expected_medal": 0x7,
                        },
                    ]

                self.verify_gameend_regist(ref_id, jid, dummyscores)
                jid = self.verify_gametop_get_pdata(card, ref_id)
                scores = self.verify_gametop_get_mdata(jid)

                for score in dummyscores:
                    chart = score["chart"] + (3 if score["hard"] else 0)
                    newscore = scores[str(score["id"])][chart]

                    if "expected_score" in score:
                        expected_score = score["expected_score"]
                    else:
                        expected_score = score["score"]

                    if "expected_rate" in score:
                        expected_rate = score["expected_rate"]
                    else:
                        expected_rate = score["rate"]

                    if newscore["score"] != expected_score:
                        raise Exception(
                            f'Expected a score of \'{expected_score}\' for song \'{score["id"]}\' chart \'{chart}\' but got score \'{newscore["score"]}\''
                        )

                    if newscore["rate"] != expected_rate:
                        raise Exception(
                            f'Expected a rate of \'{expected_rate}\' for song \'{score["id"]}\' chart \'{chart}\' but got rate \'{newscore["rate"]}\''
                        )

                    if newscore["medal"] != score["expected_medal"]:
                        raise Exception(
                            f'Expected a medal of \'{score["expected_medal"]}\' for song \'{score["id"]}\' chart \'{chart}\' but got medal \'{newscore["medal"]}\''
                        )

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