"""
Rust Borsh Compatibility Test

This test uses the same binary fixture generated by the zorsh Rust test suite
to ensure 1:1 compatibility with Rust's borsh crate.

The binary fixture was generated from this Rust code:
https://github.com/r-near/zorsh/blob/main/rust/borsh_test/src/lib.rs
"""

from pathlib import Path
from typing import Annotated, Literal

import pytest
from pydantic import BaseModel

from pyborsh import F32, I32, U8, U32, U64, Borsh, BorshEnum

# --- Schema Definitions (matching Rust structs) ---


class StatBuff(Borsh, BaseModel):
    """Rust: struct StatBuff { stat: String, amount: i32 }"""

    stat: str
    amount: Annotated[int, I32]


class Effect(BorshEnum):
    """
    Rust: enum Effect {
        Damage(u32),
        Heal(u32),
        Buff(StatBuff),
        Debuff(StatBuff),
    }
    """

    class Damage(Borsh, BaseModel):
        variant: Literal["Damage"] = "Damage"
        value: Annotated[int, U32]

    class Heal(Borsh, BaseModel):
        variant: Literal["Heal"] = "Heal"
        value: Annotated[int, U32]

    class Buff(Borsh, BaseModel):
        variant: Literal["Buff"] = "Buff"
        stat: str
        amount: Annotated[int, I32]

    class Debuff(Borsh, BaseModel):
        variant: Literal["Debuff"] = "Debuff"
        stat: str
        amount: Annotated[int, I32]


class Item(Borsh, BaseModel):
    """Rust: struct Item { id: String, name: String, weight: f32, effects: Vec<Effect> }"""

    id: str
    name: str
    weight: Annotated[float, F32]
    effects: list[Effect.Damage | Effect.Heal | Effect.Buff | Effect.Debuff]


class Stats(Borsh, BaseModel):
    """Rust: struct Stats { health, mana, attack, defense, magic_attack, magic_defense: u32 }"""

    health: Annotated[int, U32]
    mana: Annotated[int, U32]
    attack: Annotated[int, U32]
    defense: Annotated[int, U32]
    magic_attack: Annotated[int, U32]
    magic_defense: Annotated[int, U32]


class Player(Borsh, BaseModel):
    """
    Rust: struct Player {
        name: String,
        level: u8,
        stats: Stats,
        inventory: Vec<Item>,
        equipped_items: HashMap<String, String>,
        quest_log: HashSet<String>,
        last_login: Option<u64>,
    }
    """

    name: str
    level: Annotated[int, U8]
    stats: Stats
    inventory: list[Item]
    equipped_items: dict[str, str]
    quest_log: set[str]
    last_login: Annotated[int, U64] | None


class Reward(Borsh, BaseModel):
    """Rust: struct Reward { item_id: String, quantity: u32 }"""

    item_id: str
    quantity: Annotated[int, U32]


class GameEvent(BorshEnum):
    """
    Rust: enum GameEvent {
        PlayerJoined { player_id: String },
        PlayerLeft { player_id: String },
        ChatMessage { sender: String, message: String },
        ItemUsed { item_id: String, target: Option<String> },
        BattleResult { winner: String, loser: String, rewards: Vec<Reward> },
    }
    """

    class PlayerJoined(Borsh, BaseModel):
        variant: Literal["PlayerJoined"] = "PlayerJoined"
        player_id: str

    class PlayerLeft(Borsh, BaseModel):
        variant: Literal["PlayerLeft"] = "PlayerLeft"
        player_id: str

    class ChatMessage(Borsh, BaseModel):
        variant: Literal["ChatMessage"] = "ChatMessage"
        sender: str
        message: str

    class ItemUsed(Borsh, BaseModel):
        variant: Literal["ItemUsed"] = "ItemUsed"
        item_id: str
        target: str | None

    class BattleResult(Borsh, BaseModel):
        variant: Literal["BattleResult"] = "BattleResult"
        winner: str
        loser: str
        rewards: list[Reward]


class GameState(Borsh, BaseModel):
    """
    Rust: struct GameState {
        players: HashMap<String, Player>,
        current_round: u32,
        events: Vec<GameEvent>,
        game_version: String,
    }
    """

    players: dict[str, Player]
    current_round: Annotated[int, U32]
    events: list[
        GameEvent.PlayerJoined
        | GameEvent.PlayerLeft
        | GameEvent.ChatMessage
        | GameEvent.ItemUsed
        | GameEvent.BattleResult
    ]
    game_version: str


# --- Expected Data (matching Rust test) ---


def create_expected_game_state() -> GameState:
    """Create the expected GameState matching the Rust test fixture."""
    player1 = Player(
        name="Alice",
        level=10,
        stats=Stats(
            health=100,
            mana=50,
            attack=20,
            defense=15,
            magic_attack=5,
            magic_defense=10,
        ),
        inventory=[
            Item(
                id="sword_001",
                name="Iron Sword",
                weight=2.5,
                effects=[Effect.Damage(value=15)],
            ),
            Item(
                id="potion_001",
                name="Health Potion",
                weight=0.125,
                effects=[Effect.Heal(value=50)],
            ),
        ],
        equipped_items={"hand": "sword_001"},
        quest_log={"quest_start", "collect_herbs"},
        last_login=1678886400,
    )

    player2 = Player(
        name="Bob",
        level=5,
        stats=Stats(
            health=80,
            mana=75,
            attack=10,
            defense=5,
            magic_attack=30,
            magic_defense=20,
        ),
        inventory=[
            Item(
                id="staff_001",
                name="Apprentice Staff",
                weight=1.25,
                effects=[
                    Effect.Damage(value=5),
                    Effect.Buff(stat="magic_attack", amount=10),
                ],
            ),
        ],
        equipped_items={},
        quest_log=set(),
        last_login=None,
    )

    events: list[
        GameEvent.PlayerJoined
        | GameEvent.PlayerLeft
        | GameEvent.ChatMessage
        | GameEvent.ItemUsed
        | GameEvent.BattleResult
    ] = [
        GameEvent.PlayerJoined(player_id="alice_id"),
        GameEvent.ChatMessage(sender="alice_id", message="Hello, world!"),
        GameEvent.BattleResult(
            winner="alice_id",
            loser="bob_id",
            rewards=[Reward(item_id="gold_001", quantity=100)],
        ),
    ]

    return GameState(
        players={"alice_id": player1, "bob_id": player2},
        current_round=3,
        events=events,
        game_version="1.2.3",
    )


# --- Tests ---


class TestRustCompatibility:
    """Test 1:1 compatibility with Rust borsh crate."""

    @pytest.fixture
    def rust_bytes(self) -> bytes:
        """Load the binary fixture generated by Rust."""
        fixture_path = Path(__file__).parent / "fixtures" / "complex_game_state.bin"
        return fixture_path.read_bytes()

    @pytest.fixture
    def expected_game_state(self) -> GameState:
        """Get the expected game state."""
        return create_expected_game_state()

    def test_deserialize_rust_bytes(self, rust_bytes: bytes, expected_game_state: GameState):
        """Test that we can deserialize Rust-generated bytes correctly."""
        decoded = GameState.from_borsh(rust_bytes)

        # Check top-level fields
        assert decoded.current_round == expected_game_state.current_round
        assert decoded.game_version == expected_game_state.game_version

        # Check players
        assert set(decoded.players.keys()) == set(expected_game_state.players.keys())

        # Check Alice
        alice = decoded.players["alice_id"]
        expected_alice = expected_game_state.players["alice_id"]
        assert alice.name == expected_alice.name
        assert alice.level == expected_alice.level
        assert alice.stats == expected_alice.stats
        assert alice.last_login == expected_alice.last_login
        assert alice.equipped_items == expected_alice.equipped_items
        assert alice.quest_log == expected_alice.quest_log
        assert len(alice.inventory) == len(expected_alice.inventory)

        # Check Bob
        bob = decoded.players["bob_id"]
        expected_bob = expected_game_state.players["bob_id"]
        assert bob.name == expected_bob.name
        assert bob.level == expected_bob.level
        assert bob.last_login is None

        # Check events
        assert len(decoded.events) == len(expected_game_state.events)
        assert isinstance(decoded.events[0], GameEvent.PlayerJoined)
        assert decoded.events[0].player_id == "alice_id"
        assert isinstance(decoded.events[1], GameEvent.ChatMessage)
        assert decoded.events[1].message == "Hello, world!"
        assert isinstance(decoded.events[2], GameEvent.BattleResult)
        assert decoded.events[2].winner == "alice_id"

    def test_serialize_matches_rust(self, rust_bytes: bytes, expected_game_state: GameState):
        """Test that our serialization produces identical bytes to Rust."""
        # Note: HashMap/HashSet ordering may differ, so we compare via round-trip
        serialized = expected_game_state.to_borsh()

        # Deserialize both and compare
        from_rust = GameState.from_borsh(rust_bytes)
        from_python = GameState.from_borsh(serialized)

        # Compare the deserialized data (order-independent)
        assert from_python.current_round == from_rust.current_round
        assert from_python.game_version == from_rust.game_version
        assert set(from_python.players.keys()) == set(from_rust.players.keys())

    def test_roundtrip(self, expected_game_state: GameState):
        """Test Python serialize -> deserialize roundtrip."""
        serialized = expected_game_state.to_borsh()
        deserialized = GameState.from_borsh(serialized)

        assert deserialized.current_round == expected_game_state.current_round
        assert deserialized.game_version == expected_game_state.game_version
        assert set(deserialized.players.keys()) == set(expected_game_state.players.keys())


class TestIndividualSchemas:
    """Test individual schema components."""

    def test_stats_roundtrip(self):
        """Test Stats struct."""
        stats = Stats(health=100, mana=50, attack=20, defense=15, magic_attack=5, magic_defense=10)
        data = stats.to_borsh()
        assert len(data) == 6 * 4  # 6 u32 fields
        assert Stats.from_borsh(data) == stats

    def test_item_with_effects(self):
        """Test Item with nested Effect enum."""
        item = Item(
            id="sword",
            name="Iron Sword",
            weight=2.5,
            effects=[
                Effect.Damage(value=15),
                Effect.Buff(stat="attack", amount=5),
            ],
        )
        data = item.to_borsh()
        restored = Item.from_borsh(data)
        assert restored.id == item.id
        assert restored.name == item.name
        assert abs(restored.weight - item.weight) < 0.001
        assert len(restored.effects) == 2

    def test_player_with_optional_none(self):
        """Test Player with None optional field."""
        player = Player(
            name="Test",
            level=1,
            stats=Stats(health=1, mana=1, attack=1, defense=1, magic_attack=1, magic_defense=1),
            inventory=[],
            equipped_items={},
            quest_log=set(),
            last_login=None,
        )
        data = player.to_borsh()
        restored = Player.from_borsh(data)
        assert restored.last_login is None

    def test_player_with_optional_some(self):
        """Test Player with Some optional field."""
        player = Player(
            name="Test",
            level=1,
            stats=Stats(health=1, mana=1, attack=1, defense=1, magic_attack=1, magic_defense=1),
            inventory=[],
            equipped_items={},
            quest_log=set(),
            last_login=12345,
        )
        data = player.to_borsh()
        restored = Player.from_borsh(data)
        assert restored.last_login == 12345

    def test_game_event_variants(self):
        """Test all GameEvent variants."""
        events = [
            GameEvent.PlayerJoined(player_id="p1"),
            GameEvent.PlayerLeft(player_id="p2"),
            GameEvent.ChatMessage(sender="p1", message="Hello"),
            GameEvent.ItemUsed(item_id="item1", target=None),
            GameEvent.ItemUsed(item_id="item2", target="p2"),
            GameEvent.BattleResult(
                winner="p1", loser="p2", rewards=[Reward(item_id="gold", quantity=100)]
            ),
        ]

        for event in events:
            data = event.to_borsh()
            # Each event can deserialize standalone
            restored = type(event).from_borsh(data)
            assert type(restored) == type(event)
