"""
Behavioral tests for settings/manager module.

Tests pure functions: parse_boolean, _parse_number,
get_typed_setting_value, and check_env_setting.
"""

import os
from unittest.mock import patch

import pytest


class TestParseBoolean:
    """Tests for parse_boolean function (HTML checkbox semantics)."""

    def test_true_returns_true(self):
        """Bool True returns True."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean(True) is True

    def test_false_returns_false(self):
        """Bool False returns False."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean(False) is False

    def test_none_returns_false(self):
        """None returns False (missing checkbox)."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean(None) is False

    def test_empty_string_returns_false(self):
        """Empty string returns False."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean("") is False

    def test_false_string_returns_false(self):
        """'false' returns False."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean("false") is False

    def test_off_string_returns_false(self):
        """'off' returns False."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean("off") is False

    def test_zero_string_returns_false(self):
        """'0' returns False."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean("0") is False

    def test_no_string_returns_false(self):
        """'no' returns False."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean("no") is False

    def test_on_returns_true(self):
        """'on' returns True (HTML checkbox value)."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean("on") is True

    def test_true_string_returns_true(self):
        """'true' returns True."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean("true") is True

    def test_one_string_returns_true(self):
        """'1' returns True."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean("1") is True

    def test_any_nonempty_string_is_true(self):
        """Any non-empty non-falsy string returns True (checkbox semantics)."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean("enabled") is True
        assert parse_boolean("custom_value") is True
        assert parse_boolean("yes") is True

    def test_case_insensitive(self):
        """String comparison is case-insensitive."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean("FALSE") is False
        assert parse_boolean("False") is False
        assert parse_boolean("OFF") is False

    def test_strips_whitespace(self):
        """Strips whitespace from string values."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean("  false  ") is False
        assert parse_boolean("  true  ") is True

    def test_integer_truthy(self):
        """Non-zero integers return True."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean(1) is True
        assert parse_boolean(42) is True

    def test_integer_zero_is_false(self):
        """Zero returns False."""
        from local_deep_research.settings.manager import parse_boolean

        assert parse_boolean(0) is False


class TestParseJsonValue:
    """Tests for _parse_json_value function."""

    def test_parses_json_array_string(self):
        """JSON array string is parsed into a list."""
        from local_deep_research.settings.manager import _parse_json_value

        assert _parse_json_value('["general", "news"]') == ["general", "news"]

    def test_parses_json_array_with_crlf(self):
        """JSON array string with \\r\\n (browser textarea) is parsed correctly."""
        from local_deep_research.settings.manager import _parse_json_value

        raw = '[\r\n  "general"\r\n]'
        assert _parse_json_value(raw) == ["general"]

    def test_parses_json_object_string(self):
        """JSON object string is parsed into a dict."""
        from local_deep_research.settings.manager import _parse_json_value

        assert _parse_json_value('{"key": "value"}') == {"key": "value"}

    def test_returns_list_unchanged(self):
        """Already-parsed list is returned as-is."""
        from local_deep_research.settings.manager import _parse_json_value

        original = ["general", "news"]
        assert _parse_json_value(original) is original

    def test_returns_dict_unchanged(self):
        """Already-parsed dict is returned as-is."""
        from local_deep_research.settings.manager import _parse_json_value

        original = {"key": "value"}
        assert _parse_json_value(original) is original

    def test_returns_none_unchanged(self):
        """None is returned as-is."""
        from local_deep_research.settings.manager import _parse_json_value

        assert _parse_json_value(None) is None

    def test_invalid_json_string_returned_as_is(self):
        """Non-JSON string is returned unchanged."""
        from local_deep_research.settings.manager import _parse_json_value

        assert _parse_json_value("not json") == "not json"

    def test_empty_string_returned_as_is(self):
        """Empty string is returned as-is."""
        from local_deep_research.settings.manager import _parse_json_value

        assert _parse_json_value("") == ""

    def test_whitespace_only_string_returned_as_is(self):
        """Whitespace-only string is returned as-is."""
        from local_deep_research.settings.manager import _parse_json_value

        assert _parse_json_value("   ") == "   "

    def test_json_ui_element_uses_parse_json_value(self):
        """get_typed_setting_value with 'json' ui_element parses JSON strings."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", '[\r\n  "general"\r\n]', "json", check_env=False
        )
        assert result == ["general"]


class TestParseNumber:
    """Tests for _parse_number function."""

    def test_whole_float_returns_int(self):
        """Whole number float string returns int."""
        from local_deep_research.settings.manager import _parse_number

        assert _parse_number("42.0") == 42
        assert isinstance(_parse_number("42.0"), int)

    def test_fractional_float_returns_float(self):
        """Fractional float string returns float."""
        from local_deep_research.settings.manager import _parse_number

        assert _parse_number("3.14") == pytest.approx(3.14)
        assert isinstance(_parse_number("3.14"), float)

    def test_integer_string_returns_int(self):
        """Integer string returns int."""
        from local_deep_research.settings.manager import _parse_number

        assert _parse_number("10") == 10
        assert isinstance(_parse_number("10"), int)

    def test_negative_number(self):
        """Handles negative numbers."""
        from local_deep_research.settings.manager import _parse_number

        assert _parse_number("-5") == -5

    def test_zero_returns_int(self):
        """Zero returns int 0."""
        from local_deep_research.settings.manager import _parse_number

        result = _parse_number("0")
        assert result == 0
        assert isinstance(result, int)

    def test_raises_for_non_numeric(self):
        """Raises ValueError for non-numeric string."""
        from local_deep_research.settings.manager import _parse_number

        with pytest.raises(ValueError):
            _parse_number("abc")


class TestGetTypedSettingValue:
    """Tests for get_typed_setting_value function."""

    def test_text_returns_string(self):
        """Text ui_element returns string."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", "hello", "text", check_env=False
        )
        assert result == "hello"
        assert isinstance(result, str)

    def test_number_returns_int_for_whole(self):
        """Number ui_element returns int for whole number."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", "42", "number", check_env=False
        )
        assert result == 42
        assert isinstance(result, int)

    def test_number_returns_float_for_fractional(self):
        """Number ui_element returns float for fractional number."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", "3.14", "number", check_env=False
        )
        assert result == pytest.approx(3.14)

    def test_checkbox_true_values(self):
        """Checkbox ui_element converts truthy values."""
        from local_deep_research.settings.manager import get_typed_setting_value

        assert (
            get_typed_setting_value("k", "on", "checkbox", check_env=False)
            is True
        )
        assert (
            get_typed_setting_value("k", "true", "checkbox", check_env=False)
            is True
        )

    def test_checkbox_false_values(self):
        """Checkbox ui_element converts falsy values."""
        from local_deep_research.settings.manager import get_typed_setting_value

        assert (
            get_typed_setting_value("k", "off", "checkbox", check_env=False)
            is False
        )
        assert (
            get_typed_setting_value("k", "false", "checkbox", check_env=False)
            is False
        )

    def test_password_returns_string(self):
        """Password ui_element returns string."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", "secret123", "password", check_env=False
        )
        assert result == "secret123"

    def test_select_returns_string(self):
        """Select ui_element returns string."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", "option_a", "select", check_env=False
        )
        assert result == "option_a"

    def test_unknown_ui_element_returns_default(self):
        """Unknown ui_element returns default value."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key",
            "value",
            "unknown_element",
            default="fallback",
            check_env=False,
        )
        assert result == "fallback"

    def test_none_value_returns_default(self):
        """None value returns default."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", None, "text", default="default_val", check_env=False
        )
        assert result == "default_val"

    def test_range_behaves_like_number(self):
        """Range ui_element behaves like number."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", "50", "range", check_env=False
        )
        assert result == 50

    def test_invalid_number_returns_default(self):
        """Invalid number string returns default."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", "not_a_number", "number", default=0, check_env=False
        )
        assert result == 0


class TestCheckEnvSetting:
    """Tests for check_env_setting function."""

    def test_returns_none_when_not_set(self):
        """Returns None when env var not set."""
        from local_deep_research.settings.manager import check_env_setting

        result = check_env_setting("nonexistent.setting.xyz_test_12345")
        assert result is None

    def test_returns_value_when_set(self):
        """Returns value when env var is set."""
        from local_deep_research.settings.manager import check_env_setting

        with patch.dict(os.environ, {"LDR_MY_TEST_KEY": "test_value"}):
            result = check_env_setting("my.test_key")
            assert result == "test_value"

    def test_env_var_name_format(self):
        """Env var name is LDR_ + key with dots replaced by underscores, uppercased."""
        from local_deep_research.settings.manager import check_env_setting

        # Key "llm.model" -> env var "LDR_LLM_MODEL"
        with patch.dict(os.environ, {"LDR_LLM_MODEL": "gpt-4"}):
            result = check_env_setting("llm.model")
            assert result == "gpt-4"

    def test_nested_key_format(self):
        """Handles deeply nested keys."""
        from local_deep_research.settings.manager import check_env_setting

        with patch.dict(
            os.environ, {"LDR_SEARCH_BRAVE_API_KEY": "brave-key-123"}
        ):
            result = check_env_setting("search.brave.api_key")
            assert result == "brave-key-123"

    def test_returns_none_for_empty_string(self):
        """Empty string env var is treated as unset, returns None, and logs warning."""
        from loguru import logger

        from local_deep_research.settings.manager import check_env_setting

        warnings = []
        logger.enable("local_deep_research")
        sink_id = logger.add(
            lambda msg: warnings.append(str(msg)),
            level="WARNING",
            # diagnose=False keeps the captured sink consistent with the
            # production policy (#4185 / #4384) — exceptions logged through
            # this fixture would otherwise carry frame-local repr() into
            # the captured list and into pytest output.
            diagnose=False,
        )
        try:
            with patch.dict(os.environ, {"LDR_LLM_PROVIDER": ""}):
                result = check_env_setting("llm.provider")
            assert result is None
            assert any("LDR_LLM_PROVIDER" in w for w in warnings)
        finally:
            logger.remove(sink_id)
            logger.disable("local_deep_research")

    def test_empty_string_provider_key(self):
        """Empty LDR_LLM_PROVIDER returns None (Unraid scenario)."""
        from local_deep_research.settings.manager import check_env_setting

        with patch.dict(os.environ, {"LDR_LLM_PROVIDER": ""}):
            result = check_env_setting("llm.provider")
            assert result is None

    def test_empty_string_model_key(self):
        """Empty LDR_LLM_MODEL returns None."""
        from local_deep_research.settings.manager import check_env_setting

        with patch.dict(os.environ, {"LDR_LLM_MODEL": ""}):
            result = check_env_setting("llm.model")
            assert result is None

    def test_empty_string_multiple_keys(self):
        """Multiple empty env vars each independently return None."""
        from local_deep_research.settings.manager import check_env_setting

        with patch.dict(
            os.environ,
            {"LDR_LLM_PROVIDER": "", "LDR_LLM_MODEL": ""},
        ):
            assert check_env_setting("llm.provider") is None
            assert check_env_setting("llm.model") is None


class TestUIElementToSettingType:
    """Tests for UI_ELEMENT_TO_SETTING_TYPE mapping."""

    def test_has_text(self):
        """Mapping includes 'text'."""
        from local_deep_research.settings.manager import (
            UI_ELEMENT_TO_SETTING_TYPE,
        )

        assert "text" in UI_ELEMENT_TO_SETTING_TYPE

    def test_has_checkbox(self):
        """Mapping includes 'checkbox'."""
        from local_deep_research.settings.manager import (
            UI_ELEMENT_TO_SETTING_TYPE,
        )

        assert "checkbox" in UI_ELEMENT_TO_SETTING_TYPE

    def test_has_number(self):
        """Mapping includes 'number'."""
        from local_deep_research.settings.manager import (
            UI_ELEMENT_TO_SETTING_TYPE,
        )

        assert "number" in UI_ELEMENT_TO_SETTING_TYPE

    def test_has_password(self):
        """Mapping includes 'password'."""
        from local_deep_research.settings.manager import (
            UI_ELEMENT_TO_SETTING_TYPE,
        )

        assert "password" in UI_ELEMENT_TO_SETTING_TYPE

    def test_has_select(self):
        """Mapping includes 'select'."""
        from local_deep_research.settings.manager import (
            UI_ELEMENT_TO_SETTING_TYPE,
        )

        assert "select" in UI_ELEMENT_TO_SETTING_TYPE

    def test_has_json(self):
        """Mapping includes 'json'."""
        from local_deep_research.settings.manager import (
            UI_ELEMENT_TO_SETTING_TYPE,
        )

        assert "json" in UI_ELEMENT_TO_SETTING_TYPE

    def test_has_range(self):
        """Mapping includes 'range'."""
        from local_deep_research.settings.manager import (
            UI_ELEMENT_TO_SETTING_TYPE,
        )

        assert "range" in UI_ELEMENT_TO_SETTING_TYPE

    def test_text_type_is_str(self):
        """Text type is str."""
        from local_deep_research.settings.manager import (
            UI_ELEMENT_TO_SETTING_TYPE,
        )

        assert UI_ELEMENT_TO_SETTING_TYPE["text"] is str


class TestParseNumberEdgeCases:
    """Edge cases for _parse_number that affect real settings."""

    def test_inf_string_returns_float_inf(self):
        """'inf' parses to float — callers must validate range themselves."""
        from local_deep_research.settings.manager import _parse_number

        result = _parse_number("inf")
        assert result == float("inf")
        assert isinstance(result, float)

    def test_negative_inf_string(self):
        """'-inf' parses to negative infinity."""
        from local_deep_research.settings.manager import _parse_number

        result = _parse_number("-inf")
        assert result == float("-inf")

    def test_nan_string_returns_float_nan(self):
        """'nan' parses to float NaN."""
        import math

        from local_deep_research.settings.manager import _parse_number

        result = _parse_number("nan")
        assert math.isnan(result)

    def test_int_input_passthrough(self):
        """Direct int input is preserved as int (not round-tripped through float)."""
        from local_deep_research.settings.manager import _parse_number

        result = _parse_number(42)
        assert result == 42
        assert isinstance(result, int)

    def test_float_input_whole_becomes_int(self):
        """Float 5.0 becomes int 5 (is_integer() check)."""
        from local_deep_research.settings.manager import _parse_number

        result = _parse_number(5.0)
        assert result == 5
        assert isinstance(result, int)

    def test_scientific_notation(self):
        """Scientific notation string parses correctly."""
        from local_deep_research.settings.manager import _parse_number

        result = _parse_number("1e3")
        assert result == 1000
        assert isinstance(result, int)

    def test_very_large_number(self):
        """Large number beyond typical int range still works."""
        from local_deep_research.settings.manager import _parse_number

        result = _parse_number("99999999999999")
        assert result == 99999999999999
        assert isinstance(result, int)


class TestParseMultiselectEdgeCases:
    """Edge cases for _parse_multiselect with non-standard inputs."""

    def test_dict_input_returned_as_is(self):
        """Dict passes through unchanged (not list, not string)."""
        from local_deep_research.settings.manager import _parse_multiselect

        d = {"key": "value"}
        assert _parse_multiselect(d) is d

    def test_int_input_returned_as_is(self):
        """Integer passes through unchanged."""
        from local_deep_research.settings.manager import _parse_multiselect

        assert _parse_multiselect(42) == 42

    def test_none_input_returned_as_is(self):
        """None passes through unchanged."""
        from local_deep_research.settings.manager import _parse_multiselect

        assert _parse_multiselect(None) is None

    def test_json_array_of_non_list_falls_back_to_csv(self):
        """JSON string that parses to non-list falls through to CSV split."""
        from local_deep_research.settings.manager import _parse_multiselect

        # Starts with '[' but json.loads returns... actually this is a list
        # Use a case where JSON parse succeeds but isn't a list: impossible
        # with '[' prefix. Test that invalid JSON starting with '[' falls to CSV.
        result = _parse_multiselect("[not valid json")
        assert result == ["[not valid json"]

    def test_empty_string_returns_empty_list(self):
        """Empty string returns empty list after CSV split filtering."""
        from local_deep_research.settings.manager import _parse_multiselect

        assert _parse_multiselect("") == []

    def test_whitespace_only_items_filtered(self):
        """Comma-separated with blank items filters them out."""
        from local_deep_research.settings.manager import _parse_multiselect

        result = _parse_multiselect("a, , b, ,c")
        assert result == ["a", "b", "c"]


class TestParseJsonValueEdgeCases:
    """Edge cases for _parse_json_value with tricky inputs."""

    def test_json_number_string_parses_to_number(self):
        """A plain number string is valid JSON — parses to int."""
        from local_deep_research.settings.manager import _parse_json_value

        result = _parse_json_value("42")
        assert result == 42

    def test_json_boolean_string_parses_to_bool(self):
        """'true' is valid JSON — parses to Python True."""
        from local_deep_research.settings.manager import _parse_json_value

        assert _parse_json_value("true") is True
        assert _parse_json_value("false") is False

    def test_json_null_string_parses_to_none(self):
        """'null' is valid JSON — parses to None."""
        from local_deep_research.settings.manager import _parse_json_value

        assert _parse_json_value("null") is None

    def test_nested_json_object(self):
        """Nested JSON objects parse correctly."""
        from local_deep_research.settings.manager import _parse_json_value

        raw = '{"outer": {"inner": [1, 2]}}'
        result = _parse_json_value(raw)
        assert result == {"outer": {"inner": [1, 2]}}

    def test_int_input_passthrough(self):
        """Non-string types pass through unchanged."""
        from local_deep_research.settings.manager import _parse_json_value

        assert _parse_json_value(42) == 42

    def test_bool_input_passthrough(self):
        """Boolean passes through unchanged."""
        from local_deep_research.settings.manager import _parse_json_value

        assert _parse_json_value(True) is True

    def test_json_with_leading_whitespace(self):
        """JSON string with leading whitespace is stripped before parsing."""
        from local_deep_research.settings.manager import _parse_json_value

        result = _parse_json_value('  {"key": "value"}  ')
        assert result == {"key": "value"}


class TestGetTypedSettingValueEnvFallback:
    """Tests for env var → DB fallback path in get_typed_setting_value."""

    def test_invalid_env_var_falls_back_to_db_value(self):
        """When env var has invalid value for the type, DB value is used."""
        from local_deep_research.settings.manager import get_typed_setting_value

        with patch.dict(os.environ, {"LDR_TEST_ITERATIONS": "not_a_number"}):
            result = get_typed_setting_value(
                "test.iterations", 5, "number", default=0, check_env=True
            )
            # Should fall back to DB value 5, not env "not_a_number"
            assert result == 5

    def test_valid_env_var_overrides_db_value(self):
        """When env var is valid, it takes priority over DB value."""
        from local_deep_research.settings.manager import get_typed_setting_value

        with patch.dict(os.environ, {"LDR_TEST_ITERATIONS": "10"}):
            result = get_typed_setting_value(
                "test.iterations", 5, "number", default=0, check_env=True
            )
            assert result == 10

    def test_env_var_skipped_when_check_env_false(self):
        """check_env=False ignores environment variables entirely."""
        from local_deep_research.settings.manager import get_typed_setting_value

        with patch.dict(os.environ, {"LDR_TEST_ITERATIONS": "99"}):
            result = get_typed_setting_value(
                "test.iterations", 5, "number", default=0, check_env=False
            )
            assert result == 5

    def test_env_var_type_error_on_number_does_not_crash(self):
        """TypeError during env var conversion falls through to DB value.

        Note: the env path only catches ValueError (line 206), so a TypeError
        would propagate. This documents the current behavior — _parse_number
        with a string always raises ValueError, never TypeError.
        """
        from local_deep_research.settings.manager import get_typed_setting_value

        # _parse_number("") raises ValueError, not TypeError
        with patch.dict(os.environ, {"LDR_TEST_NUM": ""}):
            result = get_typed_setting_value(
                "test.num", 42, "number", default=0, check_env=True
            )
            assert result == 42

    def test_db_value_type_error_returns_default(self):
        """TypeError during DB value conversion returns default.

        This path (line 220) catches both ValueError and TypeError, unlike
        the env path which only catches ValueError.
        """
        from local_deep_research.settings.manager import get_typed_setting_value

        # Passing a type that causes TypeError in float() — e.g. a list
        result = get_typed_setting_value(
            "test.key", [1, 2, 3], "number", default=0, check_env=False
        )
        assert result == 0

    def test_textarea_preserves_multiline_string(self):
        """Textarea ui_element preserves newlines (uses str converter)."""
        from local_deep_research.settings.manager import get_typed_setting_value

        multiline = "line1\nline2\nline3"
        result = get_typed_setting_value(
            "test.key", multiline, "textarea", check_env=False
        )
        assert result == multiline

    def test_number_with_bool_input_converts_to_int(self):
        """Bool passed to number field: float(True)=1.0, is_integer()→int 1.

        This is a real scenario when frontend sends JSON true for a number field.
        """
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", True, "number", check_env=False
        )
        assert result == 1
        assert isinstance(result, int)

    def test_checkbox_with_int_zero(self):
        """Integer 0 sent for a checkbox field returns False."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", 0, "checkbox", check_env=False
        )
        assert result is False

    def test_checkbox_with_int_one(self):
        """Integer 1 sent for a checkbox field returns True."""
        from local_deep_research.settings.manager import get_typed_setting_value

        result = get_typed_setting_value(
            "test.key", 1, "checkbox", check_env=False
        )
        assert result is True
