<?php

declare(strict_types=1);

namespace Doctrine\Tests\RST\Parser;

use Doctrine\RST\Nodes\CodeNode;
use Doctrine\RST\Nodes\DocumentNode;
use Doctrine\RST\Nodes\DummyNode;
use Doctrine\RST\Nodes\ListNode;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Nodes\ParagraphNode;
use Doctrine\RST\Nodes\QuoteNode;
use Doctrine\RST\Nodes\TableNode;
use Doctrine\RST\Nodes\TitleNode;
use Doctrine\RST\Parser;
use Exception;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use RuntimeException;

use function assert;
use function count;
use function file_get_contents;
use function sprintf;
use function trim;

/**
 * Unit testing for RST
 */
class ParserTest extends TestCase
{
    /** @var Parser $parser */
    protected $parser;

    protected function setUp(): void
    {
        parent::setUp();

        $directory = __DIR__ . '/files/';
        $parser    = new Parser();

        $parser->getEnvironment()->setCurrentDirectory($directory);

        $this->parser = $parser;
    }

    public function testGetSubParserPassesConfiguration(): void
    {
        $parser = new Parser();

        $configuration = $parser->getEnvironment()->getConfiguration();

        $subParser = $parser->getSubParser();

        self::assertSame($configuration, $subParser->getEnvironment()->getConfiguration());
    }

    /**
     * Testing that code node value is good
     */
    public function testCodeNode(): void
    {
        $document = $this->parse('code-block-lastline.rst');

        $nodes = $document->getNodes(static function ($node): bool {
            return $node instanceof CodeNode;
        });

        self::assertSame(1, count($nodes));
        self::assertSame("A\nB\n C", trim($nodes[0]->getValueString()));
    }

    /**
     * Testing that code node options are parsed
     */
    public function testCodeNodeWithOptions(): void
    {
        $document = $this->parse('code-block-with-options.rst');

        $nodes = $document->getNodes(static function (Node $node): bool {
            return $node instanceof CodeNode;
        });

        self::assertSame(1, count($nodes));
        $codeNode = $nodes[0];
        assert($codeNode instanceof CodeNode);
        self::assertSame("A\nB\nC", trim($codeNode->getValueString()));
        self::assertSame(['name' => 'My Very Best Code'], $codeNode->getOptions());
    }

    /**
     * Testing paragraph nodes
     */
    public function testParagraphNode(): void
    {
        $document = $this->parse('paragraph.rst');

        self::assertHasNode($document, static function ($node): bool {
            return $node instanceof ParagraphNode;
        }, 1);
        self::assertStringContainsString('Hello world!', $document->render());
    }

    /**
     * Testing multi-paragraph nodes
     */
    public function testParagraphNodes(): void
    {
        $document = $this->parse('paragraphs.rst');

        self::assertHasNode($document, static function ($node): bool {
            return $node instanceof ParagraphNode;
        }, 3);
    }

    /**
     * Testing quote and block code
     */
    public function testBlockNode(): void
    {
        $quote = $this->parse('quote.rst');

        self::assertHasNode($quote, static function ($node): bool {
            return $node instanceof QuoteNode;
        }, 1);

        $code = $this->parse('code.rst');

        self::assertHasNode($quote, static function ($node): bool {
            return $node instanceof QuoteNode;
        }, 1);

        self::assertStringNotContainsString('::', $code->render());
    }

    /**
     * Testing the titling
     */
    public function testTitles(): void
    {
        $document = $this->parse('title.rst');

        self::assertHasNode($document, static function ($node): bool {
            return $node instanceof TitleNode
                && $node->getLevel() === 1;
        }, 1);

        $document = $this->parse('title2.rst');

        self::assertHasNode($document, static function ($node): bool {
            return $node instanceof TitleNode
                && $node->getLevel() === 2;
        }, 1);
    }

    public function testTitlesWithCustomInitialHeaderLevel(): void
    {
        $this->parser->getEnvironment()->getConfiguration()->setInitialHeaderLevel(2);

        $document = $this->parse('title.rst');

        self::assertHasNode($document, static function ($node): bool {
            return $node instanceof TitleNode && $node->getLevel() === 2;
        }, 1);

        $document = $this->parse('title2.rst');

        self::assertHasNode($document, static function ($node): bool {
            return $node instanceof TitleNode && $node->getLevel() === 3;
        }, 1);
    }

    public function testList(): void
    {
        $document = $this->parse('list.rst');

        self::assertHasNode($document, static function ($node): bool {
            return $node instanceof ListNode;
        }, 1);

        $document = $this->parse('list.rst');

        self::assertHasNode($document, static function ($node): bool {
            return $node instanceof ListNode;
        }, 1);

        $document = $this->parse('list-empty.rst');
        self::assertHasNode($document, static function ($node): bool {
            return $node instanceof ListNode;
        }, 1);
    }

    /**
     * Testing the titles retrieving
     */
    public function testGetTitles(): void
    {
        $document = $this->parse('titles.rst');

        self::assertSame($document->getTitle(), 'The main title');
        self::assertSame($document->getTitles(), [
            [
                'The main title',
                [
                    [
                        'First level title',
                        [
                            ['Second level title', []],
                            ['Other second level title', []],
                        ],
                    ],
                    [
                        'Other first level title',
                        [
                            ['Next second level title', []],
                            ['Yet another second level title', []],
                        ],
                    ],
                ],
            ],
        ]);
    }

    /**
     * Testing the table feature
     */
    public function testTable(): void
    {
        $document = $this->parse('table.rst');

        $nodes = $document->getNodes(static function ($node): bool {
            return $node instanceof TableNode;
        });

        self::assertSame(count($nodes), 1);

        $table = $nodes[0];
        assert($table instanceof TableNode);

        self::assertSame(3, $table->getCols());
        self::assertSame(3, $table->getRows());

        $document = $this->parse('pretty-table.rst');

        $nodes = $document->getNodes(static function ($node): bool {
            return $node instanceof TableNode;
        });

        self::assertSame(count($nodes), 1);

        $table = $nodes[0];
        assert($table instanceof TableNode);

        self::assertSame(3, $table->getCols());
        self::assertSame(2, $table->getRows());
    }

    /**
     * Tests that a simple replace works
     */
    public function testReplace(): void
    {
        $document = $this->parse('replace.rst');

        self::assertStringContainsString('Hello world!', $document->render());
    }

    /**
     * Test the include:: pseudo-directive
     */
    public function testInclusion(): void
    {
        $document = $this->parse('inclusion.rst');

        self::assertStringContainsString('I was actually included', $document->renderDocument());
    }

    public function testThrowExceptionOnInvalidFileInclude(): void
    {
        $parser      = new Parser();
        $environment = $parser->getEnvironment();

        $data = file_get_contents(__DIR__ . '/files/inclusion-bad.rst');

        self::assertIsString($data);

        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessage('Include ".. include:: non-existent-file.rst" does not exist or is not readable.');

        $parser->parse($data);
    }

    public function testDirective(): void
    {
        $document = $this->parse('directive.rst');

        $nodes = $document->getNodes(static function ($node): bool {
            return $node instanceof DummyNode;
        });

        self::assertSame(1, count($nodes));

        $node = $nodes[0];
        assert($node instanceof DummyNode);

        $data = $node->data;

        self::assertSame('some data', $data['data']);
        $options = $data['options'];
        self::assertTrue(isset($options['maxdepth']));
        self::assertTrue(isset($options['titlesonly']));
        self::assertTrue(isset($options['glob']));
        self::assertTrue($options['titlesonly']);
        self::assertSame('123', $options['maxdepth']);
    }

    public function testSubsequentParsesDontHaveTheSameTitleLevelOrder(): void
    {
        $directory = __DIR__ . '/files';

        $parser = new Parser();
        $parser->getEnvironment()->setCurrentDirectory($directory);

        /** @var TitleNode[] $nodes1 */
        $nodes1 = $parser->parseFile(sprintf('%s/mixed-titles-1.rst', $directory))->getNodes();
        /** @var TitleNode[] $nodes2 */
        $nodes2 = $parser->parseFile(sprintf('%s/mixed-titles-2.rst', $directory))->getNodes();

        $node = $nodes1[1];
        self::assertSame(1, $node->getLevel());

        $node = $nodes1[3];
        self::assertSame(2, $node->getLevel());

        $node = $nodes2[1];
        self::assertSame(1, $node->getLevel(), 'Title level in second parse is influenced by first parse');

        $node = $nodes2[3];
        self::assertSame(2, $node->getLevel(), 'Title level in second parse is influenced by first parse');
    }

    public function testNewlineBeforeAnIncludedIsntGobbled(): void
    {
        /** @var Node[] $nodes */
        $nodes = $this->parse('inclusion-newline.rst')->getNodes();

        self::assertCount(5, $nodes);
        self::assertInstanceOf('Doctrine\RST\Nodes\SectionBeginNode', $nodes[0]);
        self::assertInstanceOf('Doctrine\RST\Nodes\TitleNode', $nodes[1]);
        self::assertInstanceOf('Doctrine\RST\Nodes\ParagraphNode', $nodes[2]);
        self::assertInstanceOf('Doctrine\RST\Nodes\ParagraphNode', $nodes[3]);
        self::assertStringContainsString('<p>Test this paragraph is present.</p>', $nodes[2]->render());
        self::assertStringContainsString('<p>And this one as well.</p>', $nodes[3]->render());
    }

    public function testIncludesKeepScope(): void
    {
        // See http://docutils.sourceforge.net/docs/ref/rst/directives.html#including-an-external-document-fragment

        /** @var Node[] $nodes */
        $nodes = $this->parse('inclusion-scope.rst')->getNodes();

        self::assertCount(4, $nodes);

        $node = $nodes[0]->getValue();
        assert($node instanceof Node);
        self::assertSame("This first example will be parsed at the document level, and can\nthus contain any construct, including section headers.", $node->render());

        $node = $nodes[1]->getValue();
        assert($node instanceof Node);
        self::assertSame('This is included.', $node->render());

        $node = $nodes[2]->getValue();
        assert($node instanceof Node);
        self::assertSame('Back in the main document.', $node->render());

        self::assertInstanceOf('Doctrine\RST\Nodes\QuoteNode', $nodes[3]);

        $node = $nodes[3]->getValue();
        self::assertStringContainsString('This is included.', $node->render());
    }

    public function testIncludesPolicy(): void
    {
        $directory   = __DIR__ . '/files/';
        $parser      = new Parser();
        $environment = $parser->getEnvironment();
        $environment->setCurrentDirectory($directory);

        // Test defaults
        self::assertTrue($parser->getIncludeAllowed());
        self::assertSame('', $parser->getIncludeRoot());

        // Default policy:
        $document = $parser->parseFile($directory . 'inclusion-policy.rst')->render();
        self::assertStringContainsString('SUBDIRECTORY OK', $document);
        self::assertStringContainsString('EXTERNAL FILE INCLUDED!', $document);

        // Disbaled policy:
        $parser->setIncludePolicy(false);
        $nodes = $parser->parseFile($directory . 'inclusion-policy.rst')->getNodes();
        self::assertCount(1, $nodes);

        // Enabled
        $parser->setIncludePolicy(true);
        $nodes = $parser->parseFile($directory . 'inclusion-policy.rst')->getNodes();
        self::assertCount(6, $nodes);

        // Jailed
        $parser->setIncludePolicy(true, $directory);
        $nodes = $parser->parseFile($directory . 'inclusion-policy.rst')->getNodes();
        self::assertCount(5, $nodes);
    }

    public function testParseFileThrowsInvalidArgumentExceptionForMissingFile(): void
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('File at path does-not-exist.rst does not exist');

        $parser = new Parser();
        $parser->parseFile('does-not-exist.rst');
    }

    /**
     * Helper function, parses a file and returns the document
     * produced by the parser
     */
    private function parse(string $file): DocumentNode
    {
        $directory = $this->parser->getEnvironment()->getCurrentDirectory();
        $data      = file_get_contents($directory . $file);

        if ($data === false) {
            throw new Exception('Could not open file.');
        }

        return $this->parser->parse($data);
    }

    /**
     * Asserts that a document has nodes that satisfy the function
     */
    private function assertHasNode(DocumentNode $document, callable $function, ?int $count = null): void
    {
        $nodes = $document->getNodes($function);
        self::assertNotEmpty($nodes);

        if ($count === null) {
            return;
        }

        self::assertSame($count, count($nodes));
    }
}