package thb.jeanluc.adventure.game;

import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.command.CommandParser;
import thb.jeanluc.adventure.command.CommandRegistry;
import thb.jeanluc.adventure.command.ParsedCommand;
import thb.jeanluc.adventure.command.impl.ExamineCommand;
import thb.jeanluc.adventure.command.impl.GoCommand;
import thb.jeanluc.adventure.command.impl.InventoryCommand;
import thb.jeanluc.adventure.command.impl.LookCommand;
import thb.jeanluc.adventure.command.impl.TakeCommand;
import thb.jeanluc.adventure.command.impl.UseCommand;
import thb.jeanluc.adventure.io.TestIO;
import thb.jeanluc.adventure.model.Direction;
import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.Room;
import thb.jeanluc.adventure.model.Tutorial;
import thb.jeanluc.adventure.model.TutorialStep;
import thb.jeanluc.adventure.model.World;

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

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

class TutorialGuideTest {

    private CommandRegistry registry() {
        CommandRegistry r = new CommandRegistry();
        r.register(new LookCommand(), "look", "l");
        r.register(new ExamineCommand(), "examine", "x");
        r.register(new TakeCommand(), "take", "get");
        r.register(new InventoryCommand(), "inventory", "i");
        r.register(new UseCommand(), "use");
        r.register(new GoCommand(), "go", "move", "walk");
        return r;
    }

    private final Room kitchen = new Room("kitchen", "Kitchen", "d");
    private final Room hallway = new Room("hallway", "Hallway", "d");

    private GameContext ctx() {
        kitchen.addExit(Direction.NORTH, hallway);
        Map<String, Room> rooms = new LinkedHashMap<>();
        rooms.put("kitchen", kitchen);
        rooms.put("hallway", hallway);
        World w = new World(rooms, Map.of(), Map.of(), "t", "w");
        Player p = new Player(kitchen, 0);
        return new GameContext(w, p, new TestIO());
    }

    private ParsedCommand parse(String s) {
        return new CommandParser().parse(s);
    }

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

    private TutorialStep step(String instr, String expect, int minArgs) {
        return new TutorialStep(instr, expect, minArgs, "OK:" + expect, "HINT:" + expect);
    }

    @Test
    void beginPrintsIntroAndFirstInstruction() {
        GameContext ctx = ctx();
        Tutorial t = new Tutorial("INTRO", List.of(step("do look", "look", 0)), "TIPS");
        TutorialGuide g = new TutorialGuide(t, registry());
        g.begin(ctx);
        assertThat(out(ctx)).contains("INTRO").contains("do look");
        assertThat(g.isActive()).isTrue();
    }

    @Test
    void aliasSatisfiesVerbStepAndAdvancesToTips() {
        GameContext ctx = ctx();
        Tutorial t = new Tutorial("INTRO", List.of(step("do look", "look", 0)), "TIPS");
        TutorialGuide g = new TutorialGuide(t, registry());
        g.begin(ctx);
        g.onCommand(ctx, parse("l")); // alias of look
        assertThat(out(ctx)).contains("OK:look").contains("TIPS");
        assertThat(g.isActive()).isFalse();
    }

    @Test
    void minArgsGatesTakeStep() {
        GameContext ctx = ctx();
        Tutorial t = new Tutorial("I", List.of(step("take it", "take", 1)), "TIPS");
        TutorialGuide g = new TutorialGuide(t, registry());
        g.begin(ctx);
        g.onCommand(ctx, parse("take")); // no arg -> hint, not satisfied
        assertThat(out(ctx)).contains("HINT:take");
        assertThat(g.isActive()).isTrue();
        g.onCommand(ctx, parse("take lamp")); // satisfied
        assertThat(out(ctx)).contains("OK:take");
    }

    @Test
    void goDirectionRequiresAnActualMove() {
        GameContext ctx = ctx();
        Tutorial t = new Tutorial("I", List.of(step("move", "go_direction", 0)), "TIPS");
        TutorialGuide g = new TutorialGuide(t, registry());
        g.begin(ctx);
        // typed a direction but did NOT move -> hint
        g.onCommand(ctx, parse("go north"));
        assertThat(out(ctx)).contains("HINT:go_direction");
        assertThat(g.isActive()).isTrue();
        // now actually move, then the same command satisfies
        ctx.getPlayer().setCurrentRoom(hallway);
        g.onCommand(ctx, parse("go north"));
        assertThat(out(ctx)).contains("OK:go_direction");
        assertThat(g.isActive()).isFalse();
    }

    @Test
    void goToRoomNeedsNonDirectionArgAndMove() {
        GameContext ctx = ctx();
        Tutorial t = new Tutorial("I", List.of(step("travel", "go_to_room", 0)), "TIPS");
        TutorialGuide g = new TutorialGuide(t, registry());
        g.begin(ctx);
        ctx.getPlayer().setCurrentRoom(hallway); // moved
        g.onCommand(ctx, parse("go kitchen")); // "kitchen" is not a direction
        assertThat(out(ctx)).contains("OK:go_to_room");
    }

    @Test
    void skipDeactivates() {
        GameContext ctx = ctx();
        Tutorial t = new Tutorial("I", List.of(step("do look", "look", 0)), "TIPS");
        TutorialGuide g = new TutorialGuide(t, registry());
        g.begin(ctx);
        g.skip(ctx);
        assertThat(out(ctx)).contains("Tutorial skipped");
        assertThat(g.isActive()).isFalse();
    }

    @Test
    void emptyTutorialIsInactive() {
        TutorialGuide g = new TutorialGuide(Tutorial.none(), registry());
        assertThat(g.isActive()).isFalse();
    }

    @Test
    void advancingPrintsTheNextInstruction() {
        GameContext ctx = ctx();
        Tutorial t = new Tutorial("I",
                List.of(step("do look", "look", 0), step("do take", "take", 1)), "TIPS");
        TutorialGuide g = new TutorialGuide(t, registry());
        g.begin(ctx);
        g.onCommand(ctx, parse("look"));
        assertThat(out(ctx)).contains("OK:look").contains("do take");
        assertThat(g.isActive()).isTrue();
    }

    @Test
    void goToRoomWithoutMovingGivesHint() {
        GameContext ctx = ctx();
        Tutorial t = new Tutorial("I", List.of(step("travel", "go_to_room", 0)), "TIPS");
        TutorialGuide g = new TutorialGuide(t, registry());
        g.begin(ctx);
        g.onCommand(ctx, parse("go kitchen")); // non-direction arg but no room change
        assertThat(out(ctx)).contains("HINT:go_to_room");
        assertThat(g.isActive()).isTrue();
    }
}
