package thb.jeanluc.adventure.game;

import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.io.TestIO;
import thb.jeanluc.adventure.model.Combination;
import thb.jeanluc.adventure.model.Condition;
import thb.jeanluc.adventure.model.Effect;
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 java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

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

class CombinationsTest {

    private Item plain(String id, boolean light) {
        return PlainItem.builder().id(id).name(id).description("d").light(light).build();
    }

    /** World with matches/torch/lit_torch/shovel and the given recipes; player in an empty room. */
    private GameContext ctx(Combination... recipes) {
        Map<String, Item> items = new LinkedHashMap<>();
        items.put("matches", plain("matches", false));
        items.put("torch", plain("torch", false));
        items.put("lit_torch", plain("lit_torch", true));
        items.put("shovel", plain("shovel", false));
        Map<String, Combination> combos = new HashMap<>();
        for (Combination c : recipes) {
            combos.put(c.key(), c);
        }
        Room start = new Room("start", "Start", "d");
        World w = new World(Map.of("start", start), items, Map.of(), "t", "w",
                Map.of(), List.of(), combos);
        return new GameContext(w, new Player(start, 0), new TestIO());
    }

    private void give(GameContext ctx, String id) {
        ctx.getPlayer().addItem(ctx.getWorld().getItems().get(id));
    }

    private void putInRoom(GameContext ctx, String id) {
        ctx.getPlayer().getCurrentRoom().addItem(ctx.getWorld().getItems().get(id));
    }

    private String out(GameContext ctx) {
        return ((TestIO) ctx.getIo()).allOutput();
    }

    private Combination torchRecipe() {
        return new Combination("matches", "torch", List.of(),
                List.of("matches", "torch"), "lit_torch", List.of(),
                "You light the torch.", null);
    }

    @Test
    void combinesFromInventory() {
        GameContext ctx = ctx(torchRecipe());
        give(ctx, "matches");
        give(ctx, "torch");
        Combinations.tryUse(ctx, "matches", "torch");
        assertThat(ctx.getPlayer().hasItem("lit_torch")).isTrue();
        assertThat(ctx.getPlayer().hasItem("matches")).isFalse();
        assertThat(ctx.getPlayer().hasItem("torch")).isFalse();
        assertThat(out(ctx)).contains("light the torch");
    }

    @Test
    void matchIsOrderIndependent() {
        GameContext ctx = ctx(torchRecipe());
        give(ctx, "matches");
        give(ctx, "torch");
        Combinations.tryUse(ctx, "torch", "matches");
        assertThat(ctx.getPlayer().hasItem("lit_torch")).isTrue();
    }

    @Test
    void consumesOperandFromCurrentRoom() {
        GameContext ctx = ctx(torchRecipe());
        give(ctx, "matches");
        putInRoom(ctx, "torch");
        Combinations.tryUse(ctx, "matches", "torch");
        assertThat(ctx.getPlayer().hasItem("lit_torch")).isTrue();
        assertThat(ctx.getPlayer().getCurrentRoom().findItem("torch")).isEmpty();
    }

    @Test
    void missingOperandReportsNoSuch() {
        GameContext ctx = ctx(torchRecipe());
        give(ctx, "matches"); // torch absent
        Combinations.tryUse(ctx, "matches", "torch");
        assertThat(out(ctx)).contains("no 'torch'");
        assertThat(ctx.getPlayer().hasItem("lit_torch")).isFalse();
    }

    @Test
    void noRecipeForPairSaysNothingHappens() {
        GameContext ctx = ctx(torchRecipe());
        give(ctx, "shovel");
        give(ctx, "torch");
        Combinations.tryUse(ctx, "shovel", "torch");
        assertThat(out(ctx)).contains("Nothing happens.");
    }

    @Test
    void unmetRequiresShowsFailTextAndDoesNotProduce() {
        Combination gated = new Combination("matches", "torch",
                List.of(new Condition(Condition.Type.FLAG, "dry")),
                List.of("matches", "torch"), "lit_torch", List.of(),
                "It catches.", "The torch is too damp to light.");
        GameContext ctx = ctx(gated);
        give(ctx, "matches");
        give(ctx, "torch");
        Combinations.tryUse(ctx, "matches", "torch");
        assertThat(out(ctx)).contains("too damp");
        assertThat(ctx.getPlayer().hasItem("lit_torch")).isFalse();
        assertThat(ctx.getPlayer().hasItem("matches")).isTrue(); // not consumed
    }

    @Test
    void effectsOnlyRecipeAppliesEffectsWithoutProduceOrResponse() {
        Combination effectsOnly = new Combination("matches", "shovel",
                java.util.List.of(), java.util.List.of(), null,
                java.util.List.of(new Effect(Effect.Type.SET_FLAG, "rite_done")),
                null, null);
        GameContext ctx = ctx(effectsOnly);
        give(ctx, "matches");
        give(ctx, "shovel");
        Combinations.tryUse(ctx, "matches", "shovel");
        assertThat(ctx.getState().isSet("rite_done")).isTrue();
        assertThat(out(ctx)).doesNotContain("null");
    }

    @Test
    void metRequiresAppliesEffectsAndProduces() {
        Combination gated = new Combination("matches", "torch",
                List.of(new Condition(Condition.Type.FLAG, "dry")),
                List.of("matches", "torch"), "lit_torch",
                List.of(new Effect(Effect.Type.SET_FLAG, "torch_lit")),
                "It catches.", "too damp");
        GameContext ctx = ctx(gated);
        ctx.getState().set("dry");
        give(ctx, "matches");
        give(ctx, "torch");
        Combinations.tryUse(ctx, "matches", "torch");
        assertThat(ctx.getPlayer().hasItem("lit_torch")).isTrue();
        assertThat(ctx.getState().isSet("torch_lit")).isTrue();
    }
}
