package thb.jeanluc.adventure.loader;

import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.game.Combinations;
import thb.jeanluc.adventure.game.Conditions;
import thb.jeanluc.adventure.game.EndingEngine;
import thb.jeanluc.adventure.game.GameContext;
import thb.jeanluc.adventure.game.GameSession;
import thb.jeanluc.adventure.game.Light;
import thb.jeanluc.adventure.game.QuestEngine;
import thb.jeanluc.adventure.io.TestIO;
import thb.jeanluc.adventure.model.Direction;
import thb.jeanluc.adventure.model.Ending;
import thb.jeanluc.adventure.model.ExitLock;
import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.Room;
import thb.jeanluc.adventure.model.World;
import thb.jeanluc.adventure.model.item.SwitchableItem;

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

/**
 * Automated solvability guarantee for the shipped Haunted Manor: walks the full
 * critical path purely through the domain ({@link Combinations#tryUse}, flag
 * checks, light/lock gating) — no IO loop, no command parsing. If a broken lock
 * or flag makes the world unwinnable, this build fails instead of the player
 * discovering it in-game.
 */
class WorldSolvableTest {

    private GameContext freshGame() {
        WorldLoader.LoadResult loaded = new WorldLoader().load();
        GameContext ctx = new GameContext(
                new GameSession(loaded.world(), loaded.player(), "test"), new TestIO());
        ctx.setSaveCallback(() -> { }); // quest completion autosaves; no-op it here
        return ctx;
    }

    @Test
    void criticalPath_isWinnable_andYieldsSpiritAtRest() {
        GameContext ctx = freshGame();
        World w = ctx.getWorld();
        Player p = ctx.getPlayer();

        // The cellar is dark: with no carried light, you cannot see in it.
        Room cellarStairs = w.getRooms().get("cellar_stairs");
        assertThat(Light.isLit(ctx, cellarStairs)).isFalse();

        // Light the lamp (foyer) — now you carry a light source.
        p.addItem(w.getItems().get("lamp"));
        ((SwitchableItem) p.findItem("lamp").orElseThrow()).use(ctx);
        assertThat(Light.isLit(ctx, cellarStairs)).isTrue();

        // The upward stair is power-locked until the generator runs.
        Room staircase = w.getRooms().get("grand_staircase");
        ExitLock stairLock = staircase.getExitLocks().get(Direction.NORTH);
        assertThat(stairLock).isNotNull();
        assertThat(Conditions.all(stairLock.requires(), ctx)).isFalse();

        // Drain the cellar: valve_wheel (carried) + valve (fixture in the room).
        p.addItem(w.getItems().get("valve_wheel"));
        p.setCurrentRoom(w.getRooms().get("flooded_cellar"));
        Combinations.tryUse(ctx, "valve_wheel", "valve");
        assertThat(ctx.getState().isSet("cellar_drained")).isTrue();
        assertThat(p.hasItem("fuse")).isTrue();

        // Restore power: fuse (carried) + generator (fixture in the room).
        p.setCurrentRoom(w.getRooms().get("boiler_room"));
        Combinations.tryUse(ctx, "fuse", "generator");
        assertThat(ctx.getState().isSet("power_on")).isTrue();
        assertThat(p.hasItem("fuse")).isFalse();

        // With power on, the stair lock opens.
        assertThat(Conditions.all(stairLock.requires(), ctx)).isTrue();

        // One quest tick: restore_power completes and starts lay_to_rest.
        QuestEngine.tick(ctx);
        assertThat(ctx.getQuestLog().isCompleted("restore_power")).isTrue();
        assertThat(ctx.getQuestLog().isActive("lay_to_rest")
                || ctx.getQuestLog().isCompleted("lay_to_rest")).isTrue();

        // Chapel: photograph BEFORE locket must fail (requires locket_placed).
        p.addItem(w.getItems().get("locket"));
        p.addItem(w.getItems().get("photograph"));
        p.setCurrentRoom(w.getRooms().get("chapel"));
        Combinations.tryUse(ctx, "photograph", "shrine");
        assertThat(ctx.getState().isSet("ghost_banished")).isFalse();
        assertThat(p.hasItem("photograph")).isTrue();

        // Correct order: locket, then photograph -> spirit banished.
        Combinations.tryUse(ctx, "locket", "shrine");
        assertThat(ctx.getState().isSet("locket_placed")).isTrue();
        Combinations.tryUse(ctx, "photograph", "shrine");
        assertThat(ctx.getState().isSet("ghost_banished")).isTrue();

        QuestEngine.tick(ctx);
        assertThat(ctx.getQuestLog().isCompleted("lay_to_rest")).isTrue();

        // Leave via the front door.
        p.setCurrentRoom(w.getRooms().get("foyer"));
        ((SwitchableItem) w.getItems().get("front_door")).use(ctx);
        assertThat(ctx.getState().isSet("left_manor")).isTrue();

        // Highest-priority ending wins: spirit_at_rest (a victory).
        Ending end = EndingEngine.triggered(ctx);
        assertThat(end).isNotNull();
        assertThat(end.id()).isEqualTo("spirit_at_rest");
        assertThat(end.victory()).isTrue();
    }

    @Test
    void endingPriority_powerOnlyGivesManorReclaimed() {
        GameContext ctx = freshGame();
        ctx.getState().set("power_on");
        ctx.getState().set("left_manor");

        Ending end = EndingEngine.triggered(ctx);
        assertThat(end).isNotNull();
        assertThat(end.id()).isEqualTo("manor_reclaimed");
        assertThat(end.victory()).isTrue();
    }

    @Test
    void endingPriority_leftOnlyGivesFled() {
        GameContext ctx = freshGame();
        ctx.getState().set("left_manor");

        Ending end = EndingEngine.triggered(ctx);
        assertThat(end).isNotNull();
        assertThat(end.id()).isEqualTo("fled");
        assertThat(end.victory()).isFalse();
    }
}
