/*
 * This file is part of Araknemu.
 *
 * Araknemu is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Araknemu is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Araknemu.  If not, see <https://www.gnu.org/licenses/>.
 *
 * Copyright (c) 2017-2019 Vincent Quatrevieux
 */

package fr.quatrevieux.araknemu.game.fight.turn.action.move;

import fr.arakne.utils.maps.constant.Direction;
import fr.arakne.utils.maps.path.Path;
import fr.arakne.utils.maps.path.PathStep;
import fr.quatrevieux.araknemu.data.constant.Characteristic;
import fr.quatrevieux.araknemu.game.fight.Fight;
import fr.quatrevieux.araknemu.game.fight.FightBaseCase;
import fr.quatrevieux.araknemu.game.fight.fighter.PlayableFighter;
import fr.quatrevieux.araknemu.game.fight.turn.FightTurn;
import fr.quatrevieux.araknemu.game.fight.turn.action.ActionResult;
import fr.quatrevieux.araknemu.game.fight.turn.action.move.validators.FightPathValidator;
import fr.quatrevieux.araknemu.game.fight.turn.action.move.validators.StopOnEnemyValidator;
import fr.quatrevieux.araknemu.game.fight.turn.action.move.validators.TackleValidator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.time.Duration;
import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

class MoveTest extends FightBaseCase {
    private Fight fight;
    private FightTurn turn;
    private PlayableFighter fighter;

    @Override
    @BeforeEach
    public void setUp() throws Exception {
        super.setUp();

        fight = createFight();
        fighter = player.fighter();
        fighter.move(fight.map().get(185));
        turn = new FightTurn(player.fighter(), fight, Duration.ofSeconds(30));
        turn.start();
    }

    @Test
    void validateEmptyPath() {
        assertFalse(
            new Move(turn.fighter(),
                new Path<>(
                    fight.map().decoder(),
                    Arrays.asList(new PathStep<>(fight.map().get(185), Direction.EAST))
                ),
                new FightPathValidator[0]
            ).validate(turn)
        );
    }

    @Test
    void validateNotEnoughMovementPoints() {
        assertFalse(
            new Move(turn.fighter(),
                new Path<>(
                    fight.map().decoder(),
                    Arrays.asList(
                        new PathStep<>(fight.map().get(185), Direction.EAST),
                        new PathStep<>(fight.map().get(199), Direction.SOUTH_WEST),
                        new PathStep<>(fight.map().get(213), Direction.SOUTH_WEST),
                        new PathStep<>(fight.map().get(227), Direction.SOUTH_WEST),
                        new PathStep<>(fight.map().get(241), Direction.SOUTH_WEST)
                    )
                ),
                new FightPathValidator[0]
            ).validate(turn)
        );
    }

    @Test
    void validateRestrictedDirection() {
        assertFalse(
            new Move(turn.fighter(),
                new Path<>(
                    fight.map().decoder(),
                    Arrays.asList(
                        new PathStep<>(fight.map().get(185), Direction.EAST),
                        new PathStep<>(fight.map().get(186), Direction.EAST),
                        new PathStep<>(fight.map().get(187), Direction.EAST),
                        new PathStep<>(fight.map().get(188), Direction.EAST)
                    )
                ),
                new FightPathValidator[0]
            ).validate(turn)
        );
    }

    @Test
    void validateNotWalkableCells() {
        assertFalse(
            new Move(turn.fighter(),
                new Path<>(
                    fight.map().decoder(),
                    Arrays.asList(
                        new PathStep<>(fight.map().get(0), Direction.EAST),
                        new PathStep<>(fight.map().get(14), Direction.SOUTH_WEST)
                    )
                ),
                new FightPathValidator[0]
            ).validate(turn)
        );
    }

    @Test
    void validateValid() {
        assertTrue(
            new Move(turn.fighter(),
                new Path<>(
                    fight.map().decoder(),
                    Arrays.asList(
                        new PathStep<>(fight.map().get(185), Direction.EAST),
                        new PathStep<>(fight.map().get(199), Direction.SOUTH_WEST),
                        new PathStep<>(fight.map().get(213), Direction.SOUTH_WEST),
                        new PathStep<>(fight.map().get(198), Direction.NORTH_WEST)
                    )
                ),
                new FightPathValidator[0]
            ).validate(turn)
        );
    }

    @Test
    void startSuccess() {
        Move move = new Move(turn.fighter(),
            new Path<>(
                fight.map().decoder(),
                Arrays.asList(
                    new PathStep<>(fight.map().get(185), Direction.EAST),
                    new PathStep<>(fight.map().get(199), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(213), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(198), Direction.NORTH_WEST)
                )
            ),
            new FightPathValidator[0]
        );

        ActionResult result = move.start();

        assertInstanceOf(MoveSuccess.class, result);
        assertEquals(3, MoveSuccess.class.cast(result).lostMovementPoints());
        assertEquals(0, MoveSuccess.class.cast(result).lostActionPoints());
        assertEquals(Direction.NORTH_WEST, MoveSuccess.class.cast(result).orientation());
        assertEquals(198, MoveSuccess.class.cast(result).target().id());

        assertTrue(result.success());
        assertFalse(result.secret());
        assertSame(fighter, result.performer());
        assertSame(fighter, result.performer());
        assertEquals(1, result.action());
        assertArrayEquals(new String[] {"ac5ddvfdg"}, result.arguments());
        assertEquals("Move{size=3, target=198}", move.toString());
    }

    @Test
    void resultShouldBeSecretWhenHidden() {
        turn.fighter().setHidden(turn.fighter(), true);

        Move move = new Move(turn.fighter(),
            new Path<>(
                fight.map().decoder(),
                Arrays.asList(
                    new PathStep<>(fight.map().get(185), Direction.EAST),
                    new PathStep<>(fight.map().get(199), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(213), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(198), Direction.NORTH_WEST)
                )
            ),
            new FightPathValidator[0]
        );

        ActionResult result = move.start();

        assertInstanceOf(MoveSuccess.class, result);
        assertTrue(result.success());
        assertTrue(result.secret());

        move = new Move(turn.fighter(),
            new Path<>(
                fight.map().decoder(),
                Arrays.asList(
                    new PathStep<>(fight.map().get(185), Direction.EAST),
                    new PathStep<>(fight.map().get(199), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(213), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(198), Direction.NORTH_WEST)
                )
            ),
            new FightPathValidator[] {
                new StopOnEnemyValidator(),
                new TackleValidator(),
            }
        );

        other.fighter().characteristics().alter(Characteristic.AGILITY, 500);
        other.fighter().move(fight.map().get(170));

        result = move.start();

        assertInstanceOf(MoveFailed.class, result);
        assertFalse(result.success());
        assertTrue(result.secret());
    }

    @Test
    void startTruncatedBecauseOfEnemy() {
        Move move = new Move(turn.fighter(),
            new Path<>(
                fight.map().decoder(),
                Arrays.asList(
                    new PathStep<>(fight.map().get(185), Direction.EAST),
                    new PathStep<>(fight.map().get(199), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(213), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(227), Direction.NORTH_WEST)
                )
            ),
            new FightPathValidator[] {
                new StopOnEnemyValidator(),
            }
        );

        other.fighter().move(fight.map().get(198));

        ActionResult result = move.start();

        assertInstanceOf(MoveSuccess.class, result);
        assertEquals(2, MoveSuccess.class.cast(result).lostMovementPoints());
        assertEquals(213, MoveSuccess.class.cast(result).target().id());

        assertTrue(result.success());
        assertSame(fighter, result.performer());
        assertSame(fighter, result.performer());
        assertEquals(1, result.action());
        assertArrayEquals(new String[] {"ac5ddv"}, result.arguments());
        assertEquals("Move{size=3, target=227}", move.toString());
    }

    @Test
    void startWithTackle() {
        Move move = new Move(turn.fighter(),
            new Path<>(
                fight.map().decoder(),
                Arrays.asList(
                    new PathStep<>(fight.map().get(185), Direction.EAST),
                    new PathStep<>(fight.map().get(199), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(213), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(198), Direction.NORTH_WEST)
                )
            ),
            new FightPathValidator[] {
                new StopOnEnemyValidator(),
                new TackleValidator(),
            }
        );

        other.fighter().characteristics().alter(Characteristic.AGILITY, 500);
        other.fighter().move(fight.map().get(170));

        ActionResult result = move.start();

        assertInstanceOf(MoveFailed.class, result);
        assertEquals(3, MoveFailed.class.cast(result).lostMovementPoints());
        assertEquals(6, MoveFailed.class.cast(result).lostActionPoints());
        assertEquals(185, MoveFailed.class.cast(result).target().id());
        assertEquals(1, MoveFailed.class.cast(result).path().size());

        assertFalse(result.success());
        assertFalse(result.secret());
        assertSame(fighter, result.performer());
        assertSame(fighter, result.performer());
        assertEquals(104, result.action());
        assertArrayEquals(new Object[0], result.arguments());

        result.apply(turn);

        assertEquals(0, turn.points().movementPoints());
        assertEquals(0, turn.points().actionPoints());
    }

    @Test
    void startWithTackleWithoutActionPoint() {
        fighter.turn().points().removeActionPoints(6);

        Move move = new Move(turn.fighter(),
            new Path<>(
                fight.map().decoder(),
                Arrays.asList(
                    new PathStep<>(fight.map().get(185), Direction.EAST),
                    new PathStep<>(fight.map().get(199), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(213), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(198), Direction.NORTH_WEST)
                )
            ),
            new FightPathValidator[] {
                new StopOnEnemyValidator(),
                new TackleValidator(),
            }
        );

        other.fighter().characteristics().alter(Characteristic.AGILITY, 500);
        other.fighter().move(fight.map().get(170));

        ActionResult result = move.start();

        assertInstanceOf(MoveFailed.class, result);
        assertEquals(3, MoveFailed.class.cast(result).lostMovementPoints());
        assertEquals(0, MoveFailed.class.cast(result).lostActionPoints());
        assertEquals(185, MoveFailed.class.cast(result).target().id());
        assertEquals(1, MoveFailed.class.cast(result).path().size());

        assertFalse(result.success());
        assertSame(fighter, result.performer());
        assertSame(fighter, result.performer());
        assertEquals(104, result.action());
        assertArrayEquals(new Object[0], result.arguments());

        result.apply(turn);

        assertEquals(0, turn.points().movementPoints());
        assertEquals(0, turn.points().actionPoints());
    }

    @Test
    void end() {
        Move move = new Move(turn.fighter(),
            new Path<>(
                fight.map().decoder(),
                Arrays.asList(
                    new PathStep<>(fight.map().get(185), Direction.EAST),
                    new PathStep<>(fight.map().get(199), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(213), Direction.SOUTH_WEST)
                )
            ),
            new FightPathValidator[0]
        );

        move.start().apply(turn);

        assertEquals(1, turn.points().movementPoints());
        assertEquals(213, fighter.cell().id());
        assertEquals(Direction.SOUTH_WEST, fighter.orientation());
    }

    @Test
    void duration() {
        Move move = new Move(turn.fighter(),
            new Path<>(
                fight.map().decoder(),
                Arrays.asList(
                    new PathStep<>(fight.map().get(185), Direction.EAST),
                    new PathStep<>(fight.map().get(199), Direction.SOUTH_WEST),
                    new PathStep<>(fight.map().get(213), Direction.SOUTH_WEST)
                )
            ),
            new FightPathValidator[0]
        );

        assertEquals(Duration.ofMillis(900), move.duration());
    }
}
