<?php
namespace Dompdf\Tests\Css;

use DOMDocument;
use DOMElement;
use DOMXPath;
use Dompdf\Css\Stylesheet;
use Dompdf\Dompdf;
use Dompdf\Frame\FrameTree;
use Dompdf\Tests\TestCase;

class SelectorTest extends TestCase
{
    private function stylesheet()
    {
        return new class(new Dompdf()) extends Stylesheet {
            public function specificity(string $selector, int $origin = self::ORIG_AUTHOR): int
            {
                return parent::specificity($selector, $origin);
            }

            public function selectorToXpath(string $selector, bool $firstPass = false): ?array
            {
                return parent::selectorToXpath($selector, $firstPass);
            }
        };
    }

    private function preProcess(string $selector): string
    {
        // Pre-process the selector string the same way the `Stylesheet` class
        // does before passing it to `selectorToXpath`.
        // See `Stylesheet::_parse_sections()`
        $patterns = ["/\s+/", "/\s+([>.:+~#])\s+/"];
        $replacements = [" ", "\\1"];

        return preg_replace($patterns, $replacements, $selector);
    }

    public static function selectorMatchesProvider(): array
    {
        // Elements expected to matched by each selector are marked with the
        // attribute `data-match`. The optional third parameter defines whether
        // the root element (`html`) is expected to be matched
        return [
            // Next-sibling combinator
            "next sibling 1" => [
                "h1 + p",
                '<body>
                    <h1></h1>
                    <p data-match></p>
                    <p></p>
                    <h1></h1>
                    <div></div>
                    <p></p>
                </body>'
            ],
            "next sibling 2" => [
                "h1 + .child",
                '<body>
                    <h1></h1>
                    <p></p>
                    <p class="child"></p>
                    <h1></h1>
                    text
                    <p class="child" data-match></p>
                    <p></p>
                </body>'
            ],
            "next sibling 3" => [
                "h1 + p.child",
                '<body>
                    <h1></h1>
                    <p></p>
                    <p class="child"></p>
                    <h1></h1>
                    <p class="child" data-match></p>
                    <p></p>
                </body>'
            ],

            // Following-sibling combinator
            "following sibling 1" => [
                "h1 ~ p",
                '<body>
                    <h1></h1>
                    <p data-match></p>
                    <p data-match></p>
                    <div>
                        <p></p>
                    </div>
                    <p data-match></p>
                </body>'
            ],
            "following sibling 2" => [
                "h1 ~ .child",
                '<body>
                    <h1></h1>
                    <p class="child" data-match></p>
                    <p class="child" data-match></p>
                </body>'
            ],
            "following sibling 3" => [
                "h1 ~ p.child",
                '<body>
                    <h1></h1>
                    <p></p>
                    <div class="child"></div>
                </body>'
            ],

            // ID selector
            "id 1" => [
                "#test",
                '<body><p id="test" data-match></p></body>'
            ],
            "id 2" => [
                "p#test",
                '<body><p id="test" data-match></p></body>'
            ],
            "id 3" => [
                "div#test",
                '<body><p id="test"></p></body>'
            ],
            "id 4" => [
                "#test",
                '<body><p id="test test"></p></body>'
            ],

            // Class selector
            "class 1" => [
                ".test",
                '<body><p class="test" data-match></p></body>'
            ],
            "class 2" => [
                "p.test",
                '<body>
                    <p class="test more" data-match></p>
                    <div class="test"></div>
                </body>'
            ],
            "class 3" => [
                ".test.more",
                '<body>
                    <p class="test more" data-match></p>
                    <div class="
                    more
                    test
                    " data-match></div>
                </body>'
            ],

            // root
            "root 1" => [
                ":root",
                '<body><div></div></body>',
                true
            ],
            "root 2" => [
                "body:root",
                '<body><div></div></body>',
                false
            ],

            // first-child
            "first-child 1" => [
                ":first-child",
                '<body>
                    <div data-match>
                        <p data-match></p>
                        <p></p>
                    </div>
                    <div>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>',
                true
            ],
            "first-child 2" => [
                "body :first-child",
                '<body>
                    <div data-match>
                        <p data-match></p>
                        <p></p>
                    </div>
                    <div>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>'
            ],

            // last-child
            "last-child 1" => [
                ":last-child",
                '<body data-match>
                    <div>
                        <p></p>
                        <p data-match></p>
                    </div>
                    <div data-match>
                        <p></p>
                        <p data-match></p>
                    </div>
                </body>',
                true
            ],
            "last-child 2" => [
                "body :last-child",
                '<body>
                    <div>
                        <p></p>
                        <p data-match></p>
                    </div>
                    <div data-match>
                        <p></p>
                        <p data-match></p>
                    </div>
                </body>'
            ],

            // only-child
            "only-child" => [
                ":only-child",
                '<body>
                    <div>
                        <p></p>
                        <p></p>
                    </div>
                    <div>
                        <p data-match></p>
                    </div>
                </body>',
                true
            ],

            // nth-child
            "nth-child odd" => [
                "div > :nth-child(odd)",
                '<body>
                    <div>
                        <p data-match></p>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                        <p data-match></p>
                    </div>
                    <div>
                        <p data-match></p>
                        <div></div>
                        <div data-match></div>
                        <p></p>
                        <div data-match></div>
                        <p></p>
                    </div>
                </body>'
            ],
            "nth-child even" => [
                "div > *:nth-child(even)",
                '<body>
                    <div>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                    </div>
                    <div>
                        <p></p>
                        <p data-match></p>
                        <div></div>
                        <div data-match></div>
                        <p></p>
                        <div data-match></div>
                    </div>
                </body>'
            ],
            "nth-child 1" => [
                ":nth-child(1)",
                '<body>
                    <div data-match>
                        <p data-match></p>
                        <p></p>
                    </div>
                    <div>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>',
                true
            ],
            "nth-child 2" => [
                ":nth-child(2)",
                '<body data-match>
                    <div>
                        <p></p>
                        <p data-match></p>
                    </div>
                    <div data-match>
                        <div></div>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>'
            ],
            "nth-child 3" => [
                "div > p:nth-child(3n + 1)",
                '<body>
                    <div>
                        <p data-match></p>
                        <p></p>
                        <p></p>
                        <div></div>
                        <p></p>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                        <p></p>
                        <p data-match></p>
                    </div>
                    <div>
                        <p data-match></p>
                    </div>
                </body>'
            ],
            "nth-child 4" => [
                ".child:nth-child(2n + 2)",
                '<body>
                    <div>
                        <p class="child"></p>
                        <p class="child" data-match></p>
                        <p class="child"></p>
                        <p></p>
                        <p class="child"></p>
                        <div class="child" data-match></div>
                    </div>
                    <div>
                        <p class="child"></p>
                    </div>
                </body>'
            ],
            "nth-child 5" => [
                ".child:nth-child(n + 4)",
                '<body>
                    <div>
                        <p class="child"></p>
                        <p></p>
                        <p class="child"></p>
                        <p class="child" data-match></p>
                        <p class="child" data-match></p>
                    </div>
                    <div>
                        <p class="child"></p>
                    </div>
                </body>'
            ],
            "nth-child 6" => [
                "div > :nth-child(-n + 3)",
                '<body>
                    <div>
                        <p data-match></p>
                        <p data-match></p>
                        <div data-match></div>
                        <div></div>
                        <p></p>
                    </div>
                    <div>
                        <p data-match></p>
                    </div>
                </body>'
            ],

            // nth-last-child
            "nth-last-child 1" => [
                "body :nth-last-child(2)",
                '<body>
                    <div data-match>
                        <p data-match></p>
                        <p></p>
                    </div>
                    <div>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>'
            ],
            "nth-last-child 2" => [
                "body :nth-last-child(-2n + 4)",
                '<body>
                    <div data-match>
                        <p></p>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                    </div>
                    <div>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>'
            ],

            // first-of-type
            "first-of-type 1" => [
                "p:first-of-type",
                '<body>
                    <div>
                        <p data-match></p>
                        <p></p>
                    </div>
                    <div>
                        <div></div>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>'
            ],
            "first-of-type 2" => [
                "body p:first-of-type",
                '<body>
                    <div>
                        <p data-match></p>
                        <p></p>
                    </div>
                    <div>
                        <div></div>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>'
            ],

            // last-of-type
            "last-of-type 1" => [
                "p:last-of-type",
                '<body>
                    <div>
                        <p></p>
                        <p data-match></p>
                    </div>
                    <div>
                        <p></p>
                        <p data-match></p>
                        <div></div>
                    </div>
                </body>'
            ],
            "last-of-type 2" => [
                "body p:last-of-type",
                '<body>
                    <div>
                        <p></p>
                        <p data-match></p>
                    </div>
                    <div>
                        <p></p>
                        <p data-match></p>
                        <div></div>
                    </div>
                </body>'
            ],

            // only-of-type
            "only-of-type" => [
                "p:only-of-type",
                '<body>
                    <div>
                        <p></p>
                        <p></p>
                    </div>
                    <div>
                        <div></div>
                        <p data-match></p>
                        <div></div>
                    </div>
                </body>'
            ],

            // nth-of-type
            "nth-of-type odd" => [
                "div > p:nth-of-type(odd)",
                '<body>
                    <div>
                        <div></div>
                        <p data-match></p>
                        <div></div>
                        <p></p>
                        <div></div>
                        <p data-match></p>
                    </div>
                    <div>
                        <p data-match></p>
                        <div></div>
                        <p></p>
                        <div></div>
                        <p data-match></p>
                        <div></div>
                    </div>
                </body>'
            ],
            "nth-of-type even" => [
                "div > p:nth-of-type(even)",
                '<body>
                    <div>
                        <div></div>
                        <p></p>
                        <div></div>
                        <p data-match></p>
                        <div></div>
                    </div>
                    <div>
                        <div></div>
                        <p></p>
                        <div></div>
                        <p data-match></p>
                        <div></div>
                        <p></p>
                    </div>
                </body>'
            ],
            "nth-of-type 1" => [
                "p:nth-of-type(1)",
                '<body>
                    <div>
                        <div></div>
                        <p data-match></p>
                        <p></p>
                    </div>
                    <div>
                        <div></div>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>'
            ],
            "nth-of-type 2" => [
                "p:nth-of-type(2)",
                '<body>
                    <div>
                        <p></p>
                        <div></div>
                        <p data-match></p>
                    </div>
                    <div>
                        <div></div>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>'
            ],
            "nth-of-type 3" => [
                "div > p:nth-of-type(3n + 1)",
                '<body>
                    <div>
                        <div></div>
                        <div></div>
                        <p data-match></p>
                        <div></div>
                        <div></div>
                        <p></p>
                        <div></div>
                        <div></div>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                        <p></p>
                        <p data-match></p>
                    </div>
                    <div>
                        <div></div>
                        <p data-match></p>
                    </div>
                </body>'
            ],
            "nth-of-type 4" => [
                "p.child:nth-of-type(2n + 2)",
                '<body>
                    <div>
                        <div class="child"></div>
                        <p></p>
                        <p class="child" data-match></p>
                        <p class="child"></p>
                        <p></p>
                        <div></div>
                        <p class="child"></p>
                        <p class="child" data-match></p>
                        <p class="child"></p>
                    </div>
                    <div>
                        <p class="child"></p>
                        <p class="child" data-match></p>
                    </div>
                </body>'
            ],

            // nth-last-of-type
            "nth-last-of-type 1" => [
                "p:nth-last-of-type(2)",
                '<body>
                    <div>
                        <p data-match></p>
                        <p></p>
                        <div></div>
                    </div>
                    <div>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>'
            ],
            "nth-last-of-type 2" => [
                "p:nth-last-of-type(-2n + 4)",
                '<body>
                    <div>
                        <p></p>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                        <p data-match></p>
                        <p></p>
                        <div></div>
                    </div>
                    <div>
                        <p data-match></p>
                        <p></p>
                    </div>
                </body>'
            ],

            // link
            "link" => [
                ":link",
                '<body><a href="https://example.com" data-match></a></body>'
            ],
            "any-link" => [
                ":any-link",
                '<body><a href="https://example.com" data-match></a></body>'
            ],
            "visited" => [
                ":visited",
                '<body><a href="https://example.com"></a></body>'
            ],

            // attribute ~=
            "attribute ~= 1" => [
                '[title~="multiple"]',
                '<body><div title="multiple tokens" data-match></div></body>'
            ],
            "attribute ~= 2" => [
                '[title~="mult"]',
                '<body><div title="multiple tokens"></div></body>'
            ],
            "attribute ~= 3" => [
                '[title~="multiple tokens"]',
                '<body><div title="multiple tokens"></div></body>'
            ],
            "attribute ~= 4" => [
                '[title~=""]',
                '<body><div title="multiple tokens"></div><div title=""></div></body>'
            ],

            // attribute ^=
            "attribute ^= 1" => [
                '[title^="multiple"]',
                '<body><div title="multiple tokens" data-match></div></body>'
            ],
            "attribute ^= 2" => [
                '[title^="mult"]',
                '<body><div title="multiple tokens" data-match></div></body>'
            ],
            "attribute ^= 3" => [
                '[title^="kens"]',
                '<body><div title="multiple tokens"></div></body>'
            ],
            "attribute ^= 4" => [
                '[title^="le t"]',
                '<body><div title="multiple tokens"></div></body>'
            ],
            "attribute ^= 5" => [
                '[title^="multiple tokens"]',
                '<body><div title="multiple tokens" data-match></div></body>'
            ],
            "attribute ^= 6" => [
                '[title^=""]',
                '<body><div title="multiple tokens"></div><div title=""></div></body>'
            ],

            // attribute $=
            "attribute $= 1" => [
                '[title$="multiple"]',
                '<body><div title="multiple tokens"></div></body>'
            ],
            "attribute $= 2" => [
                '[title$="mult"]',
                '<body><div title="multiple tokens"></div></body>'
            ],
            "attribute $= 3" => [
                '[title$="kens"]',
                '<body><div title="multiple tokens" data-match></div></body>'
            ],
            "attribute $= 4" => [
                '[title$="le t"]',
                '<body><div title="multiple tokens"></div></body>'
            ],
            "attribute $= 5" => [
                '[title$="multiple tokens"]',
                '<body><div title="multiple tokens" data-match></div></body>'
            ],
            "attribute $= 6" => [
                '[title$=""]',
                '<body><div title="multiple tokens"></div><div title=""></div></body>'
            ],

            // attribute *=
            "attribute *= 1" => [
                '[title*="multiple"]',
                '<body><div title="multiple tokens" data-match></div></body>'
            ],
            "attribute *= 2" => [
                '[title*="mult"]',
                '<body><div title="multiple tokens" data-match></div></body>'
            ],
            "attribute *= 3" => [
                '[title*="kens"]',
                '<body><div title="multiple tokens" data-match></div></body>'
            ],
            "attribute *= 4" => [
                '[title*="le t"]',
                '<body><div title="multiple tokens" data-match></div></body>'
            ],
            "attribute *= 5" => [
                '[title*="multiple tokens"]',
                '<body><div title="multiple tokens" data-match></div></body>'
            ],
            "attribute *= 6" => [
                '[title*=""]',
                '<body><div title="multiple tokens"></div><div title=""></div></body>'
            ],

            // escaped selector characteres
            "escaped classname characters" => [
                '.w-\[var\(--sidebar-width\)\]',
                '<body><div class="w-\[var\(--sidebar-width\)\]"></div></body>'
            ]
        ];
    }

    /**
     * @dataProvider selectorMatchesProvider
     */
    #[\PHPUnit\Framework\Attributes\DataProvider('selectorMatchesProvider')]
    public function testSelectorMatches(
        string $selector,
        string $body,
        bool $matchRoot = false
    ): void {
        $sheet = $this->stylesheet();
        $dom = new DOMDocument();
        $dom->loadHTML("<html><head></head>$body</html>");
        $tree = new FrameTree($dom);
        $tree->build_tree();
        $xpath = new DOMXPath($dom);

        $query = $sheet->selectorToXpath($this->preProcess($selector));
        $this->assertNotNull($query);
        $nodeList = $xpath->query($query["query"]);
        $this->assertNotFalse($nodeList);
        $nodes = iterator_to_array($nodeList);

        foreach ($tree as $frame) {
            $node = $frame->get_node();
            $name = $node->nodeName;

            // Skip text nodes and head
            if (!($node instanceof DOMElement) || $name === "head") {
                continue;
            }

            $shouldMatch = $name === "html"
                ? $matchRoot
                : $node->hasAttribute("data-match");
            $matches = in_array($node, $nodes, true);

            $failureMessage = $shouldMatch
                ? "Node $name should be matched by selector."
                : "Node $name should not be matched by selector.";

            $this->assertSame($shouldMatch, $matches, $failureMessage);
        }
    }

    public static function selectorInvalidProvider(): array
    {
        return [
            // Invalid selectors
            [":unknown"],
            ["p:unknown"],
            ["[d~]"],
            ["[href=x"],
            ["[href"]
        ];
    }

    /**
     * @dataProvider selectorInvalidProvider
     */
    #[\PHPUnit\Framework\Attributes\DataProvider('selectorInvalidProvider')]
    public function testSelectorInvalid(string $selector): void
    {
        $sheet = $this->stylesheet();
        $query = $sheet->selectorToXpath($this->preProcess($selector));

        $this->assertNull($query);
    }
}
