<?php

declare(strict_types=1);

namespace MultiTester;

use ArrayAccess;
use Composer\Semver\Semver;
use MultiTester\Traits\Composer;
use MultiTester\Traits\ConfigHandler;
use MultiTester\Traits\GithubShortcut;
use MultiTester\Traits\PackageNameHandler;
use MultiTester\Traits\Settings;
use MultiTester\Traits\TravisShortcut;

class Project
{
    use ConfigHandler;
    use Composer;
    use GithubShortcut;
    use PackageNameHandler;
    use Settings;
    use TravisShortcut;

    public function __construct(string $package, Config $config, $settings = null)
    {
        $this->config = $config;
        $this->settings = $settings;
        $this->packageName = $package;
    }

    /**
     * @throws MultiTesterException
     * @throws TestFailedException
     */
    public function test(): bool
    {
        $this->download();
        $this->install();

        return $this->exec();
    }

    public function removeReplacedPackages(array $settings)
    {
        $tester = $this->config->getTester();

        if (isset($settings['replace'])) {
            $tester->exec($settings['replace']);

            return;
        }

        $replace = array_keys((array) ($this->config->data['replace'] ?? []));

        if ($replace === []) {
            return;
        }

        foreach ($replace as $package) {
            (new Directory('vendor/' . $package, $this->config->executor))->remove();
        }

        // Remove in "require"
        $tester->exec(
            $this->getComposerProgram($settings) .
            ' remove --no-interaction --no-update ' .
            implode(' ', $replace) .
            ($this->config->quiet ? ' --quiet' : '')
        );

        // Remove in "require-dev"
        $tester->exec(
            $this->getComposerProgram($settings) .
            ' remove --dev --no-interaction --no-update ' .
            implode(' ', $replace) .
            ($this->config->quiet ? ' --quiet' : '')
        );

        // Replace with current package
        $tester->exec(
            $this->getComposerProgram($settings) .
            ' require --no-interaction --no-update ' .
            $this->config->packageName .
            ($this->config->quiet ? ' --quiet' : '')
        );
    }

    protected function getScript($script): array
    {
        $script = is_array($script) ? $script : [$script];

        foreach ($script as &$line) {
            $line = explode(' ', $line, 2);

            if (file_exists($line[0])) {
                $line[0] = realpath($line[0]);
            }

            $line = implode(' ', $line);
        }

        return $script;
    }

    protected function getTries($settings = [], $tries = 1)
    {
        if (isset($settings['retry'])) {
            $tries = $settings['retry'];
        } elseif (($config = $this->getConfig()->config) && isset($config['retry'])) {
            $tries = $config['retry'];
        }

        return $tries;
    }

    /**
     * @param MultiTester $tester
     * @param array       $settings
     * @param string      $package
     *
     * @throws TestFailedException
     *
     * @return bool
     */
    protected function tryExec(MultiTester $tester, $settings, $package)
    {
        for ($i = $this->getTries($settings); $i > 0; $i--) {
            if ($tester->exec($this->getScript($settings['script']))) {
                return true;
            }
        }

        throw new TestFailedException("Test of $package failed.");
    }

    /**
     * @param string   $version
     * @param string[] $versions
     *
     * @return string
     */
    protected function filterVersion($version, array $versions): ?string
    {
        usort($versions, 'version_compare');

        $filteredVersions = $version
            ? Semver::satisfiedBy($versions, $version)
            : array_filter($versions, function ($version) {
                return substr($version, 0, 4) !== 'dev-';
            });

        $filteredVersions = count($filteredVersions) ? $filteredVersions : $versions;

        return end($filteredVersions) ?: null;
    }

    protected function seedSourceSetting(array &$settings): void
    {
        if (!isset($settings['source'])) {
            $settings['version'] = (string) ($settings['version'] ?? 'dev-master');
            $package = $this->getPackageName();
            $tester = $this->getConfig()->getTester();
            $tester->info("Searching source for $package:{$settings['version']}\n");
            $composerSettings = $tester->getComposerSettings($package, $settings['platforms'] ?? null);
            $version = is_array($composerSettings)
                ? $this->filterVersion($settings['version'], array_keys((array) ($composerSettings ?: [])))
                : '';
            $versionSettings = $composerSettings[$version] ?? [];

            $settings['source'] = $versionSettings['source'] ?? $this->getRepositoryUrl($versionSettings);
            $sourceDump = json_encode($settings['source'], JSON_PRETTY_PRINT);

            $tester->info("Found source for version '$version':\n$sourceDump\n");
        }

        if (
            isset($settings['source']['url']) &&
            !isset($settings['source']['type']) &&
            (
                preg_match('/\.git$/', $settings['source']['url']) ||
                preg_match('`^(https?://github\.com/|git[@:])`', $settings['source']['url'])
            )
        ) {
            $settings['source']['type'] = 'git';
        }
    }

    /** @param ArrayAccess|array|null $composerSettings */
    protected function getRepositoryUrl($composerSettings): ?array
    {
        return isset($composerSettings['repository_url'])
            ? [
                'type' => 'git',
                'url'  => $composerSettings['repository_url'],
            ]
            : null;
    }

    /**
     * @throws MultiTesterException
     */
    protected function checkSourceSetting(array $settings): void
    {
        if (!isset($settings['source'])) {
            $package = $this->getPackageName();

            throw new MultiTesterException("Source not found for $package, you must provide it manually via a 'source' entry.");
        }

        if (!isset($settings['source']['type'], $settings['source']['url'])) {
            throw new MultiTesterException("Source malformed, it should contains at least 'type' and 'url' entries.");
        }

        if ($settings['source']['type'] !== 'git') {
            throw new MultiTesterException("Git is the only supported source for now, you should provide a manual 'clone' command instead.");
        }
    }

    /**
     * @throws MultiTesterException
     */
    protected function seedCloneSetting(array &$settings): void
    {
        if (!isset($settings['clone'])) {
            $this->seedSourceSetting($settings);
            $this->checkSourceSetting($settings);
            $cloner = new Cloner($this->config);
            $settings['clone'] = $cloner->getCloneCommands($settings);
        }

        $this->asArray($settings['clone']);
    }

    protected function seedSetting(array &$settings, string $key, string $name, string $defaultCommand): void
    {
        if (!isset($settings[$key]) || $settings[$key] === 'default') {
            if (!isset($settings[$key])) {
                $this->getConfig()->getTester()->output(
                    "No $name found, '$defaultCommand' used by default, add a '$key' entry if you want to customize it.\n"
                );
            }

            $settings[$key] = $defaultCommand;
        }

        $this->replaceTravisSetting($settings, $key, $name);
        $this->replaceGitHubSetting($settings, $key, $name);
        $this->asArray($settings[$key]);
    }

    protected function seedAutoloadSetting(array &$settings): void
    {
        $this->seedSetting(
            $settings,
            'autoload',
            'autoload build script',
            $this->getComposerProgram($settings) .
                ' dump-autoload --optimize --no-interaction' .
                ($this->config->quiet ? ' --quiet' : '')
        );
    }

    protected function seedInstallSetting(array &$settings): void
    {
        $this->seedSetting(
            $settings,
            'install',
            'install script',
            $this->getComposerProgram($settings) .
                ' install --no-interaction' .
                ($this->config->quiet ? ' --quiet' : '')
        );
    }

    protected function seedScriptSetting(array &$settings): void
    {
        $this->seedSetting(
            $settings,
            'script',
            'script',
            'vendor/bin/phpunit --no-coverage'
        );
    }

    /**
     * @throws MultiTesterException
     */
    protected function download()
    {
        $settings = $this->getSettings();
        $package = $this->getPackageName();
        $config = $this->getConfig();
        $tester = $config->getTester();

        $this->seedCloneSetting($settings);

        (new Directory('.', $this->config->executor))->clean();

        if (!$config->quiet) {
            $tester->info("empty current directory\n");
            $tester->framedInfo("Cloning $package");
        }

        if (!$tester->exec($settings['clone'], $config->quiet)) {
            throw new MultiTesterException("Cloning $package failed.");
        }
    }

    /**
     * @throws MultiTesterException
     */
    protected function install()
    {
        $settings = $this->getSettings();
        $package = $this->getPackageName();
        $config = $this->getConfig();
        $tester = $config->getTester();

        if (!$config->quiet) {
            $tester->info("clear travis cache.\n");
        }

        $tester->clearTravisSettingsCache();
        $tester->clearGithubSettingsCache();

        $this->allowPlugins($settings);

        $this->removeReplacedPackages($settings);

        $this->seedInstallSetting($settings);

        if (!$config->quiet) {
            $tester->framedInfo("Installing $package");
        }

        if (!$tester->exec($settings['install'], $config->quiet)) {
            throw new MultiTesterException("Installing $package failed.");
        }
    }

    /**
     * @throws MultiTesterException
     */
    protected function autoload()
    {
        $settings = $this->getSettings();
        $package = $this->getPackageName();
        $config = $this->getConfig();
        $tester = $config->getTester();

        $this->seedAutoloadSetting($settings);

        if (!$tester->exec($settings['autoload'], $config->quiet)) {
            throw new MultiTesterException("Building autoloader of $package failed.");
        }
    }

    /**
     * @throws MultiTesterException|TestFailedException
     */
    protected function exec(): bool
    {
        $settings = $this->getSettings();
        $package = $this->getPackageName();
        $config = $this->getConfig();
        $tester = $config->getTester();

        (new Directory($config->projectDirectory, $this->config->executor))->copy(
            'vendor/' . $config->packageName,
            ['.git', 'vendor']
        );

        $this->autoload();

        $this->seedScriptSetting($settings);

        $tester->framedInfo("Testing $package");

        return $this->tryExec($tester, $settings, $package);
    }

    private function allowPlugins(array $settings): void
    {
        if (!isset($settings['allow-plugins'])) {
            return;
        }

        $tester = $this->config->getTester();

        $plugins = (array) $settings['allow-plugins'];

        foreach ($plugins as $plugin) {
            $tester->exec(
                $this->getComposerProgram($settings) .
                " config --no-plugins allow-plugins.$plugin true" .
                ($this->config->quiet ? ' --quiet' : '')
            );
        }
    }
}
