/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use cssparser::{self, SourceLocation};
use html5ever::Namespace as NsAtom;
use parking_lot::RwLock;
use selectors::attr::*;
use selectors::parser::*;
use servo_arc::Arc;
use servo_config::prefs::{PrefValue, PREFS};
use servo_url::ServoUrl;
use std::borrow::ToOwned;
use std::cell::RefCell;
use std::sync::atomic::AtomicBool;
use style::context::QuirksMode;
use style::error_reporting::{ContextualParseError, ParseErrorReporter};
use style::media_queries::MediaList;
use style::properties::longhands::{self, animation_timing_function};
use style::properties::{CSSWideKeyword, CustomDeclaration};
use style::properties::{CustomDeclarationValue, Importance};
use style::properties::{PropertyDeclaration, PropertyDeclarationBlock};
use style::shared_lock::SharedRwLock;
use style::stylesheets::keyframes_rule::{Keyframe, KeyframePercentage, KeyframeSelector};
use style::stylesheets::{
    CssRule, CssRules, KeyframesRule, NamespaceRule, StyleRule, Stylesheet, StylesheetContents,
};
use style::stylesheets::{Namespaces, Origin};
use style::values::computed::Percentage;
use style::values::specified::TimingFunction;
use style::values::specified::{LengthPercentageOrAuto, PositionComponent};
use style::values::{CustomIdent, KeyframesName};
use stylo_atoms::Atom;

pub fn block_from<I>(iterable: I) -> PropertyDeclarationBlock
where
    I: IntoIterator<Item = (PropertyDeclaration, Importance)>,
{
    let mut block = PropertyDeclarationBlock::new();
    for (d, i) in iterable {
        block.push(d, i);
    }
    block
}

#[test]
fn test_parse_stylesheet() {
    let css = r"
        @namespace url(http://www.w3.org/1999/xhtml);
        /* FIXME: only if scripting is enabled */
        input[type=hidden i] {
            display: block !important;
            display: none !important;
            display: inline;
            --a: b !important;
            --a: inherit !important;
            --a: c;
        }
        html , body /**/ {
            display: none;
            display: block;
        }
        #d1 > .ok { background: blue; }
        @keyframes foo {
            from { width: 0% }
            to {
                width: 100%;
                width: 50% !important; /* !important not allowed here */
                animation-name: 'foo'; /* animation properties not allowed here */
                animation-timing-function: ease; /* … except animation-timing-function */
            }
        }";
    let url = ServoUrl::parse("about::test").unwrap();
    let lock = SharedRwLock::new();
    let media = Arc::new(lock.wrap(MediaList::empty()));
    let stylesheet = Stylesheet::from_str(
        css,
        url.clone(),
        Origin::UserAgent,
        media,
        lock,
        None,
        None,
        QuirksMode::NoQuirks,
        0,
    );
    let mut namespaces = Namespaces::default();
    namespaces.default = Some(ns!(html));
    let expected = Stylesheet {
        contents: StylesheetContents {
            origin: Origin::UserAgent,
            namespaces: RwLock::new(namespaces),
            url_data: RwLock::new(url),
            quirks_mode: QuirksMode::NoQuirks,
            rules: CssRules::new(
                vec![
                    CssRule::Namespace(Arc::new(stylesheet.shared_lock.wrap(NamespaceRule {
                        prefix: None,
                        url: NsAtom::from("http://www.w3.org/1999/xhtml"),
                        source_location: SourceLocation {
                            line: 1,
                            column: 19,
                        },
                    }))),
                    CssRule::Style(Arc::new(stylesheet.shared_lock.wrap(StyleRule {
                        selectors: SelectorList::from_vec(vec![Selector::from_vec(
                            vec![
                                Component::DefaultNamespace(NsAtom::from(
                                    "http://www.w3.org/1999/xhtml",
                                )),
                                Component::LocalName(LocalName {
                                    name: local_name!("input"),
                                    lower_name: local_name!("input"),
                                }),
                                Component::AttributeInNoNamespace {
                                    local_name: local_name!("type"),
                                    operator: AttrSelectorOperator::Equal,
                                    value: "hidden".to_owned(),
                                    case_sensitivity: ParsedCaseSensitivity::AsciiCaseInsensitive,
                                    never_matches: false,
                                },
                            ],
                            (0 << 20) + (1 << 10) + (1 << 0),
                        )]),
                        block: Arc::new(stylesheet.shared_lock.wrap(block_from(vec![
                            (
                                PropertyDeclaration::Display(
                                    longhands::display::SpecifiedValue::None,
                                ),
                                Importance::Important,
                            ),
                            (
                                PropertyDeclaration::Custom(CustomDeclaration {
                                    name: Atom::from("a"),
                                    value: CustomDeclarationValue::CSSWideKeyword(
                                        CSSWideKeyword::Inherit,
                                    ),
                                }),
                                Importance::Important,
                            ),
                        ]))),
                        source_location: SourceLocation { line: 3, column: 9 },
                    }))),
                    CssRule::Style(Arc::new(stylesheet.shared_lock.wrap(StyleRule {
                        selectors: SelectorList::from_vec(vec![
                            Selector::from_vec(
                                vec![
                                    Component::DefaultNamespace(NsAtom::from(
                                        "http://www.w3.org/1999/xhtml",
                                    )),
                                    Component::LocalName(LocalName {
                                        name: local_name!("html"),
                                        lower_name: local_name!("html"),
                                    }),
                                ],
                                (0 << 20) + (0 << 10) + (1 << 0),
                            ),
                            Selector::from_vec(
                                vec![
                                    Component::DefaultNamespace(NsAtom::from(
                                        "http://www.w3.org/1999/xhtml",
                                    )),
                                    Component::LocalName(LocalName {
                                        name: local_name!("body"),
                                        lower_name: local_name!("body"),
                                    }),
                                ],
                                (0 << 20) + (0 << 10) + (1 << 0),
                            ),
                        ]),
                        block: Arc::new(stylesheet.shared_lock.wrap(block_from(vec![(
                            PropertyDeclaration::Display(longhands::display::SpecifiedValue::Block),
                            Importance::Normal,
                        )]))),
                        source_location: SourceLocation {
                            line: 11,
                            column: 9,
                        },
                    }))),
                    CssRule::Style(Arc::new(stylesheet.shared_lock.wrap(StyleRule {
                        selectors: SelectorList::from_vec(vec![Selector::from_vec(
                            vec![
                                Component::DefaultNamespace(NsAtom::from(
                                    "http://www.w3.org/1999/xhtml",
                                )),
                                Component::ID(Atom::from("d1")),
                                Component::Combinator(Combinator::Child),
                                Component::DefaultNamespace(NsAtom::from(
                                    "http://www.w3.org/1999/xhtml",
                                )),
                                Component::Class(Atom::from("ok")),
                            ],
                            (1 << 20) + (1 << 10) + (0 << 0),
                        )]),
                        block: Arc::new(stylesheet.shared_lock.wrap(block_from(vec![
                            (
                                PropertyDeclaration::BackgroundColor(
                                    longhands::background_color::SpecifiedValue::Numeric {
                                        authored: Some("blue".to_owned().into_boxed_str()),
                                        parsed: cssparser::RGBA::new(0, 0, 255, 255),
                                    },
                                ),
                                Importance::Normal,
                            ),
                            (
                                PropertyDeclaration::BackgroundPositionX(
                                    longhands::background_position_x::SpecifiedValue(vec![
                                        PositionComponent::zero(),
                                    ]),
                                ),
                                Importance::Normal,
                            ),
                            (
                                PropertyDeclaration::BackgroundPositionY(
                                    longhands::background_position_y::SpecifiedValue(vec![
                                        PositionComponent::zero(),
                                    ]),
                                ),
                                Importance::Normal,
                            ),
                            (
                                PropertyDeclaration::BackgroundRepeat(
                                    longhands::background_repeat::SpecifiedValue(
                                        vec![longhands::background_repeat::single_value
                                                       ::get_initial_specified_value()],
                                    ),
                                ),
                                Importance::Normal,
                            ),
                            (
                                PropertyDeclaration::BackgroundAttachment(
                                    longhands::background_attachment::SpecifiedValue(
                                        vec![longhands::background_attachment::single_value
                                                       ::get_initial_specified_value()],
                                    ),
                                ),
                                Importance::Normal,
                            ),
                            (
                                PropertyDeclaration::BackgroundImage(
                                    longhands::background_image::SpecifiedValue(
                                        vec![longhands::background_image::single_value
                                                       ::get_initial_specified_value()],
                                    ),
                                ),
                                Importance::Normal,
                            ),
                            (
                                PropertyDeclaration::BackgroundSize(
                                    longhands::background_size::SpecifiedValue(
                                        vec![longhands::background_size::single_value
                                                       ::get_initial_specified_value()],
                                    ),
                                ),
                                Importance::Normal,
                            ),
                            (
                                PropertyDeclaration::BackgroundOrigin(
                                    longhands::background_origin::SpecifiedValue(
                                        vec![longhands::background_origin::single_value
                                                       ::get_initial_specified_value()],
                                    ),
                                ),
                                Importance::Normal,
                            ),
                            (
                                PropertyDeclaration::BackgroundClip(
                                    longhands::background_clip::SpecifiedValue(
                                        vec![longhands::background_clip::single_value
                                                       ::get_initial_specified_value()],
                                    ),
                                ),
                                Importance::Normal,
                            ),
                        ]))),
                        source_location: SourceLocation {
                            line: 15,
                            column: 9,
                        },
                    }))),
                    CssRule::Keyframes(Arc::new(stylesheet.shared_lock.wrap(KeyframesRule {
                        name: KeyframesName::Ident(CustomIdent("foo".into())),
                        keyframes: vec![
                            Arc::new(stylesheet.shared_lock.wrap(Keyframe {
                                selector: KeyframeSelector::new_for_unit_testing(vec![
                                    KeyframePercentage::new(0.),
                                ]),
                                block: Arc::new(stylesheet.shared_lock.wrap(block_from(vec![(
                                    PropertyDeclaration::Width(LengthPercentageOrAuto::Percentage(
                                        Percentage(0.),
                                    )),
                                    Importance::Normal,
                                )]))),
                                source_location: SourceLocation {
                                    line: 17,
                                    column: 13,
                                },
                            })),
                            Arc::new(stylesheet.shared_lock.wrap(Keyframe {
                                selector: KeyframeSelector::new_for_unit_testing(vec![
                                    KeyframePercentage::new(1.),
                                ]),
                                block: Arc::new(stylesheet.shared_lock.wrap(block_from(vec![
                                    (
                                        PropertyDeclaration::Width(
                                            LengthPercentageOrAuto::Percentage(Percentage(1.)),
                                        ),
                                        Importance::Normal,
                                    ),
                                    (
                                        PropertyDeclaration::AnimationTimingFunction(
                                            animation_timing_function::SpecifiedValue(vec![
                                                TimingFunction::ease(),
                                            ]),
                                        ),
                                        Importance::Normal,
                                    ),
                                ]))),
                                source_location: SourceLocation {
                                    line: 18,
                                    column: 13,
                                },
                            })),
                        ],
                        vendor_prefix: None,
                        source_location: SourceLocation {
                            line: 16,
                            column: 19,
                        },
                    }))),
                ],
                &stylesheet.shared_lock,
            ),
            source_map_url: RwLock::new(None),
            source_url: RwLock::new(None),
        },
        media: Arc::new(stylesheet.shared_lock.wrap(MediaList::empty())),
        shared_lock: stylesheet.shared_lock.clone(),
        disabled: AtomicBool::new(false),
    };

    assert_eq!(format!("{:#?}", stylesheet), format!("{:#?}", expected));
}

#[derive(Debug)]
struct CSSError {
    pub url: ServoUrl,
    pub line: u32,
    pub column: u32,
    pub message: String,
}

struct TestingErrorReporter {
    errors: RefCell<Vec<CSSError>>,
}

impl TestingErrorReporter {
    pub fn new() -> Self {
        TestingErrorReporter {
            errors: RefCell::new(Vec::new()),
        }
    }

    fn assert_messages_contain(&self, expected_errors: &[(u32, u32, &str)]) {
        let errors = self.errors.borrow();
        for (i, (error, &(line, column, message))) in errors.iter().zip(expected_errors).enumerate()
        {
            assert_eq!(
                (error.line, error.column),
                (line, column),
                "line/column numbers of the {}th error: {:?}",
                i + 1,
                error.message
            );
            assert!(
                error.message.contains(message),
                "{:?} does not contain {:?}",
                error.message,
                message
            );
        }
        if errors.len() < expected_errors.len() {
            panic!("Missing errors: {:#?}", &expected_errors[errors.len()..]);
        }
        if errors.len() > expected_errors.len() {
            panic!("Extra errors: {:#?}", &errors[expected_errors.len()..]);
        }
    }
}

impl ParseErrorReporter for TestingErrorReporter {
    fn report_error(&self, url: &ServoUrl, location: SourceLocation, error: ContextualParseError) {
        self.errors.borrow_mut().push(CSSError {
            url: url.clone(),
            line: location.line,
            column: location.column,
            message: error.to_string(),
        })
    }
}

#[test]
fn test_report_error_stylesheet() {
    PREFS.set("layout.viewport.enabled", PrefValue::Boolean(true));
    let css = r"
    div {
        background-color: red;
        display: invalid;
        background-image: linear-gradient(0deg, black, invalid, transparent);
        invalid: true;
    }
    @media (min-width: 10px invalid 1000px) {}
    @font-face { src: url(), invalid, url(); }
    @counter-style foo { symbols: a 0invalid b }
    @font-feature-values Sans Sans { @foo {} @swash { foo: 1 invalid 2 } }
    @invalid;
    @media screen { @invalid; }
    @supports (color: green) and invalid and (margin: 0) {}
    @keyframes foo { from invalid {} to { margin: 0 invalid 0; } }
    @viewport { width: 320px invalid auto; }
    ";
    let url = ServoUrl::parse("about::test").unwrap();
    let error_reporter = TestingErrorReporter::new();

    let lock = SharedRwLock::new();
    let media = Arc::new(lock.wrap(MediaList::empty()));
    Stylesheet::from_str(
        css,
        url.clone(),
        Origin::UserAgent,
        media,
        lock,
        None,
        Some(&error_reporter),
        QuirksMode::NoQuirks,
        5,
    );

    error_reporter.assert_messages_contain(&[
        (
            8,
            18,
            "Unsupported property declaration: 'display: invalid;'",
        ),
        (
            9,
            27,
            "Unsupported property declaration: 'background-image:",
        ), // FIXME: column should be around 56
        (10, 17, "Unsupported property declaration: 'invalid: true;'"),
        (12, 28, "Invalid media rule"),
        (13, 30, "Unsupported @font-face descriptor declaration"),
        // When @counter-style is supported, this should be replaced with two errors
        (14, 19, "Invalid rule: '@counter-style "),
        // When @font-feature-values is supported, this should be replaced with two errors
        (15, 25, "Invalid rule: '@font-feature-values "),
        (16, 13, "Invalid rule: '@invalid'"),
        (17, 29, "Invalid rule: '@invalid'"),
        (18, 34, "Invalid rule: '@supports "),
        (19, 26, "Invalid keyframe rule: 'from invalid '"),
        (
            19,
            52,
            "Unsupported keyframe property declaration: 'margin: 0 invalid 0;'",
        ),
        (
            20,
            29,
            "Unsupported @viewport descriptor declaration: 'width: 320px invalid auto;'",
        ),
    ]);

    assert_eq!(error_reporter.errors.borrow()[0].url, url);
}

#[test]
fn test_no_report_unrecognized_vendor_properties() {
    let css = r"
    div {
        -o-background-color: red;
        _background-color: red;
        -moz-background-color: red;
    }
    ";
    let url = ServoUrl::parse("about::test").unwrap();
    let error_reporter = TestingErrorReporter::new();

    let lock = SharedRwLock::new();
    let media = Arc::new(lock.wrap(MediaList::empty()));
    Stylesheet::from_str(
        css,
        url,
        Origin::UserAgent,
        media,
        lock,
        None,
        Some(&error_reporter),
        QuirksMode::NoQuirks,
        0,
    );

    error_reporter.assert_messages_contain(&[(
        4,
        31,
        "Unsupported property declaration: '-moz-background-color: red;'",
    )]);
}

#[test]
fn test_source_map_url() {
    let tests = vec![
        ("", None),
        (
            "/*# sourceMappingURL=something */",
            Some("something".to_string()),
        ),
    ];

    for test in tests {
        let url = ServoUrl::parse("about::test").unwrap();
        let lock = SharedRwLock::new();
        let media = Arc::new(lock.wrap(MediaList::empty()));
        let stylesheet = Stylesheet::from_str(
            test.0,
            url.clone(),
            Origin::UserAgent,
            media,
            lock,
            None,
            None,
            QuirksMode::NoQuirks,
            0,
        );
        let url_opt = stylesheet.contents.source_map_url.read();
        assert_eq!(*url_opt, test.1);
    }
}

#[test]
fn test_source_url() {
    let tests = vec![
        ("", None),
        ("/*# sourceURL=something */", Some("something".to_string())),
    ];

    for test in tests {
        let url = ServoUrl::parse("about::test").unwrap();
        let lock = SharedRwLock::new();
        let media = Arc::new(lock.wrap(MediaList::empty()));
        let stylesheet = Stylesheet::from_str(
            test.0,
            url.clone(),
            Origin::UserAgent,
            media,
            lock,
            None,
            None,
            QuirksMode::NoQuirks,
            0,
        );
        let url_opt = stylesheet.contents.source_url.read();
        assert_eq!(*url_opt, test.1);
    }
}
