<?php declare(strict_types=1);
/*
 * This file is part of phpunit/php-code-coverage.
 *
 * (c) Sebastian Bergmann <sebastian@phpunit.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace SebastianBergmann\CodeCoverage\Report;

use function date;
use function htmlspecialchars;
use function is_string;
use function round;
use DOMDocument;
use SebastianBergmann\CodeCoverage\Node\Directory;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\EnsuresUtf8;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Util\Xml;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;

/**
 * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
 *
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage
 */
final readonly class Crap4j
{
    use EnsuresUtf8;
    private int $threshold;

    public function __construct(int $threshold = 30)
    {
        $this->threshold = $threshold;
    }

    /**
     * @param null|non-empty-string $target
     * @param null|non-empty-string $name
     *
     * @throws WriteOperationFailedException
     */
    public function process(Directory $report, ?string $target = null, ?string $name = null): string
    {
        $document = new DOMDocument('1.0', 'UTF-8');

        $root = $document->createElement('crap_result');
        $document->appendChild($root);

        $project = $document->createElement('project', is_string($name) ? $this->ensureUtf8($name) : '');
        $root->appendChild($project);
        $root->appendChild($document->createElement('timestamp', date('Y-m-d H:i:s')));

        $stats       = $document->createElement('stats');
        $methodsNode = $document->createElement('methods');

        $fullMethodCount     = 0;
        $fullCrapMethodCount = 0;
        $fullCrapLoad        = 0;
        $fullCrap            = 0;

        foreach ($report as $item) {
            $namespace = 'global';

            if (!$item instanceof File) {
                continue;
            }

            $file = $document->createElement('file');
            $file->setAttribute('name', $this->ensureUtf8($item->pathAsString()));

            $classes = $item->classesAndTraits();

            foreach ($classes as $className => $class) {
                foreach ($class->methods as $methodName => $method) {
                    $crapLoad = $this->crapLoad((float) $method->crap, $method->ccn, $method->coverage);

                    $fullCrap     += $method->crap;
                    $fullCrapLoad += $crapLoad;
                    $fullMethodCount++;

                    if ($method->crap >= $this->threshold) {
                        $fullCrapMethodCount++;
                    }

                    $methodNode = $document->createElement('method');

                    if ($class->namespace !== '') {
                        $namespace = $class->namespace;
                    }

                    $methodNode->appendChild($document->createElement('package', $this->ensureUtf8($namespace)));
                    $methodNode->appendChild($document->createElement('className', $this->ensureUtf8($className)));
                    $methodNode->appendChild($document->createElement('methodName', $this->ensureUtf8($methodName)));
                    $methodNode->appendChild($document->createElement('methodSignature', htmlspecialchars($this->ensureUtf8($method->signature))));
                    $methodNode->appendChild($document->createElement('fullMethod', htmlspecialchars($this->ensureUtf8($method->signature))));
                    $methodNode->appendChild($document->createElement('crap', (string) $this->roundValue((float) $method->crap)));
                    $methodNode->appendChild($document->createElement('complexity', (string) $method->ccn));
                    $methodNode->appendChild($document->createElement('coverage', (string) $this->roundValue($method->coverage)));
                    $methodNode->appendChild($document->createElement('crapLoad', (string) round($crapLoad)));

                    $methodsNode->appendChild($methodNode);
                }
            }
        }

        $stats->appendChild($document->createElement('name', 'Method Crap Stats'));
        $stats->appendChild($document->createElement('methodCount', (string) $fullMethodCount));
        $stats->appendChild($document->createElement('crapMethodCount', (string) $fullCrapMethodCount));
        $stats->appendChild($document->createElement('crapLoad', (string) round($fullCrapLoad)));
        $stats->appendChild($document->createElement('totalCrap', (string) $fullCrap));

        $crapMethodPercent = 0;

        if ($fullMethodCount > 0) {
            $crapMethodPercent = $this->roundValue((100 * $fullCrapMethodCount) / $fullMethodCount);
        }

        $stats->appendChild($document->createElement('crapMethodPercent', (string) $crapMethodPercent));

        $root->appendChild($stats);
        $root->appendChild($methodsNode);

        $buffer = Xml::asString($document);

        if ($target !== null) {
            Filesystem::write($target, $buffer);
        }

        return $buffer;
    }

    private function crapLoad(float $crapValue, int $cyclomaticComplexity, float $coveragePercent): float
    {
        $crapLoad = 0;

        if ($crapValue >= $this->threshold) {
            $crapLoad += $cyclomaticComplexity * (1.0 - $coveragePercent / 100);
            $crapLoad += $cyclomaticComplexity / $this->threshold;
        }

        return $crapLoad;
    }

    private function roundValue(float $value): float
    {
        return round($value, 2);
    }
}
