from __future__ import annotations

import pytest

from textual.validation import (
    URL,
    Failure,
    Function,
    Integer,
    Length,
    Number,
    Regex,
    ValidationResult,
    Validator,
)

VALIDATOR = Function(lambda value: True)


def test_ValidationResult_merge_successes():
    results = [ValidationResult.success(), ValidationResult.success()]
    assert ValidationResult.merge(results) == ValidationResult.success()


def test_ValidationResult_merge_failures():
    failure_one = Failure(VALIDATOR, "1")
    failure_two = Failure(VALIDATOR, "2")
    results = [
        ValidationResult.failure([failure_one]),
        ValidationResult.failure([failure_two]),
        ValidationResult.success(),
    ]
    expected_result = ValidationResult.failure([failure_one, failure_two])
    assert ValidationResult.merge(results) == expected_result


def test_ValidationResult_failure_descriptions():
    result = ValidationResult.failure(
        [
            Failure(VALIDATOR, description="One"),
            Failure(VALIDATOR, description="Two"),
            Failure(VALIDATOR, description="Three"),
        ],
    )
    assert result.failure_descriptions == ["One", "Two", "Three"]


class ValidatorWithDescribeFailure(Validator):
    def validate(self, value: str) -> ValidationResult:
        return self.failure()

    def describe_failure(self, failure: Failure) -> str | None:
        return "describe_failure"


def test_Failure_description_priorities_parameter_only():
    number_validator = Number(failure_description="ABC")
    non_number_value = "x"
    result = number_validator.validate(non_number_value)
    # The inline value takes priority over the describe_failure.
    assert result.failures[0].description == "ABC"


def test_Failure_description_priorities_parameter_and_describe_failure():
    validator = ValidatorWithDescribeFailure(failure_description="ABC")
    result = validator.validate("x")
    # Even though the validator has a `describe_failure`, we've provided it
    # inline and the inline value should take priority.
    assert result.failures[0].description == "ABC"


def test_Failure_description_priorities_describe_failure_only():
    validator = ValidatorWithDescribeFailure()
    result = validator.validate("x")
    assert result.failures[0].description == "describe_failure"


class ValidatorWithFailureMessageAndNoDescribe(Validator):
    def validate(self, value: str) -> ValidationResult:
        return self.failure(description="ABC")


def test_Failure_description_parameter_and_description_inside_validate():
    validator = ValidatorWithFailureMessageAndNoDescribe()
    result = validator.validate("x")
    assert result.failures[0].description == "ABC"


class ValidatorWithFailureMessageAndDescribe(Validator):
    def validate(self, value: str) -> ValidationResult:
        return self.failure(value=value, description="ABC")

    def describe_failure(self, failure: Failure) -> str | None:
        return "describe_failure"


def test_Failure_description_describe_and_description_inside_validate():
    # This is kind of a weird case - there's no reason to supply both of
    # these but lets still make sure we're sensible about how we handle it.
    validator = ValidatorWithFailureMessageAndDescribe()
    result = validator.validate("x")
    assert result.failures == [Failure(validator, "x", "ABC")]


@pytest.mark.parametrize(
    "value, minimum, maximum, expected_result",
    [
        ("123", None, None, True),  # valid number, no range
        ("-123", None, None, True),  # valid negative number, no range
        ("123.45", None, None, True),  # valid float, no range
        ("1.23e-4", None, None, True),  # valid scientific notation, no range
        ("abc", None, None, False),  # non-numeric string, no range
        ("123", 100, 200, True),  # valid number within range
        ("99", 100, 200, False),  # valid number but not in range
        ("201", 100, 200, False),  # valid number but not in range
        ("1.23e4", 0, 50000, True),  # valid scientific notation within range
        ("inf", None, None, False),  # infinity never valid
        ("nan", None, None, False),  # nan never valid
        ("-inf", None, None, False),  # nan never valid
        ("-4", 0, 5, False),  # valid negative number, out of range with zero
        ("2", -3, 0, False),  # valid number out of range with zero
        ("-2", -3, 0, True),  # negative in range
    ],
)
def test_Number_validate(value, minimum, maximum, expected_result):
    validator = Number(minimum=minimum, maximum=maximum)
    result = validator.validate(value)
    assert result.is_valid == expected_result


@pytest.mark.parametrize(
    "regex, value, expected_result",
    [
        (r"\d+", "123", True),  # matches regex for one or more digits
        (r"\d+", "abc", False),  # does not match regex for one or more digits
        (r"[a-z]+", "abc", True),  # matches regex for one or more lowercase letters
        (
            r"[a-z]+",
            "ABC",
            False,
        ),  # does not match regex for one or more lowercase letters
        (r"\w+", "abc123", True),  # matches regex for one or more word characters
        (r"\w+", "!@#", False),  # does not match regex for one or more word characters
    ],
)
def test_Regex_validate(regex, value, expected_result):
    validator = Regex(regex)
    result = validator.validate(value)
    assert result.is_valid == expected_result


@pytest.mark.parametrize(
    "value, minimum, maximum, expected_result",
    [
        ("123", None, None, True),  # valid integer, no range
        ("-123", None, None, True),  # valid negative integer, no range
        ("123.45", None, None, False),  # float, not a valid integer
        ("1.23e-4", None, None, False),  # scientific notation, not a valid integer
        ("abc", None, None, False),  # non-numeric string, not a valid integer
        ("123", 100, 200, True),  # valid integer within range
        ("99", 100, 200, False),  # valid integer but not in range
        ("201", 100, 200, False),  # valid integer but not in range
        ("1.23e4", None, None, False),  # valid scientific notation, even resolving to an integer, is not valid
        ("123.", None, None, False),  # periods not valid in integers
        ("123_456", None, None, True),  # underscores are valid python
        ("_123_456", None, None, False),  # leading underscores are not valid python
        ("-123", -123, -123, True),  # valid negative number in minimal range
    ],
)
def test_Integer_validate(value, minimum, maximum, expected_result):
    validator = Integer(minimum=minimum, maximum=maximum)
    result = validator.validate(value)
    assert result.is_valid == expected_result


@pytest.mark.parametrize(
    "value, min_length, max_length, expected_result",
    [
        ("", None, None, True),  # empty string
        ("test", None, None, True),  # any string with no restrictions
        ("test", 5, None, False),  # shorter than minimum length
        ("test", None, 3, False),  # longer than maximum length
        ("test", 4, 4, True),  # exactly matches minimum and maximum length
        ("test", 2, 6, True),  # within length range
    ],
)
def test_Length_validate(value, min_length, max_length, expected_result):
    validator = Length(minimum=min_length, maximum=max_length)
    result = validator.validate(value)
    assert result.is_valid == expected_result


@pytest.mark.parametrize(
    "value, expected_result",
    [
        ("http://example.com", True),  # valid URL
        ("https://example.com", True),  # valid URL with https
        ("www.example.com", False),  # missing scheme
        ("://example.com", False),  # invalid URL (no scheme)
        ("https:///path", False),  # missing netloc
        (
            "redis://username:pass[word@localhost:6379/0",
            False,
        ),  # invalid URL characters
        ("", False),  # empty string
    ],
)
def test_URL_validate(value, expected_result):
    validator = URL()
    result = validator.validate(value)
    assert result.is_valid == expected_result


@pytest.mark.parametrize(
    "function, failure_description, is_valid",
    [
        ((lambda value: True), None, True),
        ((lambda value: False), "failure!", False),
    ],
)
def test_Function_validate(function, failure_description, is_valid):
    validator = Function(function, failure_description)
    result = validator.validate("x")
    assert result.is_valid is is_valid
    if result.failure_descriptions:
        assert result.failure_descriptions[0] == failure_description


def test_Integer_failure_description_when_NotANumber():
    """Regression test for https://github.com/Textualize/textual/issues/4413"""
    validator = Integer()
    result = validator.validate("x")
    assert result.is_valid is False
    assert result.failure_descriptions[0] == "Must be a valid integer."
