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

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


class ReflecBeatLimelight(BaseClient):
    NAME = "TEST"

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

        pcb = Node.void("log")
        pcb.set_attribute("method", "pcb_status")
        pcb.add_child(Node.string("lid", loc))
        pcb.add_child(Node.s32("cnt", 0))
        call.add_child(pcb)

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

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

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

        pcb = Node.void("pcbinfo")
        pcb.set_attribute("method", "get")
        pcb.add_child(Node.string("lid", loc))
        call.add_child(pcb)

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

        # Verify that response is correct
        self.assert_path(resp, "response/pcbinfo/info/name")
        self.assert_path(resp, "response/pcbinfo/info/pref")
        self.assert_path(resp, "response/pcbinfo/info/close")
        self.assert_path(resp, "response/pcbinfo/info/hour")
        self.assert_path(resp, "response/pcbinfo/info/min")

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

        info = Node.void("sysinfo")
        info.set_attribute("method", "get")
        call.add_child(info)

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

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

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

        ranking = Node.void("ranking")
        ranking.set_attribute("method", "read")
        call.add_child(ranking)

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

        # Verify that response is correct
        self.assert_path(resp, "response/ranking/lic_10/time")
        self.assert_path(resp, "response/ranking/org_10/time")

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

        player = Node.void("player")
        player.set_attribute("method", "start")
        player.add_child(Node.string("rid", refid))
        player.add_child(Node.s32("ver", 0))
        call.add_child(player)

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

        # Verify that response is correct
        self.assert_path(resp, "response/player/is_suc")
        self.assert_path(resp, "response/player/unlock_music")
        self.assert_path(resp, "response/player/unlock_item")
        self.assert_path(resp, "response/player/item_lock_ctrl")
        self.assert_path(resp, "response/player/lincle_link_4/qpro")
        self.assert_path(resp, "response/player/lincle_link_4/glass")
        self.assert_path(resp, "response/player/lincle_link_4/treasure")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_0_0")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_0_1")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_0_2")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_0_3")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_0_4")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_0_5")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_0_6")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_0")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_1")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_2")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_3")
        self.assert_path(resp, "response/player/lincle_link_4/for_iidx_4")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_0_0")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_0_1")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_0_2")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_0_3")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_0_4")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_0_5")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_0_6")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_0")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_1")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_2")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_3")
        self.assert_path(resp, "response/player/lincle_link_4/for_rb_4")
        self.assert_path(resp, "response/player/lincle_link_4/qproflg")
        self.assert_path(resp, "response/player/lincle_link_4/glassflg")
        self.assert_path(resp, "response/player/lincle_link_4/complete")

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

        player = Node.void("player")
        player.set_attribute("method", "delete")
        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/@status")

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

        player = Node.void("player")
        player.set_attribute("method", "end")
        player.add_child(Node.string("rid", refid))
        player.add_child(Node.s32("status", 4))
        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_read(self, refid: str, location: str) -> List[Dict[str, int]]:
        call = self.call_node()

        player = Node.void("player")
        player.set_attribute("method", "read")
        player.add_child(Node.string("rid", refid))
        player.add_child(Node.string("lid", location))
        player.add_child(Node.s32("ver", 2))
        call.add_child(player)

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

        # Verify that response is correct
        self.assert_path(resp, "response/player/pdata/base/uid")
        self.assert_path(resp, "response/player/pdata/base/name")
        self.assert_path(resp, "response/player/pdata/base/icon_id")
        self.assert_path(resp, "response/player/pdata/base/lv")
        self.assert_path(resp, "response/player/pdata/base/exp")
        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/pc")
        self.assert_path(resp, "response/player/pdata/base/uattr")
        self.assert_path(resp, "response/player/pdata/con/day")
        self.assert_path(resp, "response/player/pdata/con/cnt")
        self.assert_path(resp, "response/player/pdata/con/total_cnt")
        self.assert_path(resp, "response/player/pdata/con/last")
        self.assert_path(resp, "response/player/pdata/con/now")
        self.assert_path(resp, "response/player/pdata/team/id")
        self.assert_path(resp, "response/player/pdata/team/name")
        self.assert_path(resp, "response/player/pdata/custom/s_gls")
        self.assert_path(resp, "response/player/pdata/custom/bgm_m")
        self.assert_path(resp, "response/player/pdata/custom/st_f")
        self.assert_path(resp, "response/player/pdata/custom/st_bg")
        self.assert_path(resp, "response/player/pdata/custom/st_bg_b")
        self.assert_path(resp, "response/player/pdata/custom/eff_e")
        self.assert_path(resp, "response/player/pdata/custom/se_s")
        self.assert_path(resp, "response/player/pdata/custom/se_s_v")
        self.assert_path(resp, "response/player/pdata/custom/last_music_id")
        self.assert_path(resp, "response/player/pdata/custom/last_note_grade")
        self.assert_path(resp, "response/player/pdata/custom/sort_type")
        self.assert_path(resp, "response/player/pdata/custom/narrowdown_type")
        self.assert_path(resp, "response/player/pdata/custom/is_begginer")
        self.assert_path(resp, "response/player/pdata/custom/is_tut")
        self.assert_path(resp, "response/player/pdata/custom/symbol_chat_0")
        self.assert_path(resp, "response/player/pdata/custom/symbol_chat_1")
        self.assert_path(resp, "response/player/pdata/custom/gauge_style")
        self.assert_path(resp, "response/player/pdata/custom/obj_shade")
        self.assert_path(resp, "response/player/pdata/custom/obj_size")
        self.assert_path(resp, "response/player/pdata/custom/byword")
        self.assert_path(resp, "response/player/pdata/custom/is_auto_byword")
        self.assert_path(resp, "response/player/pdata/custom/is_tweet")
        self.assert_path(resp, "response/player/pdata/custom/is_link_twitter")
        self.assert_path(resp, "response/player/pdata/custom/mrec_type")
        self.assert_path(resp, "response/player/pdata/custom/card_disp_type")
        self.assert_path(resp, "response/player/pdata/custom/tab_sel")
        self.assert_path(resp, "response/player/pdata/custom/hidden_param")
        self.assert_path(resp, "response/player/pdata/released")
        self.assert_path(resp, "response/player/pdata/record")
        self.assert_path(resp, "response/player/pdata/cmnt")
        self.assert_path(resp, "response/player/pdata/rival")
        self.assert_path(resp, "response/player/pdata/glass")
        self.assert_path(resp, "response/player/pdata/fav_music_slot")
        self.assert_path(resp, "response/player/pdata/narrow_down/adv_param")

        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!')

        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("ng"),
                "clear_type": child.child_value("mrec_0/ct"),
                "achievement_rate": child.child_value("mrec_0/ar"),
                "score": child.child_value("mrec_0/bs"),
                "combo": child.child_value("mrec_0/mc"),
                "miss_count": child.child_value("mrec_0/bmc"),
            }
            scores.append(score)
        return scores

    def verify_player_write(
        self,
        refid: str,
        extid: int,
        loc: str,
        records: List[Dict[str, int]],
        scores: List[Dict[str, int]],
    ) -> int:
        call = self.call_node()

        player = Node.void("player")
        call.add_child(player)
        player.set_attribute("method", "write")
        player.add_child(Node.string("rid", refid))
        player.add_child(Node.string("lid", loc))
        player.add_child(Node.u64("begin_time", Time.now() * 1000))
        player.add_child(Node.u64("end_time", Time.now() * 1000))
        pdata = Node.void("pdata")
        player.add_child(pdata)
        base = Node.void("base")
        pdata.add_child(base)
        base.add_child(Node.s32("uid", extid))
        base.add_child(Node.string("name", self.NAME))
        base.add_child(Node.s16("icon_id", 0))
        base.add_child(Node.s16("lv", 1))
        base.add_child(Node.s32("exp", 0))
        base.add_child(Node.s16("mg", 0))
        base.add_child(Node.s16("ap", 0))
        base.add_child(Node.s32("pc", 0))
        base.add_child(Node.s32("uattr", 0))
        con = Node.void("con")
        pdata.add_child(con)
        con.add_child(Node.s32("day", 0))
        con.add_child(Node.s32("cnt", 0))
        con.add_child(Node.s32("total_cnt", 0))
        con.add_child(Node.s32("last", 0))
        con.add_child(Node.s32("now", 0))
        custom = Node.void("custom")
        pdata.add_child(custom)
        custom.add_child(Node.u8("s_gls", 0))
        custom.add_child(Node.u8("bgm_m", 0))
        custom.add_child(Node.u8("st_f", 0))
        custom.add_child(Node.u8("st_bg", 0))
        custom.add_child(Node.u8("st_bg_b", 100))
        custom.add_child(Node.u8("eff_e", 0))
        custom.add_child(Node.u8("se_s", 0))
        custom.add_child(Node.u8("se_s_v", 100))
        custom.add_child(Node.s16("last_music_id", 85))
        custom.add_child(Node.u8("last_note_grade", 0))
        custom.add_child(Node.u8("sort_type", 0))
        custom.add_child(Node.u8("narrowdown_type", 0))
        custom.add_child(Node.bool("is_begginer", False))
        custom.add_child(Node.bool("is_tut", False))
        custom.add_child(Node.s16_array("symbol_chat_0", [0, 1, 2, 3, 4, 5]))
        custom.add_child(Node.s16_array("symbol_chat_1", [0, 1, 2, 3, 4, 5]))
        custom.add_child(Node.u8("gauge_style", 0))
        custom.add_child(Node.u8("obj_shade", 0))
        custom.add_child(Node.u8("obj_size", 0))
        custom.add_child(Node.s16_array("byword", [0, 0]))
        custom.add_child(Node.bool_array("is_auto_byword", [True, True]))
        custom.add_child(Node.bool("is_tweet", False))
        custom.add_child(Node.bool("is_link_twitter", False))
        custom.add_child(Node.s16("mrec_type", 0))
        custom.add_child(Node.s16("card_disp_type", 0))
        custom.add_child(Node.s16("tab_sel", 0))
        custom.add_child(
            Node.s32_array(
                "hidden_param",
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0],
            )
        )
        pdata.add_child(Node.void("released"))
        pdata.add_child(Node.void("rival"))
        pdata.add_child(Node.void("glass"))
        pdata.add_child(Node.void("fav_music_slot"))
        lincle_link_4 = Node.void("lincle_link_4")
        pdata.add_child(lincle_link_4)
        lincle_link_4.add_child(Node.u32("qpro_add", 0))
        lincle_link_4.add_child(Node.u32("glass_add", 0))
        lincle_link_4.add_child(Node.bool("for_iidx_0_0", False))
        lincle_link_4.add_child(Node.bool("for_iidx_0_1", False))
        lincle_link_4.add_child(Node.bool("for_iidx_0_2", False))
        lincle_link_4.add_child(Node.bool("for_iidx_0_3", False))
        lincle_link_4.add_child(Node.bool("for_iidx_0_4", False))
        lincle_link_4.add_child(Node.bool("for_iidx_0_5", False))
        lincle_link_4.add_child(Node.bool("for_iidx_0_6", False))
        lincle_link_4.add_child(Node.bool("for_iidx_0", False))
        lincle_link_4.add_child(Node.bool("for_iidx_1", False))
        lincle_link_4.add_child(Node.bool("for_iidx_2", False))
        lincle_link_4.add_child(Node.bool("for_iidx_3", False))
        lincle_link_4.add_child(Node.bool("for_iidx_4", False))
        lincle_link_4.add_child(Node.bool("for_rb_0_0", False))
        lincle_link_4.add_child(Node.bool("for_rb_0_1", False))
        lincle_link_4.add_child(Node.bool("for_rb_0_2", False))
        lincle_link_4.add_child(Node.bool("for_rb_0_3", False))
        lincle_link_4.add_child(Node.bool("for_rb_0_4", False))
        lincle_link_4.add_child(Node.bool("for_rb_0_5", False))
        lincle_link_4.add_child(Node.bool("for_rb_0_6", False))
        lincle_link_4.add_child(Node.bool("for_rb_0", False))
        lincle_link_4.add_child(Node.bool("for_rb_1", False))
        lincle_link_4.add_child(Node.bool("for_rb_2", False))
        lincle_link_4.add_child(Node.bool("for_rb_3", False))
        lincle_link_4.add_child(Node.bool("for_rb_4", False))

        # First, filter down to only records that are also in the battle log
        def key(thing: Dict[str, int]) -> str:
            return f'{thing["id"]}-{thing["chart"]}'

        updates = [key(score) for score in scores]
        sortedrecords = {key(record): record for record in records if key(record) in updates}

        # Now, see what records need updating and update them
        for score in scores:
            if key(score) in sortedrecords:
                # Had a record, need to merge
                record = sortedrecords[key(score)]
            else:
                # First time playing
                record = {
                    "clear_type": 0,
                    "achievement_rate": 0,
                    "score": 0,
                    "combo": 0,
                    "miss_count": 999999999,
                }

            sortedrecords[key(score)] = {
                "id": score["id"],
                "chart": score["chart"],
                "clear_type": max(record["clear_type"], score["clear_type"]),
                "achievement_rate": max(record["achievement_rate"], score["achievement_rate"]),
                "score": max(record["score"], score["score"]),
                "combo": max(record["combo"], score["combo"]),
                "miss_count": min(record["miss_count"], score["miss_count"]),
            }

        # Finally, send the records and battle logs
        recordnode = Node.void("record")
        pdata.add_child(recordnode)
        blog = Node.void("blog")
        pdata.add_child(blog)

        for _, record in sortedrecords.items():
            rec = Node.void("rec")
            recordnode.add_child(rec)
            rec.add_child(Node.u16("mid", record["id"]))
            rec.add_child(Node.u8("ng", record["chart"]))
            rec.add_child(Node.s32("point", 2))
            rec.add_child(Node.s32("played_time", Time.now()))
            mrec_0 = Node.void("mrec_0")
            rec.add_child(mrec_0)
            mrec_0.add_child(Node.s32("win", 1))
            mrec_0.add_child(Node.s32("lose", 0))
            mrec_0.add_child(Node.s32("draw", 0))
            mrec_0.add_child(Node.u8("ct", record["clear_type"]))
            mrec_0.add_child(Node.s16("ar", record["achievement_rate"]))
            mrec_0.add_child(Node.s32("bs", record["score"]))
            mrec_0.add_child(Node.s16("mc", record["combo"]))
            mrec_0.add_child(Node.s16("bmc", record["miss_count"]))
            mrec_1 = Node.void("mrec_1")
            rec.add_child(mrec_1)
            mrec_1.add_child(Node.s32("win", 0))
            mrec_1.add_child(Node.s32("lose", 0))
            mrec_1.add_child(Node.s32("draw", 0))
            mrec_1.add_child(Node.u8("ct", 0))
            mrec_1.add_child(Node.s16("ar", 0))
            mrec_1.add_child(Node.s32("bs", 0))
            mrec_1.add_child(Node.s16("mc", 0))
            mrec_1.add_child(Node.s16("bmc", -1))

        scoreid = 0
        for score in scores:
            log = Node.void("log")
            blog.add_child(log)
            log.add_child(Node.u8("id", scoreid))
            log.add_child(Node.u16("mid", score["id"]))
            log.add_child(Node.u8("ng", score["chart"]))
            log.add_child(Node.u8("mt", 0))
            log.add_child(Node.u8("rt", 0))
            log.add_child(Node.s32("ruid", 0))
            myself = Node.void("myself")
            log.add_child(myself)
            myself.add_child(Node.s16("mg", 0))
            myself.add_child(Node.s16("ap", 0))
            myself.add_child(Node.u8("ct", score["clear_type"]))
            myself.add_child(Node.s32("s", score["score"]))
            myself.add_child(Node.s16("ar", score["achievement_rate"]))
            rival = Node.void("rival")
            log.add_child(rival)
            rival.add_child(Node.s16("mg", 0))
            rival.add_child(Node.s16("ap", 0))
            rival.add_child(Node.u8("ct", 2))
            rival.add_child(Node.s32("s", 177))
            rival.add_child(Node.s16("ar", 500))
            log.add_child(Node.s32("time", Time.now()))
            scoreid = scoreid + 1

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

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

    def verify_log_play(self, extid: int, loc: str, scores: List[Dict[str, int]]) -> None:
        call = self.call_node()

        log = Node.void("log")
        call.add_child(log)
        log.set_attribute("method", "play")
        log.add_child(Node.s32("uid", extid))
        log.add_child(Node.string("lid", loc))
        play = Node.void("play")
        log.add_child(play)
        play.add_child(Node.s16("stage", len(scores)))
        play.add_child(Node.s32("sec", 700))

        scoreid = 0
        for score in scores:
            rec = Node.void("rec")
            log.add_child(rec)
            rec.add_child(Node.s16("idx", scoreid))
            rec.add_child(Node.s16("mid", score["id"]))
            rec.add_child(Node.s16("grade", score["chart"]))
            rec.add_child(Node.s16("color", 0))
            rec.add_child(Node.s16("match", 0))
            rec.add_child(Node.s16("res", 0))
            rec.add_child(Node.s32("score", score["score"]))
            rec.add_child(Node.s16("mc", score["combo"]))
            rec.add_child(Node.s16("jt_jr", 0))
            rec.add_child(Node.s16("jt_ju", 0))
            rec.add_child(Node.s16("jt_gr", 0))
            rec.add_child(Node.s16("jt_gd", 0))
            rec.add_child(Node.s16("jt_ms", score["miss_count"]))
            rec.add_child(Node.s32("sec", 200))
            scoreid = scoreid + 1

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

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

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

        lobby = Node.void("lobby")
        lobby.set_attribute("method", "read")
        lobby.add_child(Node.s32("uid", extid))
        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", []))
        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_entry(self, location: str, extid: int) -> int:
        call = self.call_node()

        lobby = Node.void("lobby")
        lobby.set_attribute("method", "entry")
        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.s16("mg", 0))
        e.add_child(Node.s32("mopt", 0))
        e.add_child(Node.s32("tid", 0))
        e.add_child(Node.string("tn", ""))
        e.add_child(Node.s32("topt", 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", 0))
        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]))
        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/eid")
        self.assert_path(resp, "response/lobby/interval")
        self.assert_path(resp, "response/lobby/interval_p")
        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/pn")
        self.assert_path(resp, "response/lobby/e/uattr")
        self.assert_path(resp, "response/lobby/e/mopt")
        self.assert_path(resp, "response/lobby/e/mg")
        self.assert_path(resp, "response/lobby/e/tid")
        self.assert_path(resp, "response/lobby/e/tn")
        self.assert_path(resp, "response/lobby/e/topt")
        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")
        return resp.child_value("lobby/eid")

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

        lobby = Node.void("lobby")
        lobby.set_attribute("method", "delete")
        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_event_w_update_status(self, loc: str, extid: int) -> None:
        call = self.call_node()

        event_w = Node.void("event_w")
        call.add_child(event_w)
        event_w.set_attribute("method", "update_status")
        event_w.add_child(Node.s32("uid", extid))
        event_w.add_child(Node.string("p_name", self.NAME))
        event_w.add_child(Node.s32("exp", 0))
        event_w.add_child(Node.s32("customize", 0))
        event_w.add_child(Node.s32("tid", 0))
        event_w.add_child(Node.string("t_name", ""))
        event_w.add_child(Node.string("lid", loc))
        event_w.add_child(Node.string("s_name", ""))
        event_w.add_child(Node.s8("pref", 51))
        event_w.add_child(Node.s32("time", Time.now()))
        event_w.add_child(Node.s8("status", 1))
        event_w.add_child(Node.s8("stage", 0))
        event_w.add_child(Node.s32("mid", -1))
        event_w.add_child(Node.s8("ng", -1))

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

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

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

        event_w = Node.void("event_w")
        call.add_child(event_w)
        event_w.set_attribute("method", "add_comment")
        event_w.add_child(Node.s32("uid", extid))
        event_w.add_child(Node.string("p_name", self.NAME))
        event_w.add_child(Node.s32("exp", 0))
        event_w.add_child(Node.s32("customize", 0))
        event_w.add_child(Node.s32("tid", 0))
        event_w.add_child(Node.string("t_name", ""))
        event_w.add_child(Node.string("lid", loc))
        event_w.add_child(Node.string("s_name", ""))
        event_w.add_child(Node.s8("pref", 51))
        event_w.add_child(Node.s32("time", Time.now()))
        event_w.add_child(Node.string("comment", "アメ〜〜!"))
        event_w.add_child(Node.bool("is_tweet", False))

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

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

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

        event_r = Node.void("event_r")
        call.add_child(event_r)
        event_r.set_attribute("method", "get_all")
        event_r.add_child(Node.s32("uid", extid))
        event_r.add_child(Node.s32("time", 0))
        event_r.add_child(Node.s32("limit", 30))

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

        # Verify that response is correct. We should have at least one
        # comment (the one we just wrote) and one status (because we just
        # called update_status).
        self.assert_path(resp, "response/event_r/time")
        self.assert_path(resp, "response/event_r/status/s/uid")
        self.assert_path(resp, "response/event_r/status/s/p_name")
        self.assert_path(resp, "response/event_r/status/s/exp")
        self.assert_path(resp, "response/event_r/status/s/customize")
        self.assert_path(resp, "response/event_r/status/s/tid")
        self.assert_path(resp, "response/event_r/status/s/t_name")
        self.assert_path(resp, "response/event_r/status/s/lid")
        self.assert_path(resp, "response/event_r/status/s/s_name")
        self.assert_path(resp, "response/event_r/status/s/pref")
        self.assert_path(resp, "response/event_r/status/s/time")
        self.assert_path(resp, "response/event_r/status/s/status")
        self.assert_path(resp, "response/event_r/status/s/stage")
        self.assert_path(resp, "response/event_r/status/s/mid")
        self.assert_path(resp, "response/event_r/status/s/ng")
        self.assert_path(resp, "response/event_r/comment/c/uid")
        self.assert_path(resp, "response/event_r/comment/c/p_name")
        self.assert_path(resp, "response/event_r/comment/c/exp")
        self.assert_path(resp, "response/event_r/comment/c/customize")
        self.assert_path(resp, "response/event_r/comment/c/tid")
        self.assert_path(resp, "response/event_r/comment/c/t_name")
        self.assert_path(resp, "response/event_r/comment/c/lid")
        self.assert_path(resp, "response/event_r/comment/c/s_name")
        self.assert_path(resp, "response/event_r/comment/c/pref")
        self.assert_path(resp, "response/event_r/comment/c/time")
        self.assert_path(resp, "response/event_r/comment/c/comment")
        self.assert_path(resp, "response/event_r/comment/c/is_tweet")

        # Verify we posted our comment earlier
        found = False
        for child in resp.child("event_r/comment").children:
            if child.name != "c":
                continue
            if child.child_value("uid") == extid:
                name = child.child_value("p_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!")

        # Verify our status came through
        found = False
        for child in resp.child("event_r/status").children:
            if child.name != "s":
                continue
            if child.child_value("uid") == extid:
                name = child.child_value("p_name")
                if name != self.NAME:
                    raise Exception(f"Invalid name '{name}' returned for status!")
                found = True

        if not found:
            raise Exception("Status was not found!")

    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_log_pcb_status(location)
        self.verify_pcbinfo_get(location)

        self.verify_sysinfo_get()
        self.verify_ranking_read()

        # 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_start(ref_id)
            self.verify_player_delete(ref_id)
            extid = self.verify_player_write(
                ref_id,
                0,
                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_read(location, extid)
        eid = self.verify_lobby_entry(location, extid)
        self.verify_lobby_delete(eid)

        # Verify status updates and puzzle comments
        self.verify_event_w_update_status(location, extid)
        self.verify_event_w_add_comment(location, extid)
        self.verify_event_r_get_all(extid)

        # Limelight is weird and sends only the top record for each song you played,
        # and then a separate battle log. So, emulating that is kinda hard.
        scores: List[Dict[str, int]] = []
        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": 2,
                            "achievement_rate": 7543,
                            "score": 432,
                            "combo": 123,
                            "miss_count": 5,
                        },
                        # A good score on an easier chart of the same song
                        {
                            "id": 1,
                            "chart": 0,
                            "clear_type": 3,
                            "achievement_rate": 9876,
                            "score": 543,
                            "combo": 543,
                            "miss_count": 0,
                        },
                        # A bad score on a hard chart
                        {
                            "id": 3,
                            "chart": 2,
                            "clear_type": 2,
                            "achievement_rate": 1234,
                            "score": 123,
                            "combo": 42,
                            "miss_count": 54,
                        },
                        # A terrible score on an easy chart
                        {
                            "id": 3,
                            "chart": 0,
                            "clear_type": 2,
                            "achievement_rate": 1024,
                            "score": 50,
                            "combo": 12,
                            "miss_count": 90,
                        },
                    ]
                if phase == 2:
                    dummyscores = [
                        # A better score on the same chart
                        {
                            "id": 1,
                            "chart": 1,
                            "clear_type": 3,
                            "achievement_rate": 8765,
                            "score": 469,
                            "combo": 468,
                            "miss_count": 1,
                        },
                        # A worse score on another same chart
                        {
                            "id": 1,
                            "chart": 0,
                            "clear_type": 2,
                            "achievement_rate": 8765,
                            "score": 432,
                            "combo": 321,
                            "miss_count": 15,
                            "expected_score": 543,
                            "expected_clear_type": 3,
                            "expected_achievement_rate": 9876,
                            "expected_combo": 543,
                            "expected_miss_count": 0,
                        },
                    ]
                self.verify_player_write(ref_id, extid, location, scores, dummyscores)
                self.verify_log_play(extid, location, dummyscores)

                scores = self.verify_player_read(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" in expected:
                        expected_combo = expected["expected_combo"]
                    else:
                        expected_combo = expected["combo"]
                    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"] != expected_combo:
                        raise Exception(
                            f'Expected a combo of \'{expected_combo}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got combo \'{actual["combo"]}\''
                        )
                    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_end(ref_id)

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