import os
from typing import Any

import pytest
from sqlalchemy.exc import SQLAlchemyError

from src.local_deep_research.web.services.settings_manager import (
    Setting,
    SettingsManager,
    SettingType,
    check_env_setting,
)


def test_check_env_setting_exists(monkeypatch):
    monkeypatch.setenv("LDR_APP_VERSION", "1.0.0")
    assert check_env_setting("app.version") == "1.0.0"


def test_check_env_setting_not_exists():
    # Ensure the environment variable is not set
    if "LDR_APP_VERSION" in os.environ:
        del os.environ["LDR_APP_VERSION"]
    assert check_env_setting("app.version") is None


@pytest.mark.parametrize(
    "ui_element, setting_value, expected",
    [
        ("text", "hello", "hello"),
        ("select", "option_a", "option_a"),
        ("password", "secret", "secret"),
        ("number", "3.14", 3.14),
        ("range", "3.14", 3.14),
        ("checkbox", "true", True),
        ("json", {"key": "value"}, {"key": "value"}),
    ],
)
def test_get_setting_from_db(
    mocker, ui_element: str, setting_value: str, expected: Any
):
    """
    Tests that we can successfully read settings from the DB.

    Args:
        mocker: The fixture to use for mocking.
        ui_element: The value of the `ui_element` parameter, which controls the return type.
        setting_value: The value to use for the setting in the DB.
        expected: The expected typed value of the setting.
    """
    # Arrange: Set up the mock database session and a sample setting
    mock_db_session = mocker.MagicMock()
    mock_setting = Setting(
        key="test.setting", value=setting_value, ui_element=ui_element
    )
    mock_db_session.query.return_value.filter.return_value.all.return_value = [
        mock_setting
    ]

    # Act: Call the get_setting method with the test key
    settings_manager = SettingsManager(db_session=mock_db_session)
    result = settings_manager.get_setting("test.setting")

    # Assert: Verify that the result matches the expected value
    assert result == expected, f"Expected {expected}, but got {result}"


@pytest.mark.parametrize(
    "ui_element, env_value, expected",
    [
        ("text", "hello", "hello"),
        ("select", "option_a", "option_a"),
        ("password", "secret", "secret"),
        ("number", "3.14", 3.14),
        ("range", "3.14", 3.14),
        ("checkbox", "true", True),
    ],
)
def test_get_setting_from_env(
    mocker, monkeypatch, ui_element: str, env_value: str, expected: Any
):
    """
    Tests that we can successfully override DB settings with environment
    variables.

    Args:
        mocker: The fixture to use for mocking.
        ui_element: The value of the `ui_element` parameter, which controls the return type.
        env_value: The value to use for the setting in the environment variable.
        expected: The expected typed value of the setting.
    """
    # Arrange: Set up the mock database session and ensure an environment variable is set
    mock_db_session = mocker.MagicMock()
    monkeypatch.setenv("LDR_TEST_SETTING", env_value)
    # Ensure DB query returns an old version to verify environment variable is prioritized
    mock_setting = Setting(
        key="test.setting", value="db_value", ui_element=ui_element
    )
    mock_db_session.query.return_value.filter.return_value.all.return_value = [
        mock_setting
    ]

    # Act: Call the get_setting method with the test key
    settings_manager = SettingsManager(db_session=mock_db_session)
    result = settings_manager.get_setting("test.setting")

    # Assert: Verify that the result matches the expected value
    assert result == expected, f"Expected {expected}, but got {result}"


def test_get_setting_default(mocker):
    mock_db_session = mocker.MagicMock()
    # Ensure DB query returns nothing
    mock_db_session.query.return_value.filter.return_value.all.return_value = []

    settings_manager = SettingsManager(db_session=mock_db_session)
    assert (
        settings_manager.get_setting(
            "non_existent_setting", default="default_value"
        )
        == "default_value"
    )


def test_get_setting_invalid_type(mocker):
    """
    Tests that when a setting's value cannot be converted to the type
    specified by the `ui_element`, it will always return the default value.

    Args:
        mocker: The fixture to use for mocking.

    """
    # Arrange: Set up a mock DB session and setting with an invalid type
    mock_db_session = mocker.MagicMock()
    mock_setting = Setting(
        key="test.invalid_type", value="not_a_number", ui_element="number"
    )
    mock_db_session.query.return_value.filter.return_value.all.return_value = [
        mock_setting
    ]

    # Act: Call get_setting with a default value and an invalid type
    settings_manager = SettingsManager(db_session=mock_db_session)
    result = settings_manager.get_setting("test.invalid_type", default=10)

    # Assert: Check that the default value is returned and not the invalid string
    assert result == 10


# The mocker fixture is automatically available
def test_set_setting_update_existing(mocker):
    mock_db_session = mocker.MagicMock()
    mock_setting = Setting(key="app.version", value="1.0.0", editable=True)
    mock_db_session.query.return_value.filter.return_value.first.return_value = mock_setting
    mocker.patch(
        "src.local_deep_research.web.services.settings_manager.func.now"
    )  # Patching the func.now call

    settings_manager = SettingsManager(db_session=mock_db_session)
    # Mock the _check_thread_safety method to avoid thread safety errors in tests
    mocker.patch.object(
        settings_manager, "_check_thread_safety", return_value=None
    )

    result = settings_manager.set_setting("app.version", "2.0.0")

    assert result is True
    assert mock_setting.value == "2.0.0"
    mock_db_session.commit.assert_called_once()
    mock_db_session.rollback.assert_not_called()


def test_set_setting_create_new(mocker):
    mock_db_session = mocker.MagicMock()
    mock_db_session.query.return_value.filter.return_value.first.return_value = None
    mocker.patch(
        "src.local_deep_research.web.services.settings_manager.func.now"
    )  # Patching the func.now call

    settings_manager = SettingsManager(db_session=mock_db_session)
    result = settings_manager.set_setting("new.setting", "new_value")

    assert result is True
    mock_db_session.add.assert_called_once()
    new_setting = mock_db_session.add.call_args[0][0]
    assert isinstance(new_setting, Setting)
    assert new_setting.key == "new.setting"
    assert new_setting.value == "new_value"
    assert (
        new_setting.type == SettingType.APP
    )  # Assuming 'app' type for this key
    mock_db_session.commit.assert_called_once()
    mock_db_session.rollback.assert_not_called()


def test_set_setting_db_error(mocker):
    mock_db_session = mocker.MagicMock()
    # Set the side_effect to an instance of SQLAlchemyError
    mock_db_session.query.return_value.filter.return_value.first.side_effect = (
        SQLAlchemyError("Simulated DB Error")
    )

    # Mock the logger to check if error is logged
    mock_logger = mocker.patch(
        "src.local_deep_research.web.services.settings_manager.logger"
    )

    settings_manager = SettingsManager(db_session=mock_db_session)
    result = settings_manager.set_setting("app.version", "2.0.0")

    # Assert that the method returned False
    assert result is False

    mock_db_session.rollback.assert_called_once()
    mock_db_session.commit.assert_not_called()
    mock_logger.exception.assert_called_once()
    # mock_logger.error.assert_called_once_with("Error setting value for app.version: Simulated DB Error")


def test_app_get_setting_from_real_db(
    setup_database_for_all_tests, monkeypatch
):
    """
    Tests get_setting via settings_service, using the real file-based database.
    `setup_database_for_all_tests` yields `SessionLocal`, which is a sessionmaker.
    """
    SessionLocal = setup_database_for_all_tests  # Get the SessionLocal class from the fixture
    session = SessionLocal()  # Create a new session instance for this test

    # Add a setting to the database using this session
    # Changed key to be specific to this test
    setting = Setting(
        key="test.app.version.get",  # Use a unique key for this test
        value="1.0.0",
        type=SettingType.APP,
        name="Version",
        description="App version",
        ui_element="text",
        visible=True,
        editable=True,
    )
    session.add(setting)
    session.commit()

    monkeypatch.delenv(
        "LDR_APP_VERSION", raising=False
    )  # Ensure no env override

    # Create settings manager with the session and get the setting
    settings_manager = SettingsManager(db_session=session)
    value = settings_manager.get_setting("test.app.version.get")
    assert value == "1.0.0"

    session.close()  # Close the session for this test


def test_get_all_settings_from_real_file_db(
    setup_database_for_all_tests, monkeypatch
):
    """
    Tests retrieving all settings from a real *file-based* database.
    """
    # Create a new session for this test
    session = (
        setup_database_for_all_tests()
    )  # Call the sessionmaker to get a new session

    # Add some settings to the database
    setting1 = Setting(
        key="all.settings.app.version",
        value="1.0.0",
        type=SettingType.APP,
        name="Version",
        description="App version",
        ui_element="text",
        visible=True,
        editable=True,
    )
    setting2 = Setting(
        key="all.settings.llm.temperature",
        value=0.7,
        type=SettingType.LLM,
        name="Temperature",
        description="LLM temperature",
        ui_element="number",
        visible=True,
        editable=True,
    )
    session.add_all([setting1, setting2])
    session.commit()  # Commit to save them to the database

    # Ensure no conflicting environment variables are set for this test
    monkeypatch.delenv("LDR_APP_VERSION", raising=False)
    monkeypatch.delenv("LDR_LLM_TEMPERATURE", raising=False)

    settings_manager = SettingsManager(
        db_session=session
    )  # Pass the session instance
    all_settings = settings_manager.get_all_settings()

    # Assertions should check for the unique keys
    assert "all.settings.app.version" in all_settings
    assert all_settings["all.settings.app.version"]["value"] == "1.0.0"
    assert all_settings["all.settings.app.version"]["type"] == "APP"
    assert all_settings["all.settings.app.version"]["editable"] is True
    assert all_settings["all.settings.app.version"]["name"] == "Version"

    assert "all.settings.llm.temperature" in all_settings
    assert all_settings["all.settings.llm.temperature"]["value"] == 0.7
    assert all_settings["all.settings.llm.temperature"]["type"] == "LLM"
    assert all_settings["all.settings.llm.temperature"]["editable"] is True
    assert all_settings["all.settings.llm.temperature"]["name"] == "Temperature"

    # IMPORTANT: Close the session created within the test
    session.close()


def test_get_all_settings_db_error(mock_db_session, mock_logger):
    """Tests handling a SQLAlchemyError when retrieving all settings."""
    # Configure the mock session to raise a SQLAlchemyError
    mock_db_session.query(Setting).all.side_effect = SQLAlchemyError(
        "Simulated DB Error"
    )

    settings_manager = SettingsManager(db_session=mock_db_session)
    all_settings = settings_manager.get_all_settings()

    # Assert that an empty dictionary is returned and the error was logged
    assert all_settings == {}
    mock_db_session.query(Setting).all.assert_called_once()
    mock_logger.exception.assert_called_once()
    # You can check the log message content if needed


def test_get_setting_with_substring_keys(setup_database_for_all_tests):
    """
    Tests that we can get the correct value for a setting, even when its key
    is a substring of the key for a different setting.

    Args:
        setup_database_for_all_tests: Fixture that sets up a real database
            for testing. (This is necessary because the bug fix this is
            testing depends on the actual behavior of the SQLAlchemy `filter()`
            function.)

    """
    # Arrange.
    session_local_class = setup_database_for_all_tests
    session = session_local_class()

    with session:
        # Add two settings with overlapping keys but different full keys
        setting1 = Setting(
            key="test.hello",
            value="world",
            ui_element="text",
            type="APP",
            name="Test Setting 1",
            visible=True,
            editable=True,
        )
        setting2 = Setting(
            key="test.hello_world",
            value="universe",
            ui_element="text",
            type="APP",
            name="Test Setting 2",
            visible=True,
            editable=True,
        )
        session.add(setting1)
        session.add(setting2)
        session.commit()

        settings_manager = SettingsManager(db_session=session)

        # Act and assert.
        # Test getting the "test.hello" setting
        result1 = settings_manager.get_setting("test.hello")
        assert result1 == "world"

        # Test getting the "test.hello_world" setting.
        result2 = settings_manager.get_setting("test.hello_world")
        assert result2 == "universe"
