"""Tests for the calculate_warnings orchestrator (__init__.py).

These test the integration logic: session reading, DB session lifecycle,
setting dispatch to sub-checks, and error handling.
"""

from unittest.mock import Mock, patch


def _make_settings_manager(overrides=None):
    """Build a mock SettingsManager with sensible defaults."""
    defaults = {
        "llm.provider": "ollama",
        "llm.local_context_window_size": 4096,
        "llm.model": "llama3",
        "app.warnings.dismiss_high_context": False,
        "app.warnings.dismiss_model_mismatch": False,
        "app.warnings.dismiss_context_reduced": False,
        "app.warnings.dismiss_legacy_config": False,
        "app.warnings.dismiss_no_backups": False,
        "app.warnings.dismiss_backup_disabled": False,
        "backup.enabled": True,
    }
    if overrides:
        defaults.update(overrides)

    mgr = Mock()
    mgr.get_setting.side_effect = lambda key, default=None: defaults.get(
        key, default
    )
    return mgr


def _patch_orchestrator(settings_manager, db_session=None):
    """Return a nested context manager that patches session, get_user_db_session,
    and get_settings_manager for the warnings orchestrator.
    """
    if db_session is None:
        db_session = Mock()

    class _Ctx:
        def __enter__(self_ctx):
            self_ctx.p1 = patch(
                "local_deep_research.web.warning_checks.session",
                {"username": "test"},
            )
            self_ctx.p2 = patch(
                "local_deep_research.web.warning_checks.get_user_db_session"
            )
            self_ctx.p3 = patch(
                "local_deep_research.web.warning_checks.get_settings_manager",
                return_value=settings_manager,
            )
            self_ctx.p1.start()
            mock_ctx = self_ctx.p2.start()
            mock_ctx.return_value.__enter__ = Mock(return_value=db_session)
            mock_ctx.return_value.__exit__ = Mock(return_value=False)
            self_ctx.p3.start()
            return self_ctx

        def __exit__(self_ctx, *args):
            self_ctx.p3.stop()
            self_ctx.p2.stop()
            self_ctx.p1.stop()

    return _Ctx()


class TestCalculateWarningsNoDbSession:
    """Tests for when db_session is None."""

    def test_returns_empty_list_when_db_session_is_none(self):
        from local_deep_research.web.warning_checks import calculate_warnings

        with patch(
            "local_deep_research.web.warning_checks.session",
            {"username": "test"},
        ):
            with patch(
                "local_deep_research.web.warning_checks.get_user_db_session"
            ) as mock_ctx:
                mock_ctx.return_value.__enter__ = Mock(return_value=None)
                mock_ctx.return_value.__exit__ = Mock(return_value=False)

                result = calculate_warnings()
                assert result == []

    def test_returns_empty_list_when_no_username(self):
        from local_deep_research.web.warning_checks import calculate_warnings

        with patch(
            "local_deep_research.web.warning_checks.session",
            {},
        ):
            with patch(
                "local_deep_research.web.warning_checks.get_user_db_session"
            ) as mock_ctx:
                mock_ctx.return_value.__enter__ = Mock(return_value=None)
                mock_ctx.return_value.__exit__ = Mock(return_value=False)

                result = calculate_warnings()
                assert result == []


class TestCalculateWarningsErrorHandling:
    """Tests for exception handling."""

    def test_returns_empty_list_on_exception(self):
        from local_deep_research.web.warning_checks import calculate_warnings

        with patch(
            "local_deep_research.web.warning_checks.session",
            {"username": "test"},
        ):
            with patch(
                "local_deep_research.web.warning_checks.get_user_db_session",
                side_effect=RuntimeError("DB exploded"),
            ):
                result = calculate_warnings()
                assert result == []

    def test_returns_empty_list_on_settings_manager_error(self):
        from local_deep_research.web.warning_checks import calculate_warnings

        with patch(
            "local_deep_research.web.warning_checks.session",
            {"username": "test"},
        ):
            with patch(
                "local_deep_research.web.warning_checks.get_user_db_session"
            ) as mock_ctx:
                mock_ctx.return_value.__enter__ = Mock(return_value=Mock())
                mock_ctx.return_value.__exit__ = Mock(return_value=False)

                with patch(
                    "local_deep_research.web.warning_checks.get_settings_manager",
                    side_effect=ValueError("bad settings"),
                ):
                    result = calculate_warnings()
                    assert result == []


class TestCalculateWarningsProviderNormalization:
    """Provider string is lowercased before checks."""

    def test_uppercase_provider_is_normalized(self):
        from local_deep_research.web.warning_checks import calculate_warnings

        mgr = _make_settings_manager(
            {
                "llm.provider": "OLLAMA",
                "llm.local_context_window_size": 16384,
            }
        )
        with _patch_orchestrator(mgr):
            warnings = calculate_warnings()
            assert any(w["type"] == "high_context" for w in warnings)


class TestCalculateWarningsContextCheckGating:
    """Context checks are gated on is_local AND not dismissed."""

    def test_context_checks_skipped_for_non_local_provider(self):
        """Non-local provider should never trigger context history checks,
        even when context is low and history would normally warn."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mock_db_session = Mock()

        mgr = _make_settings_manager(
            {
                "llm.provider": "openai",
                "llm.local_context_window_size": 2048,
                "app.warnings.dismiss_context_reduced": False,
            }
        )
        with _patch_orchestrator(mgr, db_session=mock_db_session):
            warnings = calculate_warnings()

        # DB should not have been queried for context history at all
        mock_db_session.query.assert_not_called()
        assert not any(w["type"] == "context_below_history" for w in warnings)
        assert not any(
            w["type"] == "context_truncation_history" for w in warnings
        )

    def test_context_checks_skipped_when_dismissed(self):
        """Local provider with dismiss_context_reduced=True should skip DB queries."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mock_db_session = Mock()

        mgr = _make_settings_manager(
            {
                "llm.provider": "ollama",
                "llm.local_context_window_size": 2048,
                "app.warnings.dismiss_context_reduced": True,
            }
        )
        with _patch_orchestrator(mgr, db_session=mock_db_session):
            warnings = calculate_warnings()

        mock_db_session.query.assert_not_called()
        assert not any(w["type"] == "context_below_history" for w in warnings)


class TestCalculateWarningsMultipleWarnings:
    """Multiple warnings can fire simultaneously."""

    def test_high_context_and_model_mismatch_together(self):
        from local_deep_research.web.warning_checks import calculate_warnings

        mgr = _make_settings_manager(
            {
                "llm.provider": "ollama",
                "llm.local_context_window_size": 16384,
                "llm.model": "llama3.1:70b",
            }
        )
        with _patch_orchestrator(mgr):
            warnings = calculate_warnings()

        types = {w["type"] for w in warnings}
        assert "high_context" in types
        assert "model_mismatch" in types

    def test_high_context_and_context_history_together(self):
        from local_deep_research.web.warning_checks import calculate_warnings

        mock_db_session = Mock()

        # DB queries for context history
        context_query = Mock()
        context_query.filter.return_value.order_by.return_value.limit.return_value.all.return_value = (
            [(32768,)] * 10
        )

        truncation_query = Mock()
        truncation_query.filter.return_value.filter.return_value.scalar.return_value = 5

        mock_db_session.query.side_effect = [context_query, truncation_query]

        mgr = _make_settings_manager(
            {
                "llm.provider": "ollama",
                "llm.local_context_window_size": 16384,
            }
        )
        with _patch_orchestrator(mgr, db_session=mock_db_session):
            warnings = calculate_warnings()

        types = {w["type"] for w in warnings}
        assert "high_context" in types
        assert "context_below_history" in types
        assert "context_truncation_history" in types

    def test_all_hardware_and_context_warnings_simultaneously(self):
        from local_deep_research.web.warning_checks import calculate_warnings

        mock_db_session = Mock()

        context_query = Mock()
        context_query.filter.return_value.order_by.return_value.limit.return_value.all.return_value = (
            [(32768,)] * 10
        )

        truncation_query = Mock()
        truncation_query.filter.return_value.filter.return_value.scalar.return_value = 2

        mock_db_session.query.side_effect = [context_query, truncation_query]

        mgr = _make_settings_manager(
            {
                "llm.provider": "ollama",
                "llm.local_context_window_size": 16384,
                "llm.model": "llama3.1:70b",
            }
        )
        with _patch_orchestrator(mgr, db_session=mock_db_session):
            with patch(
                "local_deep_research.web.server_config.get_server_config_path",
                return_value=Mock(exists=Mock(return_value=False)),
            ):
                warnings = calculate_warnings()

        types = {w["type"] for w in warnings}
        assert types == {
            "high_context",
            "model_mismatch",
            "context_below_history",
            "context_truncation_history",
            "no_backups",
            # The egress policy defaults to scope="adaptive" (unacknowledged):
            # the public-egress banner fires (adaptive can resolve to a
            # public-allowing scope) alongside the informational banner that
            # states what adaptive resolves to.
            "public_egress_enabled",
            "egress_effective_scope",
        }


class TestCalculateWarningsSingleSession:
    """Verify all settings are read from a single DB session."""

    def test_get_user_db_session_called_once(self):
        """The orchestrator should open exactly one DB session."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mgr = _make_settings_manager()

        with patch(
            "local_deep_research.web.warning_checks.session",
            {"username": "test"},
        ):
            with patch(
                "local_deep_research.web.warning_checks.get_user_db_session"
            ) as mock_ctx:
                mock_ctx.return_value.__enter__ = Mock(return_value=Mock())
                mock_ctx.return_value.__exit__ = Mock(return_value=False)

                with patch(
                    "local_deep_research.web.warning_checks.get_settings_manager",
                    return_value=mgr,
                ):
                    calculate_warnings()

        # Should be called exactly once (the whole point of the refactor)
        assert mock_ctx.call_count == 1

    def test_get_user_db_session_called_with_username(self):
        """Verify username from session is passed to get_user_db_session."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mgr = _make_settings_manager()

        with patch(
            "local_deep_research.web.warning_checks.session",
            {"username": "alice"},
        ):
            with patch(
                "local_deep_research.web.warning_checks.get_user_db_session"
            ) as mock_get_db:
                mock_get_db.return_value.__enter__ = Mock(return_value=Mock())
                mock_get_db.return_value.__exit__ = Mock(return_value=False)

                with patch(
                    "local_deep_research.web.warning_checks.get_settings_manager",
                    return_value=mgr,
                ):
                    calculate_warnings()

        mock_get_db.assert_called_once_with("alice")

    def test_get_settings_manager_called_with_session_and_username(self):
        """Verify both db_session and username are passed to get_settings_manager."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mgr = _make_settings_manager()
        mock_db = Mock()

        with patch(
            "local_deep_research.web.warning_checks.session",
            {"username": "bob"},
        ):
            with patch(
                "local_deep_research.web.warning_checks.get_user_db_session"
            ) as mock_get_db:
                mock_get_db.return_value.__enter__ = Mock(return_value=mock_db)
                mock_get_db.return_value.__exit__ = Mock(return_value=False)

                with patch(
                    "local_deep_research.web.warning_checks.get_settings_manager",
                    return_value=mgr,
                ) as mock_get_sm:
                    calculate_warnings()

        mock_get_sm.assert_called_once_with(mock_db, "bob")

    def test_all_seven_settings_read(self):
        """All 7 required settings are read from the manager."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mgr = _make_settings_manager()

        with _patch_orchestrator(mgr):
            calculate_warnings()

        called_keys = [call.args[0] for call in mgr.get_setting.call_args_list]
        assert "llm.provider" in called_keys
        assert "llm.local_context_window_size" in called_keys
        assert "llm.model" in called_keys
        assert "app.warnings.dismiss_high_context" in called_keys
        assert "app.warnings.dismiss_model_mismatch" in called_keys
        assert "app.warnings.dismiss_context_reduced" in called_keys
        assert "app.warnings.dismiss_legacy_config" in called_keys


class TestCalculateWarningsLegacyServerConfig:
    """Orchestrator coverage for the legacy_server_config check."""

    def test_legacy_server_config_warning_when_file_exists(self):
        """If server_config.json exists on disk with non-default values, warning appears."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mgr = _make_settings_manager()
        mock_path = Mock()
        mock_path.exists.return_value = True
        # Provide JSON with a non-default value so the warning fires
        mock_path.read_text.return_value = '{"port": 9999}'
        with _patch_orchestrator(mgr):
            with patch(
                "local_deep_research.web.server_config.get_server_config_path",
                return_value=mock_path,
            ):
                warnings = calculate_warnings()

        types = {w["type"] for w in warnings}
        assert "legacy_server_config" in types

    def test_no_legacy_server_config_warning_when_file_absent(self):
        """If server_config.json does not exist, no warning."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mgr = _make_settings_manager()
        with _patch_orchestrator(mgr):
            with patch(
                "local_deep_research.web.server_config.get_server_config_path",
                return_value=Mock(exists=Mock(return_value=False)),
            ):
                warnings = calculate_warnings()

        types = {w["type"] for w in warnings}
        assert "legacy_server_config" not in types

    def test_no_legacy_server_config_warning_when_dismissed(self):
        """If dismissed, no warning even when file exists."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mgr = _make_settings_manager(
            {"app.warnings.dismiss_legacy_config": True}
        )
        with _patch_orchestrator(mgr):
            with patch(
                "local_deep_research.web.server_config.get_server_config_path",
                return_value=Mock(exists=Mock(return_value=True)),
            ):
                warnings = calculate_warnings()

        types = {w["type"] for w in warnings}
        assert "legacy_server_config" not in types


class TestCalculateWarningsFailureIsolation:
    """Verify _safe_check isolates individual check failures."""

    def test_first_check_crash_does_not_kill_second(self):
        """If check_high_context raises, check_model_mismatch still runs."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mgr = _make_settings_manager(
            {
                "llm.provider": "ollama",
                "llm.local_context_window_size": 16384,
                "llm.model": "llama3.1:70b",
            }
        )
        with _patch_orchestrator(mgr):
            with patch(
                "local_deep_research.web.warning_checks.check_high_context",
                side_effect=RuntimeError("boom"),
            ):
                warnings = calculate_warnings()

        types = {w["type"] for w in warnings}
        assert "high_context" not in types
        assert "model_mismatch" in types

    def test_context_check_crash_does_not_kill_other_context_check(self):
        """If check_context_below_history raises, check_context_truncation_history still runs."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mock_db_session = Mock()

        truncation_query = Mock()
        truncation_query.filter.return_value.filter.return_value.scalar.return_value = 3

        mock_db_session.query.return_value = truncation_query

        mgr = _make_settings_manager(
            {
                "llm.provider": "ollama",
                "llm.local_context_window_size": 4096,
            }
        )
        with _patch_orchestrator(mgr, db_session=mock_db_session):
            with patch(
                "local_deep_research.web.warning_checks.check_context_below_history",
                side_effect=RuntimeError("db query failed"),
            ):
                warnings = calculate_warnings()

        types = {w["type"] for w in warnings}
        assert "context_below_history" not in types
        assert "context_truncation_history" in types


class TestCalculateWarningsNoneProvider:
    """Document behavior when provider setting is None."""

    def test_none_provider_does_not_crash(self):
        """If get_setting returns None for provider, outer try/except catches the AttributeError."""
        from local_deep_research.web.warning_checks import calculate_warnings

        mgr = Mock()
        mgr.get_setting.side_effect = lambda key, default=None: {
            "llm.provider": None,
            "llm.local_context_window_size": 4096,
            "llm.model": "llama3",
            "app.warnings.dismiss_high_context": False,
            "app.warnings.dismiss_model_mismatch": False,
            "app.warnings.dismiss_context_reduced": False,
        }.get(key, default)

        with _patch_orchestrator(mgr):
            result = calculate_warnings()

        # Should not crash — outer except catches AttributeError from None.lower()
        assert isinstance(result, list)


class TestCalculateWarningsBackupGlobHardening:
    """The backup glob in calculate_warnings() must skip symlinks, so a
    planted symlink cannot inflate the backup count/size used for warnings.
    """

    def test_symlink_is_not_counted_as_a_backup(self, tmp_path):
        from local_deep_research.web.warning_checks import calculate_warnings

        backup_dir = tmp_path / "backups"
        backup_dir.mkdir()

        # The ONLY entry matching the glob is a symlink to an existing file
        # outside the backup dir. With the hardening it is skipped, leaving
        # zero real backups; without it, stat() would follow the link and
        # count it.
        outside = tmp_path / "outside_secret.db"
        outside.write_bytes(b"x" * 4096)
        (backup_dir / "ldr_backup_29991231_235959.db").symlink_to(outside)

        settings_manager = _make_settings_manager()  # backup.enabled=True
        with _patch_orchestrator(settings_manager):
            with patch(
                "local_deep_research.config.paths.get_user_backup_directory",
                return_value=backup_dir,
            ):
                warnings = calculate_warnings()

        types = {w["type"] for w in warnings}
        # Symlink excluded -> backup_count == 0 -> the "no backups" warning
        # fires and no "backup_info" (which would carry the link target size).
        assert "no_backups" in types
        assert "backup_info" not in types
