// TODO: clean up these deep relative paths? jsconfig.json is not working...
const Game = require('../../../../server/model/Game');
const globals = require('../../../../server/config/globals');
const USER_TYPES = globals.USER_TYPES;
const STATUS = globals.STATUS;
const Person = require('../../../../server/model/Person');
const GameManager = require('../../../../server/modules/GameManager.js');
const GameStateCurator = require('../../../../server/modules/GameStateCurator');
const logger = require('../../../../server/modules/Logger.js')(false);

describe('GameManager', () => {
    let gameManager, namespace;

    beforeAll(() => {
        spyOn(logger, 'debug');
        spyOn(logger, 'error');

        const inObj = { emit: () => {} };
        namespace = { in: () => { return inObj; } };
        gameManager = new GameManager(logger, globals.ENVIRONMENT.PRODUCTION).getInstance();
        gameManager.setGameSocketNamespace(namespace);
    });

    beforeEach(() => {
    });

    describe('#transferModerator', () => {
        it('Should transfer successfully from a dedicated moderator to a killed player', () => {
            const personToTransferTo = new Person('1', '123', 'Joe', USER_TYPES.KILLED_PLAYER);
            personToTransferTo.out = true;
            const moderator = new Person('3', '789', 'Jack', USER_TYPES.MODERATOR);
            const game = new Game(
                'abc',
                globals.STATUS.IN_PROGRESS,
                [personToTransferTo, new Person('2', '456', 'Jane', USER_TYPES.PLAYER)],
                [],
                false,
                moderator,
                true,
                moderator.id,
                new Date().toJSON()
            );
            gameManager.transferModeratorPowers(game, personToTransferTo, logger);

            expect(game.moderator).toEqual(personToTransferTo);
            expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR);
            expect(moderator.userType).toEqual(USER_TYPES.SPECTATOR);
        });

        it('Should transfer successfully from a dedicated moderator to a spectator', () => {
            const personToTransferTo = new Person('1', '123', 'Joe', USER_TYPES.SPECTATOR);
            const moderator = new Person('3', '789', 'Jack', USER_TYPES.MODERATOR);
            const game = new Game(
                'abc',
                globals.STATUS.IN_PROGRESS,
                [new Person('2', '456', 'Jane', USER_TYPES.PLAYER)],
                [],
                false,
                moderator,
                true,
                moderator.id,
                new Date().toJSON()
            );
            game.spectators.push(personToTransferTo);
            gameManager.transferModeratorPowers(game, personToTransferTo, logger);

            expect(game.moderator).toEqual(personToTransferTo);
            expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR);
            expect(moderator.userType).toEqual(USER_TYPES.SPECTATOR);
        });

        it('Should transfer successfully from a temporary moderator to a killed player', () => {
            const personToTransferTo = new Person('1', '123', 'Joe', USER_TYPES.KILLED_PLAYER);
            personToTransferTo.out = true;
            const tempMod = new Person('3', '789', 'Jack', USER_TYPES.TEMPORARY_MODERATOR);
            const game = new Game(
                'abc',
                globals.STATUS.IN_PROGRESS,
                [personToTransferTo, tempMod, new Person('2', '456', 'Jane', USER_TYPES.PLAYER)],
                [],
                false,
                tempMod,
                false,
                tempMod.id,
                new Date().toJSON()
            );
            gameManager.transferModeratorPowers(game, personToTransferTo, logger);

            expect(game.moderator).toEqual(personToTransferTo);
            expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR);
            expect(tempMod.userType).toEqual(USER_TYPES.PLAYER);
        });

        it('Should make the temporary moderator a dedicated moderator when they take themselves out of the game', () => {
            const tempMod = new Person('3', '789', 'Jack', USER_TYPES.TEMPORARY_MODERATOR);
            const personToTransferTo = tempMod;
            tempMod.out = true;
            const game = new Game(
                'abc',
                globals.STATUS.IN_PROGRESS,
                [personToTransferTo, tempMod, new Person('2', '456', 'Jane', USER_TYPES.PLAYER)],
                [],
                false,
                tempMod,
                true,
                tempMod.id,
                new Date().toJSON()
            );
            gameManager.transferModeratorPowers(game, personToTransferTo, logger);

            expect(game.moderator).toEqual(personToTransferTo);
            expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR);
            expect(tempMod.userType).toEqual(USER_TYPES.MODERATOR);
        });
    });

    describe('#killPlayer', () => {
        it('Should mark a player as out and broadcast it, and should not transfer moderators if the moderator is a dedicated mod.', () => {
            spyOn(namespace.in(), 'emit');
            spyOn(gameManager, 'transferModeratorPowers');
            const player = new Person('1', '123', 'Joe', USER_TYPES.PLAYER);
            const mod = new Person('2', '456', 'Jane', USER_TYPES.MODERATOR);
            const game = new Game(
                'abc',
                globals.STATUS.IN_PROGRESS,
                [player],
                [],
                false,
                mod,
                true,
                mod.id,
                new Date().toJSON()
            );
            gameManager.killPlayer(game, player, namespace, logger);

            expect(player.out).toEqual(true);
            expect(player.userType).toEqual(USER_TYPES.KILLED_PLAYER);
            expect(namespace.in().emit).toHaveBeenCalled();
            expect(gameManager.transferModeratorPowers).not.toHaveBeenCalled();
        });

        it('Should mark a temporary moderator as out but preserve their user type, and call the transfer mod function', () => {
            spyOn(namespace.in(), 'emit');
            spyOn(gameManager, 'transferModeratorPowers');
            const tempMod = new Person('1', '123', 'Joe', USER_TYPES.TEMPORARY_MODERATOR);
            const game = new Game(
                'abc',
                globals.STATUS.IN_PROGRESS,
                [tempMod],
                [],
                false,
                tempMod,
                true,
                tempMod.id,
                new Date().toJSON()
            );
            gameManager.killPlayer(game, tempMod, namespace, logger);

            expect(tempMod.out).toEqual(true);
            expect(tempMod.userType).toEqual(USER_TYPES.TEMPORARY_MODERATOR);
            expect(namespace.in().emit).toHaveBeenCalled();
            expect(gameManager.transferModeratorPowers).toHaveBeenCalled();
        });
    });

    describe('#handleRequestForGameState', () => {
        let gameRunner, mod, player;

        beforeEach(() => {
            mod = new Person('2', '456', 'Jane', USER_TYPES.MODERATOR);
            player = new Person('1', '123', 'Joe', USER_TYPES.PLAYER);
            gameRunner = {
                activeGames: new Map([
                    ['abc', new Game(
                        'abc',
                        globals.STATUS.IN_PROGRESS,
                        [player],
                        [],
                        false,
                        mod,
                        true,
                        mod.id,
                        new Date().toJSON())
                    ]
                ])
            };
        });

        it('should send the game state to a matching person with an active connection to the room', () => {
            const socket = { id: 'socket1' };
            spyOn(GameStateCurator, 'getGameStateFromPerspectiveOfPerson');
            player.socketId = 'socket1';
            spyOn(namespace.in(), 'emit');
            gameManager.handleRequestForGameState(
                gameRunner.activeGames.get('abc'),
                namespace,
                logger,
                gameRunner,
                'abc',
                '123',
                (arg) => {},
                socket
            );

            expect(GameStateCurator.getGameStateFromPerspectiveOfPerson)
                .toHaveBeenCalledWith(gameRunner.activeGames.get('abc'), player, gameRunner, socket, logger);
        });

        it('should send the game state to a matching person who reset their connection', () => {
            const socket = { id: 'socket_222222', join: () => {} };
            spyOn(socket, 'join');
            spyOn(GameStateCurator, 'getGameStateFromPerspectiveOfPerson');
            player.socketId = 'socket_111111';
            spyOn(namespace.in(), 'emit');
            gameManager.handleRequestForGameState(
                gameRunner.activeGames.get('abc'),
                namespace,
                logger,
                gameRunner,
                'abc',
                '123',
                (arg) => {},
                socket
            );

            expect(GameStateCurator.getGameStateFromPerspectiveOfPerson)
                .toHaveBeenCalledWith(gameRunner.activeGames.get('abc'), player, gameRunner, socket, logger);
            expect(player.socketId).toEqual(socket.id);
            expect(socket.join).toHaveBeenCalled();
        });
    });

    describe('#joinGame', () => {
        let game, person, moderator;

        beforeEach(() => {
            person = new Person('1', '123', 'Placeholder', USER_TYPES.KILLED_PLAYER);
            moderator = new Person('3', '789', 'Jack', USER_TYPES.MODERATOR);
            game = new Game(
                'abc',
                globals.STATUS.IN_PROGRESS,
                [person],
                [],
                false,
                moderator,
                true,
                moderator.id,
                new Date().toJSON()
            );
        });

        it('should mark the game as full when all players have been assigned', () => {
            moderator.assigned = true;

            gameManager.joinGame(game, 'Jill', 'x');

            expect(game.isFull).toEqual(true);
            expect(game.people[0].name).toEqual('Jill');
            expect(game.people[0].assigned).toEqual(true);
        });

        it('should create a spectator if the game is already full and broadcast it to the room', () => {
            moderator.assigned = true;
            person.assigned = true;
            game.isFull = true;
            spyOn(gameManager.namespace.in(), 'emit');

            gameManager.joinGame(game, 'Jane', 'x');

            expect(game.isFull).toEqual(true);
            expect(game.people[0].name).toEqual('Placeholder');
            expect(game.moderator.name).toEqual('Jack');
            expect(game.spectators.length).toEqual(1);
            expect(game.spectators[0].name).toEqual('Jane');
            expect(game.spectators[0].userType).toEqual(USER_TYPES.SPECTATOR);
            expect(gameManager.namespace.in().emit).toHaveBeenCalledWith(globals.EVENTS.NEW_SPECTATOR, jasmine.anything());
        });
    });

    describe('#generateAccessCode', () => {
        it('should continue to generate access codes up to the max attempts when the generated code is already in use by another game', () => {
            gameManager.activeGameRunner.activeGames = new Map([['AAAA', {}]]);

            const accessCode = gameManager.generateAccessCode(['A']);
            expect(accessCode).toEqual(null); // we might the max generation attempts of 50.
        });

        it('should generate and return a unique access code', () => {
            gameManager.activeGameRunner.activeGames = new Map([['AAAA', {}]]);

            const accessCode = gameManager.generateAccessCode(['B']);
            expect(accessCode).toEqual('BBBB');
        });
    });

    describe('#restartGame', () => {
        let person1,
            person2,
            person3,
            shuffleSpy,
            game,
            moderator;

        beforeEach(() => {
            person1 = new Person('1', '123', 'Placeholder1', USER_TYPES.KILLED_PLAYER);
            person2 = new Person('2', '456', 'Placeholder2', USER_TYPES.PLAYER);
            person3 = new Person('3', '789', 'Placeholder3', USER_TYPES.PLAYER);
            moderator = new Person('4', '000', 'Jack', USER_TYPES.MODERATOR);
            person1.out = true;
            person2.revealed = true;
            moderator.assigned = true;
            shuffleSpy = spyOn(gameManager, 'shuffle').and.stub();
            game = new Game(
                'test',
                STATUS.ENDED,
                [person1, person2, person3],
                [
                    { role: 'Villager', description: 'test', team: 'good', quantity: 1 },
                    { role: 'Seer', description: 'test', team: 'good', quantity: 1 },
                    { role: 'Werewolf', description: 'test', team: 'evil', quantity: 1 }
                ],
                false,
                moderator,
                true,
                '4',
                null
            );
        });

        it('should reset all relevant game parameters', async () => {
            const emitSpy = spyOn(namespace.in(), 'emit');

            await gameManager.restartGame(game, namespace);

            expect(game.status).toEqual(STATUS.IN_PROGRESS);
            expect(game.moderator.id).toEqual('4');
            expect(game.moderator.userType).toEqual(USER_TYPES.MODERATOR);
            expect(person1.out).toEqual(false);
            expect(person2.revealed).toEqual(false);
            for (const person of game.people) {
                expect(person.gameRole).toBeDefined();
            }
            expect(shuffleSpy).toHaveBeenCalled();
            expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
        });

        it('should reset all relevant game parameters, including when the game has a timer', async () => {
            game.timerParams = { hours: 2, minutes: 2, paused: false };
            game.hasTimer = true;
            gameManager.activeGameRunner.timerThreads = { test: { kill: () => {} } };

            const threadKillSpy = spyOn(gameManager.activeGameRunner.timerThreads.test, 'kill');
            const runGameSpy = spyOn(gameManager.activeGameRunner, 'runGame').and.stub();
            const emitSpy = spyOn(namespace.in(), 'emit');

            await gameManager.restartGame(game, namespace);

            expect(game.status).toEqual(STATUS.IN_PROGRESS);
            expect(game.timerParams.paused).toBeTrue();
            expect(game.moderator.id).toEqual('4');
            expect(game.moderator.userType).toEqual(USER_TYPES.MODERATOR);
            expect(person1.out).toEqual(false);
            expect(person2.revealed).toEqual(false);
            for (const person of game.people) {
                expect(person.gameRole).toBeDefined();
            }
            expect(threadKillSpy).toHaveBeenCalled();
            expect(runGameSpy).toHaveBeenCalled();
            expect(Object.keys(gameManager.activeGameRunner.timerThreads).length).toEqual(0);
            expect(shuffleSpy).toHaveBeenCalled();
            expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
        });

        it('should reset all relevant game parameters and preserve temporary moderator', async () => {
            const emitSpy = spyOn(namespace.in(), 'emit');
            game.moderator = game.people[0];
            game.moderator.userType = USER_TYPES.TEMPORARY_MODERATOR;
            game.hasDedicatedModerator = false;

            await gameManager.restartGame(game, namespace);

            expect(game.status).toEqual(STATUS.IN_PROGRESS);
            expect(game.moderator.id).toEqual('1');
            expect(game.moderator.userType).toEqual(USER_TYPES.TEMPORARY_MODERATOR);
            expect(game.moderator.gameRole).toBeDefined();
            expect(person1.out).toEqual(false);
            expect(person2.revealed).toEqual(false);
            for (const person of game.people) {
                expect(person.gameRole).toBeDefined();
            }
            expect(shuffleSpy).toHaveBeenCalled();
            expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
        });

        it('should reset all relevant game parameters and restore a temporary moderator from a dedicated moderator', async () => {
            const emitSpy = spyOn(namespace.in(), 'emit');
            game.moderator = game.people[0];
            game.moderator.userType = USER_TYPES.MODERATOR;
            game.hasDedicatedModerator = false;

            await gameManager.restartGame(game, namespace);

            expect(game.status).toEqual(STATUS.IN_PROGRESS);
            expect(game.moderator.id).toEqual('1');
            expect(game.moderator.userType).toEqual(USER_TYPES.TEMPORARY_MODERATOR);
            expect(game.moderator.gameRole).toBeDefined();
            expect(person1.out).toEqual(false);
            expect(person2.revealed).toEqual(false);
            for (const person of game.people) {
                expect(person.gameRole).toBeDefined();
            }
            expect(shuffleSpy).toHaveBeenCalled();
            expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
        });

        it('should reset all relevant game parameters and create a temporary mod if a dedicated mod transferred to a killed player', async () => {
            const emitSpy = spyOn(namespace.in(), 'emit');
            game.moderator = game.people[0];
            game.moderator.userType = USER_TYPES.MODERATOR;
            game.hasDedicatedModerator = true;

            await gameManager.restartGame(game, namespace);

            expect(game.status).toEqual(STATUS.IN_PROGRESS);
            expect(game.moderator.id).toEqual('1');
            expect(game.moderator.userType).toEqual(USER_TYPES.TEMPORARY_MODERATOR);
            expect(game.moderator.gameRole).toBeDefined();
            expect(person1.out).toEqual(false);
            expect(person2.revealed).toEqual(false);
            for (const person of game.people) {
                expect(person.gameRole).toBeDefined();
            }
            expect(shuffleSpy).toHaveBeenCalled();
            expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
        });
    });

    describe('#pruneStaleGames', () => {
        it('delete a game if it was created more than 24 hours ago', () => {
            const moreThan24HoursAgo = new Date();
            moreThan24HoursAgo.setDate(moreThan24HoursAgo.getDate() - 1);
            moreThan24HoursAgo.setHours(moreThan24HoursAgo.getHours() - 1);
            gameManager.activeGameRunner.activeGames = new Map([['AAAA', { createTime: moreThan24HoursAgo.toJSON() }]]);

            gameManager.pruneStaleGames();

            expect(gameManager.activeGameRunner.activeGames.size).toEqual(0);
        });

        it('should not delete a game if it was not created more than 24 hours ago', () => {
            const lessThan24HoursAgo = new Date();
            lessThan24HoursAgo.setHours(lessThan24HoursAgo.getHours() - 23);
            gameManager.activeGameRunner.activeGames = new Map([['AAAA', { createTime: lessThan24HoursAgo.toJSON() }]]);

            gameManager.pruneStaleGames();

            expect(gameManager.activeGameRunner.activeGames.size).toEqual(1);
        });
    });
});
