package thb.jeanluc.adventure.save;

import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.game.GameSession;
import thb.jeanluc.adventure.model.Direction;
import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.Room;
import thb.jeanluc.adventure.model.World;
import thb.jeanluc.adventure.model.item.Item;
import thb.jeanluc.adventure.model.item.PlainItem;
import thb.jeanluc.adventure.model.item.SwitchableItem;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

class SaveCodecTest {

    /** Builds a fresh 2-room world: shovel in cellar, lamp (off) in kitchen. */
    private GameSession freshWorld(String slot) {
        Room kitchen = new Room("kitchen", "Kitchen", "k");
        Room cellar = new Room("cellar", "Cellar", "c");
        kitchen.addExit(Direction.SOUTH, cellar);
        Item shovel = PlainItem.builder().id("shovel").name("Shovel").description("d").light(false).build();
        SwitchableItem lamp = SwitchableItem.builder()
                .id("lamp").name("Lamp").description("d").light(true)
                .onText("on").offText("off").state(false).effects(List.of()).build();
        cellar.addItem(shovel);
        kitchen.addItem(lamp);
        Map<String, Item> items = new LinkedHashMap<>();
        items.put("shovel", shovel);
        items.put("lamp", lamp);
        Map<String, Room> rooms = new LinkedHashMap<>();
        rooms.put("kitchen", kitchen);
        rooms.put("cellar", cellar);
        World w = new World(rooms, items, Map.of(), "Haunted Manor", "welcome");
        return new GameSession(w, new Player(kitchen, 0), slot);
    }

    @Test
    void captureThenApplyToFreshWorldReproducesState() {
        GameSession src = freshWorld("slot-a");
        // mutate: move to cellar, pick up shovel, light the lamp, set a flag, gold
        src.getPlayer().setCurrentRoom(src.getWorld().getRooms().get("cellar"));
        Item shovel = src.getWorld().getRooms().get("cellar").removeItem("shovel").orElseThrow();
        src.getPlayer().addItem(shovel);
        ((SwitchableItem) src.getWorld().getItems().get("lamp")).setState(true);
        src.getState().set("power_on");
        src.getPlayer().setGold(5);
        src.setTurn(12);

        SaveData data = SaveCodec.capture(src);

        GameSession dst = freshWorld("slot-a");
        SaveCodec.apply(data, dst);

        assertThat(dst.getPlayer().getCurrentRoom().getId()).isEqualTo("cellar");
        assertThat(dst.getPlayer().hasItem("shovel")).isTrue();
        assertThat(dst.getWorld().getRooms().get("cellar").findItem("shovel")).isEmpty();
        assertThat(((SwitchableItem) dst.getWorld().getItems().get("lamp")).isOn()).isTrue();
        assertThat(dst.getState().isSet("power_on")).isTrue();
        assertThat(dst.getPlayer().getGold()).isEqualTo(5);
        assertThat(dst.getTurn()).isEqualTo(12);
        assertThat(dst.getPlayer().getVisitedRoomIds()).contains("kitchen", "cellar");
    }

    @Test
    void applyRejectsUnknownRoom() {
        GameSession src = freshWorld("slot-a");
        SaveData data = SaveCodec.capture(src);
        SaveData broken = new SaveData(
                data.schemaVersion(), data.worldTitle(), data.slotName(),
                data.savedAtEpochMillis(), data.turn(), "ballroom",
                data.visitedRoomIds(), data.gold(), data.inventoryItemIds(),
                data.flags(), data.questStages(), data.questCompleted(),
                data.roomItemIds(), data.switchStates());
        org.junit.jupiter.api.Assertions.assertThrows(SaveException.class,
                () -> SaveCodec.apply(broken, freshWorld("slot-a")));
    }

    @Test
    void applyToleratesMissingCollections() {
        // Construct a SaveData with null collections (simulates a hand-edited / truncated save).
        // The compact constructor should normalize nulls to empty collections.
        SaveData data = new SaveData(
                SaveData.CURRENT_VERSION, "Haunted Manor", "slot-a",
                System.currentTimeMillis(), 0,
                "kitchen",
                null, // visitedRoomIds
                0,
                null, // inventoryItemIds
                null, // flags
                null, // questStages
                null, // questCompleted
                null, // roomItemIds
                null  // switchStates
        );

        GameSession dst = freshWorld("slot-a");
        // Should not throw NPE; world items remain in their default positions or cleared
        org.junit.jupiter.api.Assertions.assertDoesNotThrow(() -> SaveCodec.apply(data, dst));
        // Post-condition: applying a save with null/empty roomItemIds clears all room item tables.
        assertThat(dst.getWorld().getRooms().get("kitchen").findItem("lamp")).isEmpty();
    }
}
