<?php
/**
 * MibTest.php
 *
 * Test Mib files for errors
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 * @package    LibreNMS
 * @link       http://librenms.org
 * @copyright  2017 Tony Murray
 * @author     Tony Murray <murraytony@gmail.com>
 */

namespace LibreNMS\Tests;

use Exception;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;

/**
 * Class MibTest
 * @package LibreNMS\Tests
 *
 * Tests mib files for errors.
 * Will not be run by default, use --group=mibs to test.
 * You may test specific directories or files by specifying them with filter. Example: --filter=@RFC1284-MIB
 */
class MibTest extends TestCase
{
    /**
     * Test mib file in a directory for errors
     *
     * @group mibs
     * @dataProvider mibDirs
     * @param $dir
     */
    public function testMibDirectory($dir)
    {
        global $config;

        $output = shell_exec("snmptranslate -M +{$config['mib_dir']}:$dir -m +ALL SNMPv2-MIB::system 2>&1");
        $errors = str_replace("SNMPv2-MIB::system\n", '', $output);

        $this->assertEmpty($errors, "MIBs in $dir have errors!\n$errors");
    }

    /**
     * Test that each mib only exists once.
     *
     * @group mibs
     * @dataProvider mibFiles
     * @param $path
     * @param $file
     * @param $mib_name
     */
    public function testDuplicateMibs($path, $file, $mib_name)
    {
        global $console_color;

        $file_path = "$path/$file";
        $highligted_mib = $console_color->convert("%r$mib_name%n");

        static $existing_mibs;
        if (is_null($existing_mibs)) {
            $existing_mibs = array();
        }

        if (isset($existing_mibs[$mib_name])) {
            $existing_mibs[$mib_name][] = $file_path;

            $this->fail("$highligted_mib has duplicates: " . implode(', ', $existing_mibs[$mib_name]));
        } else {
            $existing_mibs[$mib_name] = array($file_path);
        }
    }

    /**
     * Test that the file name matches the mib name
     *
     * @group mibs
     * @dataProvider mibFiles
     * @param $path
     * @param $file
     * @param $mib_name
     */
    public function testMibNameMatches($path, $file, $mib_name)
    {
        global $console_color;

        $file_path = "$path/$file";
        $highlighted_file = $console_color->convert("%r$file_path%n");
        $this->assertEquals($mib_name, $file, "$highlighted_file should be named $mib_name");
    }


    /**
     * Test each mib file for errors
     *
     * @group mibs
     * @dataProvider mibFiles
     * @param $path
     * @param $file
     * @param $mib_name
     */
    public function testMibContents($path, $file, $mib_name)
    {
        global $config, $console_color;
        $file_path = "$path/$file";
        $highlighted_file = $console_color->convert("%r$file_path%n");

        $output = shell_exec("snmptranslate -M +{$config['mib_dir']}:$path -m +$mib_name SNMPv2-MIB::system 2>&1");
        $errors = str_replace("SNMPv2-MIB::system\n", '', $output);

        $this->assertEmpty($errors, "$highlighted_file has errors!\n$errors");
    }

    /**
     * Get a list of all mib files with the name of the mib.
     * Called for each test that uses it before class setup.
     * @return array path, filename, mib_name
     */
    public function mibFiles()
    {
        global $config;

        $file_list = array();
        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($config['mib_dir'])) as $file) {
            /** @var SplFileInfo $file */
            if ($file->isDir()) {
                continue;
            }
            $mib_path = str_replace($config['mib_dir'].'/', '', $file->getPathName());
            $file_list[$mib_path] = array(
                str_replace($config['install_dir'], '.', $file->getPath()),
                $file->getFilename(),
                $this->extractMibName($file->getPathname())
            );
        }

        return $file_list;
    }

    /**
     * List all directories inside the mib directory
     * @return array
     */
    public function mibDirs()
    {
        global $config;

        $dirs = glob($config['mib_dir'] . '/*', GLOB_ONLYDIR);
        array_unshift($dirs, $config['mib_dir']);

        $final_list = array();
        foreach ($dirs as $dir) {
            $relative_dir = str_replace($config['mib_dir'].'/', '', $dir);
            $final_list[$relative_dir] = array($dir);
        }
        return $final_list;
    }

    /**
     * Extract the mib name from a file
     *
     * @param $file
     * @return mixed
     * @throws Exception
     */
    private function extractMibName($file)
    {
        // extract the mib name (tried regex, but was too complex and I had to read the whole file)
        $mib_name = null;
        if ($handle = fopen($file, "r")) {
            $header = '';
            while (($line = fgets($handle)) !== false) {
                $trimmed = trim($line);

                if (empty($trimmed) || starts_with($trimmed, '--')) {
                    continue;
                }

                $header .= " $trimmed";
                if (str_contains($trimmed, 'DEFINITIONS')) {
                    preg_match('/(\S+)\s+(?=DEFINITIONS)/', $header, $matches);
                    fclose($handle);
                    return $matches[1];
                }
            }
            fclose($handle);
        }

        throw new Exception("Could not extract mib name from file ($file)");
    }
}
