<?php

namespace Tests\Unit\Services\Eggs\Variables;

use Exception;
use Mockery as m;
use Tests\TestCase;
use BadMethodCallException;
use Pterodactyl\Models\EggVariable;
use Illuminate\Contracts\Validation\Factory;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Services\Eggs\Variables\VariableUpdateService;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException;
use Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException;

class VariableUpdateServiceTest extends TestCase
{
    /**
     * @var \Pterodactyl\Models\EggVariable|\Mockery\Mock
     */
    private $model;

    /**
     * @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface|\Mockery\Mock
     */
    private $repository;

    /**
     * @var \Illuminate\Contracts\Validation\Factory|\Mockery\Mock
     */
    private $validator;

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

        $this->model = factory(EggVariable::class)->make();
        $this->repository = m::mock(EggVariableRepositoryInterface::class);
        $this->validator = m::mock(Factory::class);
    }

    /**
     * Test the function when no env_variable key is passed into the function.
     */
    public function testVariableIsUpdatedWhenNoEnvironmentVariableIsPassed()
    {
        $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
            ->shouldReceive('update')->with($this->model->id, m::subset([
                'user_viewable' => false,
                'user_editable' => false,
            ]))->once()->andReturn(true);

        $this->assertTrue($this->getService()->handle($this->model, []));
    }

    /**
     * Test that a null value passed in for the default is converted to a string.
     *
     * @see https://github.com/Pterodactyl/Panel/issues/934
     */
    public function testNullDefaultValue()
    {
        $this->repository->shouldReceive('withoutFreshModel->update')->with($this->model->id, m::subset([
            'user_viewable' => false,
            'user_editable' => false,
            'default_value' => '',
        ]))->once()->andReturn(true);

        $this->assertTrue($this->getService()->handle($this->model, ['default_value' => null]));
    }

    /**
     * Test the function when a valid env_variable key is passed into the function.
     */
    public function testVariableIsUpdatedWhenValidEnvironmentVariableIsPassed()
    {
        $this->repository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
            ->shouldReceive('findCountWhere')->with([
                ['env_variable', '=', 'TEST_VAR_123'],
                ['egg_id', '=', $this->model->option_id],
                ['id', '!=', $this->model->id],
            ])->once()->andReturn(0);

        $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
            ->shouldReceive('update')->with($this->model->id, m::subset([
                'user_viewable' => false,
                'user_editable' => false,
                'env_variable' => 'TEST_VAR_123',
            ]))->once()->andReturn(true);

        $this->assertTrue($this->getService()->handle($this->model, ['env_variable' => 'TEST_VAR_123']));
    }

    /**
     * Test that an empty (null) value passed in the option key is handled
     * properly as an array. Also tests that a null description is handled.
     *
     * @see https://github.com/Pterodactyl/Panel/issues/841
     */
    public function testNullOptionValueIsPassedAsArray()
    {
        $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
            ->shouldReceive('update')->with($this->model->id, m::subset([
                'user_viewable' => false,
                'user_editable' => false,
                'description' => '',
            ]))->once()->andReturn(true);

        $this->assertTrue($this->getService()->handle($this->model, ['options' => null, 'description' => null]));
    }

    /**
     * Test that data passed into the handler is overwritten inside the handler.
     */
    public function testDataPassedIntoHandlerTakesLowerPriorityThanDataSet()
    {
        $this->repository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
            ->shouldReceive('findCountWhere')->with([
                ['env_variable', '=', 'TEST_VAR_123'],
                ['egg_id', '=', $this->model->option_id],
                ['id', '!=', $this->model->id],
            ])->once()->andReturn(0);

        $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf()
            ->shouldReceive('update')->with($this->model->id, m::subset([
                'user_viewable' => false,
                'user_editable' => false,
                'env_variable' => 'TEST_VAR_123',
            ]))->once()->andReturn(true);

        $this->assertTrue($this->getService()->handle($this->model, ['user_viewable' => 123456, 'env_variable' => 'TEST_VAR_123']));
    }

    /**
     * Test that a non-unique environment variable triggers an exception.
     */
    public function testExceptionIsThrownIfEnvironmentVariableIsNotUnique()
    {
        $this->repository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf()
            ->shouldReceive('findCountWhere')->with([
                ['env_variable', '=', 'TEST_VAR_123'],
                ['egg_id', '=', $this->model->option_id],
                ['id', '!=', $this->model->id],
            ])->once()->andReturn(1);

        try {
            $this->getService()->handle($this->model, ['env_variable' => 'TEST_VAR_123']);
        } catch (Exception $exception) {
            $this->assertInstanceOf(DisplayException::class, $exception);
            $this->assertEquals(trans('exceptions.service.variables.env_not_unique', [
                'name' => 'TEST_VAR_123',
            ]), $exception->getMessage());
        }
    }

    /**
     * Test that all of the reserved variables defined in the model trigger an exception.
     *
     * @dataProvider reservedNamesProvider
     */
    public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames(string $variable)
    {
        $this->expectException(ReservedVariableNameException::class);

        $this->getService()->handle($this->model, ['env_variable' => $variable]);
    }

    /**
     * Test that validation errors due to invalid rules are caught and handled properly.
     */
    public function testInvalidValidationRulesResultInException()
    {
        $this->expectException(BadValidationRuleException::class);
        $this->expectExceptionMessage('The validation rule "hodor_door" is not a valid rule for this application.');

        $data = ['env_variable' => 'TEST_VAR_123', 'rules' => 'string|hodorDoor'];

        $this->repository->shouldReceive('setColumns->findCountWhere')->once()->andReturn(0);

        $this->validator->shouldReceive('make')->once()
            ->with(['__TEST' => 'test'], ['__TEST' => 'string|hodorDoor'])
            ->andReturnSelf();

        $this->validator->shouldReceive('fails')->once()
            ->withNoArgs()
            ->andThrow(new BadMethodCallException('Method [validateHodorDoor] does not exist.'));

        $this->getService()->handle($this->model, $data);
    }

    /**
     * Test that an exception not stemming from a bad rule is not caught.
     */
    public function testExceptionNotCausedByBadRuleIsNotCaught()
    {
        $this->expectException(BadMethodCallException::class);
        $this->expectExceptionMessage('Received something, but no expectations were specified.');

        $data = ['rules' => 'string'];

        $this->validator->shouldReceive('make')->once()
            ->with(['__TEST' => 'test'], ['__TEST' => 'string'])
            ->andReturnSelf();

        $this->validator->shouldReceive('fails')->once()
            ->withNoArgs()
            ->andThrow(new BadMethodCallException('Received something, but no expectations were specified.'));

        $this->getService()->handle($this->model, $data);
    }

    /**
     * Provides the data to be used in the tests.
     *
     * @return array
     */
    public function reservedNamesProvider()
    {
        $data = [];
        $exploded = explode(',', EggVariable::RESERVED_ENV_NAMES);
        foreach ($exploded as $e) {
            $data[] = [$e];
        }

        return $data;
    }

    /**
     * Return an instance of the service with mocked dependencies for testing.
     *
     * @return \Pterodactyl\Services\Eggs\Variables\VariableUpdateService
     */
    private function getService(): VariableUpdateService
    {
        return new VariableUpdateService($this->repository, $this->validator);
    }
}
