<?php

namespace Tests\Unit\Jobs\Schedule;

use Mockery as m;
use Carbon\Carbon;
use Tests\TestCase;
use Cake\Chronos\Chronos;
use Pterodactyl\Models\Task;
use Pterodactyl\Models\User;
use GuzzleHttp\Psr7\Response;
use InvalidArgumentException;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Schedule;
use Illuminate\Support\Facades\Bus;
use Pterodactyl\Jobs\Schedule\RunTaskJob;
use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Services\Backups\InitiateBackupService;
use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;

class RunTaskJobTest extends TestCase
{
    /**
     * @var \Mockery\MockInterface
     */
    private $commandRepository;

    /**
     * @var \Mockery\MockInterface
     */
    private $powerRepository;

    /**
     * @var \Mockery\MockInterface
     */
    private $initiateBackupService;

    /**
     * @var \Mockery\MockInterface
     */
    private $taskRepository;

    /**
     * @var \Mockery\MockInterface
     */
    private $scheduleRepository;

    /**
     * Setup tests.
     */
    public function setUp(): void
    {
        parent::setUp();

        Bus::fake();
        Carbon::setTestNow(Carbon::now());

        $this->commandRepository = m::mock(DaemonCommandRepository::class);
        $this->powerRepository = m::mock(DaemonPowerRepository::class);
        $this->taskRepository = m::mock(TaskRepository::class);
        $this->initiateBackupService = m::mock(InitiateBackupService::class);
        $this->scheduleRepository = m::mock(ScheduleRepository::class);

        $this->app->instance(TaskRepositoryInterface::class, $this->taskRepository);
        $this->app->instance(ScheduleRepositoryInterface::class, $this->scheduleRepository);
    }

    /**
     * Test power option passed to job.
     */
    public function testPowerAction()
    {
        /** @var \Pterodactyl\Models\Schedule $schedule */
        $schedule = factory(Schedule::class)->make(['is_active' => true]);

        /** @var \Pterodactyl\Models\Task $task */
        $task = factory(Task::class)->make(['action' => 'power', 'sequence_id' => 1]);

        /* @var \Pterodactyl\Models\Server $server */
        $task->setRelation('server', $server = factory(Server::class)->make());
        $task->setRelation('schedule', $schedule);
        $server->setRelation('user', factory(User::class)->make());

        $this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task);
        $this->powerRepository->expects('setServer')->with($task->server)->andReturnSelf()
            ->getMock()->expects('send')->with($task->payload)->andReturn(new Response);

        $this->taskRepository->shouldReceive('update')->with($task->id, ['is_queued' => false])->once()->andReturnNull();
        $this->taskRepository->shouldReceive('getNextTask')->with($schedule->id, $task->sequence_id)->once()->andReturnNull();

        $this->scheduleRepository->shouldReceive('withoutFreshModel->update')->with($schedule->id, [
            'is_processing' => false,
            'last_run_at' => Chronos::now()->toDateTimeString(),
        ])->once()->andReturnNull();

        $this->getJobInstance($task->id, $schedule->id);

        Bus::assertNotDispatched(RunTaskJob::class);
    }

    /**
     * Test command action passed to job.
     */
    public function testCommandAction()
    {
        $schedule = factory(Schedule::class)->make();
        $task = factory(Task::class)->make(['action' => 'command', 'sequence_id' => 1]);
        $task->setRelation('server', $server = factory(Server::class)->make());
        $task->setRelation('schedule', $schedule);
        $server->setRelation('user', factory(User::class)->make());

        $this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task);
        $this->commandRepository->expects('setServer')->with($task->server)->andReturnSelf()
            ->getMock()->expects('send')->with($task->payload)->andReturn(new Response);

        $this->taskRepository->expects('update')->with($task->id, ['is_queued' => false])->andReturnNull();
        $this->taskRepository->expects('getNextTask')->with($schedule->id, $task->sequence_id)->andReturnNull();

        $this->scheduleRepository->shouldReceive('withoutFreshModel->update')->with($schedule->id, [
            'is_processing' => false,
            'last_run_at' => Chronos::now()->toDateTimeString(),
        ])->once()->andReturnNull();

        $this->getJobInstance($task->id, $schedule->id);

        Bus::assertNotDispatched(RunTaskJob::class);
    }

    /**
     * Test that the next task in the list is queued if the current one is not the last.
     */
    public function testNextTaskQueuedIfExists()
    {
        $schedule = factory(Schedule::class)->make();
        $task = factory(Task::class)->make(['action' => 'command', 'sequence_id' => 1]);
        $task->setRelation('server', $server = factory(Server::class)->make());
        $task->setRelation('schedule', $schedule);
        $server->setRelation('user', factory(User::class)->make());

        $this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task);
        $this->commandRepository->expects('setServer')->with($task->server)->andReturnSelf()
            ->getMock()->expects('send')->with($task->payload)->andReturn(new Response);

        $this->taskRepository->shouldReceive('update')->with($task->id, ['is_queued' => false])->once()->andReturnNull();

        $nextTask = factory(Task::class)->make();
        $this->taskRepository->expects('getNextTask')->with($schedule->id, $task->sequence_id)->andReturn($nextTask);
        $this->taskRepository->expects('update')->with($nextTask->id, [
            'is_queued' => true,
        ])->andReturnNull();

        $this->getJobInstance($task->id, $schedule->id);

        Bus::assertDispatched(RunTaskJob::class, function ($job) use ($nextTask, $schedule) {
            $this->assertEquals($nextTask->id, $job->task, 'Assert correct task ID is passed to job.');
            $this->assertEquals($schedule->id, $job->schedule, 'Assert correct schedule ID is passed to job.');
            $this->assertEquals($nextTask->time_offset, $job->delay, 'Assert correct job delay time is set.');

            return true;
        });
    }

    /**
     * Test that an exception is thrown if an invalid task action is supplied.
     */
    public function testInvalidActionPassedToJob()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('Cannot run a task that points to a non-existent action.');

        $schedule = factory(Schedule::class)->make();
        $task = factory(Task::class)->make(['action' => 'invalid', 'sequence_id' => 1]);
        $task->setRelation('server', $server = factory(Server::class)->make());
        $task->setRelation('schedule', $schedule);
        $server->setRelation('user', factory(User::class)->make());

        $this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task);

        $this->getJobInstance($task->id, 1234);
    }

    /**
     * Test that a schedule marked as disabled does not get processed.
     */
    public function testScheduleMarkedAsDisabledDoesNotProcess()
    {
        $schedule = factory(Schedule::class)->make(['is_active' => false]);
        $task = factory(Task::class)->make(['action' => 'invalid', 'sequence_id' => 1]);
        $task->setRelation('server', $server = factory(Server::class)->make());
        $task->setRelation('schedule', $schedule);
        $server->setRelation('user', factory(User::class)->make());

        $this->taskRepository->shouldReceive('getTaskForJobProcess')->with($task->id)->once()->andReturn($task);

        $this->scheduleRepository->shouldReceive('withoutFreshModel->update')->with($schedule->id, [
            'is_processing' => false,
            'last_run_at' => Chronos::now()->toDateTimeString(),
        ])->once()->andReturn(1);

        $this->taskRepository->shouldReceive('update')->with($task->id, ['is_queued' => false])->once()->andReturn(1);

        $this->getJobInstance($task->id, $schedule->id);
        $this->assertTrue(true);
    }

    /**
     * Run the job using the mocks provided.
     *
     * @param int $task
     * @param int $schedule
     *
     * @throws \Pterodactyl\Exceptions\Model\DataValidationException
     */
    private function getJobInstance($task, $schedule)
    {
        return (new RunTaskJob($task, $schedule))->handle(
            $this->commandRepository,
            $this->initiateBackupService,
            $this->powerRepository,
            $this->taskRepository
        );
    }
}