import inspect
from typing import Annotated, Optional

import attrs
import pytest

from cyclopts import Parameter


def test_parameter_get_negatives_bool_default():
    p = Parameter(name=("--foo", "--bar"))
    assert ("--no-foo", "--no-bar") == p.get_negatives(bool)


@pytest.mark.parametrize("type_", [list, set, list[str], set[str]])
def test_parameter_get_negatives_iterable_default(type_):
    p = Parameter(name=("--foo", "--bar"))
    assert ("--empty-foo", "--empty-bar") == p.get_negatives(type_)


@pytest.mark.parametrize("type_", [list, set, list[str], set[str]])
def test_parameter_get_negatives_iterable_custom_prefix(type_):
    p = Parameter(negative_iterable="vacant-", name=("--foo", "--bar"))
    assert ("--vacant-foo", "--vacant-bar") == p.get_negatives(type_)


@pytest.mark.parametrize("type_", [list, set, list[str], set[str]])
def test_parameter_get_negatives_iterable_empty_string_prefix(type_):
    p = Parameter(negative_iterable="", name=("--foo", "--bar"))
    assert () == p.get_negatives(type_)


@pytest.mark.parametrize("type_", [list, set, list[str], set[str]])
def test_parameter_get_negatives_iterable_custom_prefix_list(type_):
    p = Parameter(negative_iterable=["vacant-", "blank-"], name=("--foo", "--bar"))
    assert {"--vacant-foo", "--vacant-bar", "--blank-foo", "--blank-bar"} == set(p.get_negatives(type_))


@pytest.mark.parametrize("type_", [type(None), None])
def test_parameter_get_negatives_none_custom_prefix_list(type_):
    p = Parameter(negative_none=["vacant-", "blank-"], name=("--foo", "--bar"))
    assert {"--vacant-foo", "--vacant-bar", "--blank-foo", "--blank-bar"} == set(p.get_negatives(type_))


def test_parameter_negative_iterable_invalid_name(app, assert_parse_args):
    Parameter(negative_iterable=())  # Valid
    with pytest.raises(ValueError):
        Parameter(negative_iterable="--starts-with-hyphens")


@pytest.mark.parametrize("type_", [bool, list, set])
def test_parameter_get_negatives_custom_single(type_):
    p = Parameter(negative="--foo", name=("this-string-doesnt-matter", "neither-does-this-one"))
    assert ("--foo",) == p.get_negatives(type_)


@pytest.mark.parametrize("type_", [bool, list, set])
def test_parameter_get_negatives_bool_custom_list(type_):
    p = Parameter(negative=["--foo", "--bar"], name="this-string-doesnt-matter")
    assert ("--foo", "--bar") == p.get_negatives(type_)


@pytest.mark.parametrize("type_", [bool, list, set])
def test_parameter_get_negatives_bool_custom_prefix(type_):
    p = Parameter(negative_bool="yesnt-", name=("--foo", "--bar"))
    assert ("--yesnt-foo", "--yesnt-bar") == p.get_negatives(bool)


def test_parameter_negative_bool_invalid_name(app, assert_parse_args):
    Parameter(negative_bool=())  # Valid
    with pytest.raises(ValueError):
        Parameter(negative_bool="--starts-with-hyphens")


@pytest.mark.parametrize("type_", [bool, list, set])
def test_parameter_get_negatives_bool_custom_prefix_list(type_):
    p = Parameter(negative_bool=["yesnt-", "not-"], name=("--foo", "--bar"))
    assert {"--yesnt-foo", "--yesnt-bar", "--not-foo", "--not-bar"} == set(p.get_negatives(bool))


def test_parameter_from_annotation_basic():
    expected_cparam = Parameter(
        name=["--help", "-h"],
        negative="",
        show_default=False,
        help="Display this message and exit.",
    )

    assert (bool, expected_cparam) == Parameter.from_annotation(Annotated[bool, expected_cparam], Parameter())


def test_parameter_from_annotation_optional_annotated():
    expected_cparam = Parameter(
        name=["--help", "-h"],
        negative="",
        show_default=False,
        help="Display this message and exit.",
    )

    assert (bool, expected_cparam) == Parameter.from_annotation(Optional[Annotated[bool, expected_cparam]], Parameter())


def test_parameter_from_annotation_empty_annotation():
    assert (inspect.Parameter.empty, Parameter()) == Parameter.from_annotation(inspect.Parameter.empty, Parameter())


def test_parameter_combine():
    p1 = Parameter(negative="--foo")
    p2 = Parameter(show_default=False)
    p_combined = Parameter.combine(p1, None, p2)

    assert p_combined.negative == ("--foo",)
    assert p_combined.show_default is False


def test_parameter_combine_priority():
    p1 = Parameter(negative="--foo")
    p2 = Parameter(negative="--bar")
    p_combined = Parameter.combine(p1, p2)

    assert p_combined.negative == ("--bar",)


def test_parameter_combine_priority_none():
    p1 = Parameter(negative="--foo")
    p2 = Parameter(negative=None)
    p_combined = Parameter.combine(p1, p2)

    assert p_combined.negative is None


def test_parameter_default():
    p1 = Parameter()
    p2 = Parameter.default()

    # The two parameters should be equivalent.
    assert p1 == p2

    # However, the _provided_args field should differ
    assert p1._provided_args == ()
    # Just testing a few
    assert {"name", "converter", "validator"}.issubset(p2._provided_args)


def test_parameter_attributes_are_kw_only():
    """Test that all Parameter attributes except 'name' are keyword-only."""
    attrs_fields = attrs.fields(Parameter)

    for field in attrs_fields:
        if field.name == "name":
            # The 'name' field should NOT be keyword-only
            assert not field.kw_only, "Field 'name' should not be keyword-only"
        elif field.init:  # Only check fields that are part of __init__
            # All other init fields should be keyword-only
            assert field.kw_only, f"Field '{field.name}' should be keyword-only"
