import pytest

from pandas.errors import CSSWarning

import pandas._testing as tm

from pandas.io.formats.css import CSSResolver


def assert_resolves(css, props, inherited=None):
    resolve = CSSResolver()
    actual = resolve(css, inherited=inherited)
    assert props == actual


def assert_same_resolution(css1, css2, inherited=None):
    resolve = CSSResolver()
    resolved1 = resolve(css1, inherited=inherited)
    resolved2 = resolve(css2, inherited=inherited)
    assert resolved1 == resolved2


@pytest.mark.parametrize(
    "name,norm,abnorm",
    [
        (
            "whitespace",
            "hello: world; foo: bar",
            " \t hello \t :\n  world \n  ;  \n foo: \tbar\n\n",
        ),
        ("case", "hello: world; foo: bar", "Hello: WORLD; foO: bar"),
        ("empty-decl", "hello: world; foo: bar", "; hello: world;; foo: bar;\n; ;"),
        ("empty-list", "", ";"),
    ],
)
def test_css_parse_normalisation(name, norm, abnorm):
    assert_same_resolution(norm, abnorm)


@pytest.mark.parametrize(
    "invalid_css,remainder,msg",
    [
        # No colon
        ("hello-world", "", "expected a colon"),
        ("border-style: solid; hello-world", "border-style: solid", "expected a colon"),
        (
            "border-style: solid; hello-world; font-weight: bold",
            "border-style: solid; font-weight: bold",
            "expected a colon",
        ),
        # Unclosed string fail
        # Invalid size
        ("font-size: blah", "font-size: 1em", "Unhandled size"),
        ("font-size: 1a2b", "font-size: 1em", "Unhandled size"),
        ("font-size: 1e5pt", "font-size: 1em", "Unhandled size"),
        ("font-size: 1+6pt", "font-size: 1em", "Unhandled size"),
        ("font-size: 1unknownunit", "font-size: 1em", "Unhandled size"),
        ("font-size: 10", "font-size: 1em", "Unhandled size"),
        ("font-size: 10 pt", "font-size: 1em", "Unhandled size"),
        # Too many args
        ("border-top: 1pt solid red green", "border-top: 1pt solid green", "Too many"),
    ],
)
def test_css_parse_invalid(invalid_css, remainder, msg):
    with tm.assert_produces_warning(CSSWarning, match=msg):
        assert_same_resolution(invalid_css, remainder)


@pytest.mark.parametrize(
    "shorthand,expansions",
    [
        ("margin", ["margin-top", "margin-right", "margin-bottom", "margin-left"]),
        ("padding", ["padding-top", "padding-right", "padding-bottom", "padding-left"]),
        (
            "border-width",
            [
                "border-top-width",
                "border-right-width",
                "border-bottom-width",
                "border-left-width",
            ],
        ),
        (
            "border-color",
            [
                "border-top-color",
                "border-right-color",
                "border-bottom-color",
                "border-left-color",
            ],
        ),
        (
            "border-style",
            [
                "border-top-style",
                "border-right-style",
                "border-bottom-style",
                "border-left-style",
            ],
        ),
    ],
)
def test_css_side_shorthands(shorthand, expansions):
    top, right, bottom, left = expansions

    assert_resolves(
        f"{shorthand}: 1pt", {top: "1pt", right: "1pt", bottom: "1pt", left: "1pt"}
    )

    assert_resolves(
        f"{shorthand}: 1pt 4pt", {top: "1pt", right: "4pt", bottom: "1pt", left: "4pt"}
    )

    assert_resolves(
        f"{shorthand}: 1pt 4pt 2pt",
        {top: "1pt", right: "4pt", bottom: "2pt", left: "4pt"},
    )

    assert_resolves(
        f"{shorthand}: 1pt 4pt 2pt 0pt",
        {top: "1pt", right: "4pt", bottom: "2pt", left: "0pt"},
    )

    with tm.assert_produces_warning(CSSWarning, match="Could not expand"):
        assert_resolves(f"{shorthand}: 1pt 1pt 1pt 1pt 1pt", {})


@pytest.mark.parametrize(
    "shorthand,sides",
    [
        ("border-top", ["top"]),
        ("border-right", ["right"]),
        ("border-bottom", ["bottom"]),
        ("border-left", ["left"]),
        ("border", ["top", "right", "bottom", "left"]),
    ],
)
def test_css_border_shorthand_sides(shorthand, sides):
    def create_border_dict(sides, color=None, style=None, width=None):
        resolved = {}
        for side in sides:
            if color:
                resolved[f"border-{side}-color"] = color
            if style:
                resolved[f"border-{side}-style"] = style
            if width:
                resolved[f"border-{side}-width"] = width
        return resolved

    assert_resolves(
        f"{shorthand}: 1pt red solid", create_border_dict(sides, "red", "solid", "1pt")
    )


@pytest.mark.parametrize(
    "prop, expected",
    [
        ("1pt red solid", ("red", "solid", "1pt")),
        ("red 1pt solid", ("red", "solid", "1pt")),
        ("red solid 1pt", ("red", "solid", "1pt")),
        ("solid 1pt red", ("red", "solid", "1pt")),
        ("red solid", ("red", "solid", "1.500000pt")),
        # Note: color=black is not CSS conforming
        # (See https://drafts.csswg.org/css-backgrounds/#border-shorthands)
        ("1pt solid", ("black", "solid", "1pt")),
        ("1pt red", ("red", "none", "1pt")),
        ("red", ("red", "none", "1.500000pt")),
        ("1pt", ("black", "none", "1pt")),
        ("solid", ("black", "solid", "1.500000pt")),
        # Sizes
        ("1em", ("black", "none", "12pt")),
    ],
)
def test_css_border_shorthands(prop, expected):
    color, style, width = expected

    assert_resolves(
        f"border-left: {prop}",
        {
            "border-left-color": color,
            "border-left-style": style,
            "border-left-width": width,
        },
    )


@pytest.mark.parametrize(
    "style,inherited,equiv",
    [
        ("margin: 1px; margin: 2px", "", "margin: 2px"),
        ("margin: 1px", "margin: 2px", "margin: 1px"),
        ("margin: 1px; margin: inherit", "margin: 2px", "margin: 2px"),
        (
            "margin: 1px; margin-top: 2px",
            "",
            "margin-left: 1px; margin-right: 1px; margin-bottom: 1px; margin-top: 2px",
        ),
        ("margin-top: 2px", "margin: 1px", "margin: 1px; margin-top: 2px"),
        ("margin: 1px", "margin-top: 2px", "margin: 1px"),
        (
            "margin: 1px; margin-top: inherit",
            "margin: 2px",
            "margin: 1px; margin-top: 2px",
        ),
    ],
)
def test_css_precedence(style, inherited, equiv):
    resolve = CSSResolver()
    inherited_props = resolve(inherited)
    style_props = resolve(style, inherited=inherited_props)
    equiv_props = resolve(equiv)
    assert style_props == equiv_props


@pytest.mark.parametrize(
    "style,equiv",
    [
        (
            "margin: 1px; margin-top: inherit",
            "margin-bottom: 1px; margin-right: 1px; margin-left: 1px",
        ),
        ("margin-top: inherit", ""),
        ("margin-top: initial", ""),
    ],
)
def test_css_none_absent(style, equiv):
    assert_same_resolution(style, equiv)


@pytest.mark.parametrize(
    "size,resolved",
    [
        ("xx-small", "6pt"),
        ("x-small", f"{7.5:f}pt"),
        ("small", f"{9.6:f}pt"),
        ("medium", "12pt"),
        ("large", f"{13.5:f}pt"),
        ("x-large", "18pt"),
        ("xx-large", "24pt"),
        ("8px", "6pt"),
        ("1.25pc", "15pt"),
        (".25in", "18pt"),
        ("02.54cm", "72pt"),
        ("25.4mm", "72pt"),
        ("101.6q", "72pt"),
    ],
)
@pytest.mark.parametrize("relative_to", [None, "16pt"])  # invariant to inherited size
def test_css_absolute_font_size(size, relative_to, resolved):
    if relative_to is None:
        inherited = None
    else:
        inherited = {"font-size": relative_to}
    assert_resolves(f"font-size: {size}", {"font-size": resolved}, inherited=inherited)


@pytest.mark.parametrize(
    "size,relative_to,resolved",
    [
        ("1em", None, "12pt"),
        ("1.0em", None, "12pt"),
        ("1.25em", None, "15pt"),
        ("1em", "16pt", "16pt"),
        ("1.0em", "16pt", "16pt"),
        ("1.25em", "16pt", "20pt"),
        ("1rem", "16pt", "12pt"),
        ("1.0rem", "16pt", "12pt"),
        ("1.25rem", "16pt", "15pt"),
        ("100%", None, "12pt"),
        ("125%", None, "15pt"),
        ("100%", "16pt", "16pt"),
        ("125%", "16pt", "20pt"),
        ("2ex", None, "12pt"),
        ("2.0ex", None, "12pt"),
        ("2.50ex", None, "15pt"),
        ("inherit", "16pt", "16pt"),
        ("smaller", None, "10pt"),
        ("smaller", "18pt", "15pt"),
        ("larger", None, f"{14.4:f}pt"),
        ("larger", "15pt", "18pt"),
    ],
)
def test_css_relative_font_size(size, relative_to, resolved):
    if relative_to is None:
        inherited = None
    else:
        inherited = {"font-size": relative_to}
    assert_resolves(f"font-size: {size}", {"font-size": resolved}, inherited=inherited)
