<?php
declare(strict_types=1);

namespace App\Tests\Game;

use App\Game\Entity\Game;
use App\Game\Entity\Player;
use App\Game\Entity\Session;
use App\Game\Entity\SessionSetting;
use App\Game\Enum\GameStatus;
use App\Game\Enum\SessionStatus;
use App\Game\Enum\SessionSettingType;
use App\Game\Service\GameDashboardService;
use App\Tech\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use App\Game\Repository\GameRepository;
use App\Game\Repository\SessionRepository;
use Symfony\Component\Mercure\HubInterface;
use PHPUnit\Framework\TestCase;

class GameDashboardServiceTest extends TestCase
{
    private $entityManager;
    private $gameRepository;
    private $sessionRepository;
    private $hub;
    private $service;

    protected function setUp(): void
    {
        $this->entityManager = $this->createMock(EntityManagerInterface::class);
        $this->gameRepository = $this->createMock(GameRepository::class);
        $this->sessionRepository = $this->createMock(SessionRepository::class);
        $this->hub = $this->createMock(HubInterface::class);

        $this->service = new GameDashboardService(
            $this->gameRepository,
            $this->sessionRepository,
            $this->entityManager,
            $this->hub,
            'http://localhost/topic'
        );
    }

    public function testCreateSessionDoesNotInitializeSettings(): void
    {
        $game = new Game();
        $game->setStatus(GameStatus::OPEN);

        $user = new User();
        $user->setUsername('testuser');

        $this->entityManager->expects($this->exactly(2))
            ->method('persist');
            // 1. Session, 2. Player

        $session = $this->service->createSession($game, $user, false);

        $this->assertInstanceOf(Session::class, $session);
    }

    public function testJoinSessionDoesNotInitializeSettings(): void
    {
        $user = new User();
        $user->setUsername('testuser');
        $game = new Game();
        $game->setNumberOfPlayers(3);
        $session = new Session();
        $session->setGame($game);

        $setting = new SessionSetting();
        $setting->setSession($session);
        $setting->setName(SessionSettingType::INVITE_CODE);
        $setting->setValue('abc-123');

        $repo = $this->createMock(\Doctrine\ORM\EntityRepository::class);
        $this->entityManager->method('getRepository')
            ->willReturn($repo);

        $repo->method('findOneBy')
            ->willReturn($setting);

        $this->entityManager->expects($this->exactly(1))
            ->method('persist');
            // 1. Player

        $result = $this->service->joinSession('abc-123', $user);

        $this->assertTrue($result);
    }

    public function testStartSessionAssignsScreensAndInitializesSettings(): void
    {
        $game = new Game();
        $game->setNumberOfPlayers(2);
        $session = new Session();
        $session->setGame($game);
        $session->setStatus(\App\Game\Enum\SessionStatus::CREATED);

        $user1 = new User();
        $user1->setUsername('user1');
        $player1 = new Player();
        $player1->setUser($user1);
        $session->addPlayer($player1);

        $user2 = new User();
        $user2->setUsername('user2');
        $player2 = new Player();
        $player2->setUser($user2);
        $session->addPlayer($player2);

        // For each player (2):
        // Player (persist)
        // SessionSetting (rights)
        // SessionSetting (pwd)
        // SessionSetting (chat tracking)
        // SessionSetting (verify codes)
        // SessionSetting (verification progress)
        // Total = 2 * 6 = 12
        // PLUS Session (persist)
        // Total = 13

        $this->entityManager->expects($this->exactly(13))
            ->method('persist');

        $result = $this->service->startSession($session);

        $this->assertTrue($result);
        $this->assertEquals(\App\Game\Enum\SessionStatus::READY, $session->getStatus());
        $this->assertNotNull($player1->getScreen());
        $this->assertNotNull($player2->getScreen());
        $this->assertNotEquals($player1->getScreen(), $player2->getScreen());
    }

    public function testStartSessionWithFourPlayers(): void
    {
        $game = new Game();
        $game->setNumberOfPlayers(4);
        $session = new Session();
        $session->setGame($game);
        $session->setStatus(\App\Game\Enum\SessionStatus::CREATED);

        for ($i = 1; $i <= 4; $i++) {
            $user = new User();
            $user->setUsername('user' . $i);
            $player = new Player();
            $player->setUser($user);
            $session->addPlayer($player);
        }

        // For each player (4):
        // Player (persist) + 5 Settings = 6 persists
        // Total = 4 * 6 = 24
        // PLUS Session (persist)
        // Total = 25

        $this->entityManager->expects($this->exactly(25))
            ->method('persist');

        $result = $this->service->startSession($session);

        $this->assertTrue($result);
        foreach ($session->getPlayers() as $player) {
            $this->assertNotNull($player->getScreen());
            $this->assertGreaterThanOrEqual(1, $player->getScreen());
            $this->assertLessThanOrEqual(4, $player->getScreen());
        }
    }

    public function testLeaveSession(): void
    {
        $user = new User();
        $session = new Session();
        $session->setStatus(\App\Game\Enum\SessionStatus::CREATED);
        $session->setTimer(0);

        $player1 = new Player();
        $player1->setUser($user);
        $player1->setSession($session);
        $session->addPlayer($player1);

        $player2 = new Player();
        $player2->setUser(new User());
        $player2->setSession($session);
        $session->addPlayer($player2);

        $setting = new SessionSetting();
        $setting->setPlayer($player1);
        $setting->setSession($session);
        $session->addSetting($setting);

        $this->entityManager->expects($this->exactly(2))
            ->method('remove');
            // 1. SessionSetting, 2. Player

        $result = $this->service->leaveSession($session, $user);

        $this->assertTrue($result);
        $this->assertCount(1, $session->getPlayers());
    }

    public function testToggleReady(): void
    {
        $user = new User();
        $game = new Game();
        $game->setNumberOfPlayers(1);
        $session = new Session();
        $session->setGame($game);
        $session->setStatus(SessionStatus::READY);

        $player = new Player();
        $player->setUser($user);
        $player->setScreen(1);
        $session->addPlayer($player);

        $repo = $this->createMock(\App\Game\Repository\SessionSettingRepository::class);
        $this->entityManager->method('getRepository')
            ->willReturn($repo);

        // First call: toggle ON
        $repo->expects($this->atLeastOnce())
            ->method('getSetting')
            ->willReturn(null); // No setting initially

        $this->entityManager->expects($this->atLeastOnce())
            ->method('persist')
            ->with($this->callback(function($entity) {
                return $entity instanceof SessionSetting && $entity->getName() === SessionSettingType::READY_AT_FOR_PLAYER1;
            }));

        $result = $this->service->toggleReady($session, $user);
        $this->assertTrue($result);
    }

    public function testCheckAllPlayersReadyTransitionsStatus(): void
    {
        $game = new Game();
        $game->setNumberOfPlayers(2);
        $session = new Session();
        $session->setGame($game);
        $session->setStatus(SessionStatus::READY);

        $player1 = new Player();
        $player1->setScreen(1);
        $session->addPlayer($player1);

        $player2 = new Player();
        $player2->setScreen(2);
        $session->addPlayer($player2);

        $repo = $this->createMock(\App\Game\Repository\SessionSettingRepository::class);
        $this->entityManager->method('getRepository')
            ->willReturn($repo);

        $setting1 = new SessionSetting();
        $setting1->setName(SessionSettingType::READY_AT_FOR_PLAYER1);
        $setting1->setValue((string)time());
        $setting1->setPlayer($player1);

        $setting2 = new SessionSetting();
        $setting2->setName(SessionSettingType::READY_AT_FOR_PLAYER2);
        $setting2->setValue((string)time());
        $setting2->setPlayer($player2);

        $repo->method('getSetting')
            ->willReturnCallback(function($s, $name, $p) use ($setting1, $setting2) {
                if ($name === SessionSettingType::READY_AT_FOR_PLAYER1) return $setting1;
                if ($name === SessionSettingType::READY_AT_FOR_PLAYER2) return $setting2;
                return null;
            });

        $this->service->checkAllPlayersReady($session);

        $this->assertEquals(SessionStatus::PLAYING, $session->getStatus());
    }

    public function testCheckAllPlayersReadyTimeouts(): void
    {
        $game = new Game();
        $game->setNumberOfPlayers(2);
        $session = new Session();
        $session->setGame($game);
        $session->setStatus(SessionStatus::READY);

        $player1 = new Player();
        $player1->setScreen(1);
        $session->addPlayer($player1);

        $player2 = new Player();
        $player2->setScreen(2);
        $session->addPlayer($player2);

        $repo = $this->createMock(\App\Game\Repository\SessionSettingRepository::class);
        $this->entityManager->method('getRepository')
            ->willReturn($repo);

        $setting1 = new SessionSetting();
        $setting1->setName(SessionSettingType::READY_AT_FOR_PLAYER1);
        $setting1->setValue((string)(time() - 70)); // Timed out
        $setting1->setPlayer($player1);
        $session->addSetting($setting1);

        $repo->method('getSetting')
            ->willReturnCallback(function($s, $name, $p) use ($setting1) {
                if ($name === SessionSettingType::READY_AT_FOR_PLAYER1) return $setting1;
                return null;
            });

        $this->entityManager->expects($this->once())
            ->method('remove')
            ->with($setting1);

        $this->entityManager->expects($this->atLeastOnce())
            ->method('flush');

        $this->hub->expects($this->once())
            ->method('publish');

        $this->service->checkAllPlayersReady($session);

        $this->assertEquals(SessionStatus::READY, $session->getStatus());
    }

    public function testJoinSessionTriggersStartSessionWhenFull(): void
    {
        $user = new User();
        $user->setUsername('testuser');
        $game = new Game();
        $game->setNumberOfPlayers(2);
        $session = new Session();
        $session->setGame($game);
        $session->setStatus(\App\Game\Enum\SessionStatus::CREATED);

        $existingUser = new User();
        $existingUser->setUsername('existing');
        $existingPlayer = new Player();
        $existingPlayer->setUser($existingUser);
        $session->addPlayer($existingPlayer);

        $setting = new SessionSetting();
        $setting->setSession($session);
        $setting->setName(SessionSettingType::INVITE_CODE);
        $setting->setValue('abc-123');

        $repo = $this->createMock(\Doctrine\ORM\EntityRepository::class);
        $this->entityManager->method('getRepository')
            ->willReturn($repo);

        $repo->method('findOneBy')
            ->willReturn($setting);

        // Expectation:
        // 1. Persist new Player
        // 2. Persist Player 1 (existing) screen update
        // 3. Persist Player 2 (new) screen update
        // 4. Persist Session status update
        // 5. 5 Settings for Player 1
        // 6. 5 Settings for Player 2
        // Total = 1 + 1 + 1 + 1 + 5 + 5 = 14 persists
        $this->entityManager->expects($this->exactly(14))
            ->method('persist');

        $result = $this->service->joinSession('abc-123', $user);

        $this->assertTrue($result);
        $this->assertEquals(\App\Game\Enum\SessionStatus::READY, $session->getStatus());
    }

    public function testLeaveSessionRevertsFromReady(): void
    {
        $user = new User();
        $user->setUsername('testuser');
        $session = new Session();
        $session->setStatus(\App\Game\Enum\SessionStatus::READY);
        $session->setTimer(0);

        $player1 = new Player();
        $player1->setUser($user);
        $player1->setScreen(1);
        $player1->setSession($session);
        $session->addPlayer($player1);

        $player2 = new Player();
        $player2->setUser(new User());
        $player2->setScreen(2);
        $player2->setSession($session);
        $session->addPlayer($player2);

        $this->entityManager->expects($this->exactly(1))
            ->method('remove');
            // 1. Player1, any settings (none in this test setup)

        $result = $this->service->leaveSession($session, $user);

        $this->assertTrue($result);
        $this->assertEquals(\App\Game\Enum\SessionStatus::CREATED, $session->getStatus());
        $this->assertNull($player2->getScreen());
    }

    public function testCreateSessionForOnePlayerGame(): void
    {
        $game = new Game();
        $game->setStatus(GameStatus::OPEN);
        $game->setNumberOfPlayers(1);

        $user = new User();
        $user->setUsername('testuser');

        // 1. Session persist
        // 2. Player persist
        // 3. Player screen persist (during startSession)
        // 4. Session status persist (during startSession)
        // 5. 5 Settings for Player (during startSession)
        // Total = 1 + 1 + 1 + 1 + 5 = 9 persists
        $this->entityManager->expects($this->exactly(9))
            ->method('persist');

        $session = $this->service->createSession($game, $user, false);

        $this->assertInstanceOf(Session::class, $session);
        $this->assertEquals(\App\Game\Enum\SessionStatus::READY, $session->getStatus());
    }
}
