"""
Tests for server_config module.

Covers server configuration management for web app startup.
All settings are read from environment variables (LDR_* naming convention).
Includes tests for the legacy server_config.json migration path.
"""

import json
from pathlib import Path
from unittest.mock import patch

import pytest

from local_deep_research.web.server_config import (
    get_server_config_path,
    has_legacy_customizations,
    load_server_config,
)


# ---------------------------------------------------------------------------
# Shared fixture: mock get_typed_setting_value to return defaults
# ---------------------------------------------------------------------------
@pytest.fixture()
def _default_typed_setting():
    """Mock get_typed_setting_value to always return default."""
    with patch(
        "local_deep_research.web.server_config.get_typed_setting_value",
        side_effect=lambda key, val, typ, default: default,
    ):
        yield


@pytest.fixture()
def _env_typed_setting():
    """Mock get_typed_setting_value to simulate env var override.

    Stores overrides in a dict; returns the override if present, else default.
    """
    overrides = {}

    def _side_effect(key, val, typ, default):
        return overrides.get(key, default)

    with patch(
        "local_deep_research.web.server_config.get_typed_setting_value",
        side_effect=_side_effect,
    ):
        yield overrides


# ===================================================================
# load_server_config — structure and defaults
# ===================================================================
class TestLoadServerConfig:
    """Tests for load_server_config function."""

    # -- keys and types -------------------------------------------------

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_returns_dict(self):
        """Should return a dict."""
        result = load_server_config()
        assert isinstance(result, dict)

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_contains_expected_keys(self):
        """Should contain all expected configuration keys."""
        result = load_server_config()
        expected_keys = {
            "host",
            "port",
            "debug",
            "use_https",
            "allow_registrations",
            "rate_limit_default",
            "rate_limit_login",
            "rate_limit_registration",
            "rate_limit_settings",
        }
        assert expected_keys.issubset(result.keys())

    # -- defaults -------------------------------------------------------

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_default_host(self):
        """Default host should be 0.0.0.0."""
        assert load_server_config()["host"] == "0.0.0.0"

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_default_port(self):
        """Default port should be 5000."""
        assert load_server_config()["port"] == 5000

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_default_debug(self):
        """Default debug should be False."""
        assert load_server_config()["debug"] is False

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_default_use_https(self):
        """Default use_https should be True."""
        assert load_server_config()["use_https"] is True

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_default_allow_registrations(self):
        """Default allow_registrations should be True."""
        assert load_server_config()["allow_registrations"] is True

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_default_rate_limit_default(self):
        """Default rate_limit_default value."""
        assert (
            load_server_config()["rate_limit_default"]
            == "5000 per hour;50000 per day"
        )

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_default_rate_limit_login(self):
        """Default rate_limit_login value."""
        assert load_server_config()["rate_limit_login"] == "5 per 15 minutes"

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_default_rate_limit_registration(self):
        """Default rate_limit_registration value."""
        assert load_server_config()["rate_limit_registration"] == "3 per hour"

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_default_rate_limit_settings(self):
        """Default rate_limit_settings value."""
        assert load_server_config()["rate_limit_settings"] == "30 per minute"

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_defaults_when_no_env_vars(self):
        """Should return all defaults when no environment variables are set."""
        result = load_server_config()

        assert result["host"] == "0.0.0.0"
        assert result["port"] == 5000
        assert result["debug"] is False
        assert result["use_https"] is True
        assert result["allow_registrations"] is True
        assert result["rate_limit_default"] == "5000 per hour;50000 per day"
        assert result["rate_limit_login"] == "5 per 15 minutes"
        assert result["rate_limit_registration"] == "3 per hour"
        assert result["rate_limit_settings"] == "30 per minute"

    # -- env var overrides -----------------------------------------------

    def test_env_var_overrides_host(self, _env_typed_setting):
        """LDR_WEB_HOST env var should override default host."""
        _env_typed_setting["web.host"] = "127.0.0.1"
        result = load_server_config()
        assert result["host"] == "127.0.0.1"

    def test_env_var_overrides_port(self, _env_typed_setting):
        """LDR_WEB_PORT env var should override default port."""
        _env_typed_setting["web.port"] = 8080
        result = load_server_config()
        assert result["port"] == 8080

    def test_env_var_overrides_debug(self, _env_typed_setting):
        """LDR_APP_DEBUG env var should override default debug."""
        _env_typed_setting["app.debug"] = True
        result = load_server_config()
        assert result["debug"] is True

    def test_env_var_overrides_use_https(self, _env_typed_setting):
        """LDR_WEB_USE_HTTPS env var should override default use_https."""
        _env_typed_setting["web.use_https"] = False
        result = load_server_config()
        assert result["use_https"] is False

    def test_env_var_overrides_allow_registrations(self, _env_typed_setting):
        """Env var override for allow_registrations should be reflected in config."""
        _env_typed_setting["app.allow_registrations"] = False
        result = load_server_config()
        assert result["allow_registrations"] is False

    def test_env_var_overrides_rate_limit_default(self, _env_typed_setting):
        """LDR_SECURITY_RATE_LIMIT_DEFAULT env var should override default."""
        _env_typed_setting["security.rate_limit_default"] = "100 per hour"
        result = load_server_config()
        assert result["rate_limit_default"] == "100 per hour"

    def test_env_var_overrides_rate_limit_login(self, _env_typed_setting):
        """LDR_SECURITY_RATE_LIMIT_LOGIN env var should override default."""
        _env_typed_setting["security.rate_limit_login"] = "10 per minute"
        result = load_server_config()
        assert result["rate_limit_login"] == "10 per minute"

    def test_env_var_overrides_rate_limit_registration(
        self, _env_typed_setting
    ):
        """LDR_SECURITY_RATE_LIMIT_REGISTRATION env var should override default."""
        _env_typed_setting["security.rate_limit_registration"] = "1 per hour"
        result = load_server_config()
        assert result["rate_limit_registration"] == "1 per hour"

    def test_env_var_overrides_rate_limit_settings(self, _env_typed_setting):
        """LDR_SECURITY_RATE_LIMIT_SETTINGS env var should override default."""
        _env_typed_setting["security.rate_limit_settings"] = "60 per minute"
        result = load_server_config()
        assert result["rate_limit_settings"] == "60 per minute"


# ===================================================================
# LDR_APP_ALLOW_REGISTRATIONS fail-closed logic
# ===================================================================
class TestAllowRegistrationsFailClosed:
    """Tests for fail-closed behavior of LDR_APP_ALLOW_REGISTRATIONS."""

    @pytest.mark.usefixtures("_default_typed_setting")
    @pytest.mark.parametrize(
        "value", ["true", "false", "1", "0", "yes", "no", "on", "off"]
    )
    def test_recognized_allow_registrations_values(self, value, monkeypatch):
        """Recognized boolean env-var values should NOT trigger fail-closed override."""
        monkeypatch.setenv("LDR_APP_ALLOW_REGISTRATIONS", value)

        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            result = load_server_config()

            warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
            assert not any("recognized" in call for call in warning_calls), (
                f"Unexpected warning for recognized value '{value}': {warning_calls}"
            )
        # Verify the value was actually processed (not silently dropped)
        assert result["allow_registrations"] is not None

    @pytest.mark.usefixtures("_default_typed_setting")
    @pytest.mark.parametrize(
        "value", ["disabled", "nein", "flase", "MAYBE", "2", "  "]
    )
    def test_unrecognized_allow_registrations_defaults_to_false(
        self, value, monkeypatch
    ):
        """Unrecognized env-var value should force allow_registrations=False."""
        monkeypatch.setenv("LDR_APP_ALLOW_REGISTRATIONS", value)

        result = load_server_config()

        assert result["allow_registrations"] is False

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_unrecognized_allow_registrations_logs_warning(self, monkeypatch):
        """Should log a warning for unrecognized env-var value."""
        monkeypatch.setenv("LDR_APP_ALLOW_REGISTRATIONS", "nein")

        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            load_server_config()

            warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
            assert any(
                "nein" in call and "recognized" in call
                for call in warning_calls
            ), f"Expected warning about 'nein', got: {warning_calls}"

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_allow_registrations_env_not_set_uses_default(self, monkeypatch):
        """When env var is not set, allow_registrations should use default value."""
        monkeypatch.delenv("LDR_APP_ALLOW_REGISTRATIONS", raising=False)

        result = load_server_config()

        # Default is True
        assert result["allow_registrations"] is True

    @pytest.mark.usefixtures("_default_typed_setting")
    def test_empty_string_allow_registrations_fails_closed(self, monkeypatch):
        """Empty-string env var should trigger fail-closed (registrations=False)."""
        monkeypatch.setenv("LDR_APP_ALLOW_REGISTRATIONS", "")

        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            result = load_server_config()

        assert result["allow_registrations"] is False
        warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
        assert any("recognized" in call for call in warning_calls), (
            f"Expected fail-closed warning for empty string, got: {warning_calls}"
        )


# ===================================================================
# Fixtures for legacy migration tests
# ===================================================================
@pytest.fixture(autouse=True)
def _mock_data_dir(tmp_path):
    """Point get_data_dir to a temp directory for all tests in this module."""
    with patch(
        "local_deep_research.web.server_config.get_data_dir",
        return_value=str(tmp_path),
    ):
        yield tmp_path


@pytest.fixture(autouse=True)
def _clean_allow_registrations_env(monkeypatch):
    """Ensure LDR_APP_ALLOW_REGISTRATIONS is not set from the developer's shell.

    The fail-closed logic reads this env var directly via os.getenv(),
    bypassing mocks. If a developer has it set, tests could fail unexpectedly.
    """
    monkeypatch.delenv("LDR_APP_ALLOW_REGISTRATIONS", raising=False)


@pytest.fixture()
def _passthrough_typed_setting():
    """Mock get_typed_setting_value: returns val if not None, else default.

    This simulates the priority logic without env var checks, so we can
    verify that legacy file values are passed through correctly.
    """
    with patch(
        "local_deep_research.web.server_config.get_typed_setting_value",
        side_effect=lambda key, val, typ, default: (
            val if val is not None else default
        ),
    ):
        yield


# ===================================================================
# get_server_config_path
# ===================================================================
class TestGetServerConfigPath:
    """Tests for get_server_config_path function."""

    def test_returns_path_object(self):
        assert isinstance(get_server_config_path(), Path)

    def test_filename_is_server_config_json(self):
        assert get_server_config_path().name == "server_config.json"

    def test_path_is_under_data_dir(self, _mock_data_dir):
        assert get_server_config_path().parent == _mock_data_dir


# ===================================================================
# Legacy migration — _load_legacy_config via load_server_config
# ===================================================================
class TestLegacyMigration:
    """Tests for legacy server_config.json migration path."""

    def _write_legacy(self, tmp_path, data):
        """Write a legacy server_config.json file."""
        (tmp_path / "server_config.json").write_text(
            json.dumps(data), encoding="utf-8"
        )

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_legacy_allow_registrations_false_honored(self, _mock_data_dir):
        """Security-critical: allow_registrations=false from legacy file must be honored."""
        self._write_legacy(_mock_data_dir, {"allow_registrations": False})
        result = load_server_config()
        assert result["allow_registrations"] is False

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_env_var_overrides_legacy_value(self, _mock_data_dir):
        """Env var should take priority over legacy file value.

        We use _env_typed_setting-style mock to simulate env override.
        """
        self._write_legacy(_mock_data_dir, {"port": 9999})

        # Simulate env var override: get_typed_setting_value returns env value
        def _side_effect(key, val, typ, default):
            if key == "web.port":
                return 8080  # env var wins
            return val if val is not None else default

        with patch(
            "local_deep_research.web.server_config.get_typed_setting_value",
            side_effect=_side_effect,
        ):
            result = load_server_config()

        assert result["port"] == 8080

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_no_legacy_file_returns_defaults(self):
        """Without a legacy file, defaults should be returned."""
        result = load_server_config()
        assert result["host"] == "0.0.0.0"
        assert result["port"] == 5000
        assert result["allow_registrations"] is True

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_corrupt_json_falls_back_to_defaults(self, _mock_data_dir):
        """Corrupt JSON should log a warning and return defaults."""
        (_mock_data_dir / "server_config.json").write_text(
            "not valid json{{{", encoding="utf-8"
        )
        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            result = load_server_config()

        assert result["host"] == "0.0.0.0"
        assert result["allow_registrations"] is True
        warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
        assert any("Could not read" in call for call in warning_calls)

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_corrupt_encoding_falls_back_to_defaults(self, _mock_data_dir):
        """Invalid UTF-8 bytes should log a warning and return defaults."""
        (_mock_data_dir / "server_config.json").write_bytes(
            b"\x80\xfe\xff invalid utf-8"
        )
        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            result = load_server_config()

        assert result["host"] == "0.0.0.0"
        assert result["allow_registrations"] is True
        warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
        assert any("Could not read" in call for call in warning_calls)

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_non_dict_json_falls_back_to_defaults(self, _mock_data_dir):
        """Non-dict JSON (e.g. array) should log a warning and return defaults."""
        self._write_legacy(_mock_data_dir, [1, 2, 3])
        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            result = load_server_config()

        assert result["host"] == "0.0.0.0"
        warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
        assert any("does not contain" in call for call in warning_calls)

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_partial_legacy_file(self, _mock_data_dir):
        """Partial legacy file: port from file, rest defaults."""
        self._write_legacy(_mock_data_dir, {"port": 8080})
        result = load_server_config()
        assert result["port"] == 8080
        assert result["host"] == "0.0.0.0"
        assert result["allow_registrations"] is True

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_security_warning_logged_for_allow_registrations(
        self, _mock_data_dir
    ):
        """Security warning with 'SECURITY' should be logged for allow_registrations."""
        self._write_legacy(_mock_data_dir, {"allow_registrations": False})
        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            load_server_config()

        warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
        assert any("SECURITY" in call for call in warning_calls)

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_info_logged_for_non_security_settings(self, _mock_data_dir):
        """Non-security settings should log at info level, not warning."""
        self._write_legacy(_mock_data_dir, {"host": "127.0.0.1"})
        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            load_server_config()

        info_calls = [str(c) for c in mock_logger.info.call_args_list]
        assert any("host" in call for call in info_calls)

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_unrecognized_keys_logged_as_warning(self, _mock_data_dir):
        """Unrecognized keys in legacy file produce a warning listing them."""
        self._write_legacy(
            _mock_data_dir, {"unknown_key": "value", "another": 42}
        )
        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            result = load_server_config()

        warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
        # Should warn about unrecognized keys
        assert any("unrecognized" in call.lower() for call in warning_calls)
        # No banner since no recognized keys
        info_calls = [str(c) for c in mock_logger.info.call_args_list]
        assert not any(
            "server_config.json detected" in call for call in info_calls
        )
        # Defaults returned
        assert result["host"] == "0.0.0.0"

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_empty_dict_no_warnings(self, _mock_data_dir):
        """Empty dict {} should produce no warnings and return defaults."""
        self._write_legacy(_mock_data_dir, {})
        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            result = load_server_config()

        assert result["host"] == "0.0.0.0"
        assert result["allow_registrations"] is True
        info_calls = [str(c) for c in mock_logger.info.call_args_list]
        assert not any(
            "server_config.json detected" in call for call in info_calls
        )

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_banner_logged_for_recognized_keys(self, _mock_data_dir):
        """Info banner should be logged when recognized keys are present."""
        self._write_legacy(_mock_data_dir, {"port": 8080})
        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            load_server_config()
        info_calls = [str(c) for c in mock_logger.info.call_args_list]
        assert any("server_config.json detected" in call for call in info_calls)

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_all_defaults_no_per_key_messages(self, _mock_data_dir):
        """Legacy file with all default values should not produce per-key messages."""
        from local_deep_research.web.server_config import _DEFAULTS

        self._write_legacy(_mock_data_dir, dict(_DEFAULTS))
        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            load_server_config()

        # Banner should still appear (file exists with recognized keys)
        info_calls = [str(c) for c in mock_logger.info.call_args_list]
        assert any("server_config.json detected" in call for call in info_calls)

        # But no per-key info or warning messages about non-default values
        warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
        assert not any("non-default" in call for call in info_calls)
        assert not any("non-default" in call for call in warning_calls)


class TestLegacyAllowRegistrationsFailClosed:
    """Fail-closed for legacy JSON allow_registrations string values.

    The guard fires when the legacy JSON has an unrecognized string for
    allow_registrations and no env var overrides it.
    """

    @staticmethod
    def _write_legacy(tmp_path, data):
        (tmp_path / "server_config.json").write_text(
            json.dumps(data), encoding="utf-8"
        )

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_unrecognized_string_defaults_to_false(self, _mock_data_dir):
        """Legacy JSON 'allow_registrations': 'disabled' should fail closed."""
        self._write_legacy(_mock_data_dir, {"allow_registrations": "disabled"})
        result = load_server_config()
        assert result["allow_registrations"] is False

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    @pytest.mark.parametrize("value", ["true", "yes", "1", "on"])
    def test_recognized_truthy_string_not_overridden(
        self, value, _mock_data_dir
    ):
        """Truthy recognized strings should NOT be overridden by the guard."""
        self._write_legacy(_mock_data_dir, {"allow_registrations": value})
        result = load_server_config()
        assert result["allow_registrations"] == value  # passthrough, not False

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    @pytest.mark.parametrize("value", ["false", "no", "0", "off"])
    def test_recognized_falsy_string_not_overridden(
        self, value, _mock_data_dir
    ):
        """Falsy recognized strings — guard should not override."""
        self._write_legacy(_mock_data_dir, {"allow_registrations": value})
        result = load_server_config()
        assert (
            result["allow_registrations"] == value
        )  # passthrough, not bool False

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_native_bool_not_affected(self, _mock_data_dir):
        """Native JSON bool should not trigger the string guard."""
        self._write_legacy(_mock_data_dir, {"allow_registrations": False})
        result = load_server_config()
        assert result["allow_registrations"] is False

    @pytest.mark.usefixtures("_passthrough_typed_setting")
    def test_env_var_guard_takes_priority_over_legacy_guard(
        self, _mock_data_dir, monkeypatch
    ):
        """When both env var and legacy JSON have bad values, the env var guard fires."""
        self._write_legacy(_mock_data_dir, {"allow_registrations": "disabled"})
        monkeypatch.setenv("LDR_APP_ALLOW_REGISTRATIONS", "nein")
        with patch(
            "local_deep_research.web.server_config.logger"
        ) as mock_logger:
            result = load_server_config()
        assert result["allow_registrations"] is False
        warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
        assert any(
            "LDR_APP_ALLOW_REGISTRATIONS" in call for call in warning_calls
        )
        assert not any(
            "Legacy server_config.json" in call for call in warning_calls
        )


# ===================================================================
# has_legacy_customizations — direct tests using config_path parameter
# ===================================================================
class TestHasLegacyCustomizations:
    """Tests for has_legacy_customizations helper."""

    def test_returns_false_when_no_file(self, tmp_path):
        """Non-existent path should return False."""
        assert has_legacy_customizations(tmp_path / "nope.json") is False

    def test_returns_true_when_non_default_value(self, tmp_path):
        """File with a non-default value should return True."""
        cfg = tmp_path / "server_config.json"
        cfg.write_text('{"port": 9999}', encoding="utf-8")
        assert has_legacy_customizations(cfg) is True

    def test_returns_false_when_all_defaults(self, tmp_path):
        """File with all default values should return False."""
        from local_deep_research.web.server_config import _DEFAULTS

        cfg = tmp_path / "server_config.json"
        cfg.write_text(json.dumps(_DEFAULTS), encoding="utf-8")
        assert has_legacy_customizations(cfg) is False

    def test_returns_false_on_malformed_json(self, tmp_path):
        """Malformed JSON should return False, not raise."""
        cfg = tmp_path / "server_config.json"
        cfg.write_text("not valid json", encoding="utf-8")
        assert has_legacy_customizations(cfg) is False

    def test_returns_false_on_corrupt_encoding(self, tmp_path):
        """Invalid UTF-8 bytes should return False, not raise."""
        cfg = tmp_path / "server_config.json"
        cfg.write_bytes(b"\x80\xfe\xff invalid utf-8")
        assert has_legacy_customizations(cfg) is False
