<?php

declare(strict_types=1);

/*
 * This file is part of the RegexParser package.
 *
 * (c) Younes ENNAJI <younes.ennaji.pro@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace RegexParser\Tests\Unit\NodeVisitor;

use PHPUnit\Framework\TestCase;
use RegexParser\LiteralSet;
use RegexParser\Node\AlternationNode;
use RegexParser\Node\AssertionNode;
use RegexParser\Node\CharLiteralNode;
use RegexParser\Node\CharLiteralType;
use RegexParser\Node\LiteralNode;
use RegexParser\Node\NodeInterface;
use RegexParser\Node\PosixClassNode;
use RegexParser\Node\QuantifierNode;
use RegexParser\Node\QuantifierType;
use RegexParser\Node\RangeNode;
use RegexParser\Node\SequenceNode;
use RegexParser\NodeVisitor\LiteralExtractorNodeVisitor;
use RegexParser\NodeVisitor\NodeVisitorInterface;
use RegexParser\Regex;

final class LiteralExtractorNodeVisitorCoverageTest extends TestCase
{
    public function test_case_insensitive_char_class_expands_literals(): void
    {
        $regex = Regex::create();
        $visitor = new LiteralExtractorNodeVisitor();

        $ast = $regex->parse('/[ab]/i');
        $result = $ast->accept($visitor);

        $this->assertContains('a', $result->prefixes);
        $this->assertContains('A', $result->prefixes);
    }

    public function test_visit_assertion_returns_empty_literal_set(): void
    {
        $visitor = new LiteralExtractorNodeVisitor();
        $result = $visitor->visitAssertion(new AssertionNode('A', 0, 0));

        $this->assertSame([''], $result->prefixes);
    }

    public function test_visit_range_returns_empty_literal_set(): void
    {
        $visitor = new LiteralExtractorNodeVisitor();
        $range = new RangeNode(new LiteralNode('a', 0, 0), new LiteralNode('z', 0, 0), 0, 0);

        $this->assertTrue($visitor->visitRange($range)->isVoid());
    }

    public function test_visit_char_literal_returns_empty_literal_set(): void
    {
        $visitor = new LiteralExtractorNodeVisitor();
        $literal = new CharLiteralNode('\\x41', 0x41, CharLiteralType::UNICODE, 0, 0);

        $this->assertTrue($visitor->visitCharLiteral($literal)->isVoid());
    }

    public function test_visit_posix_class_returns_empty_literal_set(): void
    {
        $visitor = new LiteralExtractorNodeVisitor();
        $posix = new PosixClassNode('alpha', 0, 0);

        $this->assertTrue($visitor->visitPosixClass($posix)->isVoid());
    }

    public function test_visit_sequence_caps_large_literal_sets(): void
    {
        $visitor = new LiteralExtractorNodeVisitor();
        $largeSet = $this->makeLiteralSetWithPrefixes(200);
        $node = new class($largeSet) implements NodeInterface {
            public function __construct(private readonly LiteralSet $set) {}

            public function accept(NodeVisitorInterface $visitor): LiteralSet
            {
                return $this->set;
            }

            public function getStartPosition(): int
            {
                return 0;
            }

            public function getEndPosition(): int
            {
                return 0;
            }
        };

        $sequence = new SequenceNode([$node], 0, 0);
        $result = $visitor->visitSequence($sequence);

        $this->assertFalse($result->isVoid());
        $this->assertCount(100, $result->prefixes);
    }

    public function test_visit_quantifier_caps_large_literal_sets(): void
    {
        $visitor = new LiteralExtractorNodeVisitor();
        $largeSet = $this->makeLiteralSetWithPrefixes(200);
        $node = new class($largeSet) implements NodeInterface {
            public function __construct(private readonly LiteralSet $set) {}

            public function accept(NodeVisitorInterface $visitor): LiteralSet
            {
                return $this->set;
            }

            public function getStartPosition(): int
            {
                return 0;
            }

            public function getEndPosition(): int
            {
                return 0;
            }
        };

        $quantifier = new QuantifierNode($node, '{2}', QuantifierType::T_GREEDY, 0, 0);
        $result = $visitor->visitQuantifier($quantifier);

        $this->assertFalse($result->isVoid());
        $this->assertCount(100, $result->prefixes);
    }

    public function test_visit_alternation_exits_on_large_literal_sets(): void
    {
        $visitor = new LiteralExtractorNodeVisitor();
        $largeSet = $this->makeLiteralSetWithPrefixes(200);
        $node = new class($largeSet) implements NodeInterface {
            public function __construct(private readonly LiteralSet $set) {}

            public function accept(NodeVisitorInterface $visitor): LiteralSet
            {
                return $this->set;
            }

            public function getStartPosition(): int
            {
                return 0;
            }

            public function getEndPosition(): int
            {
                return 0;
            }
        };

        $alternation = new AlternationNode([$node], 0, 0);
        $result = $visitor->visitAlternation($alternation);

        $this->assertTrue($result->isVoid());
    }

    private function makeLiteralSetWithPrefixes(int $count): LiteralSet
    {
        $ref = new \ReflectionClass(LiteralSet::class);
        $set = $ref->newInstanceWithoutConstructor();

        $prefixes = [];
        for ($i = 0; $i < $count; $i++) {
            $prefixes[] = 'p'.$i;
        }

        $ref->getProperty('prefixes')->setValue($set, $prefixes);
        $ref->getProperty('suffixes')->setValue($set, $prefixes);
        $ref->getProperty('complete')->setValue($set, true);

        return $set;
    }
}
