<?php

namespace Typo3Update\Tests;

/*
 * Copyright (C) 2017  Daniel Siepmann <coding@daniel-siepmann.de>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

use PHPUnit\Framework\TestCase;
use Symfony\Component\Finder\Finder;

/**
 * Will test all sniffs where fixtures are available.
 *
 * To add a test, just create the necessary fixture folder structure with files.
 */
abstract class SniffsTest extends TestCase
{
    /**
     * Get all fixtures for sniffs.
     *
     * Execute each sniff based on found fixtures and compare result.
     *
     * @dataProvider getSniffs
     * @test
     *
     * @param \SplFileInfo $folder
     * @param array $arguments
     */
    public function sniffs(\SplFileInfo $folder, array $arguments = [])
    {
        if ($arguments !== []) {
            $this->executeSniffSubfolders($folder, $arguments);
            return;
        }

        $this->executeSniff($folder);
    }

    /**
     * Returns all sniffs to test.
     * Use e.g. as data provider.
     *
     * @return array
     */
    public function getSniffs()
    {
        $sniffs = [];

        $classnameParts = array_slice(explode('\\', get_class($this)), 3);
        $lastIndex = count($classnameParts) - 1;
        $classnameParts[$lastIndex] = substr($classnameParts[$lastIndex], 0, -4);
        $folderName = array_pop($classnameParts);
        $folder = array_merge([
            __DIR__,
            'Fixtures', 'Standards', 'Typo3Update', 'Sniffs',
        ], $classnameParts);

        $finder = new Finder();
        $finder->in(implode(DIRECTORY_SEPARATOR, $folder));

        foreach ($finder->directories()->name($folderName) as $folder) {
            $sniff = [
                $folder,
                [],
            ];

            if (is_file($this->getArgumentsFile($folder))) {
                $arguments = require $this->getArgumentsFile($folder);
                $sniff[1] = $arguments;
            }

            $sniffs[] = $sniff;
        }

        return $sniffs;
    }

    /**
     * Execute sniff using subfolders.
     *
     * @param \SplFileInfo $folder
     * @param array $arguments
     * @return void
     */
    protected function executeSniffSubfolders(\SplFileInfo $folder, array $arguments = [])
    {
        $finder = new Finder();
        $finder->in($folder->getRealPath());

        foreach ($arguments as $subFolder => $values) {
            $folderName = $folder->getRealPath() . DIRECTORY_SEPARATOR . $subFolder;
            $this->executeSniff(new \SplFileInfo($folderName), $values);
        }
    }

    /**
     * Execute phpunit assertion for sniff based on $folder.
     *
     * @param \SplFileInfo $folder
     * @param array $arguments
     * @return void
     */
    protected function executeSniff(\SplFileInfo $folder, array $arguments = [])
    {
        $fileName = '';
        if (isset($arguments['inputFileName'])) {
            $fileName = $arguments['inputFileName'];
            unset($arguments['inputFileName']);
        }

        $internalArguments = array_merge_recursive([
            'standard' => 'Typo3Update',
            'runtime-set' => [
                'mappingFile' => __DIR__ . DIRECTORY_SEPARATOR
                    . 'Fixtures' . DIRECTORY_SEPARATOR
                    . 'LegacyClassnames.php',
            ],
            'report' => 'json',
            'sniffs' => $this->getSniffByFolder($folder),
            'inputFile' => $this->getInputFile($folder, $fileName),
        ], $arguments);

        if (strpos($internalArguments['inputFile'], '.ts') !== false) {
            $internalArguments['extensions'] = 'ts/TypoScript';
        }

        $this->assertEquals(
            $this->getExpectedJsonOutput($folder),
            $this->getOutput($internalArguments)['output'],
            'Checking Sniff "' . $this->getSniffByFolder($folder) . '"'
                . ' did not produce expected output,'
                . ' called: ' . $this->getPhpcsCall($internalArguments)
        );

        try {
            $internalArguments['report'] = 'diff';
            $this->assertEquals(
                $this->getExpectedDiffOutput($folder),
                $this->getOutput($internalArguments)['output'],
                'Fixing Sniff "' . $this->getSniffByFolder($folder) . '"'
                    . ' did not produce expected diff,'
                    . ' called: ' . $this->getPhpcsCall($internalArguments)
            );
        } catch (FileNotFoundException $e) {
            // Ok, ignore, we don't have an diff.
        }
    }

    /**
     * Get expected json output for comparison.
     *
     * @param \SplFileInfo $folder
     * @return array
     */
    protected function getExpectedJsonOutput(\SplFileInfo $folder)
    {
        $file = $folder->getPathname() . DIRECTORY_SEPARATOR . 'Expected.json';
        if (!is_file($file)) {
            throw new \Exception('Could not load file: ' . $file, 1491486050);
        }

        return json_decode(file_get_contents($file), true);
    }

    /**
     * Returns absolute file path to diff file containing expected output.
     *
     * @param \SplFileInfo $folder
     * @return string
     *
     * @throws FileNotFoundException
     */
    protected function getExpectedDiffOutput(\SplFileInfo $folder)
    {
        $file = $folder->getRealPath() . DIRECTORY_SEPARATOR . 'Expected.diff';
        if (!is_file($file)) {
            throw new FileNotFoundException('File does not exist.', 1491469621);
        }

        return file_get_contents($file);
    }

    /**
     * Returns PHPCS Sniff name for given folder.
     *
     * @param \SplFileInfo $folder
     * @return string
     */
    protected function getSniffByFolder(\SplFileInfo $folder)
    {
        $folderParts = array_filter(explode(DIRECTORY_SEPARATOR, $folder->getPathName()));
        $sniffNamePosition;

        foreach ($folderParts as $index => $folderPart) {
            if (strpos($folderPart, 'Sniff', 1) !== false) {
                $sniffNamePosition = $index;
                break;
            }
        }

        if ($sniffNamePosition === null) {
            throw new \Exception('Could not detect sniff name by folder: ' . var_export($folder, true), 1491485369);
        }

        return $folderParts[$sniffNamePosition - 3]
            . '.' . $folderParts[$sniffNamePosition - 1]
            . '.' . substr($folderParts[$sniffNamePosition], 0, -5);
    }

    /**
     * Get absolute file path to file containing further arguments.
     *
     * @param \SplFileInfo $folder
     * @return string
     */
    protected function getArgumentsFile(\SplFileInfo $folder)
    {
        return $folder->getRealPath() . DIRECTORY_SEPARATOR . 'Arguments.php';
    }

    /**
     * Get absolute file path to file to use for testing.
     *
     * @param \SplFileInfo $folder
     * @param string $fileName
     * @return string
     */
    protected function getInputFile(\SplFileInfo $folder, $fileName = '')
    {
        $folderPath = $folder->getRealPath() . DIRECTORY_SEPARATOR;
        $file = $folderPath . $fileName;

        if (!is_file($file)) {
            $file = $folderPath . $fileName;
        }
        if (!is_file($file)) {
            $file = $folderPath . 'InputFileForIssues.php';
        }
        if (!is_file($file)) {
            $file = $folderPath . 'InputFileForIssues.ts';
        }

        if (!is_file($file)) {
            throw new \Exception('message', 1492083289);
        }

        return $file;
    }

    /**
     * Build cli call for phpcs.
     *
     * @param array $arguments
     * @return string
     */
    protected function getPhpcsCall(array $arguments)
    {
        $bin = './vendor/bin/phpcs';
        $preparedArguments = [];

        foreach ($arguments as $argumentName => $argumentValue) {
            if ($argumentName === 'inputFile') {
                continue;
            }

            if ($argumentName === 'runtime-set') {
                foreach ($argumentValue as $runtimeName => $runtimeValue) {
                    $preparedArguments[] = "--$argumentName $runtimeName $runtimeValue";
                }

                continue;
            }

            $preparedArguments[] = "--$argumentName=$argumentValue";
        }

        return $bin
            . ' ' . implode(' ', $preparedArguments)
            . ' ' . $arguments['inputFile']
            ;
    }

    /**
     * Executes phpcs for sniff based on $folder and returns the generated output.
     *
     * @param array $arguments
     * @return array
     */
    protected function getOutput(array $arguments)
    {
        $output = '';
        $returnValue;
        exec($this->getPhpcsCall($arguments), $output, $returnValue);

        if ($arguments['report'] === 'json') {
            try {
                $output = $this->prepareJsonOutput($output);
            } catch (\Exception $e) {
                throw new \Exception(
                    'Error during preparing json output by invoking '
                        . $this->getPhpcsCall($arguments)  . ' ' . $e->getMessage(),
                    1491487079
                );
            }
        } if ($arguments['report'] === 'diff') {
            $output = $this->prepareDiffOutput($output);
        }

        return [
            'output' => $output,
            'returnValue' => $returnValue,
        ];
    }

    /**
     * Prepare phpcs output for comparison.
     *
     * @param array $output
     * @return array
     */
    protected function prepareJsonOutput(array $output)
    {
        $preparedOutput = json_decode($output[0], true);

        if ($preparedOutput === null) {
            throw new \Exception('Output for phpcs was not valid json: ' . var_export($output, true), 1491485173);
        }

        foreach (array_keys($preparedOutput['files']) as $fileName) {
            $newKey = basename($fileName);
            $preparedOutput['files'][$newKey] = $preparedOutput['files'][$fileName];
            unset($preparedOutput['files'][$fileName]);
        }

        return $preparedOutput;
    }

    /**
     * Prepare phpcs output for comparison.
     *
     * @param array $output
     * @return string
     */
    protected function prepareDiffOutput(array $output)
    {
        return implode("\n", $output);
    }
}