from contextlib import nullcontext as does_not_raise
from typing import Any

import pytest

from textual.color import Color
from textual.css.stylesheet import CssSource, Stylesheet, StylesheetParseError
from textual.css.tokenizer import TokenError
from textual.dom import DOMNode
from textual.geometry import Spacing
from textual.widget import Widget


def _make_user_stylesheet(css: str) -> Stylesheet:
    stylesheet = Stylesheet()
    stylesheet.source["test.css"] = CssSource(css, is_defaults=False)
    stylesheet.parse()
    return stylesheet


def test_stylesheet_apply_highest_specificity_wins():
    """#ids have higher specificity than .classes"""
    css = "#id {color: red;} .class {color: blue;}"
    stylesheet = _make_user_stylesheet(css)
    node = DOMNode(classes="class", id="id")
    stylesheet.apply(node)

    assert node.styles.color == Color(255, 0, 0)


def test_stylesheet_apply_doesnt_override_defaults():
    css = "#id {color: red;}"
    stylesheet = _make_user_stylesheet(css)
    node = DOMNode(id="id")
    stylesheet.apply(node)

    assert node.styles.margin == Spacing.all(0)
    assert node.styles.box_sizing == "border-box"


def test_stylesheet_apply_highest_specificity_wins_multiple_classes():
    """When we use two selectors containing only classes, then the selector
    `.b.c` has greater specificity than the selector `.a`"""
    css = ".b.c {background: blue;} .a {background: red; color: lime;}"
    stylesheet = _make_user_stylesheet(css)
    node = DOMNode(classes="a b c")
    stylesheet.apply(node)

    assert node.styles.background == Color(0, 0, 255)
    assert node.styles.color == Color(0, 255, 0)


def test_stylesheet_many_classes_dont_overrule_id():
    """#id is further to the left in the specificity tuple than class, and
    a selector containing multiple classes cannot take priority over even a
    single class."""
    css = "#id {color: red;} .a.b.c.d {color: blue;}"
    stylesheet = _make_user_stylesheet(css)
    node = DOMNode(classes="a b c d", id="id")
    stylesheet.apply(node)

    assert node.styles.color == Color(255, 0, 0)


def test_stylesheet_last_rule_wins_when_same_rule_twice_in_one_ruleset():
    css = "#id {color: red; color: blue;}"
    stylesheet = _make_user_stylesheet(css)
    node = DOMNode(id="id")
    stylesheet.apply(node)

    assert node.styles.color == Color(0, 0, 255)


def test_stylesheet_rulesets_merged_for_duplicate_selectors():
    css = "#id {color: red; background: lime;} #id {color:blue;}"
    stylesheet = _make_user_stylesheet(css)
    node = DOMNode(id="id")
    stylesheet.apply(node)

    assert node.styles.color == Color(0, 0, 255)
    assert node.styles.background == Color(0, 255, 0)


def test_stylesheet_apply_takes_final_rule_in_specificity_clash():
    """.a and .b both contain background and have same specificity, so .b wins
    since it was declared last - the background should be blue."""
    css = ".a {background: red; color: lime;} .b {background: blue;}"
    stylesheet = _make_user_stylesheet(css)
    node = DOMNode(classes="a b", id="c")
    stylesheet.apply(node)

    assert node.styles.color == Color(0, 255, 0)  # color: lime
    assert node.styles.background == Color(0, 0, 255)  # background: blue


def test_stylesheet_apply_empty_rulesets():
    """Ensure that we don't crash when working with empty rulesets"""
    css = ".a {} .b {}"
    stylesheet = _make_user_stylesheet(css)
    node = DOMNode(classes="a b")
    stylesheet.apply(node)


def test_stylesheet_apply_user_css_over_widget_css():
    user_css = ".a {color: red; tint: yellow;}"

    class MyWidget(Widget):
        DEFAULT_CSS = ".a {color: blue !important; background: lime;}"

    node = MyWidget()
    node.add_class("a")

    stylesheet = _make_user_stylesheet(user_css)
    stylesheet.add_source(
        MyWidget.DEFAULT_CSS, "widget.py:MyWidget", is_default_css=True
    )
    stylesheet.apply(node)

    # The node is red because user CSS overrides Widget.DEFAULT_CSS
    assert node.styles.color == Color(255, 0, 0)
    # The background colour defined in the Widget still applies, since user CSS doesn't override it
    assert node.styles.background == Color(0, 255, 0)
    # As expected, the tint colour is yellow, since there's no competition between user or widget CSS
    assert node.styles.tint == Color(255, 255, 0)


@pytest.mark.parametrize(
    "css_value,expectation,expected_color",
    [
        # Valid values:
        ["transparent", does_not_raise(), Color(0, 0, 0, 0)],
        ["ansi_red", does_not_raise(), Color(128, 0, 0)],
        ["ansi_bright_magenta", does_not_raise(), Color(255, 0, 255)],
        ["red", does_not_raise(), Color(255, 0, 0)],
        ["lime", does_not_raise(), Color(0, 255, 0)],
        ["coral", does_not_raise(), Color(255, 127, 80)],
        ["aqua", does_not_raise(), Color(0, 255, 255)],
        ["deepskyblue", does_not_raise(), Color(0, 191, 255)],
        ["rebeccapurple", does_not_raise(), Color(102, 51, 153)],
        ["#ffcc00", does_not_raise(), Color(255, 204, 0)],
        ["#ffcc0033", does_not_raise(), Color(255, 204, 0, 0.2)],
        ["rgb(200,90,30)", does_not_raise(), Color(200, 90, 30)],
        ["rgba(200,90,30,0.3)", does_not_raise(), Color(200, 90, 30, 0.3)],
        # Some invalid ones:
        ["coffee", pytest.raises(StylesheetParseError), None],  # invalid color name
        ["ansi_dark_cyan", pytest.raises(StylesheetParseError), None],
        ["red 4", pytest.raises(StylesheetParseError), None],  # space in it
        ["1", pytest.raises(StylesheetParseError), None],  # invalid value
        ["()", pytest.raises(TokenError), None],  # invalid tokens
    ],
)
def test_color_property_parsing(css_value, expectation, expected_color):
    stylesheet = Stylesheet()
    css = """
    * {
      background: ${COLOR};
    }
    """.replace(
        "${COLOR}", css_value
    )

    with expectation:
        stylesheet.add_source(css)
        stylesheet.parse()

    if expected_color:
        css_rule = stylesheet.rules[0]
        assert css_rule.styles.background == expected_color


@pytest.mark.parametrize(
    "css_property_name,expected_property_name_suggestion",
    [
        ["backgroundu", "background"],
        ["bckgroundu", "background"],
        ["ofset-x", "offset-x"],
        ["ofst_y", "offset-y"],
        ["colr", "color"],
        ["colour", "color"],
        ["wdth", "width"],
        ["wth", "width"],
        ["wh", None],
        ["xkcd", None],
    ],
)
def test_did_you_mean_for_css_property_names(
    css_property_name: str, expected_property_name_suggestion
):
    stylesheet = Stylesheet()
    css = """
    * {
      border: blue;
      ${PROPERTY}: red;
    }
    """.replace(
        "${PROPERTY}", css_property_name
    )

    stylesheet.add_source(css)
    with pytest.raises(StylesheetParseError) as err:
        stylesheet.parse()

    _, help_text = err.value.errors.rules[0].errors[0]  # type: Any, HelpText
    displayed_css_property_name = css_property_name.replace("_", "-")
    expected_summary = f"Invalid CSS property {displayed_css_property_name!r}"
    if expected_property_name_suggestion:
        expected_summary += f'. Did you mean "{expected_property_name_suggestion}"?'
    assert help_text.summary == expected_summary


@pytest.mark.parametrize(
    "css_property_name,css_property_value,expected_color_suggestion",
    [
        ["color", "blu", "blue"],
        ["background", "chartruse", "chartreuse"],
        ["tint", "ansi_whi", "ansi_white"],
        ["scrollbar-color", "transprnt", "transparent"],
        ["color", "xkcd", None],
    ],
)
def test_did_you_mean_for_color_names(
    css_property_name: str, css_property_value: str, expected_color_suggestion
):
    stylesheet = Stylesheet()
    css = """
    * {
      border: blue;
      ${PROPERTY}: ${VALUE};
    }
    """.replace(
        "${PROPERTY}", css_property_name
    ).replace(
        "${VALUE}", css_property_value
    )

    stylesheet.add_source(css)
    with pytest.raises(StylesheetParseError) as err:
        stylesheet.parse()

    _, help_text = err.value.errors.rules[0].errors[0]  # type: Any, HelpText
    displayed_css_property_name = css_property_name.replace("_", "-")
    expected_error_summary = (
        f"Invalid value for the [i]{displayed_css_property_name}[/] property"
    )

    if expected_color_suggestion is not None:
        expected_error_summary += f'. Did you mean "{expected_color_suggestion}"?'

    assert help_text.summary == expected_error_summary
