"""Tests for save/load game state serialization."""

from __future__ import annotations

import json
import shutil
from pathlib import Path

import pytest

from h2g2.engine.game_object import Flag
from h2g2.engine.save_load import (
    SAVE_DIR,
    SAVE_EXTENSION,
    _serialize_state,
    _restore_state,
    save_game,
    load_game,
    list_saves,
)


@pytest.fixture(autouse=True)
def _clean_save_dir():
    """Ensure a clean save directory for every test."""
    if SAVE_DIR.exists():
        shutil.rmtree(SAVE_DIR)
    SAVE_DIR.mkdir(parents=True, exist_ok=True)
    yield
    if SAVE_DIR.exists():
        shutil.rmtree(SAVE_DIR)


# ---------------------------------------------------------------------------
# list_saves
# ---------------------------------------------------------------------------

class TestListSaves:
    def test_empty_directory(self):
        assert list_saves() == []

    def test_returns_save_names(self):
        (SAVE_DIR / "alpha.json").write_text("{}")
        (SAVE_DIR / "beta.json").write_text("{}")
        names = list_saves()
        assert "alpha" in names
        assert "beta" in names

    def test_ignores_non_json_files(self):
        (SAVE_DIR / "notes.txt").write_text("not a save")
        (SAVE_DIR / "real.json").write_text("{}")
        assert list_saves() == ["real"]


# ---------------------------------------------------------------------------
# Round-trip: serialize -> restore
# ---------------------------------------------------------------------------

class TestSerializeRestore:
    def test_scalar_state_round_trips(self, game):
        state, _, _ = game
        state.score = 42
        state.moves = 17
        state.verbosity = 2
        state.lying_down = False
        state.dreaming = True
        state.flags["headache"] = False
        state.flags["beer_counter"] = 3

        data = _serialize_state(state)
        # Mutate state to prove restore actually changes it
        state.score = 0
        state.moves = 0
        state.verbosity = 0
        state.lying_down = True
        state.dreaming = False
        state.flags["headache"] = True
        state.flags["beer_counter"] = 0

        _restore_state(state, data)

        assert state.score == 42
        assert state.moves == 17
        assert state.verbosity == 2
        assert state.lying_down is False
        assert state.dreaming is True
        assert state.flags["headache"] is False
        assert state.flags["beer_counter"] == 3

    def test_room_round_trips(self, game):
        state, _, _ = game
        bedroom = state.world.get_room("BEDROOM")
        state.here = bedroom

        data = _serialize_state(state)
        state.here = None
        _restore_state(state, data)

        assert state.here is bedroom

    def test_protagonist_round_trips(self, game):
        state, _, _ = game
        proto = state.protagonist

        data = _serialize_state(state)
        state.protagonist = None
        _restore_state(state, data)

        assert state.protagonist is proto

    def test_object_flags_round_trip(self, game):
        state, _, _ = game
        obj = state.world.get("TOWEL")
        obj.fset(Flag.WORNBIT)
        assert obj.fset_q(Flag.WORNBIT)

        data = _serialize_state(state)
        obj.fclear(Flag.WORNBIT)
        assert not obj.fset_q(Flag.WORNBIT)

        _restore_state(state, data)
        assert obj.fset_q(Flag.WORNBIT)

    def test_containment_round_trip(self, game):
        state, _, _ = game
        towel = state.world.get("TOWEL")
        proto = state.protagonist

        # Move towel into inventory
        towel.move_to(proto)
        assert towel.parent is proto
        assert towel in proto.children

        data = _serialize_state(state)

        # Move towel somewhere else to prove restore works
        bedroom = state.world.get_room("BEDROOM")
        towel.move_to(bedroom)
        assert towel.parent is bedroom

        _restore_state(state, data)
        assert towel.parent is proto
        assert towel in proto.children

    def test_last_action_round_trips(self, game):
        state, _, _ = game
        towel = state.world.get("TOWEL")
        state.l_prsa = "take"
        state.l_prso = towel
        state.l_prsi = None

        data = _serialize_state(state)
        state.l_prsa = None
        state.l_prso = None
        _restore_state(state, data)

        assert state.l_prsa == "take"
        assert state.l_prso is towel

    def test_clock_state_round_trips(self, game):
        state, _, _ = game

        # Find a named clock entry and modify it
        named = [e for e in state.clock.entries if e.name]
        if not named:
            pytest.skip("No named clock entries in test fixture")
        entry = named[0]
        original_tick = entry.tick
        entry.tick = 99
        entry.enabled = False

        data = _serialize_state(state)
        entry.tick = 0
        entry.enabled = True
        _restore_state(state, data)

        assert entry.tick == 99
        assert entry.enabled is False

        # Restore original for other tests
        entry.tick = original_tick
        entry.enabled = True

    def test_identity_flag_round_trips(self, game):
        state, _, _ = game
        arthur = state.world.objects.get("ARTHUR")
        if arthur is None:
            pytest.skip("ARTHUR object not in test fixture")
        state.identity_flag = arthur

        data = _serialize_state(state)
        state.identity_flag = None
        _restore_state(state, data)

        assert state.identity_flag is arthur


# ---------------------------------------------------------------------------
# File I/O: save_game / load_game
# ---------------------------------------------------------------------------

class TestFileIO:
    def test_save_creates_file(self, game):
        state, _, _ = game
        path = save_game(state, "test1")
        assert Path(path).exists()
        assert Path(path).suffix == SAVE_EXTENSION

    def test_save_file_is_valid_json(self, game):
        state, _, _ = game
        save_game(state, "test1")
        data = json.loads((SAVE_DIR / "test1.json").read_text())
        assert data["version"] == 1
        assert "objects" in data
        assert "flags" in data

    def test_load_restores_state(self, game):
        state, _, _ = game
        state.score = 100
        state.moves = 50
        save_game(state, "mysave")

        state.score = 0
        state.moves = 0
        load_game(state, "mysave")

        assert state.score == 100
        assert state.moves == 50

    def test_load_nonexistent_raises(self, game):
        state, _, _ = game
        with pytest.raises(FileNotFoundError):
            load_game(state, "doesnotexist")

    def test_overwrite_existing_save(self, game):
        state, _, _ = game
        state.score = 10
        save_game(state, "slot")

        state.score = 99
        save_game(state, "slot")

        state.score = 0
        load_game(state, "slot")
        assert state.score == 99

    def test_multiple_independent_saves(self, game):
        state, _, _ = game
        state.score = 10
        save_game(state, "early")

        state.score = 200
        save_game(state, "late")

        load_game(state, "early")
        assert state.score == 10

        load_game(state, "late")
        assert state.score == 200

    def test_full_round_trip_with_mutations(self, game):
        state, _, _ = game
        proto = state.protagonist
        towel = state.world.get("TOWEL")

        # Mutate the game
        state.score = 42
        state.moves = 15
        state.lying_down = False
        state.flags["earth_demolished"] = True
        towel.move_to(proto)
        towel.fset(Flag.TOUCHBIT)

        save_game(state, "full")

        # Trash everything
        state.score = 0
        state.moves = 0
        state.lying_down = True
        state.flags["earth_demolished"] = False
        bedroom = state.world.get_room("BEDROOM")
        towel.move_to(bedroom)
        towel.fclear(Flag.TOUCHBIT)

        load_game(state, "full")

        assert state.score == 42
        assert state.moves == 15
        assert state.lying_down is False
        assert state.flags["earth_demolished"] is True
        assert towel.parent is proto
        assert towel.fset_q(Flag.TOUCHBIT)
