"""
Comprehensive coverage tests for utilities/type_utils.py.

Uses parametrized tests for systematic coverage of the to_bool() function
across all code paths: bool input, string input, None input, and fallback
bool() conversion for other types.
"""

import pytest

from local_deep_research.utilities.type_utils import to_bool


# ---------------------------------------------------------------------------
# 1. Boolean passthrough (isinstance(value, bool) branch)
# ---------------------------------------------------------------------------
class TestToBoolBooleanPassthrough:
    """Bool values must pass through unchanged, before any other check."""

    @pytest.mark.parametrize(
        "value, expected",
        [
            (True, True),
            (False, False),
        ],
    )
    def test_bool_passthrough(self, value, expected):
        assert to_bool(value) is expected

    def test_bool_true_ignores_default(self):
        """Default should be irrelevant when a bool is passed."""
        assert to_bool(True, default=False) is True
        assert to_bool(False, default=True) is False

    def test_bool_is_checked_before_int(self):
        """bool is a subclass of int; ensure the bool branch fires first."""
        # If bool were not checked first, True (== 1) would fall through
        # to the generic bool(value) path and still work, but we want the
        # early-return for clarity.
        result = to_bool(True)
        assert result is True
        assert type(result) is bool


# ---------------------------------------------------------------------------
# 2. String truthy values (isinstance(value, str) branch — True results)
# ---------------------------------------------------------------------------
class TestToBoolStringTruthy:
    """Strings in the truthy set: 'true', '1', 'yes', 'on', 'enabled'."""

    @pytest.mark.parametrize(
        "value",
        [
            "true",
            "True",
            "TRUE",
            "tRuE",
            "trUE",
            "1",
            "yes",
            "Yes",
            "YES",
            "yES",
            "on",
            "On",
            "ON",
            "oN",
            "enabled",
            "Enabled",
            "ENABLED",
            "eNaBlEd",
        ],
    )
    def test_truthy_strings(self, value):
        result = to_bool(value)
        assert result is True
        assert type(result) is bool

    @pytest.mark.parametrize(
        "value",
        [
            "  true",
            "true  ",
            "  true  ",
            "\ttrue",
            "true\t",
            "\ttrue\t",
            "\ntrue",
            "true\n",
            "\ntrue\n",
            "\r\ntrue\r\n",
            " \t yes \n ",
            "  1  ",
            "\ton\t",
            "  enabled  ",
        ],
    )
    def test_truthy_strings_with_whitespace(self, value):
        """Whitespace around truthy strings should be stripped."""
        assert to_bool(value) is True


# ---------------------------------------------------------------------------
# 3. String falsy values (isinstance(value, str) branch — False results)
# ---------------------------------------------------------------------------
class TestToBoolStringFalsy:
    """Strings NOT in the truthy set should return False."""

    @pytest.mark.parametrize(
        "value",
        [
            "false",
            "False",
            "FALSE",
            "0",
            "no",
            "No",
            "NO",
            "off",
            "Off",
            "OFF",
            "disabled",
            "Disabled",
            "DISABLED",
            "",  # empty string
            "   ",  # whitespace only
            "\t\n",  # whitespace only (tabs/newlines)
            "random",
            "null",
            "undefined",
            "NaN",
            "none",
            "None",
            "maybe",
            "nope",
            "absolutely",
        ],
    )
    def test_falsy_strings(self, value):
        result = to_bool(value)
        assert result is False
        assert type(result) is bool

    @pytest.mark.parametrize(
        "value",
        [
            "enable",  # missing 'd'
            "tru",  # truncated
            "ye",  # truncated
            "yess",  # extra char
            "truee",  # extra char
            "onoff",  # compound
            "enabled_on",  # with suffix
            "true_value",  # with suffix
            "x_true",  # with prefix
            "is_on",  # with prefix
            "honest",  # contains 'on'
            "python",  # contains 'on'
            "yesterday",  # contains 'yes'
            "condition",  # contains 'on'
        ],
    )
    def test_partial_or_substring_matches_are_falsy(self, value):
        """Only exact matches (after strip+lower) should be truthy."""
        assert to_bool(value) is False

    @pytest.mark.parametrize(
        "value",
        [
            "2",
            "42",
            "-1",
            "00",
            "01",
            "001",
            "+1",
            "3.14",
            "0.0",
            "1.0",
        ],
    )
    def test_numeric_strings_other_than_one(self, value):
        """Only '1' is truthy; other numeric strings are not."""
        assert to_bool(value) is False

    @pytest.mark.parametrize(
        "value",
        [
            '"true"',
            "'true'",
            '"1"',
            "'yes'",
        ],
    )
    def test_quoted_strings_are_falsy(self, value):
        """Quotes around truthy values should NOT match."""
        assert to_bool(value) is False

    def test_default_ignored_for_empty_string(self):
        """Empty string is a string, not None — default must be ignored."""
        assert to_bool("", default=True) is False

    def test_default_ignored_for_falsy_string(self):
        assert to_bool("false", default=True) is False
        assert to_bool("0", default=True) is False


# ---------------------------------------------------------------------------
# 4. None handling (value is None branch)
# ---------------------------------------------------------------------------
class TestToBoolNone:
    """None should return the default parameter."""

    def test_none_default_false(self):
        assert to_bool(None) is False

    def test_none_default_true(self):
        assert to_bool(None, default=True) is True

    def test_none_explicit_default_false(self):
        assert to_bool(None, default=False) is False

    def test_none_return_type(self):
        result = to_bool(None, default=True)
        assert type(result) is bool


# ---------------------------------------------------------------------------
# 5. Fallback bool() conversion (int, float, collections, objects)
# ---------------------------------------------------------------------------
class TestToBoolFallback:
    """For non-bool, non-str, non-None types, Python's bool() is used."""

    @pytest.mark.parametrize(
        "value, expected",
        [
            (0, False),
            (1, True),
            (-1, True),
            (42, True),
            (100, True),
            (0.0, False),
            (1.0, True),
            (0.5, True),
            (-0.1, True),
            (0.001, True),
            (3.14, True),
        ],
    )
    def test_numeric_values(self, value, expected):
        result = to_bool(value)
        assert result is expected
        assert type(result) is bool

    @pytest.mark.parametrize(
        "value, expected",
        [
            ([], False),
            ([1], True),
            ([0], True),  # list with falsy element is still non-empty
            ({}, False),
            ({"k": "v"}, True),
            (set(), False),
            ({1, 2}, True),
            ((), False),
            ((1,), True),
            (frozenset(), False),
            (frozenset({1}), True),
            (b"", False),
            (b"x", True),
            (bytearray(), False),
            (bytearray(b"x"), True),
        ],
    )
    def test_collection_values(self, value, expected):
        assert to_bool(value) is expected

    def test_custom_object_truthy(self):
        class AlwaysTrue:
            def __bool__(self):
                return True

        assert to_bool(AlwaysTrue()) is True

    def test_custom_object_falsy(self):
        class AlwaysFalse:
            def __bool__(self):
                return False

        assert to_bool(AlwaysFalse()) is False

    def test_plain_object_is_truthy(self):
        """Objects without __bool__ override default to truthy."""

        class Plain:
            pass

        assert to_bool(Plain()) is True

    def test_object_with_len_zero(self):
        """Objects with __len__ returning 0 are falsy via bool()."""

        class EmptyContainer:
            def __len__(self):
                return 0

        assert to_bool(EmptyContainer()) is False

    def test_object_with_len_nonzero(self):
        class NonEmptyContainer:
            def __len__(self):
                return 5

        assert to_bool(NonEmptyContainer()) is True

    def test_default_ignored_for_non_none_falsy(self):
        """Default only applies to None, not to other falsy values."""
        assert to_bool(0, default=True) is False
        assert to_bool([], default=True) is False
        assert to_bool({}, default=True) is False
        assert to_bool(0.0, default=True) is False


# ---------------------------------------------------------------------------
# 6. Type priority / branch ordering
# ---------------------------------------------------------------------------
class TestToBoolTypePriority:
    """Verify the intended branch ordering: bool > str > None > fallback."""

    def test_bool_before_int(self):
        """bool is subclass of int; must hit bool branch, not fallback."""
        # Both branches give same answer, but we verify the bool branch
        # fires by confirming True/False are identity-equal.
        assert to_bool(True) is True
        assert to_bool(False) is False

    def test_string_before_none(self):
        """A string is never None, so string branch should fire."""
        assert to_bool("") is False  # string branch, not None branch

    def test_none_before_fallback(self):
        """None should use the default, not bool(None) which is False."""
        # With default=True, if None fell through to bool(None) we'd get
        # False — but the None branch should return True.
        assert to_bool(None, default=True) is True


# ---------------------------------------------------------------------------
# 7. Return-type guarantees
# ---------------------------------------------------------------------------
class TestToBoolReturnType:
    """Every code path must return an actual bool, never a truthy/falsy proxy."""

    @pytest.mark.parametrize(
        "value, kwargs",
        [
            (True, {}),
            (False, {}),
            ("true", {}),
            ("false", {}),
            ("", {}),
            (None, {}),
            (None, {"default": True}),
            (1, {}),
            (0, {}),
            ([], {}),
            ([1], {}),
        ],
    )
    def test_return_is_bool(self, value, kwargs):
        result = to_bool(value, **kwargs)
        assert type(result) is bool
