<?php

/*
 * This file is part of the Behat.
 * (c) Konstantin Kudryashov <ever.zet@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Behat\Behat\Gherkin\Specification;

use Behat\Gherkin\Filter\FeatureFilterInterface;
use Behat\Gherkin\Filter\NameFilter;
use Behat\Gherkin\Filter\NarrativeFilter;
use Behat\Gherkin\Filter\RoleFilter;
use Behat\Gherkin\Filter\TagFilter;
use Behat\Gherkin\Gherkin;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Testwork\Specification\SpecificationIterator;
use Behat\Testwork\Suite\Exception\SuiteConfigurationException;
use Behat\Testwork\Suite\Suite;

/**
 * Lazily iterates (parses one-by-one) over features.
 *
 * @author Konstantin Kudryashov <ever.zet@gmail.com>
 *
 * @implements SpecificationIterator<FeatureNode>
 */
final class LazyFeatureIterator implements SpecificationIterator
{
    /**
     * @var string[]
     */
    private $paths = [];
    /**
     * @var FeatureFilterInterface[]
     */
    private $filters = [];
    /**
     * @var int
     */
    private $position = 0;
    /**
     * @var FeatureNode[]
     */
    private $features = [];
    /**
     * @var FeatureNode
     */
    private $currentFeature;

    /**
     * Initializes specifications.
     *
     * @param string[]          $paths
     * @param FeatureFilterInterface[] $filters
     */
    public function __construct(
        private readonly Suite $suite,
        private readonly Gherkin $gherkin,
        array $paths,
        array $filters = [],
    ) {
        $this->paths = array_values($paths);
        $this->filters = array_merge($this->getSuiteFilters($this->suite), $filters);
    }

    public function getSuite()
    {
        return $this->suite;
    }

    public function rewind(): void
    {
        $this->position = 0;
        $this->moveToNextAvailableFeature();
    }

    public function next(): void
    {
        $this->moveToNextAvailableFeature();
    }

    public function valid(): bool
    {
        return null !== $this->currentFeature;
    }

    public function key(): int
    {
        return $this->position;
    }

    public function current(): FeatureNode
    {
        return $this->currentFeature;
    }

    /**
     * Returns list of filters from suite settings.
     *
     * @return FeatureFilterInterface[]
     */
    private function getSuiteFilters(Suite $suite)
    {
        if (!$suite->hasSetting('filters') || !is_array($suite->getSetting('filters'))) {
            return [];
        }

        $filters = [];
        foreach ($suite->getSetting('filters') as $type => $filterString) {
            $filters[] = $this->createFilter($type, $filterString, $suite);
        }

        return $filters;
    }

    /**
     * Creates filter of provided type.
     *
     * @param string $type
     * @param string $filterString
     *
     * @return FeatureFilterInterface
     *
     * @throws SuiteConfigurationException If filter type is not recognised
     */
    private function createFilter($type, $filterString, Suite $suite)
    {
        if ('role' === $type) {
            return new RoleFilter($filterString);
        }

        if ('name' === $type) {
            return new NameFilter($filterString);
        }

        if ('tags' === $type) {
            return new TagFilter($filterString);
        }

        if ('narrative' === $type) {
            return new NarrativeFilter($filterString);
        }

        throw new SuiteConfigurationException(sprintf(
            '`%s` filter is not supported by the `%s` suite. Supported types are `%s`.',
            $type,
            $suite->getName(),
            implode('`, `', ['narrative', 'role', 'name', 'tags'])
        ), $suite->getName());
    }

    /**
     * Parses paths consequently.
     */
    private function moveToNextAvailableFeature()
    {
        while (!count($this->features) && $this->position < count($this->paths)) {
            $this->features = $this->parseFeature($this->paths[$this->position]);
            ++$this->position;
        }

        $this->currentFeature = array_shift($this->features);
    }

    /**
     * Parses feature at path.
     *
     * @param string $path
     *
     * @return FeatureNode[]
     */
    private function parseFeature($path)
    {
        return $this->gherkin->load($path, $this->filters);
    }
}
