/*****************************************************************************
 * css_test.c: CSS Parser test
 *****************************************************************************
 * Copyright (C) 2019 VideoLabs, VideoLAN and VLC Authors
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#undef NDEBUG

#include <assert.h>
#include <vlc_common.h>
#include "css_parser.h"

#define BAILOUT(run) { fprintf(stderr, "failed %s line %d\n", run, __LINE__); \
                        goto error; }
#define CHECK(test) run = (test); fprintf(stderr, "* Running test %s\n", run);
#define EXPECT(foo) if(!(foo)) BAILOUT(run)

#define PARSE_CSS(r) \
    const char *run = r;\
    vlc_css_parser_t p;\
    vlc_css_parser_Init(&p);\
    bool b = vlc_css_parser_ParseBytes(&p, (const uint8_t *)css, strlen(css));\
    EXPECT(b);\
    vlc_css_parser_Debug(&p);\
    const vlc_css_rule_t *rule = p.rules.p_first


static int test_element_selectors(void)
{
    const char * css =
        "el1 { float0: 1; }\n"
        ".class1 { hex1: #F0000f; }\n"
        "#id1 { text2: \"foo bar\"; }\n"
    ;

    PARSE_CSS("test_element_selectors");

    CHECK("element selector");
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"el1"));
    EXPECT(rule->p_selectors->type == SELECTOR_SIMPLE);
    const vlc_css_declaration_t *decl = rule->p_declarations;
    EXPECT(decl && !strcmp(rule->p_declarations->psz_property, "float0"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_NONE);
    EXPECT(decl->expr->seq[0].term.val == 1.0);

    CHECK("class selector");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"class1"));
    EXPECT(rule->p_selectors->type == SPECIFIER_CLASS);
    decl = rule->p_declarations;
    EXPECT(decl && !strcmp(rule->p_declarations->psz_property, "hex1"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_HEXCOLOR);
    EXPECT(!strcmp(decl->expr->seq[0].term.psz,"#F0000f"));

    CHECK("id selector");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"#id1"));
    EXPECT(rule->p_selectors->type == SPECIFIER_ID);
    decl = rule->p_declarations;
    EXPECT(decl && !strcmp(rule->p_declarations->psz_property, "text2"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_STRING);
    EXPECT(!strcmp(decl->expr->seq[0].term.psz,"foo bar"));

    vlc_css_parser_Clean(&p);
    return 0;

error:
    vlc_css_parser_Clean(&p);
    return 1;
}

static int test_attributes_selectors(void)
{
    const char * css =
        "attrib[foo=\"bar\"] { text2: \"foobar\"; }\n"
        "attrib2[foo] { text2: \"foobar\"; }\n"
        "attribincludes[foo~=\"bar\"] { text2: \"foobar\"; }\n"
        "attribdashmatch[foo|=\"bar\"] { text2: \"foobar\"; }\n"
        "attribstarts[foo^=\"bar\"] { text2: \"foobar\"; }\n"
        "attribends[foo$=\"bar\"] { text2: \"foobar\"; }\n"
        "attribcontains[foo*=\"bar\"] { text2: \"foobar\"; }\n"
    ;

    PARSE_CSS("test_attributes_selectors");

    CHECK("attribute selector equals");
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"attrib"));
    EXPECT(rule->p_selectors->type == SELECTOR_SIMPLE);
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->type == SPECIFIER_ATTRIB);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "foo"));
    EXPECT(rule->p_selectors->specifiers.p_first->p_matchsel);
    EXPECT(rule->p_selectors->specifiers.p_first->p_matchsel->match == MATCH_EQUALS);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->p_matchsel->psz_name, "bar"));

    CHECK("attribute selector key only");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"attrib2"));
    EXPECT(rule->p_selectors->type == SELECTOR_SIMPLE);
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->type == SPECIFIER_ATTRIB);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "foo"));
    EXPECT(rule->p_selectors->specifiers.p_first->p_matchsel == NULL);

    CHECK("attribute selector ~=");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"attribincludes"));
    EXPECT(rule->p_selectors->type == SELECTOR_SIMPLE);
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->type == SPECIFIER_ATTRIB);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "foo"));
    EXPECT(rule->p_selectors->specifiers.p_first->match == MATCH_INCLUDES);
    EXPECT(rule->p_selectors->specifiers.p_first->p_matchsel);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->p_matchsel->psz_name, "bar"));

    CHECK("attribute selector |=");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"attribdashmatch"));
    EXPECT(rule->p_selectors->type == SELECTOR_SIMPLE);
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->type == SPECIFIER_ATTRIB);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "foo"));
    EXPECT(rule->p_selectors->specifiers.p_first->match == MATCH_DASHMATCH);
    EXPECT(rule->p_selectors->specifiers.p_first->p_matchsel);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->p_matchsel->psz_name, "bar"));

    CHECK("attribute selector ^=");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"attribstarts"));
    EXPECT(rule->p_selectors->type == SELECTOR_SIMPLE);
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->type == SPECIFIER_ATTRIB);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "foo"));
    EXPECT(rule->p_selectors->specifiers.p_first->match == MATCH_BEGINSWITH);
    EXPECT(rule->p_selectors->specifiers.p_first->p_matchsel);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->p_matchsel->psz_name, "bar"));

    CHECK("attribute selector $=");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"attribends"));
    EXPECT(rule->p_selectors->type == SELECTOR_SIMPLE);
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->type == SPECIFIER_ATTRIB);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "foo"));
    EXPECT(rule->p_selectors->specifiers.p_first->match == MATCH_ENDSWITH);
    EXPECT(rule->p_selectors->specifiers.p_first->p_matchsel);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->p_matchsel->psz_name, "bar"));

    CHECK("attribute selector *=");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"attribcontains"));
    EXPECT(rule->p_selectors->type == SELECTOR_SIMPLE);
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->type == SPECIFIER_ATTRIB);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "foo"));
    EXPECT(rule->p_selectors->specifiers.p_first->match == MATCH_CONTAINS);
    EXPECT(rule->p_selectors->specifiers.p_first->p_matchsel);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->p_matchsel->psz_name, "bar"));

    vlc_css_parser_Clean(&p);
    return 0;

error:
    vlc_css_parser_Clean(&p);
    return 1;
}

static int test_pseudo_selectors(void)
{
    const char * css =
        ":pseudo { text2: \"foobar\"; }\n"
        "::cue(b) { color: red; }\n" /* WebVTT special */
        "::cue(v[voice=\"Tom\"]) { color: blue; }\n"
    ;

    PARSE_CSS("test_pseudo_selectors");

    CHECK("pseudoclass selector");
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"pseudo"));
    EXPECT(rule->p_selectors->type == SELECTOR_PSEUDOCLASS);

    CHECK("pseudoelement ::cue");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"cue"));
    EXPECT(rule->p_selectors->type == SELECTOR_PSEUDOELEMENT);

    CHECK("pseudoelement ::cue(v[voice])");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"cue"));
    EXPECT(rule->p_selectors->type == SELECTOR_PSEUDOELEMENT);
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->type == SELECTOR_SIMPLE);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "v"));
    EXPECT(rule->p_selectors->specifiers.p_first->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->specifiers.p_first->type == SPECIFIER_ATTRIB);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->specifiers.p_first->psz_name, "voice"));
    EXPECT(rule->p_selectors->specifiers.p_first->specifiers.p_first->p_matchsel);
    EXPECT(rule->p_selectors->specifiers.p_first->specifiers.p_first->p_matchsel->match == MATCH_EQUALS);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->specifiers.p_first->p_matchsel->psz_name, "Tom"));

    vlc_css_parser_Clean(&p);
    return 0;

error:
    vlc_css_parser_Clean(&p);
    return 1;
}

static int test_combinators(void)
{
    const char * css =
        "parent1 child1 { float0: 1; }\n"
        "el2,el3 { float0: 1; }\n"
        "el4+el0 { float0: 1; }\n"
        "el5~el0 { float0: 1; }\n"
        "el6>el0 { float0: 1; }\n"
    ;

    PARSE_CSS("test_combinators");

    CHECK("selectors combination parent child");
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"parent1"));
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->combinator == RELATION_DESCENDENT);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "child1"));

    CHECK("selectors combination alternative");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"el2"));
    EXPECT(rule->p_selectors->p_next);
    EXPECT(!strcmp(rule->p_selectors->p_next->psz_name,"el3"));

    CHECK("selectors combination directadjacent");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"el4"));
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->combinator == RELATION_DIRECTADJACENT);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "el0"));

    CHECK("selectors combination directprecedent");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"el5"));
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->combinator == RELATION_INDIRECTADJACENT);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "el0"));

    CHECK("selectors combination child");
    rule = rule->p_next;
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"el6"));
    EXPECT(rule->p_selectors->specifiers.p_first);
    EXPECT(rule->p_selectors->specifiers.p_first->combinator == RELATION_CHILD);
    EXPECT(!strcmp(rule->p_selectors->specifiers.p_first->psz_name, "el0"));

    vlc_css_parser_Clean(&p);
    return 0;

error:
    vlc_css_parser_Clean(&p);
    return 1;
}

static int test_values(void)
{
    const char * css =
        "values { "
        "  neg: -1;"
        "  ems: 100em;"
        "  exs: 100ex;"
        "  pixels: 100px;"
        "  points: 100pt; "
        "  mm: 100mm;"
        "  percent: 100%;"
        "  ms: 100ms;"
        "  hz: 100Hz;"
        "  degrees: 100deg;"
        "  dimension: 100 -200em 300px;"
        "  string: \"foobar\";"
        "  function: foo(1);"
        "  identifier: foobar;"
        "  hexcolor: #ff00ff;"
        "  unicoderange: U+00-FF;"
        "  uri: url(http://crap/);"
        "}\n"
    ;

    PARSE_CSS("test_values");

    CHECK("values");
    EXPECT(rule && rule->b_valid);
    EXPECT(!strcmp(rule->p_selectors->psz_name,"values"));
    const vlc_css_declaration_t *decl = rule->p_declarations;
    EXPECT(decl && !strcmp(decl->psz_property, "neg"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_NONE);
    EXPECT(decl->expr->seq[0].term.val == -1.0);
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "ems"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_EMS);
    EXPECT(decl->expr->seq[0].term.val == 100);
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "exs"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_EXS);
    EXPECT(decl->expr->seq[0].term.val == 100);
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "pixels"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_PIXELS);
    EXPECT(decl->expr->seq[0].term.val == 100);
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "points"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_POINTS);
    EXPECT(decl->expr->seq[0].term.val == 100);
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "mm"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_MILLIMETERS);
    EXPECT(decl->expr->seq[0].term.val == 100);
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "percent"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_PERCENT);
    EXPECT(decl->expr->seq[0].term.val == 100);
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "ms"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_MILLISECONDS);
    EXPECT(decl->expr->seq[0].term.val == 100);
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "hz"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_HERTZ);
    EXPECT(decl->expr->seq[0].term.val == 100);
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "degrees"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_DEGREES);
    EXPECT(decl->expr->seq[0].term.val == 100);
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "dimension"));
    EXPECT(decl->expr && decl->expr->i_count == 3);
    EXPECT(decl->expr->seq[0].term.type == TYPE_NONE);
    EXPECT(decl->expr->seq[0].term.val == 100);
    EXPECT(decl->expr->seq[1].term.type == TYPE_EMS);
    EXPECT(decl->expr->seq[1].term.val == -200);
    EXPECT(decl->expr->seq[2].term.type == TYPE_PIXELS);
    EXPECT(decl->expr->seq[2].term.val == 300);
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "string"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_STRING);
    EXPECT(!strcmp(decl->expr->seq[0].term.psz, "foobar"));
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "function"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_FUNCTION);
    /* todo */
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "identifier"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_IDENTIFIER);
    EXPECT(!strcmp(decl->expr->seq[0].term.psz, "foobar"));
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "hexcolor"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_HEXCOLOR);
    EXPECT(!strcmp(decl->expr->seq[0].term.psz, "#ff00ff"));
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "unicoderange"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_UNICODERANGE);
    EXPECT(!strcmp(decl->expr->seq[0].term.psz, "U+00-FF"));
    decl = decl->p_next;
    EXPECT(decl && !strcmp(decl->psz_property, "uri"));
    EXPECT(decl->expr && decl->expr->i_count);
    EXPECT(decl->expr->seq[0].term.type == TYPE_URI);
    EXPECT(!strcmp(decl->expr->seq[0].term.psz, "url(http://crap/)"));

    vlc_css_parser_Clean(&p);
    return 0;

error:
    vlc_css_parser_Clean(&p);
    return 1;
}

static int test_error_cases(void)
{
    const char *css =
        /* Invalid charset */
        "@charset \"UTF-8\" extra;\n"
        "@charset ;\n"
        /* Invalid block */
        "el1 { color { ; color: red } }\n"
        "el2 { color: red; }\n"
        "el3 { color: red !important fail; }\n"
        "el4 { font-family: ; }\n"
        "el5 { weight: *; }\n"
        "el6 { color: red; !important }\n"
        /* Invalid selectors */
        "#123abc { color: red; }\n" /* HASH starting with digit triggers YYERROR */
        "el7 + ;\n" /* combinators without right side */
        "el8 ~ ;\n"
        "el9 > ;\n"
        "el10 , ;\n"
        /* Invalid declarations */
        "div { ; }\n"
        "div { color: ; }\n"
        "div { color: *; }\n"
        /* Invalid function / expr */
        "div { foo(): 1; }\n"
        "div { bar( ; }\n"
    ;

    PARSE_CSS("test_error_cases");

    /* We just want to make sure the parser
     * does not crash on error handling and backtracking
     * and frees memory */

    VLC_UNUSED(rule);

    vlc_css_parser_Clean(&p);
    return 0;

error:
    vlc_css_parser_Clean(&p);
    return 1;
}

int main(void)
{
    return test_element_selectors() ||
           test_attributes_selectors() ||
           test_pseudo_selectors() ||
           test_combinators() ||
           test_values() ||
           test_error_cases();
}
