<?php
namespace Codeception\Module;

use Codeception\Lib\Interfaces\DataMapper;
use Codeception\Lib\Interfaces\DependsOnModule;
use Codeception\Lib\Interfaces\ORM;
use Codeception\Exception\ModuleException;
use Codeception\Lib\Interfaces\RequiresPackage;
use Codeception\TestInterface;
use League\FactoryMuffin\FactoryMuffin;
use League\FactoryMuffin\Stores\RepositoryStore;

/**
 * DataFactory allows you to easily generate and create test data using [**FactoryMuffin**](https://github.com/thephpleague/factory-muffin).
 * DataFactory uses an ORM of your application to define, save and cleanup data. Thus, should be used with ORM or Framework modules.
 *
 * This module requires packages installed:
 *
 * ```json
 * {
 *  "league/factory-muffin": "^3.0",
 * }
 * ```
 *
 * Generation rules can be defined in a factories file. You will need to create `factories.php` (it is recommended to store it in `_support` dir)
 * Follow [FactoryMuffin documentation](https://github.com/thephpleague/factory-muffin) to set valid rules.
 * Random data provided by [Faker](https://github.com/fzaninotto/Faker) library.
 *
 * ```php
 * <?php
 * use League\FactoryMuffin\Faker\Facade as Faker;
 *
 * $fm->define(User::class)->setDefinitions([
 *  'name'   => Faker::name(),
 *
 *     // generate email
 *    'email'  => Faker::email(),
 *    'body'   => Faker::text(),
 *
 *    // generate a profile and return its Id
 *    'profile_id' => 'factory|Profile'
 * ]);
 * ```
 *
 * Configure this module to load factory definitions from a directory.
 * You should also specify a module with an ORM as a dependency.
 *
 * ```yaml
 * modules:
 *     enabled:
 *         - Yii2:
 *             configFile: path/to/config.php
 *         - DataFactory:
 *             factories: tests/_support/factories
 *             depends: Yii2
 * ```
 *
 * (you can also use Laravel5 and Phalcon).
 *
 * In this example factories are loaded from `tests/_support/factories` directory. Please note that this directory is relative from the codeception.yml file (so for Yii2 it would be codeception/_support/factories).
 * You should create this directory manually and create PHP files in it with factories definitions following [official documentation](https://github.com/thephpleague/factory-muffin#usage).
 *
 * In cases you want to use data from database inside your factory definitions you can define them in Helper.
 * For instance, if you use Doctrine, this allows you to access `EntityManager` inside a definition.
 *
 * To proceed you should create Factories helper via `generate:helper` command and enable it:
 *
 * ```
 * modules:
 *     enabled:
 *         - DataFactory:
 *             depends: Doctrine2
 *         - \Helper\Factories
 *
 * ```
 *
 * In this case you can define factories from a Helper class with `_define` method.
 *
 * ```php
 * <?php
 * public function _beforeSuite()
 * {
 *      $factory = $this->getModule('DataFactory');
 *      // let us get EntityManager from Doctrine
 *      $em = $this->getModule('Doctrine2')->_getEntityManager();
 *
 *      $factory->_define(User::class, [
 *
 *          // generate random user name
 *          // use League\FactoryMuffin\Faker\Facade as Faker;
 *          'name' => Faker::name(),
 *
 *          // get real company from database
 *          'company' => $em->getRepository(Company::class)->find(),
 *
 *          // let's generate a profile for each created user
 *          // receive an entity and set it via `setProfile` method
 *          // UserProfile factory should be defined as well
 *          'profile' => 'entity|'.UserProfile::class
 *      ]);
 * }
 * ```
 *
 * Factory Definitions are described in official [Factory Muffin Documentation](https://github.com/thephpleague/factory-muffin)
 *
 * ### Related Models Generators
 *
 * If your module relies on other model you can generate them both.
 * To create a related module you can use either `factory` or `entity` prefix, depending on ORM you use.
 *
 * In case your ORM expects an Id of a related record (Eloquent) to be set use `factory` prefix:
 *
 * ```php
 * 'user_id' => 'factory|User'
 * ```
 *
 * In case your ORM expects a related record itself (Doctrine) then you should use `entity` prefix:
 *
 * ```php
 * 'user' => 'entity|User'
 * ```
 */
class DataFactory extends \Codeception\Module implements DependsOnModule, RequiresPackage
{
    protected $dependencyMessage = <<<EOF
ORM module (like Doctrine2) or Framework module with ActiveRecord support is required:
--
modules:
    enabled:
        - DataFactory:
            depends: Doctrine2
--
EOF;

    /**
     * ORM module on which we we depend on.
     *
     * @var ORM
     */
    public $ormModule;

    /**
     * @var FactoryMuffin
     */
    public $factoryMuffin;

    protected $config = ['factories' => null];

    public function _requires()
    {
        return [
            'League\FactoryMuffin\FactoryMuffin' => '"league/factory-muffin": "^3.0"',
        ];
    }

    public function _beforeSuite($settings = [])
    {
        $store = $this->getStore();
        $this->factoryMuffin = new FactoryMuffin($store);

        if ($this->config['factories']) {
            foreach ((array) $this->config['factories'] as $factoryPath) {
                $realpath = realpath(codecept_root_dir().$factoryPath);
                if ($realpath === false) {
                    throw new ModuleException($this, 'The path to one of your factories is not correct. Please specify the directory relative to the codeception.yml file (ie. _support/factories).');
                }
                $this->factoryMuffin->loadFactories($realpath);
            }
        }
    }

    /**
     * @return StoreInterface|null
     */
    protected function getStore()
    {
        return $this->ormModule instanceof DataMapper
            ? new RepositoryStore($this->ormModule->_getEntityManager()) // for Doctrine
            : null;
    }

    public function _inject(ORM $orm)
    {
        $this->ormModule = $orm;
    }

    public function _after(TestInterface $test)
    {
        $skipCleanup = array_key_exists('cleanup', $this->config) && $this->config['cleanup'] === false;
        if ($skipCleanup || $this->ormModule->_getConfig('cleanup')) {
            return;
        }
        $this->factoryMuffin->deleteSaved();
    }

    public function _depends()
    {
        return ['Codeception\Lib\Interfaces\ORM' => $this->dependencyMessage];
    }


    /**
     * @throws ModuleException
     */
    public function onReconfigure($settings = [])
    {
        $skipCleanup = array_key_exists('cleanup', $this->config) && $this->config['cleanup'] === false;
        if (!$skipCleanup && !$this->ormModule->_getConfig('cleanup')) {
            $this->factoryMuffin->deleteSaved();
        }
        $this->_beforeSuite($settings);
    }

    /**
     * Creates a model definition. This can be used from a helper:.
     *
     * ```php
     * $this->getModule('{{MODULE_NAME}}')->_define('User', [
     *     'name' => $faker->name,
     *     'email' => $faker->email
     * ]);
     *
     * ```
     *
     * @param string $model
     * @param array $fields
     *
     * @return \League\FactoryMuffin\Definition
     *
     * @throws \League\FactoryMuffin\Exceptions\DefinitionAlreadyDefinedException
     */
    public function _define($model, $fields)
    {
        return $this->factoryMuffin->define($model)->setDefinitions($fields);
    }

    /**
     * Generates and saves a record,.
     *
     * ```php
     * $I->have('User'); // creates user
     * $I->have('User', ['is_active' => true]); // creates active user
     * ```
     *
     * Returns an instance of created user.
     *
     * @param string $name
     * @param array $extraAttrs
     *
     * @return object
     */
    public function have($name, array $extraAttrs = [])
    {
        return $this->factoryMuffin->create($name, $extraAttrs);
    }

    /**
     * Generates a record instance.
     *
     * This does not save it in the database. Use `have` for that.
     *
     * ```php
     * $user = $I->make('User'); // return User instance
     * $activeUser = $I->make('User', ['is_active' => true]); // return active user instance
     * ```
     *
     * Returns an instance of created user without creating a record in database.
     *
     * @param string $name
     * @param array $extraAttrs
     *
     * @return object
     */
    public function make($name, array $extraAttrs = [])
    {
        return $this->factoryMuffin->instance($name, $extraAttrs);
    }

    /**
     * Generates and saves a record multiple times.
     *
     * ```php
     * $I->haveMultiple('User', 10); // create 10 users
     * $I->haveMultiple('User', 10, ['is_active' => true]); // create 10 active users
     * ```
     *
     * @param string $name
     * @param int $times
     * @param array $extraAttrs
     *
     * @return \object[]
     */
    public function haveMultiple($name, $times, array $extraAttrs = [])
    {
        return $this->factoryMuffin->seed($times, $name, $extraAttrs);
    }
}
