<?php

declare(strict_types = 1);

/**
 * A helper class for Codeception (http://codeception.com/) that allows automated accessility checks
 * (WCAG 2.0, Section508) using the pa11y (http://pa11y.org/) command line tool
 * during acceptance testing.
 * It uses local binaries and can therefore be run offline.
 *
 * Requirements:
 * =============
 *
 * - Codeception with WebDriver or PhpBrowser set up
 * - pa11y is installed locally (e.g. using "npm insgall -g pa11y")
 *
 *
 * Installation:
 * =============
 *
 * - Copy this file to _support/Helper/ in the codeception directory
 * - Merge the following configuration to acceptance.suite.yml:
 *
 * modules:
 *   enabled:
 *     - \Helper\AccessibilityValidator
 *   config:
 *     \Helper\AccessibilityValidator:
 *       pa11yPath: /usr/local/bin/pa11y
 *
 *
 * Usage:
 * ======
 *
 * Validate the current site against WCAG 2.0 (AAA):
 * $I->validatePa11y(\Helper\AccessibilityValidator::STANDARD_WCAG2AAA);
 *
 * Validate the current site against WCAG 2.0 (AA):
 * $I->validatePa11y(); // or:
 * $I->validatePa11y(\Helper\AccessibilityValidator::STANDARD_WCAG2A);
 *
 * Validate the current site against WCAG 2.0 (A):
 * $I->validatePa11y(\Helper\AccessibilityValidator::STANDARD_WCAG2A);
 *
 * Validate the current site against Section 508:
 * $I->validatePa11y(\Helper\AccessibilityValidator::STANDARD_SECTION508);
 *
 * Validate against WCAG 2.0 (AA), but ignore errors containing the string "Ignoreme":
 * $I->validatePa11y(\Helper\AccessibilityValidator::STANDARD_WCAG2A, ["Ignoreme"]);
 *
 * @license http://www.opensource.org/licenses/mit-license.html  MIT License
 * @author Tobias Hößl <tobias@hoessl.eu>
 */

namespace Helper;

use Exception;
use PHPUnit\Framework\Assert;

class AccessibilityValidator extends \Codeception\Module
{
    public static $SUPPORTED_STANDARDS = [
        'WCAG2AAA',
        'WCAG2AA',
        'WCAG2A',
        'Section508',
    ];
    public const STANDARD_WCAG2AAA   = 'WCAG2AAA';
    public const STANDARD_WCAG2AA    = 'WCAG2AA';
    public const STANDARD_WCAG2A     = 'WCAG2A';
    public const STANDARD_SECTION508 = 'Section508';

    private function getPageUrl(): string
    {
        if ($this->hasModule('WebDriver')) {
            /** @var \Codeception\Module\WebDriver $webdriver */
            $webdriver = $this->getModule('WebDriver');
            return $webdriver->webDriver->getCurrentURL();
        } else {
            /** @var \Codeception\Module\PhpBrowser $phpBrowser */
            $phpBrowser = $this->getModule('PhpBrowser');
            return trim($phpBrowser->_getUrl(), '/') . $phpBrowser->_getCurrentUri();
        }
    }

    /**
     * @throws Exception
     */
    private function validateByPa11y(string $url, string $standard): array
    {
        if (!\in_array($standard, static::$SUPPORTED_STANDARDS)) {
            throw new Exception('Unknown standard: ' . $standard);
        }

        exec('sshpass -p pa11y ssh -o StrictHostKeyChecking=no pa11y 2>/dev/null pa11y -c /pa11y/config.json' . ' -s ' . $standard . " -r json '" . addslashes($url) . "'", $output);

        if (!empty($output)) {
            $data = json_decode($output[0], true);
            if (!$data) {
                throw new Exception('Invalid data returned from validation service: ' . implode("\n", $output));
            }
            return $data;
        }
        return [];
    }

    /**
     * @param string[] $ignoreMessages
     */
    public function validatePa11y(string $standard = 'WCAG2AA', array $ignoreMessages = []): void
    {
        try {
            $url      = $this->getPageUrl();
            $messages = $this->validateByPa11y($url, $standard);
        } catch (Exception $e) {
            $this->fail($e->getMessage());
            return;
        }
        $failMessages = [];
        foreach ($messages as $message) {
            if ($message['type'] == 'error') {
                $string = $message['code'] . "\n" . $message['selector'] . ': ';
                $string .= $message['context'] . "\n";
                $string .= $message['message'];
                $ignoring = false;
                foreach ($ignoreMessages as $ignoreMessage) {
                    if (mb_stripos($string, $ignoreMessage) !== false) {
                        $ignoring = true;
                    }
                }
                if (!$ignoring) {
                    $failMessages[] = $string;
                }
            }
        }
        if (\count($failMessages) > 0) {
            $failStr = 'Failed ' . $standard . ' check: ' . "\n";
            $failStr .= implode("\n\n", $failMessages);
            Assert::fail($failStr);
        }
    }
}