"""Tests targeting uncovered lines in settings_routes.py.

Covered areas:
- save_all_settings: corrupted value detection ([, ], {}, "[object Object]"),
  report key -> empty dict, search.tool/app.theme defaults, new setting creation
  with automatic UI element detection (checkbox, number, textarea), and creation
  failure path
- fix_corrupted_settings: duplicate key detection and removal, per-key default
  value assignment (search.*, app.*, report.*), report unknown key fallback to
  empty dict, empty dict corruption detection
- save_settings: individual setting exception handling with failed_count
- api_update_setting: new setting creation path when setting does not exist
- api_get_data_location: platform detection (Windows/macOS/Linux)
"""

from contextlib import contextmanager
from unittest.mock import MagicMock, Mock, patch

from flask import Flask, jsonify

from local_deep_research.web.auth.routes import auth_bp
from local_deep_research.web.routes.settings_routes import settings_bp

MODULE = "local_deep_research.web.routes.settings_routes"
DECORATOR_MODULE = "local_deep_research.web.utils.route_decorators"
SETTINGS_PREFIX = "/settings"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_setting(
    key="test.key",
    value="val",
    ui_element="text",
    name="Test Key",
    description="desc",
    category="general",
    setting_type="app",
    editable=True,
    visible=True,
    options=None,
    min_value=None,
    max_value=None,
    step=None,
    updated_at=None,
):
    """Build a mock Setting ORM object."""
    s = MagicMock()
    s.key = key
    s.value = value
    s.ui_element = ui_element
    s.name = name
    s.description = description
    s.category = category
    s.type = setting_type
    s.editable = editable
    s.visible = visible
    s.options = options
    s.min_value = min_value
    s.max_value = max_value
    s.step = step
    s.updated_at = updated_at
    return s


def _create_test_app():
    """Create a minimal Flask app with only the settings blueprint."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"
    app.config["WTF_CSRF_ENABLED"] = False
    app.register_blueprint(auth_bp)
    app.register_blueprint(settings_bp)

    @app.errorhandler(500)
    def _handle_500(error):
        return jsonify({"error": "Internal server error"}), 500

    return app


@contextmanager
def _authenticated_client(app):
    """Provide an authenticated test client with mocked auth and settings_limit."""
    mock_db = Mock()
    mock_db.connections = {"testuser": True}
    mock_db.has_encryption = False

    @contextmanager
    def _fake_session(*args, **kwargs):
        yield MagicMock()

    patches = [
        patch("local_deep_research.web.auth.decorators.db_manager", mock_db),
        patch(
            f"{DECORATOR_MODULE}.get_user_db_session", side_effect=_fake_session
        ),
        patch(f"{MODULE}.settings_limit", lambda f: f),
    ]

    started = []
    try:
        for p in patches:
            started.append(p.start())
        with app.test_client() as client:
            with client.session_transaction() as sess:
                sess["username"] = "testuser"
                sess["session_id"] = "test-session-id"
            yield client
    finally:
        for p in patches:
            p.stop()


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestSaveAllSettingsCorruptedValues:
    """save_all_settings: corrupted value detection and replacement."""

    @patch(f"{MODULE}.set_setting", return_value=True)
    @patch(
        f"{MODULE}.coerce_setting_for_write",
        side_effect=lambda key, value, ui_element: value,
    )
    @patch(f"{MODULE}.validate_setting", return_value=(True, None))
    def test_bracket_open_detected_as_corrupted_for_search_tool(
        self, mock_v, mock_c, mock_s
    ):
        """Value '[' is detected as corrupted; search.tool gets default 'auto'."""
        setting = _make_setting(
            key="search.tool", value="auto", ui_element="text", editable=True
        )
        setting.type = "search"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"search.tool": "["},
                )
        assert resp.status_code == 200
        # The coerced value should have been replaced with "auto"
        call_args = mock_c.call_args
        assert (
            call_args[1]["value"] == "auto" or call_args[0][0] == "search.tool"
        )

    @patch(f"{MODULE}.set_setting", return_value=True)
    @patch(
        f"{MODULE}.coerce_setting_for_write",
        side_effect=lambda key, value, ui_element: value,
    )
    @patch(f"{MODULE}.validate_setting", return_value=(True, None))
    def test_empty_braces_detected_as_corrupted_for_app_theme(
        self, mock_v, mock_c, mock_s
    ):
        """Value '{}' is detected as corrupted; app.theme gets default 'dark'."""
        setting = _make_setting(
            key="app.theme", value="dark", ui_element="text", editable=True
        )
        setting.type = "app"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"app.theme": "{}"},
                )
        assert resp.status_code == 200
        # coerce_setting_for_write should receive "dark" (the default)
        _, kwargs = mock_c.call_args
        assert kwargs["value"] == "dark"

    @patch(f"{MODULE}.set_setting", return_value=True)
    @patch(
        f"{MODULE}.coerce_setting_for_write",
        side_effect=lambda key, value, ui_element: value,
    )
    @patch(f"{MODULE}.validate_setting", return_value=(True, None))
    def test_corrupted_report_key_gets_empty_dict(self, mock_v, mock_c, mock_s):
        """report.* keys with corrupted values get replaced with empty dict."""
        setting = _make_setting(
            key="report.structure",
            value={},
            ui_element="textarea",
            editable=True,
        )
        setting.type = "report"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"report.structure": "[]"},
                )
        assert resp.status_code == 200
        _, kwargs = mock_c.call_args
        assert kwargs["value"] == {}


class TestSaveAllSettingsNewSettingCreationFailure:
    """save_all_settings: new setting creation failure returns validation error."""

    @patch(f"{MODULE}.create_or_update_setting", return_value=None)
    def test_new_setting_creation_failure_gives_error(self, mock_create):
        """When create_or_update_setting returns None, a validation error is recorded."""
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                # First call: no existing settings; second call: return list for response
                mock_session.query.return_value.all.side_effect = [[], []]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"app.new_flag": "some_value"},
                )
        assert resp.status_code == 400
        data = resp.get_json()
        assert data["status"] == "error"
        assert any(e["key"] == "app.new_flag" for e in data["errors"])

    @patch(f"{MODULE}.create_or_update_setting")
    def test_new_list_setting_gets_textarea(self, mock_create):
        """A new setting with a list value gets ui_element 'textarea'."""
        mock_new = _make_setting(key="app.items", value=[1, 2])
        mock_new.type = "app"
        mock_create.return_value = mock_new

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [[], []]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"app.items": [1, 2]},
                )
        assert resp.status_code == 200
        call_args = mock_create.call_args[0][0]
        assert call_args["ui_element"] == "textarea"


class TestFixCorruptedSettingsDuplicatesAndDefaults:
    """fix_corrupted_settings: duplicate removal and per-key default values."""

    def test_duplicates_removed_and_corrupted_values_fixed(self):
        """Duplicate keys are removed; corrupted values get per-key defaults."""
        from datetime import datetime, UTC

        now = datetime.now(UTC)

        # Build two settings with the same key (duplicates)
        dupe1 = _make_setting(
            key="search.max_results", value=10, updated_at=now
        )
        dupe2 = _make_setting(key="search.max_results", value=5, updated_at=now)

        # Build settings with various corrupted values
        corrupted_search_region = _make_setting(
            key="search.region", value="null"
        )
        corrupted_search_tool = _make_setting(
            key="search.tool", value="[object Object]"
        )
        corrupted_app_theme = _make_setting(key="app.theme", value="{}")
        corrupted_app_port = _make_setting(key="app.port", value=None)
        corrupted_report_unknown = _make_setting(
            key="report.custom_layout", value="undefined"
        )
        corrupted_empty_dict = _make_setting(
            key="search.questions_per_iteration", value={}
        )
        clean_setting = _make_setting(key="llm.model", value="gpt-4")

        all_settings = [
            corrupted_search_region,
            corrupted_search_tool,
            corrupted_app_theme,
            corrupted_app_port,
            corrupted_report_unknown,
            corrupted_empty_dict,
            clean_setting,
        ]

        # Mock duplicate key query result
        mock_dupe_key = MagicMock()
        mock_dupe_key.__getitem__ = Mock(return_value="search.max_results")

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()

                # Chain query mocks for duplicate detection
                mock_group_query = MagicMock()
                mock_group_query.group_by.return_value.having.return_value.all.return_value = [
                    mock_dupe_key
                ]

                # Chain for fetching duplicate settings by key
                mock_filter_query = MagicMock()
                mock_filter_query.filter.return_value.order_by.return_value.all.return_value = [
                    dupe1,
                    dupe2,
                ]

                # All settings query for corruption check
                mock_all_query = MagicMock()
                mock_all_query.all.return_value = all_settings

                # Wire up the three query() calls
                mock_session.query.side_effect = [
                    mock_group_query,  # duplicate key detection
                    mock_filter_query,  # fetch duplicates for removal
                    mock_all_query,  # all settings for corruption check
                ]

                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)

                resp = client.post(
                    f"{SETTINGS_PREFIX}/fix_corrupted_settings",
                )

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["status"] == "success"
        # Duplicate should have been deleted
        mock_session.delete.assert_called_once_with(dupe2)
        # Corrupted values should be fixed with defaults
        assert corrupted_search_region.value == "us"
        assert corrupted_search_tool.value == "auto"
        assert corrupted_app_theme.value == "dark"
        assert corrupted_app_port.value == 5000
        # search.questions_per_iteration (empty dict -> corrupted)
        assert corrupted_empty_dict.value == 3
        # report.custom_layout has no known default -> fallback to empty dict
        assert corrupted_report_unknown.value == {}

    def test_fix_corrupted_settings_exception_returns_500(self):
        """When an exception occurs, rollback and return 500."""
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.side_effect = RuntimeError("db failure")
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)

                resp = client.post(
                    f"{SETTINGS_PREFIX}/fix_corrupted_settings",
                )

        assert resp.status_code == 500
        data = resp.get_json()
        assert data["status"] == "error"
        mock_session.rollback.assert_called_once()


class TestSaveSettingsExceptionInLoop:
    """save_settings (POST fallback): exception inside the per-setting loop."""

    def test_setting_exception_increments_failed_count(self):
        """When a single setting raises, failed_count increments and flash warns."""
        setting = _make_setting(
            key="llm.temperature", value=0.7, ui_element="number", editable=True
        )

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.return_value = [setting]

                mock_sm = MagicMock()
                mock_sm.set_setting.side_effect = RuntimeError("save error")

                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)

                with patch(
                    f"{DECORATOR_MODULE}.SettingsManager", return_value=mock_sm
                ):
                    with patch(
                        f"{MODULE}.coerce_setting_for_write",
                        side_effect=RuntimeError("coerce error"),
                    ):
                        resp = client.post(
                            f"{SETTINGS_PREFIX}/save_settings",
                            data={"llm.temperature": "0.5"},
                        )

        # save_settings redirects on completion
        assert resp.status_code == 302


class TestApiUpdateSettingCreatesNew:
    """api_update_setting: when the setting does not exist, create a new one."""

    def test_create_new_setting_via_put(self):
        """PUT to a non-existent key creates a new setting and returns 201."""
        mock_new = _make_setting(key="llm.new_param", value="hello")
        mock_new.type = MagicMock()
        mock_new.type.value = "app"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                # Setting does not exist
                mock_session.query.return_value.filter.return_value.first.return_value = None
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)

                with patch(
                    f"{MODULE}.create_or_update_setting",
                    return_value=mock_new,
                ):
                    resp = client.put(
                        f"{SETTINGS_PREFIX}/api/llm.new_param",
                        json={"value": "hello", "type": "app"},
                    )

        assert resp.status_code == 201
        data = resp.get_json()
        assert "created successfully" in data["message"]
        assert data["setting"]["key"] == "llm.new_param"

    def test_create_new_setting_failure_returns_500(self):
        """PUT to a non-existent key returns 500 when creation fails."""
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.filter.return_value.first.return_value = None
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)

                with patch(
                    f"{MODULE}.create_or_update_setting",
                    return_value=None,
                ):
                    resp = client.put(
                        f"{SETTINGS_PREFIX}/api/llm.new_param",
                        json={"value": "hello"},
                    )

        assert resp.status_code == 500
        data = resp.get_json()
        assert "Failed to create" in data["error"]


class TestApiGetDataLocationPlatform:
    """api_get_data_location: platform detection branches."""

    def _call_data_location(self, platform_system_return):
        """Helper that calls the data-location endpoint with mocked platform."""
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)

                mock_sm = MagicMock()
                mock_sm.get_setting.return_value = None

                with (
                    patch(f"{MODULE}.get_data_directory", return_value="/data"),
                    patch(
                        f"{MODULE}.get_encrypted_database_path",
                        return_value="/data/db",
                    ),
                    patch(
                        f"{DECORATOR_MODULE}.SettingsManager",
                        return_value=mock_sm,
                    ),
                    patch(f"{MODULE}.platform") as mock_platform,
                    patch(f"{MODULE}.db_manager") as mock_db_mgr,
                ):
                    mock_platform.system.return_value = platform_system_return
                    mock_db_mgr.has_encryption = False

                    with patch(
                        "local_deep_research.web.auth.decorators.db_manager",
                        mock_db_mgr,
                    ):
                        # Re-set session since we re-patched db_manager
                        with client.session_transaction() as sess:
                            sess["username"] = "testuser"
                            sess["session_id"] = "test-session-id"

                        resp = client.get(
                            f"{SETTINGS_PREFIX}/api/data-location",
                        )

        return resp

    def test_linux_platform(self):
        resp = self._call_data_location("Linux")
        assert resp.status_code == 200
        data = resp.get_json()
        assert data["platform"] == "Linux"
        assert "Linux" in data["platform_info"]

    def test_darwin_mapped_to_macos(self):
        resp = self._call_data_location("Darwin")
        assert resp.status_code == 200
        data = resp.get_json()
        assert data["platform"] == "macOS"

    def test_windows_platform(self):
        resp = self._call_data_location("Windows")
        assert resp.status_code == 200
        data = resp.get_json()
        assert data["platform"] == "Windows"
