import time
import json
from typing import Literal
import dictdiffer
import pytest

from lpsim.server.struct import Cost, ObjectPosition
from lpsim.server.event import UseCardEventArguments
from lpsim.server.action import ActionTypes, UseCardAction
from lpsim.utils.class_registry import get_instance
from lpsim.server.summon.base import SummonBase
from lpsim.server.consts import ObjectPositionType, ObjectType
from lpsim.agents.nothing_agent import NothingAgent
from lpsim.server.event_handler import OmnipotentGuideEventHandler_3_3
from lpsim.server.match import Match, MatchState
from lpsim.server.deck import Deck
from lpsim.agents.random_agent import RandomAgent
from lpsim.agents.interaction_agent import InteractionAgent
from lpsim.server.character.electro.fischl_3_3 import Oz_3_3
from tests.utils_for_test import (
    get_test_id_from_command,
    set_16_omni,
    make_respond,
    remove_ids,
    get_random_state,
    check_hp,
)


TEST_DECK = {
    "name": "Deck",
    "characters": [
        {
            "name": "DendroMobMage",
            "element": "DENDRO",
            "hp": 99,
            "max_hp": 99,
        },
        {
            "name": "DendroMobMage",
            "element": "DENDRO",
        },
        {
            "name": "ElectroMobMage",
            "element": "ELECTRO",
        },
    ],
    "cards": [
        {
            "name": "Strategize",
        }
    ]
    * 30,
}


def test_match_pipeline():
    agent_0 = RandomAgent(player_idx=0)
    agent_1 = NothingAgent(player_idx=1)
    match = Match()
    deck = TEST_DECK
    deck = Deck(**deck)
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    set_16_omni(match)
    # main.match.event_handlers[0].version = '3.3'
    assert match.start()[0]
    match.step()  # switch card

    while match.round_number < 100 and not match.is_game_end():
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)

    assert match.state != MatchState.ERROR


def test_save_load():
    """
    use v3.3 Catalyzing Field to test save & load.
    when save on field generated, load should be same. otherwise, load should
    be different only on id.
    """
    agent_0 = NothingAgent(player_idx=0)
    agent_1 = InteractionAgent(
        player_idx=1,
        verbose_level=0,
        commands=[
            "sw_card",
            "choose 2",
            # 'reroll',
            "skill 0 omni omni omni",
            "sw_char 0 omni",
            "skill 0 omni omni omni",  # quiken activated
            "skill 0 omni omni omni",
            "sw_char 2 omni",
            "skill 0 omni omni omni",  # quiken activated, renew in v3.3
            "end",
            "skill 0 omni omni omni",
            "skill 0 omni omni omni",
            "skill 0 omni omni omni",  # run out of field
        ],
        only_use_command=True,
    )
    match = Match()
    deck = TEST_DECK
    deck = Deck(**deck)
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    set_16_omni(match)
    match.event_handlers[0].version = "3.3"  # type: ignore
    assert match.start()[0]
    match.step()  # switch card

    saves = {}
    agent_saves = {}

    while True:
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
        if len(agent_1.commands) not in saves:
            length = len(agent_1.commands)
            saves[length] = match.copy(deep=True)
            agent_saves[length] = agent_1.copy(deep=True)
        else:
            raise AssertionError()
        if len(agent_1.commands) == 0:
            break

    def check_range(saves, agent_saves, start, end, same=True, ignore_id=False):
        """
        load start and run until end, then check if they are same.
        if ignore_id, before compare will remove ids from objects.
        """
        match = saves[start].copy(deep=True)
        agent_1 = agent_saves[start].copy(deep=True)
        while True:
            make_respond(agent_0, match, assertion=False)
            make_respond(agent_1, match, assertion=False)
            if len(agent_1.commands) == end:
                break
        saves_end = saves[end]
        if ignore_id:
            match = match.copy(deep=True)
            saves_end = saves_end.copy(deep=True)
            remove_ids(match)
            remove_ids(saves_end)
        is_same = match == saves_end
        assert is_same == same

    # from any position, run reuslts should same
    for _ in range(1, 11):
        check_range(saves, agent_saves, _, 0, same=True, ignore_id=True)
    # generate status, id should not change
    check_range(saves, agent_saves, 8, 7, same=False, ignore_id=False)
    # ignore id, should same
    check_range(saves, agent_saves, 8, 7, same=True, ignore_id=True)
    # renew status, id should not change
    check_range(saves, agent_saves, 6, 4, same=True, ignore_id=False)

    assert len(agent_1.commands) == 0

    assert match.state != MatchState.ERROR


def test_copy_speed():
    agent_0 = NothingAgent(player_idx=0)
    agent_1 = InteractionAgent(
        player_idx=1,
        verbose_level=0,
        commands=[
            "sw_card",
            "choose 0",
            "card 1 0 0 1",
            "skill 1 0 1 2",
            "sw_char 1 1",
            "skill 1 omni omni hydro",
            "end",
            "skill 1 0 1 2",
            "sw_char 2 0",
            "skill 2 omni omni dendro omni omni",
            "end",
            "skill 1 0 1 2",
            "card 0 0 0 1 2",
        ],
        only_use_command=True,
    )
    match = Match(random_state=get_random_state())
    deck = Deck.from_str(
        """
        character:Fischl
        character:Mona
        character:Nahida
        Rana*10
        Wine-Stained Tricorne*10
        The Seed of Stored Knowledge*10
        """
    )
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    set_16_omni(match)
    assert match.start()[0]
    match.step()

    while True:
        if match.need_respond(0):
            make_respond(agent_0, match)
        elif match.need_respond(1):
            make_respond(agent_1, match)
        else:
            raise AssertionError("No need respond.")
        if len(agent_1.commands) == 0:
            break

    assert match.state != MatchState.ERROR

    run_time = 100
    match = Match()
    deck = TEST_DECK
    deck = Deck(**deck)
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    assert match.start()[0]
    starttime = time.time()
    for i in range(run_time):
        _ = match.copy(deep=True)
    endtime = time.time()
    print("not json speed", (endtime - starttime) / run_time)

    match = Match()
    match.config.max_same_card_number = 30
    match.set_deck([deck, deck])
    mainjson = match.json()
    starttime = time.time()
    assert match.start()[0]
    for i in range(run_time):
        mainjson = match.json()
        _ = Match(**json.loads(mainjson))
    endtime = time.time()
    print("json speed", (endtime - starttime) / run_time)
    assert True


def test_random_same_after_load():
    agent_0 = RandomAgent(player_idx=0, random_seed=42)
    agent_1 = RandomAgent(player_idx=1, random_seed=19260817)
    match = Match(random_state=get_random_state())
    deck = Deck.from_str(
        """
        default_version:4.0
        character:Rhodeia of Loch
        character:Kamisato Ayaka
        Traveler's Handy Sword*5
        Gambler's Earrings*5
        Kanten Senmyou Blessing*5
        Sweet Madame*5
        Abyssal Summons*5
        Fatui Conspiracy*5
        """
    )
    for character in deck.characters:
        character.hp = character.max_hp = 99
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    match.config.max_hand_size = 30
    match.config.initial_hand_size = 10
    match.config.card_number = None
    match.config.character_number = None
    match.config.check_deck_restriction = False
    initial_match = match.copy(deep=True)
    initial_agent_0 = agent_0.copy(deep=True)
    initial_agent_1 = agent_1.copy(deep=True)
    results_1 = []
    test_step = 100
    assert match.start()[0]
    match.step()
    for _ in range(test_step):
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
        results_1.append(match.copy(deep=True))
    assert match.state != MatchState.ERROR

    # use deepcopy to rerun
    match = initial_match.copy(deep=True)
    agent_0 = initial_agent_0.copy(deep=True)
    agent_1 = initial_agent_1.copy(deep=True)
    assert match.start()[0]
    match.step()
    for i in range(test_step):
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
        remove_ids(results_1[i])
        now = match.copy(deep=True)
        remove_ids(now)
        assert now == results_1[i], f"deepcopy error at {i}"
    assert match.state != MatchState.ERROR

    match = Match(**json.loads(initial_match.json()))
    assert match.json() == initial_match.json()
    assert match == initial_match
    # random agent do not support json, use copy instead
    agent_0 = initial_agent_0.copy()
    agent_1 = initial_agent_1.copy()
    assert agent_0 == initial_agent_0
    assert agent_1 == initial_agent_1
    assert match.start()[0]
    match.step()
    for i in range(test_step):
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
        remove_ids(results_1[i])
        now = match.copy(deep=True)
        remove_ids(now)
        assert now == results_1[i], f"json error at {i}"
    assert match.state != MatchState.ERROR


def test_save_load_same():
    agent_0 = RandomAgent(player_idx=0, random_seed=42)
    agent_1 = RandomAgent(player_idx=1, random_seed=19260817)
    match = Match(random_state=get_random_state(100))
    deck = Deck.from_str(
        """
        default_version:4.0
        character:Rhodeia of Loch
        character:Kamisato Ayaka
        Traveler's Handy Sword*5
        Gambler's Earrings*5
        Kanten Senmyou Blessing*5
        Sweet Madame*5
        Abyssal Summons*5
        Fatui Conspiracy*5
        Timmie*5
        """
    )
    for character in deck.characters:
        character.hp = character.max_hp = 99
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    match.config.max_hand_size = 30
    match.config.initial_hand_size = 10
    match.config.card_number = None
    match.config.character_number = None
    match.config.check_deck_restriction = False
    test_step = 100
    assert match.start()[0]
    match.step()
    for _ in range(test_step):
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
        match_copy = match.copy(deep=True)
        assert remove_ids(match_copy) == remove_ids(
            Match(**json.loads(match_copy.json()))
        )
        match = Match(**json.loads(match.json()))
    assert match.state != MatchState.ERROR


def test_use_card():
    agent_0 = NothingAgent(player_idx=0)
    agent_1 = InteractionAgent(
        player_idx=1,
        commands=[
            "sw_card",
            "choose 2",
            "card 0 0 omni",
            "card 0 0 omni",
            "card 0 0 omni",
            "card 0 0 omni",
            "end",
            "card 0 0 omni",
            "card 0 0 omni",
            "card 0 0 omni",
            "card 0 0 omni",
        ],
    )
    match = Match()
    deck = TEST_DECK
    deck = Deck(**deck)
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    match.config.initial_dice_number = 16
    match.event_handlers = [OmnipotentGuideEventHandler_3_3()] + match.event_handlers
    assert match.start()[0]
    match.step()

    hand_numbers = [10, 10, 10, 10, 10, 9, 8, 7, 6, 5, 5, 5]
    deck_numbers = [7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 25, 25]

    while len(agent_1.commands) > 0:
        if match.need_respond(0):
            current_agent = agent_0
        elif match.need_respond(1):
            current_agent = agent_1
            assert (
                len(match.player_tables[1].hands) == hand_numbers[len(agent_1.commands)]
            )
            assert (
                len(match.player_tables[1].table_deck)
                == deck_numbers[len(agent_1.commands)]
            )
        else:
            raise AssertionError("no need respond")
        make_respond(current_agent, match)

    assert match.state != MatchState.ERROR


def test_support_over_maximum_and_error_tests():
    """
    put 6 Ranas and check usages and order.
    """
    agent_0 = NothingAgent(player_idx=0)
    agent_1 = InteractionAgent(
        player_idx=1,
        verbose_level=0,
        commands=[
            "sw_card",
            "choose 1",
            "card 0 0 omni omni",
            "card 0 0 omni omni",
            "card 0 0 omni omni",
            "end",
            "end",
            "skill 1 omni omni omni",
            "card 0 0 omni electro",
            "card 0 3 electro electro",
            # 4 rana, usage 0 0 0 1
            "card 0 0 omni omni",
            # 4 rana, usage 0 0 1 1
            "skill 1 omni omni omni",
            # dice 7 omni + 2 electro
            "end",
            "skill 1 omni omni omni",
            # 13 omni and 3 electro and 0 usage
            "end",
            "skill 2 omni omni omni",
            # 13 omni, all 1 usage
            "end",
        ],
        only_use_command=True,
    )
    match = Match(random_state=get_random_state())
    deck = {
        "name": "Deck",
        "characters": [
            {
                "name": "DendroMobMage",
                "element": "DENDRO",
                "hp": 99,
                "max_hp": 99,
            },
            {
                "name": "DendroMobMage",
                "element": "DENDRO",
            },
            {
                "name": "ElectroMobMage",
                "element": "ELECTRO",
            },
        ],
        "cards": [
            {
                "name": "Rana",
            }
        ]
        * 30,
    }
    deck = Deck(**deck)
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    set_16_omni(match)
    assert match.start()[0]
    match.step()

    while True:
        if match.need_respond(0):
            make_respond(agent_0, match)
        elif match.need_respond(1):
            # asserts
            if len(agent_1.commands) == 7:
                # 4 rana, usage 0 0 0 1
                assert len(match.player_tables[1].supports) == 4
                for support, usage in zip(
                    match.player_tables[1].supports, [0, 0, 0, 1]
                ):
                    assert support.name == "Rana"
                    assert support.usage == usage
            elif len(agent_1.commands) == 6:
                # 4 rana, usage 0 0 1 1
                assert len(match.player_tables[1].supports) == 4
                for support, usage in zip(
                    match.player_tables[1].supports, [0, 0, 1, 1]
                ):
                    assert support.name == "Rana"
                    assert support.usage == usage
            elif len(agent_1.commands) == 5:
                # dice 7 omni + 2 electro
                assert len(match.player_tables[1].dice.colors) == 9
                omni_counter = 0
                electro_counter = 0
                for d in match.player_tables[1].dice.colors:
                    if d == "OMNI":
                        omni_counter += 1
                    else:
                        assert d == "ELECTRO"
                        electro_counter += 1
                assert omni_counter == 7
                assert electro_counter == 2
            elif len(agent_1.commands) == 3:
                # 13 omni and 3 electro and 0 usage
                assert len(match.player_tables[1].dice.colors) == 16
                omni_counter = 0
                electro_counter = 0
                for d in match.player_tables[1].dice.colors:
                    if d == "OMNI":
                        omni_counter += 1
                    else:
                        assert d == "ELECTRO"
                        electro_counter += 1
                assert omni_counter == 13
                assert electro_counter == 3
                for support in match.player_tables[1].supports:
                    assert support.name == "Rana"
                    assert support.usage == 0
            elif len(agent_1.commands) == 1:
                # 13 omni, all 1 usage
                assert len(match.player_tables[1].dice.colors) == 13
                for d in match.player_tables[1].dice.colors:
                    assert d == "OMNI"
                for support in match.player_tables[1].supports:
                    assert support.name == "Rana"
                    assert support.usage == 1

            make_respond(agent_1, match)
        else:
            raise AssertionError("no need respond")
        if len(agent_1.commands) == 0:
            break

    assert len(agent_1.commands) == 0
    assert match.round_number == 6
    check_hp(match, [[85, 10, 10], [99, 10, 10]])
    assert len(match.player_tables[1].supports) == 4
    for support in match.player_tables[1].supports:
        assert support.name == "Rana"

    assert match.state != MatchState.ERROR

    # after start, set deck will raise error
    with pytest.raises(ValueError):
        match.set_deck([deck, deck])
    assert not match.start()[0]


def test_summon_over_maximum():
    cmd_records = [
        [
            "sw_card",
            "choose 2",
            "skill 2 0 1 2 3 4",
            "skill 2 0 1 2 3 4",
            "sw_char 1 0",
            "skill 1 0 1 2",
            "end",
            "sw_char 0 0",
            "skill 1 0 1 2",
            "end",
        ],
        [
            "sw_card",
            "choose 2",
            "skill 2 0 1 2 3 4",
            "skill 2 0 1 2 3 4",
            "sw_char 1 0",
            "skill 1 0 1 2",
            "end",
            "sw_char 0 0",
            "sw_char 2 0",
            "sw_char 0 0",
            "skill 1 0 1 2",
            "sw_char 2 0",
            "skill 1 0 1 2",
            "skill 2 0 1 2 3 4",
            "TEST 1 frog squi rap oz rap squi oz ref",
            "end",
        ],
    ]
    agent_0 = InteractionAgent(
        player_idx=0, verbose_level=0, commands=cmd_records[0], only_use_command=True
    )
    agent_1 = InteractionAgent(
        player_idx=1, verbose_level=0, commands=cmd_records[1], only_use_command=True
    )
    # initialize match. It is recommended to use default random state to make
    # replay unchanged.
    match = Match(random_state=get_random_state())
    # deck information
    deck = Deck.from_str(
        """
        default_version:4.0
        character:Mona
        character:Fischl
        character:Rhodeia of Loch
        Send Off*30
        """
    )
    match.set_deck([deck, deck])
    match.config.max_same_card_number = None
    match.config.character_number = None
    match.config.card_number = None
    match.config.check_deck_restriction = False
    # check whether random_first_player is enabled.
    match.config.random_first_player = False
    # check whether in rich mode (16 omni each round)
    set_16_omni(match)
    match.start()
    match.step()

    while True:
        if match.need_respond(0):
            agent = agent_0
        elif match.need_respond(1):
            agent = agent_1
        else:
            raise AssertionError("No need respond.")
        # do tests
        while True:
            cmd = agent.commands[0]
            test_id = get_test_id_from_command(agent)
            if test_id == 0:
                # id 0 means current command is not a test command.
                break
            elif test_id == 1:
                cmd = cmd.split()
                summons = cmd[-8:]
                summons = [summons[:4], summons[4:]]
                for table, name in zip(match.player_tables, summons):
                    assert len(table.summons) == 4
                    for s, n in zip(table.summons, name):
                        assert n in s.name.lower()
            else:
                raise AssertionError(f"Unknown test id {test_id}")
        # respond
        make_respond(agent, match)
        if len(agent_1.commands) == 0 and len(agent_0.commands) == 0:
            break

    # simulate ends, check final state
    assert match.state != MatchState.ERROR


def test_plunge_mark():
    cmd_records = [
        [
            "sw_card",
            "choose 0",
            "TEST 1 p0 true",
            "TEST 1 p1 true",
            "skill 1 0 1 2",
            "TEST 1 p1 true",
            "sw_char 2 0",
            "TEST 1 p1 false",
            "card 0 0 0 1",
            "TEST 1 p0 true",
            "skill 0 0 1 2",
        ],
        [
            "sw_card",
            "choose 0",
            "TEST 1 p0 false",
            "sw_char 1 0",
            "TEST 1 p0 true",
            "skill 1 0 1 2",
            "TEST 1 p0 false",
            "end",
        ],
    ]
    agent_0 = InteractionAgent(
        player_idx=0, verbose_level=0, commands=cmd_records[0], only_use_command=True
    )
    agent_1 = InteractionAgent(
        player_idx=1, verbose_level=0, commands=cmd_records[1], only_use_command=True
    )
    # initialize match. It is recommended to use default random state to make
    # replay unchanged.
    match = Match(random_state=get_random_state())
    # deck information
    deck = Deck.from_str(
        """
        default_version:4.0
        character:Nahida
        character:Fischl
        character:Rhodeia of Loch
        Send Off*30
        """
    )
    match.set_deck([deck, deck])
    match.config.max_same_card_number = None
    match.config.character_number = None
    match.config.card_number = None
    match.config.check_deck_restriction = False
    # check whether random_first_player is enabled.
    match.config.random_first_player = False
    # check whether in rich mode (16 omni each round)
    set_16_omni(match)
    match.start()
    match.step()

    while True:
        if match.need_respond(0):
            agent = agent_0
        elif match.need_respond(1):
            agent = agent_1
        else:
            raise AssertionError("No need respond.")
        # do tests
        while True:
            cmd = agent.commands[0]
            test_id = get_test_id_from_command(agent)
            if test_id == 0:
                # id 0 means current command is not a test command.
                break
            elif test_id == 1:
                # a sample of HP check based on the command string.
                cmd = cmd.split()
                b = bool(["false", "true"].index(cmd[-1].lower()))
                pid = int(cmd[2][1])
                assert match.player_tables[pid].plunge_satisfied == b
            else:
                raise AssertionError(f"Unknown test id {test_id}")
        # respond
        make_respond(agent, match)
        if len(agent_1.commands) == 0 and len(agent_0.commands) == 0:
            break

    # simulate ends, check final state
    assert match.state != MatchState.ERROR


def test_higher_version_compatible():
    deck = Deck.from_str(
        """
        default_version:4.0
        character:Yoimiya@3.3
        character:Yoimiya@3.4
        character:Yoimiya@3.5
        character:Yoimiya@3.6
        character:Yoimiya@3.7
        character:Yoimiya@3.8
        character:Yoimiya@4.0
        character:Yoimiya
        Send Off@3.3
        Send Off@3.4
        Send Off@3.5*1
        Send Off@3.6*1
        Send Off@3.7*1
        Send Off@3.8*1
        Send Off@4.0*1
        Send Off*1
        """
    )
    res = ["3.3", "3.4", "3.4", "3.4", "3.4", "3.8", "3.8", "3.8"]
    res2 = ["3.3", "3.3", "3.3", "3.3", "3.7", "3.7", "3.7", "3.7"]
    for c, r in zip(deck.characters, res):
        assert c.version == r
    for c, r in zip(deck.cards, res2):
        assert c.version == r


def test_save_history():
    agent_0 = RandomAgent(player_idx=0, random_seed=42)
    agent_1 = RandomAgent(player_idx=1, random_seed=19260817)
    match = Match(random_state=get_random_state(100))
    deck = Deck.from_str(
        """
        default_version:4.0
        character:Rhodeia of Loch
        character:Kamisato Ayaka
        Traveler's Handy Sword*5
        Gambler's Earrings*5
        Kanten Senmyou Blessing*5
        Sweet Madame*5
        Abyssal Summons*5
        Fatui Conspiracy*5
        Timmie*5
        """
    )
    for character in deck.characters:
        character.hp = character.max_hp = 99
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    match.config.max_hand_size = 30
    match.config.initial_hand_size = 10
    match.config.card_number = None
    match.config.character_number = None
    match.config.check_deck_restriction = False
    match.config.history_level = 10  # record important history
    match.config.compress_history = False
    test_step = 100
    assert match.start()[0]
    match.step()
    for _ in range(test_step):
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
    assert len(match._history) > test_step  # should record history
    match_dict = match._history[0].copy(deep=True).dict()
    starttime = time.time()
    for patch in match._history_diff[1:]:
        match_dict = dictdiffer.patch(patch, match_dict)
    print("patch speed", (time.time() - starttime) / len(match._history_diff))
    assert match._history[-1].dict() == match_dict

    match_dict = match._history[0].copy(deep=True).dict()
    starttime = time.time()
    for patch in match._history_diff[1:]:
        dictdiffer.patch(patch, match_dict, in_place=True)
    print("patch speed in-place", (time.time() - starttime) / len(match._history_diff))
    assert match._history[-1].dict() == match_dict

    # for prev, next, diff in zip(match._history[:-1], match._history[1:],
    #                             match._history_diff[1:]):
    #     assert next.dict() == dictdiffer.patch(diff, prev.dict())
    #     assert next.dict() == Match(**next.dict()).dict()
    #     assert next == Match(**dictdiffer.patch(diff, prev.dict()))
    assert match.state != MatchState.ERROR


def test_save_history_2():
    agent_0 = RandomAgent(player_idx=0, random_seed=42)
    agent_1 = RandomAgent(player_idx=1, random_seed=19260817)
    match = Match(random_state=get_random_state(100))
    deck = Deck.from_str(
        """
        default_version:4.0
        character:Rhodeia of Loch
        character:Kamisato Ayaka
        Toss-Up*10
        Nature and Wisdom*10
        """
    )
    for character in deck.characters:
        character.hp = character.max_hp = 1
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    match.config.max_hand_size = 30
    match.config.initial_hand_size = 10
    match.config.card_number = None
    match.config.character_number = None
    match.config.check_deck_restriction = False
    match.config.history_level = 10  # record important history
    match.config.compress_history = False
    test_step = 10
    assert match.start()[0]
    match.step()
    for _ in range(test_step):
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
    assert len(match._history) > test_step  # should record history
    assert match.state != MatchState.ERROR


def test_generate_unused_cards():
    agent_0 = RandomAgent(player_idx=0, random_seed=42)
    agent_1 = RandomAgent(player_idx=1, random_seed=19260817)
    match = Match(random_state=get_random_state(100))
    deck = Deck.from_str(
        """
        default_version:4.0
        character:AnemoMob
        character:PyroMob
        character:DendroMob
        character:ElectroMob
        character:HydroMob
        character:CryoMob
        character:GeoMob
        Elemental Resonance: Woven Stone
        Skyward Harp
        """
    )
    for character in deck.characters:
        character.hp = character.max_hp = 1
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    match.config.max_hand_size = 30
    match.config.initial_hand_size = 10
    match.config.card_number = None
    match.config.character_number = None
    match.config.check_deck_restriction = False
    match.config.history_level = 10  # record important history
    test_step = 10
    assert match.start()[0]
    match.step()
    for _ in range(test_step):
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
    assert len(match._history_diff) > test_step  # should record history
    assert match.state != MatchState.ERROR


def test_prediction():
    """
    3335 + E + talent
    """
    agent_0 = NothingAgent(player_idx=0)
    agent_1 = InteractionAgent(
        player_idx=1,
        verbose_level=0,
        commands=[
            "sw_card",
            "choose 0",
            "card 1 0 0 1",
            "skill 1 0 1 2",
            "sw_char 1 1",
            "skill 1 omni omni hydro",
            "end",
            "TEST 1 hp 4 8 8",
            "skill 1 0 1 2",
            "sw_char 2 0",
            "skill 2 omni omni dendro omni omni",
            "TEST 2 hp 0 7 7 and 1 seed for alive",
            "end",
            "TEST 3 hp 0 1 5",
            "skill 1 0 1 2",
            "card 0 0 0 1 2",
        ],
        only_use_command=True,
    )
    match = Match(random_state=get_random_state())
    deck = Deck.from_str(
        """
        character:Fischl
        character:Mona
        character:Nahida
        Rana*10
        Wine-Stained Tricorne*10
        The Seed of Stored Knowledge*10
        """
    )
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    match.config.make_skill_prediction = True
    match.config.history_level = 10
    set_16_omni(match)
    assert match.start()[0]
    match.step()

    while True:
        if match.need_respond(0):
            make_respond(agent_0, match)
        elif match.need_respond(1):
            while True:
                test_id = get_test_id_from_command(agent_1)
                if test_id == 1:
                    check_hp(match, [[4, 8, 8], [10, 10, 10]])
                elif test_id == 2:
                    check_hp(match, [[0, 7, 7], [10, 10, 10]])
                    for character in match.player_tables[0].characters:
                        if character.is_alive:
                            assert len(character.status) == 1
                            assert character.status[0].name == "Seed of Skandha"
                        else:
                            assert len(character.status) == 0
                elif test_id == 3:
                    check_hp(match, [[0, 1, 5], [10, 10, 10]])
                else:
                    break
            prediction_found = None
            prediction_all = None
            if len(agent_1.commands) > 0 and agent_1.commands[0][:5] == "skill":
                # is skill, check prediction
                prediction_all = match.skill_predictions
                for p in prediction_all:
                    assert p["player_idx"] == 1
                    assert (
                        p["character_idx"]
                        == match.player_tables[1].active_character_idx
                    )
                    skill_idx = int(agent_1.commands[0].split()[1])
                    if skill_idx == p["skill_idx"]:
                        prediction_found = p
            if prediction_all is not None:
                assert prediction_found is not None
                prediction_found["current_dict"] = match.dict(
                    exclude={"skill_predictions"}
                )
            make_respond(agent_1, match)
            after_dict = match.dict(exclude={"skill_predictions"})
            if prediction_found is not None:
                patch_dict = dictdiffer.patch(
                    prediction_found["diff"], prediction_found["current_dict"]
                )
                # remove ids, as newly created ids may differ
                patch_match = Match(**patch_dict)
                after_match = Match(**after_dict)
                remove_ids(patch_match)
                remove_ids(after_match)
                for d in (patch_match, after_match):
                    for tb in d.player_tables:
                        # remove dice, as prediction will not use dice
                        tb.dice.colors = []
                        tb.dice.colors = []
                        # as dice number may differ, charge may differ
                        tb.charge_satisfied = False
                    # remove requests, as prediction will not use requests
                    d.requests = []
                    # remove event frames, as it contains ids in some action,
                    # which is hard to remove
                    d.event_frames = []
                assert after_match == patch_match
        else:
            raise AssertionError("No need respond.")
        if len(agent_1.commands) == 0:
            break

    assert len(agent_1.commands) == 0
    assert match.round_number == 3
    check_hp(match, [[0, 0, 1], [10, 10, 10]])
    assert len(match.player_tables[1].team_status) == 1
    assert match.player_tables[1].team_status[0].name == "Shrine of Maya"
    assert match.player_tables[1].team_status[0].usage == 3

    assert match.state != MatchState.ERROR


def test_player_table_character_order():
    deck_str = """
    default_version:4.0
    character:Fischl
    character:Mona
    character:Nahida
    Strategize*30
    """
    deck = Deck.from_str(deck_str)
    match = Match(random_state=get_random_state())
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    assert match.start()[0]
    table = match.player_tables[0].copy(deep=True)
    for active_idx in range(3):
        table.active_character_idx = active_idx
        objs = table.get_object_lists()
        result = []
        for obj in objs:
            if obj.type == ObjectType.CHARACTER:
                result.append(obj.position.character_idx)
        if active_idx == 0:
            assert result == [0, 1, 2]
        elif active_idx == 1:
            assert result == [1, 2, 0]
        elif active_idx == 2:
            assert result == [2, 0, 1]
        else:
            raise AssertionError()


def test_frozen_cannot_use_skill_talent():
    cmd_records = [
        ["sw_card", "choose 2", "skill 1 15 14 13", "sw_char 1 12", "skill 1 11 10 9"],
        ["sw_card", "choose 1", "skill 1 15 14 13", "TEST 1 no card can use", "end"],
    ]
    agent_0 = InteractionAgent(
        player_idx=0, verbose_level=0, commands=cmd_records[0], only_use_command=True
    )
    agent_1 = InteractionAgent(
        player_idx=1, verbose_level=0, commands=cmd_records[1], only_use_command=True
    )
    # initialize match. It is recommended to use default random state to make
    # replay unchanged.
    match = Match(random_state=get_random_state())
    # deck information
    deck = Deck.from_str(
        """
        default_version:4.2
        character:Electro Hypostasis
        character:Kaeya
        character:Mona
        Cold-Blooded Strike*15
        """
    )
    match.set_deck([deck, deck])
    match.config.max_same_card_number = None
    match.config.character_number = None
    match.config.card_number = None
    match.config.check_deck_restriction = False
    # check whether random_first_player is enabled.
    match.config.random_first_player = False
    # check whether in rich mode (16 omni each round)
    set_16_omni(match)
    match.start()
    match.step()

    while True:
        if match.need_respond(0):
            agent = agent_0
        elif match.need_respond(1):
            agent = agent_1
        else:
            raise AssertionError("No need respond.")
        while True:
            test_id = get_test_id_from_command(agent)
            if test_id == 0:
                # id 0 means current command is not a test command.
                break
            elif test_id == 1:
                for req in match.requests:
                    assert req.name != "UseCardRequest"
            else:
                raise AssertionError(f"Unknown test id {test_id}")
        # respond
        make_respond(agent, match)
        if len(agent_1.commands) == 0 and len(agent_0.commands) == 0:
            break

    # simulate ends, check final state
    assert match.state != MatchState.ERROR


def test_new_match_from_history():
    agent_0 = RandomAgent(player_idx=0, random_seed=42)
    agent_1 = RandomAgent(player_idx=1, random_seed=19260817)
    match = Match(random_state=get_random_state(100))
    deck = Deck.from_str(
        """
        default_version:4.0
        character:Rhodeia of Loch
        character:Kamisato Ayaka
        Traveler's Handy Sword*5
        Gambler's Earrings*5
        Kanten Senmyou Blessing*5
        Sweet Madame*5
        Abyssal Summons*5
        Fatui Conspiracy*5
        Timmie*5
        """
    )
    for character in deck.characters:
        character.hp = character.max_hp = 99
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    match.config.max_hand_size = 30
    match.config.initial_hand_size = 10
    match.config.card_number = None
    match.config.character_number = None
    match.config.check_deck_restriction = False
    match.config.history_level = 10  # record important history
    match.config.compress_history = False
    assert match.start()[0]
    match.step()
    for _ in range(10):
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
    idx_10 = len(match._history) - 1
    for _ in range(10):
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
    assert len(match._history) > 20  # should record history
    initial_match = match.new_match_from_history(0)
    match_10 = match.new_match_from_history(idx_10)
    agent_0 = RandomAgent(player_idx=0, random_seed=42)
    agent_1 = RandomAgent(player_idx=1, random_seed=19260817)
    initial_match.step()
    for _ in range(10):
        make_respond(agent_0, initial_match, assertion=False)
        make_respond(agent_1, initial_match, assertion=False)
    assert len(initial_match._history) == len(match_10._history)
    assert len(initial_match._history_diff) == len(match_10._history_diff)
    assert remove_ids(initial_match.copy(deep=True)) == remove_ids(match_10)
    for _ in range(10):
        make_respond(agent_0, initial_match, assertion=False)
        make_respond(agent_1, initial_match, assertion=False)
    assert len(initial_match._history) == len(match._history)
    assert len(initial_match._history_diff) == len(match._history_diff)
    assert remove_ids(initial_match) == remove_ids(match)


def test_new_match_from_history_compressed():
    agent_0 = RandomAgent(player_idx=0, random_seed=42)
    agent_1 = RandomAgent(player_idx=1, random_seed=19260817)
    match = Match(random_state=get_random_state(100))
    deck = Deck.from_str(
        """
        default_version:4.0
        character:Rhodeia of Loch
        character:Kamisato Ayaka
        Traveler's Handy Sword*5
        Gambler's Earrings*5
        Kanten Senmyou Blessing*5
        Sweet Madame*5
        Abyssal Summons*5
        Fatui Conspiracy*5
        Timmie*5
        """
    )
    for character in deck.characters:
        character.hp = character.max_hp = 99
    match.set_deck([deck, deck])
    match.config.max_same_card_number = 30
    match.config.max_hand_size = 30
    match.config.initial_hand_size = 10
    match.config.card_number = None
    match.config.character_number = None
    match.config.check_deck_restriction = False
    match.config.history_level = 10  # record important history
    assert match.start()[0]
    match.step()
    for _ in range(10):
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
    idx_10 = len(match._history_diff) - 1
    for _ in range(10):
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
    assert len(match._history_diff) > 20  # should record history
    initial_match = match.new_match_from_history(0)
    match_10 = match.new_match_from_history(idx_10)
    agent_0 = RandomAgent(player_idx=0, random_seed=42)
    agent_1 = RandomAgent(player_idx=1, random_seed=19260817)
    initial_match.step()
    for _ in range(10):
        make_respond(agent_0, initial_match, assertion=False)
        make_respond(agent_1, initial_match, assertion=False)
    assert len(initial_match._history) == len(match_10._history)
    assert len(initial_match._history_diff) == len(match_10._history_diff)
    assert remove_ids(initial_match.copy(deep=True)) == remove_ids(match_10)
    for _ in range(10):
        make_respond(agent_0, initial_match, assertion=False)
        make_respond(agent_1, initial_match, assertion=False)
    assert remove_ids(initial_match) == remove_ids(match)
    assert len(initial_match._history) == len(match._history)
    assert len(initial_match._history_diff) == len(match._history_diff)


def test_version_validation():
    with pytest.raises(ValueError):
        _ = get_instance(SummonBase, {"name": "Oz", "version": 3.7})
    with pytest.raises(AssertionError):
        _ = get_instance(SummonBase, {"name": "Oz", "version": "2.5"})
    with pytest.raises(ValueError):
        _ = get_instance(
            SummonBase,
            {"name": "Oz", "strict_version_validation": True, "version": "3.5"},
        )
    default_pos = {"player_idx": 0, "id": -1, "area": "SUMMON"}
    _ = get_instance(
        SummonBase,
        {
            "name": "Oz",
            "version": "3.5",
            "position": default_pos,
        },
    )
    with pytest.raises(ValueError):
        _ = Oz_3_3(
            **{
                "name": "Oz",
                "version": 3.7,
            }
        )
    with pytest.raises(ValueError):
        _ = Oz_3_3(
            **{
                "name": "Oz",
                "version": "2.5",
                "position": default_pos,
            }
        )
    with pytest.raises(ValueError):
        _ = Oz_3_3(
            **{
                "name": "Oz",
                "strict_version_validation": True,
                "version": "3.5",
                "position": default_pos,
            }
        )
    _ = Oz_3_3(
        **{
            "name": "Oz",
            "version": "3.5",
            "position": default_pos,
        }
    )

    # multiple version hints
    class OOZZ(Oz_3_3):
        version: Literal["3.4", "3.5"]

    with pytest.raises(ValueError):
        _ = OOZZ(
            **{
                "name": "Oz",
                "version": "3.5",
                "position": default_pos,
            }
        )


def test_round_end_all_lose():
    agent_0 = NothingAgent(player_idx=0)
    agent_1 = NothingAgent(player_idx=1)
    match = Match()
    deck = Deck.from_str(
        """
        default_version:4.0
        character:Rhodeia of Loch
        character:Kamisato Ayaka
        Traveler's Handy Sword*5
        Gambler's Earrings*5
        Kanten Senmyou Blessing*5
        Sweet Madame*5
        Abyssal Summons*5
        Fatui Conspiracy*5
        Timmie*5
        """
    )
    round_number = 5
    match.set_deck([deck, deck])
    match.config.max_round_number = round_number
    match.config.max_same_card_number = 30
    match.config.max_hand_size = 30
    match.config.initial_hand_size = 10
    match.config.card_number = None
    match.config.character_number = None
    match.config.check_deck_restriction = False
    match.config.random_first_player = False
    match.config.history_level = 0
    set_16_omni(match)
    assert match.start()[0]
    match.step()
    for i in range(round_number + 1):  # +1 to perform sw_card and choose
        assert match.state not in [MatchState.ENDED, MatchState.ERROR]
        make_respond(agent_0, match, assertion=False)
        make_respond(agent_1, match, assertion=False)
        target_round_number = max(i, 0)
        assert match.round_number == target_round_number
    assert match.state == MatchState.ENDED


def test_use_card_event_serialize():
    card_event = UseCardEventArguments(
        action=UseCardAction(
            card_position=ObjectPosition(
                player_idx=0, area=ObjectPositionType.HAND, id=0
            ),
            target=None,
        ),
        card_cost=Cost(),
        card_name="Test",
        use_card_success=True,
    )
    assert card_event.card_cost.original_value is None
    card_event = UseCardEventArguments(**json.loads(card_event.json()))
    assert card_event.card_cost.original_value is None
    assert card_event.action.type == ActionTypes.USE_CARD
    assert card_event.action.card_position.area == ObjectPositionType.HAND


if __name__ == "__main__":
    # test_match_pipeline()
    # test_save_load()
    # test_random_same_after_load()
    # test_use_card()
    # test_copy_speed()
    # test_save_load_same()
    # test_support_over_maximum_and_error_tests()
    # test_summon_over_maximum()
    # test_plunge_mark()
    # test_higher_version_compatible()
    # test_save_history()
    # test_save_history_2()
    # test_generate_unused_cards()
    # test_prediction()
    # test_player_table_character_order()
    # test_frozen_cannot_use_skill_talent()
    # test_new_match_from_history()
    # test_new_match_from_history_compressed()
    # test_version_validation()
    # test_round_end_all_lose()
    test_use_card_event_serialize()
