<?php

declare(strict_types=1);

namespace Codeception\Reporter;

use Codeception\Event\FailEvent;
use Codeception\Event\PrintResultEvent;
use Codeception\Event\SuiteEvent;
use Codeception\Event\TestEvent;
use Codeception\Events;
use Codeception\Lib\Console\Output;
use Codeception\Step;
use Codeception\Step\Meta;
use Codeception\Subscriber\Shared\StaticEventsTrait;
use Codeception\Test\Descriptor;
use Codeception\Test\Interfaces\ScenarioDriven;
use Codeception\Test\Test;
use Codeception\Util\PathResolver;
use SebastianBergmann\Template\Template;
use SebastianBergmann\Timer\Timer;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

use function trigger_error;

class HtmlReporter implements EventSubscriberInterface
{
    use StaticEventsTrait;

    /**
     * @var array<string, string>
     */
    protected static array $events = [
        Events::SUITE_BEFORE       => 'beforeSuite',
        Events::RESULT_PRINT_AFTER => 'afterResult',
        Events::TEST_SUCCESS       => 'testSuccess',
        Events::TEST_FAIL          => 'testFailure',
        Events::TEST_ERROR         => 'testError',
        Events::TEST_INCOMPLETE    => 'testIncomplete',
        Events::TEST_SKIPPED       => 'testSkipped',
        Events::TEST_USELESS       => 'testUseless',
        Events::TEST_WARNING       => 'testWarning',
    ];

    protected int $id = 0;
    protected string $scenarios = '';
    protected string $templatePath;
    private string $reportFile;
    private Timer $timer;

    public function __construct(array $options, private Output $output)
    {
        $this->reportFile = $options['html'];
        if (!codecept_is_path_absolute($this->reportFile)) {
            $this->reportFile = codecept_output_dir($this->reportFile);
        }
        codecept_debug(sprintf("Printing HTML report to %s", $this->reportFile));

        $this->templatePath = sprintf(
            '%s%stemplate%s',
            __DIR__,
            DIRECTORY_SEPARATOR,
            DIRECTORY_SEPARATOR
        );

        $this->timer = new Timer();
        $this->timer->start();
    }

    public function beforeSuite(SuiteEvent $event): void
    {
        $suite = $event->getSuite();
        if (!$suite->getName()) {
            return;
        }

        $suiteTemplate = new Template($this->templatePath . 'suite.html');
        $suiteTemplate->setVar(['suite' => ucfirst($suite->getName())]);
        $this->scenarios .= $suiteTemplate->render();
    }

    public function testSuccess(TestEvent $event): void
    {
        $this->printTestEvent($event, 'scenarioSuccess');
    }

    public function testError(FailEvent $event): void
    {
        $this->printTestEvent($event, 'scenarioFailed');
    }

    public function testFailure(FailEvent $event): void
    {
        $this->printTestEvent($event, 'scenarioFailed');
    }

    public function testWarning(FailEvent $event): void
    {
        $this->printTestEvent($event, 'scenarioSuccess');
    }

    public function testSkipped(FailEvent $event): void
    {
        $this->printTestEvent($event, 'scenarioSkipped');
    }

    public function testIncomplete(FailEvent $event): void
    {
        $this->printTestEvent($event, 'scenarioIncomplete');
    }

    public function testUseless(FailEvent $event): void
    {
        $this->printTestEvent($event, 'scenarioUseless');
    }

    public function printTestEvent(TestEvent $event, string $scenarioStatus): void
    {
        $test = $event->getTest();
        $failure = '';

        if ($event instanceof FailEvent) {
            $failTemplate = new Template($this->templatePath . 'fail.html');
            $failTemplate->setVar(['fail' => nl2br($event->getFail()->getMessage())]);
            $failure = $failTemplate->render() . PHP_EOL;
        }

        $stepsBuffer = $this->buildStepsBuffer($test);
        $artifacts = $this->getFailureArtifacts($test);
        $png = $artifacts['png'];
        $html = $artifacts['html'];
        $toggle = $stepsBuffer ? '<span class="toggle">+</span>' : '';
        $testString = $this->formatTestName($test);

        $this->scenarios .= $this->renderScenarioTemplate([
            'id'             => ++$this->id,
            'name'           => $testString,
            'scenarioStatus' => $scenarioStatus,
            'steps'          => $stepsBuffer,
            'toggle'         => $toggle,
            'failure'        => $failure,
            'png'            => $png,
            'html'           => $html,
            'time'           => round($event->getTime(), 2),
        ]);
    }

    /**
     * @deprecated This method is deprecated and will be removed in Codeception 6.0.
     */
    public function printTestResult(Test $test, float $time, string $scenarioStatus): void
    {
        trigger_error(
            __METHOD__ . ' is deprecated, please use printTestEvent instead',
            E_USER_DEPRECATED,
        );

        $stepsBuffer = $this->buildStepsBuffer($test);
        $artifacts = $this->getFailureArtifacts($test);
        $png = $artifacts['png'];
        $html = $artifacts['html'];
        $toggle = $stepsBuffer ? '<span class="toggle">+</span>' : '';
        $testString = $this->formatTestName($test);

        $this->scenarios .= $this->renderScenarioTemplate([
            'id'             => ++$this->id,
            'name'           => $testString,
            'scenarioStatus' => $scenarioStatus,
            'steps'          => $stepsBuffer,
            'toggle'         => $toggle,
            'failure'        => '',
            'png'            => $png,
            'html'           => $html,
            'time'           => round($time, 2),
        ]);
    }

    private function buildStepsBuffer(Test $test): string
    {
        $steps = [];

        if ($test instanceof ScenarioDriven) {
            $steps = $test->getScenario()->getSteps();
        }

        $stepsBuffer = '';
        $subStepsRendered = [];

        foreach ($steps as $step) {
            $metaStep = $step->getMetaStep();
            if ($metaStep) {
                $key = $this->getMetaStepKey($metaStep);
                $subStepsRendered[$key][] = $this->renderStep($step);
            }
        }

        foreach ($steps as $step) {
            $metaStep = $step->getMetaStep();
            if ($metaStep) {
                $key = $this->getMetaStepKey($metaStep);
                if (isset($subStepsRendered[$key]) && $subStepsRendered[$key] !== []) {
                    $subStepsBuffer = implode('', $subStepsRendered[$key]);
                    unset($subStepsRendered[$key]);
                    $stepsBuffer .= $this->renderSubsteps($step->getMetaStep(), $subStepsBuffer);
                }
            } else {
                $stepsBuffer .= $this->renderStep($step);
            }
        }

        return $stepsBuffer;
    }

    private function getFailureArtifacts(Test $test): array
    {
        $png = '';
        $html = '';
        $reports = $test->getMetadata()->getReports();

        if (isset($reports['png'])) {
            $localPath = PathResolver::getRelativeDir($reports['png'], codecept_output_dir());
            $png = "<tr><td class='error'><div class='screenshot'><img src='$localPath' alt='failure screenshot'></div></td></tr>";
        }
        if (isset($reports['html'])) {
            $localPath = PathResolver::getRelativeDir($reports['html'], codecept_output_dir());
            $html = "<tr><td class='error'>See <a href='$localPath' target='_blank'>HTML snapshot</a> of a failed page</td></tr>";
        }

        return ['png' => $png, 'html' => $html];
    }

    private function formatTestName(Test $test): string
    {
        $testString = htmlspecialchars(ucfirst(Descriptor::getTestAsString($test)), ENT_QUOTES | ENT_SUBSTITUTE);
        return preg_replace('~^([\s\w\\\]+):\s~', '<span class="quiet">$1 &raquo;</span> ', $testString);
    }

    private function getMetaStepKey(Meta $metaStep): string
    {
        $key = '';
        $filePath = $metaStep->getFilePath();
        if ($filePath !== null) {
            $key = $filePath;
            $lineNumber = $metaStep->getLineNumber();
            if ($lineNumber !== null) {
                $key .= ':' . $lineNumber;
            }
        }
        return $key . $metaStep->getAction();
    }

    protected function renderStep(Step $step): string
    {
        $stepTemplate = new Template($this->templatePath . 'step.html');
        $stepTemplate->setVar(['action' => $step->getHtml(), 'error' => $step->hasFailed() ? 'failedStep' : '']);
        return $stepTemplate->render();
    }

    protected function renderSubsteps(Meta $metaStep, string $substepsBuffer): string
    {
        $metaTemplate = new Template($this->templatePath . 'substeps.html');
        $metaTemplate->setVar([
            'metaStep' => $metaStep->getHtml(),
            'error'    => $metaStep->hasFailed() ? 'failedStep' : '',
            'steps'    => $substepsBuffer,
            'id'       => uniqid(),
        ]);
        return $metaTemplate->render();
    }

    private function renderScenarioTemplate(array $vars): string
    {
        $scenarioTemplate = new Template($this->templatePath . 'scenario.html');
        $scenarioTemplate->setVar($vars);
        return $scenarioTemplate->render();
    }

    public function afterResult(PrintResultEvent $event): void
    {
        $timeTaken = $this->timer->stop()->asString();
        $result = $event->getResult();

        $status = $result->wasSuccessfulIgnoringWarnings()
            ? '<span style="color: green">OK</span>'
            : '<span style="color: #e74c3c">FAILED</span>';

        $scenarioHeaderTemplate = new Template($this->templatePath . 'scenario_header.html');
        $scenarioHeaderTemplate->setVar([
            'name'   => 'Codeception Results',
            'status' => $status,
            'time'   => $timeTaken,
        ]);

        $header = $scenarioHeaderTemplate->render();

        $scenariosTemplate = new Template($this->templatePath . 'scenarios.html');
        $scenariosTemplate->setVar([
            'header'              => $header,
            'scenarios'           => $this->scenarios,
            'successfulScenarios' => $result->successfulCount(),
            'failedScenarios'     => $result->failureCount(),
            'skippedScenarios'    => $result->skippedCount(),
            'incompleteScenarios' => $result->incompleteCount(),
            'uselessScenarios'    => $result->uselessCount(),
        ]);

        file_put_contents($this->reportFile, $scenariosTemplate->render());
        $this->output->message(
            "- <bold>HTML</bold> report generated in <comment>file://%s</comment>",
            $this->reportFile
        )->writeln();
    }
}
