jaroslavtyc/drd-plus-profession-levels

View on GitHub
tests/Person/ProfessionLevels/ProfessionLevelsTest.php

Summary

Maintainability
F
6 days
Test Coverage
<?php declare(strict_types=1);

namespace DrdPlus\Tests\Person\ProfessionLevels;

use DrdPlus\Codes\ProfessionCode;
use DrdPlus\Codes\Properties\PropertyCode;
use DrdPlus\Person\ProfessionLevels\Exceptions\MultiProfessionsAreProhibited;
use DrdPlus\Person\ProfessionLevels\LevelRank;
use DrdPlus\Person\ProfessionLevels\ProfessionFirstLevel;
use DrdPlus\Person\ProfessionLevels\ProfessionLevel;
use DrdPlus\Person\ProfessionLevels\ProfessionLevels;
use DrdPlus\Person\ProfessionLevels\ProfessionNextLevel;
use DrdPlus\Person\ProfessionLevels\ProfessionZeroLevel;
use DrdPlus\BaseProperties\Agility;
use DrdPlus\BaseProperties\BaseProperty;
use DrdPlus\BaseProperties\Charisma;
use DrdPlus\BaseProperties\Intelligence;
use DrdPlus\BaseProperties\Knack;
use DrdPlus\BaseProperties\Strength;
use DrdPlus\BaseProperties\Will;
use \DrdPlus\Professions\Fighter;
use \DrdPlus\Professions\Priest;
use \DrdPlus\Professions\Profession;
use \DrdPlus\Professions\Ranger;
use \DrdPlus\Professions\Theurgist;
use \DrdPlus\Professions\Thief;
use \DrdPlus\Professions\Wizard;
use Granam\TestWithMockery\TestWithMockery;
use Mockery\MockInterface;

class ProfessionLevelsTest extends TestWithMockery
{

    /**
     * @test
     */
    public function I_can_create_it()
    {
        $zeroLevel = $this->createZeroLevel();
        $firstLevel = $this->createFirstLevel('fighter');
        $withFirstLevelOnly = new ProfessionLevels($zeroLevel, $firstLevel);
        self::assertNotNull($withFirstLevelOnly);

        $anotherInstance = ProfessionLevels::createIt($zeroLevel, $firstLevel);
        self::assertEquals($withFirstLevelOnly, $anotherInstance);

        $yetAnotherInstance = ProfessionLevels::createIt($zeroLevel, $firstLevel);
        self::assertNotSame($anotherInstance, $yetAnotherInstance);

        $withExplicitlyEmptyNextLevels = ProfessionLevels::createIt($zeroLevel, $firstLevel);
        self::assertEquals($withFirstLevelOnly, $withExplicitlyEmptyNextLevels);

        $withNextLevels = ProfessionLevels::createIt(
            $zeroLevel,
            $firstLevel,
            $nextLevels = [
                ProfessionNextLevel::createNextLevel(
                    Fighter::getIt(),
                    LevelRank::getIt(2),
                    Strength::getIt(1),
                    Agility::getIt(0),
                    Knack::getIt(0),
                    Will::getIt(0),
                    Intelligence::getIt(1),
                    Charisma::getIt(0)
                ),
            ]
        );
        self::assertNotEquals($withFirstLevelOnly, $withNextLevels);
    }

    /**
     * @return MockInterface|ProfessionZeroLevel
     */
    private function createZeroLevel()
    {
        $professionZeroLevel = $this->mockery(ProfessionZeroLevel::class);
        $professionZeroLevel->shouldReceive('getBasePropertyIncrement')
            ->with($this->type(PropertyCode::class))
            ->andReturn($baseProperty = $this->mockery(BaseProperty::class));
        $baseProperty->shouldReceive('getValue')
            ->andReturn(0);

        return $professionZeroLevel;
    }

    /**
     * @param string $professionCode
     * @return ProfessionFirstLevel|\Mockery\MockInterface
     */
    private function createFirstLevel($professionCode)
    {
        $firstLevel = $this->mockery(ProfessionFirstLevel::class);
        $this->addProfessionGetter($firstLevel, $professionCode);
        $firstLevel->shouldReceive('getLevelRank')
            ->andReturn($levelRank = $this->mockery(LevelRank::class));
        $levelRank->shouldReceive('getValue')
            ->andReturn(1);
        $this->addPropertyIncrementGetters(
            $firstLevel,
            $strength = $this->isPrimaryProperty(PropertyCode::STRENGTH, $professionCode) ? 1 : 0,
            $agility = $this->isPrimaryProperty(PropertyCode::AGILITY, $professionCode) ? 1 : 0,
            $knack = $this->isPrimaryProperty(PropertyCode::KNACK, $professionCode) ? 1 : 0,
            $will = $this->isPrimaryProperty(PropertyCode::WILL, $professionCode) ? 1 : 0,
            $intelligence = $this->isPrimaryProperty(PropertyCode::INTELLIGENCE, $professionCode) ? 1 : 0,
            $charisma = $this->isPrimaryProperty(PropertyCode::CHARISMA, $professionCode) ? 1 : 0
        );
        $this->addPrimaryPropertiesAnswer($firstLevel, $professionCode);

        return $firstLevel;
    }

    private function addProfessionGetter(MockInterface $professionLevel, $professionCode)
    {
        $professionLevel->shouldReceive('getProfession')
            ->andReturn($profession = $this->mockery(Profession::class));
        $profession->shouldReceive('getValue')
            ->andReturn($professionCode);
    }

    /*
     * EMPTY AFTER INITIALIZATION
     */

    /**
     * @test
     */
    public function I_got_everything_empty_or_zeroed_from_empty_new_levels()
    {
        $professionLevels = new ProfessionLevels(
            $zeroLevel = $this->createZeroLevel(),
            $firstLevel = $this->createFirstLevel(ProfessionCode::FIGHTER)
        );

        self::assertSame(0, $professionLevels->getNextLevelsStrengthModifier());
        self::assertSame(0, $professionLevels->getNextLevelsPropertyModifier(PropertyCode::getIt(PropertyCode::STRENGTH)));
        self::assertSame(0, $professionLevels->getNextLevelsAgilityModifier());
        self::assertSame(0, $professionLevels->getNextLevelsPropertyModifier(PropertyCode::getIt(PropertyCode::AGILITY)));
        self::assertSame(0, $professionLevels->getNextLevelsKnackModifier());
        self::assertSame(0, $professionLevels->getNextLevelsPropertyModifier(PropertyCode::getIt(PropertyCode::KNACK)));
        self::assertSame(0, $professionLevels->getNextLevelsWillModifier());
        self::assertSame(0, $professionLevels->getNextLevelsPropertyModifier(PropertyCode::getIt(PropertyCode::WILL)));
        self::assertSame(0, $professionLevels->getNextLevelsIntelligenceModifier());
        self::assertSame(0, $professionLevels->getNextLevelsPropertyModifier(PropertyCode::getIt(PropertyCode::INTELLIGENCE)));
        self::assertSame(0, $professionLevels->getNextLevelsCharismaModifier());
        self::assertSame(0, $professionLevels->getNextLevelsPropertyModifier(PropertyCode::getIt(PropertyCode::CHARISMA)));

        self::assertCount(0, $professionLevels->getProfessionNextLevels());
        self::assertEquals([$zeroLevel, $firstLevel], $professionLevels->getSortedProfessionLevels());
        $levelsFromIteration = [];
        foreach ($professionLevels as $professionLevel) {
            $levelsFromIteration[] = $professionLevel;
        }
        self::assertEquals($levelsFromIteration, $professionLevels->getSortedProfessionLevels());
    }

    /*
     * FIRST LEVELS
     */

    /**
     * @test
     */
    public function I_will_get_proper_value_of_first_level_properties()
    {
        $firstLevel = $this->createProfessionFirstLevel(ProfessionCode::FIGHTER);
        $this->addFirstLevelPropertyIncrementGetters($firstLevel, ProfessionCode::FIGHTER);
        $this->addPrimaryPropertiesAnswer($firstLevel, ProfessionCode::FIGHTER);
        $zeroLevel = $this->createZeroLevel();
        $professionLevels = $this->createProfessionLevelsWith($zeroLevel, $firstLevel);
        self::assertSame(
            $this->isPrimaryProperty(PropertyCode::STRENGTH, ProfessionCode::FIGHTER) ? 1 : 0,
            $professionLevels->getFirstLevelStrengthModifier()
        );
        self::assertSame(
            $this->isPrimaryProperty(PropertyCode::AGILITY, ProfessionCode::FIGHTER) ? 1 : 0,
            $professionLevels->getFirstLevelAgilityModifier()
        );
        self::assertSame(
            $this->isPrimaryProperty(PropertyCode::KNACK, ProfessionCode::FIGHTER) ? 1 : 0,
            $professionLevels->getFirstLevelKnackModifier()
        );
        self::assertSame(
            $this->isPrimaryProperty(PropertyCode::WILL, ProfessionCode::FIGHTER) ? 1 : 0,
            $professionLevels->getFirstLevelWillModifier()
        );
        self::assertSame(
            $this->isPrimaryProperty(PropertyCode::INTELLIGENCE, ProfessionCode::FIGHTER) ? 1 : 0,
            $professionLevels->getFirstLevelIntelligenceModifier()
        );
        self::assertSame(
            $this->isPrimaryProperty(PropertyCode::CHARISMA, ProfessionCode::FIGHTER) ? 1 : 0,
            $professionLevels->getFirstLevelCharismaModifier()
        );
    }

    private function addFirstLevelPropertyIncrementGetters(MockInterface $professionLevel, $professionCode)
    {
        $modifiers = [];
        foreach ($this->getPropertyNames() as $propertyName) {
            $modifiers[$propertyName] = $this->isPrimaryProperty($propertyName, $professionCode) ? 1 : 0;
        }
        $this->addPropertyIncrementGetters(
            $professionLevel,
            $modifiers[PropertyCode::STRENGTH],
            $modifiers[PropertyCode::AGILITY],
            $modifiers[PropertyCode::KNACK],
            $modifiers[PropertyCode::WILL],
            $modifiers[PropertyCode::INTELLIGENCE],
            $modifiers[PropertyCode::CHARISMA]
        );
    }

    private function addPropertyIncrementGetters(
        MockInterface $professionLevel,
        $strengthValue = 0,
        $agilityValue = 0,
        $knackValue = 0,
        $willValue = 0,
        $intelligenceValue = 0,
        $charismaValue = 0
    )
    {
        $professionLevel->shouldReceive('getStrengthIncrement')
            ->andReturn($strength = $this->mockery(Strength::class));
        $professionLevel->shouldReceive('getBasePropertyIncrement')
            ->with(PropertyCode::getIt(PropertyCode::STRENGTH))
            ->andReturn($strength);
        $this->addValueGetter($strength, $strengthValue);
        $this->addCodeGetter($strength, PropertyCode::STRENGTH);
        $professionLevel->shouldReceive('getAgilityIncrement')
            ->andReturn($agility = $this->mockery(Agility::class));
        $professionLevel->shouldReceive('getBasePropertyIncrement')
            ->with(PropertyCode::getIt(PropertyCode::AGILITY))
            ->andReturn($agility);
        $this->addValueGetter($agility, $agilityValue);
        $this->addCodeGetter($agility, PropertyCode::AGILITY);
        $professionLevel->shouldReceive('getKnackIncrement')
            ->andReturn($knack = $this->mockery(Knack::class));
        $professionLevel->shouldReceive('getBasePropertyIncrement')
            ->with(PropertyCode::getIt(PropertyCode::KNACK))
            ->andReturn($knack);
        $this->addValueGetter($knack, $knackValue);
        $this->addCodeGetter($knack, PropertyCode::KNACK);
        $professionLevel->shouldReceive('getWillIncrement')
            ->andReturn($will = $this->mockery(Will::class));
        $professionLevel->shouldReceive('getBasePropertyIncrement')
            ->with(PropertyCode::getIt(PropertyCode::WILL))
            ->andReturn($will);
        $this->addValueGetter($will, $willValue);
        $this->addCodeGetter($will, PropertyCode::WILL);
        $professionLevel->shouldReceive('getIntelligenceIncrement')
            ->andReturn($intelligence = $this->mockery(Intelligence::class));
        $professionLevel->shouldReceive('getBasePropertyIncrement')
            ->with(PropertyCode::getIt(PropertyCode::INTELLIGENCE))
            ->andReturn($intelligence);
        $this->addValueGetter($intelligence, $intelligenceValue);
        $this->addCodeGetter($intelligence, PropertyCode::INTELLIGENCE);
        $professionLevel->shouldReceive('getCharismaIncrement')
            ->andReturn($charisma = $this->mockery(Charisma::class));
        $professionLevel->shouldReceive('getBasePropertyIncrement')
            ->with(PropertyCode::getIt(PropertyCode::CHARISMA))
            ->andReturn($charisma);
        $this->addValueGetter($charisma, $charismaValue);
        $this->addCodeGetter($charisma, PropertyCode::CHARISMA);
    }

    private function addValueGetter(MockInterface $property, $value)
    {
        $property->shouldReceive('getValue')
            ->andReturn($value);
    }

    private function addCodeGetter(MockInterface $property, string $code)
    {
        $property->shouldReceive('getCode')
            ->andReturn(PropertyCode::getIt($code));
    }

    private function addPrimaryPropertiesAnswer(MockInterface $professionLevel, string $professionCode)
    {
        $modifiers = [];
        foreach ($this->getPropertyNames() as $propertyName) {
            $modifiers[$propertyName] = $this->isPrimaryProperty($propertyName, $professionCode) ? 1 : 0;
        }
        $primaryProperties = array_keys(array_filter($modifiers));

        foreach ($this->getPropertyNames() as $propertyName) {
            $professionLevel->shouldReceive('isPrimaryProperty')
                ->with(PropertyCode::getIt($propertyName))
                ->andReturn(in_array($propertyName, $primaryProperties, true));
        }
    }

    /**
     * @return array|string[]
     */
    private function getPropertyNames(): array
    {
        return PropertyCode::getBasePropertyPossibleValues();
    }

    private function addFirstLevelAnswer(MockInterface $professionLevel, $isFirstLevel)
    {
        $professionLevel->shouldReceive('isFirstLevel')
            ->andReturn($isFirstLevel);
    }

    private function addNextLevelAnswer(MockInterface $professionLevel, $isNextLevel)
    {
        $professionLevel->shouldReceive('isNextLevel')
            ->andReturn($isNextLevel);
    }

    /**
     * @param string $professionCode
     * @return ProfessionFirstLevel|ProfessionNextLevel|\Mockery\MockInterface
     */
    private function createProfessionFirstLevel($professionCode)
    {
        return $this->createProfessionLevel($professionCode, 1);
    }

    /**
     * @param string $professionCode
     * @param int $levelValue
     * @param ProfessionLevels|null $professionLevels
     * @return ProfessionFirstLevel|ProfessionNextLevel|MockInterface
     */
    private function createProfessionLevel($professionCode, $levelValue, ProfessionLevels $professionLevels = null)
    {
        /** @var \Mockery\MockInterface|ProfessionLevel $professionLevel */
        $professionLevel = $this->mockery($this->getProfessionLevelClass($levelValue));
        $professionLevel->shouldReceive('getProfession')
            ->andReturn($profession = $this->mockery(Profession::class));
        $profession->shouldReceive('getValue')
            ->andReturn($professionCode);
        $this->addFirstLevelAnswer($professionLevel, $levelValue === 1);
        $this->addNextLevelAnswer($professionLevel, $levelValue > 1);
        $professionLevel->shouldReceive('getLevelRank')
            ->andReturn($levelRank = $this->mockery(LevelRank::class));
        $levelRank->shouldReceive('getValue')
            ->andReturn($levelValue);
        if ($professionLevels !== null) {
            $professionLevel->shouldReceive('setProfessionLevels')
                ->with($professionLevels)
                ->atMost()
                ->once();
        }

        return $professionLevel;
    }

    /**
     * @param string $propertyName
     * @param string $professionCode
     * @return bool
     */
    private function isPrimaryProperty($propertyName, $professionCode): bool
    {
        return in_array($propertyName, $this->getPrimaryProperties($professionCode), true);
    }

    /**
     * @param string $professionCode
     * @return array
     * @throws \RuntimeException
     */
    private function getPrimaryProperties(string $professionCode): array
    {
        switch ($professionCode) {
            case ProfessionCode::FIGHTER :
                return [PropertyCode::STRENGTH, PropertyCode::AGILITY];
            case ProfessionCode::PRIEST :
                return [PropertyCode::WILL, PropertyCode::CHARISMA];
            case ProfessionCode::RANGER :
                return [PropertyCode::STRENGTH, PropertyCode::KNACK];
            case ProfessionCode::THEURGIST :
                return [PropertyCode::INTELLIGENCE, PropertyCode::CHARISMA];
            case ProfessionCode::THIEF :
                return [PropertyCode::KNACK, PropertyCode::AGILITY];
            case ProfessionCode::WIZARD :
                return [PropertyCode::WILL, PropertyCode::INTELLIGENCE];
            default :
                throw new \RuntimeException('Unknown profession name ' . var_export($professionCode, true));
        }
    }

    /**
     * @param ProfessionZeroLevel $zeroLevel
     * @param ProfessionFirstLevel $firstLevel
     * @return ProfessionLevels
     */
    private function createProfessionLevelsWith(ProfessionZeroLevel $zeroLevel, ProfessionFirstLevel $firstLevel): ProfessionLevels
    {
        $professionLevels = new ProfessionLevels($zeroLevel, $firstLevel);
        self::assertSame($firstLevel, $professionLevels->getFirstLevel());
        self::assertEquals([$zeroLevel, $firstLevel], $professionLevels->getSortedProfessionLevels());

        return $professionLevels;
    }

    /**
     * @test
     */
    public function I_can_add_profession_level()
    {
        $professionLevels = new ProfessionLevels(
            $zeroLevel = $this->createZeroLevel(),
            $firstLevel = $this->createFirstLevel(ProfessionCode::FIGHTER)
        );
        $nextLevel = $this->createProfessionLevel(ProfessionCode::FIGHTER, $levelValue = 2, $professionLevels);
        $this->addPropertyIncrementGetters(
            $nextLevel,
            $strength = $this->isPrimaryProperty(PropertyCode::STRENGTH, ProfessionCode::FIGHTER) ? 1 : 0,
            $agility = $this->isPrimaryProperty(PropertyCode::AGILITY, ProfessionCode::FIGHTER) ? 1 : 0,
            $knack = $this->isPrimaryProperty(PropertyCode::KNACK, ProfessionCode::FIGHTER) ? 1 : 0,
            $will = $this->isPrimaryProperty(PropertyCode::WILL, ProfessionCode::FIGHTER) ? 1 : 0,
            $intelligence = $this->isPrimaryProperty(PropertyCode::INTELLIGENCE, ProfessionCode::FIGHTER) ? 1 : 0,
            $charisma = $this->isPrimaryProperty(PropertyCode::CHARISMA, ProfessionCode::FIGHTER) ? 1 : 0
        );
        $this->addPrimaryPropertiesAnswer($nextLevel, ProfessionCode::FIGHTER);
        $professionLevels->addLevel($nextLevel);

        $strength += $firstLevel->getStrengthIncrement()->getValue();
        $agility += $firstLevel->getAgilityIncrement()->getValue();
        $knack += $firstLevel->getKnackIncrement()->getValue();
        $will += $firstLevel->getWillIncrement()->getValue();
        $intelligence += $firstLevel->getIntelligenceIncrement()->getValue();
        $charisma += $firstLevel->getCharismaIncrement()->getValue();

        self::assertSame($zeroLevel, $professionLevels->getZeroLevel());
        self::assertSame($firstLevel, $professionLevels->getFirstLevel());
        self::assertSame([$nextLevel], $professionLevels->getProfessionNextLevels());
        self::assertEquals([$zeroLevel, $firstLevel, $nextLevel], $professionLevels->getSortedProfessionLevels());
        self::assertEquals($nextLevel, $professionLevels->getCurrentLevel());

        self::assertSame($strength, $professionLevels->getStrengthModifierSummary());
        self::assertSame($strength, $professionLevels->getPropertyModifierSummary(PropertyCode::getIt(PropertyCode::STRENGTH)));
        self::assertSame($agility, $professionLevels->getAgilityModifierSummary());
        self::assertSame($agility, $professionLevels->getPropertyModifierSummary(PropertyCode::getIt(PropertyCode::AGILITY)));
        self::assertSame($knack, $professionLevels->getKnackModifierSummary());
        self::assertSame($knack, $professionLevels->getPropertyModifierSummary(PropertyCode::getIt(PropertyCode::KNACK)));
        self::assertSame($will, $professionLevels->getWillModifierSummary());
        self::assertSame($will, $professionLevels->getPropertyModifierSummary(PropertyCode::getIt(PropertyCode::WILL)));
        self::assertSame($intelligence, $professionLevels->getIntelligenceModifierSummary());
        self::assertSame($intelligence, $professionLevels->getPropertyModifierSummary(PropertyCode::getIt(PropertyCode::INTELLIGENCE)));
        self::assertSame($charisma, $professionLevels->getCharismaModifierSummary());
        self::assertSame($charisma, $professionLevels->getPropertyModifierSummary(PropertyCode::getIt(PropertyCode::CHARISMA)));

        self::assertSame($firstLevel->getStrengthIncrement()->getValue(), $professionLevels->getFirstLevelStrengthModifier());
        self::assertSame($firstLevel->getAgilityIncrement()->getValue(), $professionLevels->getFirstLevelAgilityModifier());
        self::assertSame($firstLevel->getKnackIncrement()->getValue(), $professionLevels->getFirstLevelKnackModifier());
        self::assertSame($firstLevel->getWillIncrement()->getValue(), $professionLevels->getFirstLevelWillModifier());
        self::assertSame($firstLevel->getIntelligenceIncrement()->getValue(), $professionLevels->getFirstLevelIntelligenceModifier());
        self::assertSame($firstLevel->getCharismaIncrement()->getValue(), $professionLevels->getFirstLevelCharismaModifier());

        self::assertSame($nextLevel->getStrengthIncrement()->getValue(), $professionLevels->getNextLevelsStrengthModifier());
        self::assertSame($nextLevel->getAgilityIncrement()->getValue(), $professionLevels->getNextLevelsAgilityModifier());
        self::assertSame($nextLevel->getKnackIncrement()->getValue(), $professionLevels->getNextLevelsKnackModifier());
        self::assertSame($nextLevel->getWillIncrement()->getValue(), $professionLevels->getNextLevelsWillModifier());
        self::assertSame($nextLevel->getIntelligenceIncrement()->getValue(), $professionLevels->getNextLevelsIntelligenceModifier());
        self::assertSame($nextLevel->getCharismaIncrement()->getValue(), $professionLevels->getNextLevelsCharismaModifier());
    }

    /**
     * @param int $levelValue
     * @return string
     */
    private function getProfessionLevelClass($levelValue): string
    {
        return (int)$levelValue === 1
            ? ProfessionFirstLevel::class
            : ProfessionNextLevel::class;
    }

    /*
     * MORE LEVELS
     */

    /**
     * @test
     */
    public function I_can_add_more_levels_of_same_profession()
    {
        $firstLevel = $this->createProfessionFirstLevel(ProfessionCode::FIGHTER);
        $this->addPrimaryPropertiesAnswer($firstLevel, ProfessionCode::FIGHTER);
        $this->addFirstLevelAnswer($firstLevel, true);
        $this->addNextLevelAnswer($firstLevel, false);
        $this->addPropertyIncrementGetters(
            $firstLevel, $strength = 1, $agility = 2, $knack = 3, $will = 4, $intelligence = 5, $charisma = 6
        );
        $zeroLevel = $this->createZeroLevel();
        $professionLevels = $this->createProfessionLevelsWith($zeroLevel, $firstLevel);

        self::assertCount(2, $professionLevels->getSortedProfessionLevels());
        self::assertSame($zeroLevel, $professionLevels->getZeroLevel());
        self::assertSame($firstLevel, $professionLevels->getFirstLevel());
        self::assertSame([$zeroLevel, $firstLevel], $professionLevels->getSortedProfessionLevels());

        $propertiesSummary = $firstLevelProperties = [];
        foreach ($this->getPropertyNames() as $propertyName) {
            $firstLevelProperties[$propertyName] = $propertiesSummary[$propertyName] = $$propertyName;
        }
        $secondLevel = $this->createProfessionLevel(ProfessionCode::FIGHTER, 2, $professionLevels);
        $this->addPropertyIncrementGetters(
            $secondLevel, $strength = 1, $agility = 2, $knack = 3, $will = 4, $intelligence = 5, $charisma = 6
        );
        $this->addPrimaryPropertiesAnswer($secondLevel, ProfessionCode::FIGHTER);
        $this->addNextLevelAnswer($secondLevel, true);
        $nextLevelProperties = [];
        foreach ($this->getPropertyNames() as $propertyName) {
            $nextLevelProperties[$propertyName] = $$propertyName;
            $propertiesSummary[$propertyName] += $$propertyName;
        }
        $professionLevels->addLevel($secondLevel);

        $thirdLevel = $this->createProfessionLevel(ProfessionCode::FIGHTER, 3, $professionLevels);
        $this->addPropertyIncrementGetters(
            $thirdLevel,
            $strength = ($this->isPrimaryProperty(PropertyCode::STRENGTH, ProfessionCode::FIGHTER) ? 7 : 0),
            $agility = ($this->isPrimaryProperty(PropertyCode::AGILITY, ProfessionCode::FIGHTER) ? 8 : 0),
            $knack = ($this->isPrimaryProperty(PropertyCode::KNACK, ProfessionCode::FIGHTER) ? 9 : 0),
            $will = ($this->isPrimaryProperty(PropertyCode::WILL, ProfessionCode::FIGHTER) ? 10 : 0),
            $intelligence = ($this->isPrimaryProperty(PropertyCode::INTELLIGENCE, ProfessionCode::FIGHTER) ? 11 : 0),
            $charisma = ($this->isPrimaryProperty(PropertyCode::CHARISMA, ProfessionCode::FIGHTER) ? 12 : 0)
        );
        $this->addPrimaryPropertiesAnswer($thirdLevel, ProfessionCode::FIGHTER);
        foreach ($this->getPropertyNames() as $propertyName) {
            $propertiesSummary[$propertyName] += $$propertyName;
            $nextLevelProperties[$propertyName] += $$propertyName;
        }
        $professionLevels->addLevel($thirdLevel);

        self::assertSame($zeroLevel, $professionLevels->getZeroLevel());
        self::assertSame($firstLevel, $professionLevels->getFirstLevel(), 'After adding a new level the old one is no more the first.');
        self::assertSame([$zeroLevel, $firstLevel, $secondLevel, $thirdLevel], $professionLevels->getSortedProfessionLevels());
        self::assertSame([$secondLevel, $thirdLevel], $professionLevels->getProfessionNextLevels());

        $levelsArray = [];
        foreach ($professionLevels as $professionLevel) {
            $levelsArray[] = $professionLevel;
        }
        self::assertEquals($professionLevels->getSortedProfessionLevels(), $levelsArray);
        self::assertSame($thirdLevel, $professionLevels->getCurrentLevel());
        self::assertSame(count($levelsArray), $professionLevels->getCurrentLevel()->getLevelRank()->getValue() + 1 /* zero level */);

        self::assertSame($propertiesSummary[PropertyCode::STRENGTH], $professionLevels->getStrengthModifierSummary());
        self::assertSame($propertiesSummary[PropertyCode::AGILITY], $professionLevels->getAgilityModifierSummary());
        self::assertSame($propertiesSummary[PropertyCode::KNACK], $professionLevels->getKnackModifierSummary());
        self::assertSame($propertiesSummary[PropertyCode::WILL], $professionLevels->getWillModifierSummary());
        self::assertSame($propertiesSummary[PropertyCode::INTELLIGENCE], $professionLevels->getIntelligenceModifierSummary());
        self::assertSame($propertiesSummary[PropertyCode::CHARISMA], $professionLevels->getCharismaModifierSummary());

        self::assertSame($firstLevelProperties[PropertyCode::STRENGTH], $professionLevels->getFirstLevelStrengthModifier());
        self::assertSame($firstLevelProperties[PropertyCode::AGILITY], $professionLevels->getFirstLevelAgilityModifier());
        self::assertSame($firstLevelProperties[PropertyCode::KNACK], $professionLevels->getFirstLevelKnackModifier());
        self::assertSame($firstLevelProperties[PropertyCode::WILL], $professionLevels->getFirstLevelWillModifier());
        self::assertSame($firstLevelProperties[PropertyCode::INTELLIGENCE], $professionLevels->getFirstLevelIntelligenceModifier());
        self::assertSame($firstLevelProperties[PropertyCode::CHARISMA], $professionLevels->getFirstLevelCharismaModifier());

        self::assertSame($nextLevelProperties[PropertyCode::STRENGTH], $professionLevels->getNextLevelsStrengthModifier());
        self::assertSame($nextLevelProperties[PropertyCode::AGILITY], $professionLevels->getNextLevelsAgilityModifier());
        self::assertSame($nextLevelProperties[PropertyCode::KNACK], $professionLevels->getNextLevelsKnackModifier());
        self::assertSame($nextLevelProperties[PropertyCode::WILL], $professionLevels->getNextLevelsWillModifier());
        self::assertSame($nextLevelProperties[PropertyCode::INTELLIGENCE], $professionLevels->getNextLevelsIntelligenceModifier());
        self::assertSame($nextLevelProperties[PropertyCode::CHARISMA], $professionLevels->getNextLevelsCharismaModifier());
    }

    /**
     * @test
     */
    public function I_can_not_add_level_with_occupied_sequence()
    {
        $this->expectException(\DrdPlus\Person\ProfessionLevels\Exceptions\InvalidLevelRank::class);
        $professionLevels = $this->createProfessionLevelsForChangeResistTest(ProfessionCode::FIGHTER);

        $levelsCount = count($professionLevels->getSortedProfessionLevels());
        self::assertGreaterThan(2, $levelsCount /* already occupied level rank to achieve conflict */);

        $anotherLevel = $this->createProfessionLevel(ProfessionCode::FIGHTER, $levelsCount - 1 /* zero level */);

        $professionLevels->addLevel($anotherLevel);
    }

    /**
     * @test
     */
    public function I_can_not_add_level_with_out_of_sequence_rank()
    {
        $this->expectException(\DrdPlus\Person\ProfessionLevels\Exceptions\InvalidLevelRank::class);
        $professionLevels = $this->createProfessionLevelsForChangeResistTest(ProfessionCode::FIGHTER);
        $levelsCount = count($professionLevels->getSortedProfessionLevels());
        self::assertGreaterThan(1, $levelsCount);

        $anotherLevel = $this->createProfessionLevel(ProfessionCode::FIGHTER, $levelsCount + 2 /* skipping a rank by one */, $professionLevels);

        $professionLevels->addLevel($anotherLevel);
    }

    private function createProfessionLevelsForChangeResistTest(string $professionCode): ProfessionLevels
    {
        $firstLevel = $this->createProfessionFirstLevel($professionCode);
        $this->addPrimaryPropertiesAnswer($firstLevel, $professionCode);
        $this->addFirstLevelAnswer($firstLevel, true);
        $this->addNextLevelAnswer($firstLevel, false);
        $this->addPropertyIncrementGetters(
            $firstLevel, $strength = 1, $agility = 2, $knack = 3, $will = 4, $intelligence = 5, $charisma = 6
        );
        $zeroLevel = $this->createZeroLevel();
        $professionLevels = $this->createProfessionLevelsWith($zeroLevel, $firstLevel);

        self::assertCount(2, $professionLevels->getSortedProfessionLevels());
        self::assertSame($zeroLevel, $professionLevels->getZeroLevel());
        self::assertSame($firstLevel, $professionLevels->getFirstLevel());
        self::assertEquals([$zeroLevel, $firstLevel], $professionLevels->getSortedProfessionLevels());

        $secondLevel = $this->createProfessionLevel($professionCode, 2, $professionLevels);
        $this->addPropertyIncrementGetters(
            $secondLevel, $strength = 1, $agility = 2, $knack = 3, $will = 4, $intelligence = 5, $charisma = 6
        );
        $this->addPrimaryPropertiesAnswer($secondLevel, $professionCode);
        $this->addNextLevelAnswer($secondLevel, true);

        $professionLevels->addLevel($secondLevel);

        $thirdLevel = $this->createProfessionLevel($professionCode, 3, $professionLevels);
        $this->addPropertyIncrementGetters(
            $thirdLevel,
            $strength = ($this->isPrimaryProperty(PropertyCode::STRENGTH, $professionCode) ? 7 : 0),
            $agility = ($this->isPrimaryProperty(PropertyCode::AGILITY, $professionCode) ? 8 : 0),
            $knack = ($this->isPrimaryProperty(PropertyCode::KNACK, $professionCode) ? 9 : 0),
            $will = ($this->isPrimaryProperty(PropertyCode::WILL, $professionCode) ? 10 : 0),
            $intelligence = ($this->isPrimaryProperty(PropertyCode::INTELLIGENCE, $professionCode) ? 11 : 0),
            $charisma = ($this->isPrimaryProperty(PropertyCode::CHARISMA, $professionCode) ? 12 : 0)
        );
        $this->addPrimaryPropertiesAnswer($thirdLevel, $professionCode);
        $professionLevels->addLevel($thirdLevel);

        return $professionLevels;
    }

    /*
     * ONLY SINGLE PROFESSION IS ALLOWED
     */

    /**
     * @test
     */
    public function I_can_not_mix_professions()
    {
        $professionLevels = $this->createProfessionLevelsForMixTest(ProfessionCode::FIGHTER);
        /** @var ProfessionFirstLevel|\Mockery\MockInterface $firstLevel */
        $firstLevel = $professionLevels->getFirstLevel();
        self::assertInstanceOf(ProfessionFirstLevel::class, $firstLevel);

        $otherLevels = $this->getLevelsExcept($firstLevel);
        self::assertNotEmpty($otherLevels);

        foreach ($otherLevels as $professionCode => $otherProfessionLevel) {
            try {
                $professionLevels->addLevel($otherProfessionLevel);
                self::fail(
                    "Adding $professionCode to levels already set to {$firstLevel->getProfession()->getValue()} should throw exception."
                );
            } catch (MultiProfessionsAreProhibited $exception) {
                self::assertNotNull($exception);
            }
        }
    }

    /**
     * @param string $professionCode
     * @return ProfessionLevels
     */
    private function createProfessionLevelsForMixTest(string $professionCode): ProfessionLevels
    {
        return new ProfessionLevels(
            $this->createZeroLevel(),
            $firstLevel = $this->createFirstLevel($professionCode)
        );
    }

    /**
     * @param ProfessionLevel $excludedProfession
     * @return \Mockery\MockInterface[]|ProfessionFirstLevel[]|ProfessionNextLevel[]
     */
    private function getLevelsExcept(ProfessionLevel $excludedProfession): array
    {
        $professionLevels = $this->buildProfessionLevels();

        return array_filter(
            $professionLevels,
            fn(ProfessionLevel $level) => $level->getProfession()->getValue() !== $excludedProfession->getProfession()->getValue()
        );
    }

    /**
     * @return array|ProfessionNextLevel[]
     */
    private function buildProfessionLevels(): array
    {
        $professions = [
            ProfessionCode::FIGHTER => Fighter::class,
            ProfessionCode::PRIEST => Priest::class,
            ProfessionCode::RANGER => Ranger::class,
            ProfessionCode::THEURGIST => Theurgist::class,
            ProfessionCode::THIEF => Thief::class,
            ProfessionCode::WIZARD => Wizard::class,
        ];
        $professionLevels = [];
        foreach ($professions as $professionCode => $professionClass) {
            $professionLevels[$professionCode] = $level = $this->mockery(ProfessionNextLevel::class);
            $profession = $this->mockery($professionClass);
            $profession->shouldReceive('getValue')
                ->andReturn($professionCode);
            $level->shouldReceive('getProfession')
                ->andReturn($profession);
        }

        return $professionLevels;
    }

    /*
     * SAME PROPERTY INCREMENT IN A ROW
     */

    /**
     * @test
     */
    public function I_can_not_increase_primary_property_three_times_in_a_row()
    {
        $this->expectException(\DrdPlus\Person\ProfessionLevels\Exceptions\TooHighPrimaryPropertyIncrease::class);
        try {
            $firstLevel = $this->createProfessionLevelWithPrimaryPropertiesIncreased(ProfessionCode::FIGHTER, 1);
            $zeroLevel = $this->createZeroLevel();
            // the first level does not come to property increment check
            $professionLevels = new ProfessionLevels($zeroLevel, $firstLevel);

            // the second level will be taken into account on check of fourth level
            $secondLevel = $this->createProfessionLevelWithPrimaryPropertiesIncreased(
                ProfessionCode::FIGHTER,
                $professionLevels->getCurrentLevel()->getLevelRank()->getValue() + 1,
                true, // first primary property increment
                false, // no second primary property increment
                $professionLevels
            );
            $professionLevels->addLevel($secondLevel);

            // with both primary properties increased
            $thirdLevel = $this->createProfessionLevelWithPrimaryPropertiesIncreased(
                ProfessionCode::FIGHTER,
                $professionLevels->getCurrentLevel()->getLevelRank()->getValue() + 1,
                false, // no first primary property increment
                true, // second primary property increment
                $professionLevels
            );
            $professionLevels->addLevel($thirdLevel);

            // again with both primary properties increased
            $fourthLevel = $this->createProfessionLevelWithPrimaryPropertiesIncreased(
                ProfessionCode::FIGHTER,
                $professionLevels->getCurrentLevel()->getLevelRank()->getValue() + 1,
                true, // first primary property increment
                true, // second primary property increment
                $professionLevels
            );
            $professionLevels->addLevel($fourthLevel); //should pass
        } catch (\Exception $exception) {
            self::fail(
                'No exception should happen this far: ' . $exception->getMessage()
                . ' (' . $exception->getTraceAsString() . ')'
            );

            return;
        }

        // and again with both primary properties increased
        $fifthLevel = $this->createProfessionLevelWithPrimaryPropertiesIncreased(
            ProfessionCode::FIGHTER,
            $professionLevels->getCurrentLevel()->getLevelRank()->getValue() + 1
        );
        $professionLevels->addLevel($fifthLevel); // should fail
    }

    private function createProfessionLevelWithPrimaryPropertiesIncreased(
        $professionCode,
        $levelValue,
        $increaseFirstPrimaryProperty = true,
        $increaseSecondPrimaryProperty = true,
        ProfessionLevels $professionLevels = null
    )
    {
        $professionLevel = $this->createProfessionLevel($professionCode, $levelValue, $professionLevels);
        $propertyIncrements = [];
        $isFirst = true;
        foreach ($this->getPropertyNames() as $propertyCode) {
            $increment = $this->isPrimaryProperty($propertyCode, $professionCode) ? 1 : 0;
            if ($increment) {
                if ($isFirst) {
                    $isFirst = false;
                    $increment &= $increaseFirstPrimaryProperty;
                } else {
                    $increment &= $increaseSecondPrimaryProperty;
                }
            }
            $propertyIncrements[$propertyCode] = $increment;
        }
        $this->addPropertyIncrementGetters(
            $professionLevel,
            $propertyIncrements[PropertyCode::STRENGTH],
            $propertyIncrements[PropertyCode::AGILITY],
            $propertyIncrements[PropertyCode::KNACK],
            $propertyIncrements[PropertyCode::WILL],
            $propertyIncrements[PropertyCode::INTELLIGENCE],
            $propertyIncrements[PropertyCode::CHARISMA]
        );
        $this->addPrimaryPropertiesAnswer($professionLevel, $professionCode);

        return $professionLevel;
    }

    /**
     * @param string $professionCode
     * @test
     * dataProvider provideProfessionCode
     */
    public function I_can_not_increase_secondary_property_two_times_in_a_row($professionCode = 'fighter')
    {
        $this->expectException(\DrdPlus\Person\ProfessionLevels\Exceptions\TooHighSecondaryPropertyIncrease::class);
        try {
            $firstLevel = $this->createProfessionLevelWithSecondaryPropertiesIncreased($professionCode, 1);
            // the first level does not come to property increment check
            $zeroLevel = $this->createZeroLevel();
            $professionLevels = new ProfessionLevels($zeroLevel, $firstLevel);

            // the second level will be taken into account on check of third level
            $secondLevel = $this->createProfessionLevelWithSecondaryPropertiesIncreased(
                $professionCode,
                $professionLevels->getCurrentLevel()->getLevelRank()->getValue() + 1,
                false, // without secondary properties increment
                $professionLevels
            );
            $professionLevels->addLevel($secondLevel);

            $thirdLevel = $this->createProfessionLevelWithSecondaryPropertiesIncreased(
                $professionCode,
                $professionLevels->getCurrentLevel()->getLevelRank()->getValue() + 1,
                true, // with secondary properties increment
                $professionLevels
            );
            $professionLevels->addLevel($thirdLevel); // should pass
        } catch (\Exception $exception) {
            self::fail(
                "No exception should happen this far: {$exception->getMessage()} ({$exception->getTraceAsString()})"
            );

            return;
        }
        $fourthLevel = $this->createProfessionLevelWithSecondaryPropertiesIncreased(
            $professionCode,
            $professionLevels->getCurrentLevel()->getLevelRank()->getValue() + 1,
            true, // with secondary properties increment
            $professionLevels
        );
        $professionLevels->addLevel($fourthLevel); // should fail
    }

    private function createProfessionLevelWithSecondaryPropertiesIncreased(
        $professionCode,
        $levelValue,
        $increaseSecondaryProperties = true,
        ProfessionLevels $professionLevels = null
    )
    {
        $professionLevel = $this->createProfessionLevel($professionCode, $levelValue, $professionLevels);
        $propertyIncrements = [];
        foreach ($this->getPropertyNames() as $propertyCode) {
            $increment = $increaseSecondaryProperties
            && !$this->isPrimaryProperty($propertyCode, $professionCode) ? 1 : 0;
            $propertyIncrements[$propertyCode] = $increment;
        }
        $this->addPropertyIncrementGetters(
            $professionLevel,
            $propertyIncrements[PropertyCode::STRENGTH],
            $propertyIncrements[PropertyCode::AGILITY],
            $propertyIncrements[PropertyCode::KNACK],
            $propertyIncrements[PropertyCode::WILL],
            $propertyIncrements[PropertyCode::INTELLIGENCE],
            $propertyIncrements[PropertyCode::CHARISMA]
        );
        $this->addPrimaryPropertiesAnswer($professionLevel, $professionCode);

        return $professionLevel;
    }
}