import bouquin.settings_dialog as sd
from bouquin.db import DBConfig, DBManager
from bouquin.key_prompt import KeyPrompt
from bouquin.settings import get_settings
from bouquin.settings_dialog import SettingsDialog
from bouquin.theme import Theme, ThemeConfig, ThemeManager
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QDialog, QMessageBox, QWidget


def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db):
    # Provide a parent that exposes a real ThemeManager (dialog calls parent().themes.set(...))
    app = QApplication.instance()
    parent = QWidget()
    parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
    dlg = SettingsDialog(tmp_db_cfg, fresh_db, parent=parent)
    qtbot.addWidget(dlg)
    dlg.show()

    dlg.idle_spin.setValue(3)
    dlg.theme_light.setChecked(True)
    dlg.move_todos.setChecked(True)
    dlg.tags.setChecked(False)
    dlg.time_log.setChecked(False)
    dlg.reminders.setChecked(False)

    # Auto-accept the modal QMessageBox that _compact_btn_clicked() shows
    def _auto_accept_msgbox():
        for w in QApplication.topLevelWidgets():
            if isinstance(w, QMessageBox):
                w.accept()

    QTimer.singleShot(0, _auto_accept_msgbox)
    dlg._compact_btn_clicked()
    qtbot.wait(50)

    dlg._save()
    cfg = dlg.config
    assert cfg.idle_minutes == 3
    assert cfg.move_todos is True
    assert cfg.tags is False
    assert cfg.time_log is False
    assert cfg.reminders is False
    assert cfg.theme in ("light", "dark", "system")


def test_save_key_toggle_roundtrip(qtbot, tmp_db_cfg, fresh_db, app):
    parent = QWidget()
    parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))

    dlg = SettingsDialog(tmp_db_cfg, fresh_db, parent=parent)
    qtbot.addWidget(dlg)
    dlg.show()

    # Ensure a clean starting state (suite may leave settings toggled on)
    dlg.save_key_btn.setChecked(False)
    dlg.key = ""

    # Robust popup pump so we never miss late dialogs
    def _pump():
        for w in QApplication.topLevelWidgets():
            if isinstance(w, KeyPrompt):
                w.key_entry.setText("supersecret")
                w.accept()
            elif isinstance(w, QMessageBox):
                w.accept()

    timer = QTimer()
    timer.setInterval(10)
    timer.timeout.connect(_pump)
    timer.start()
    try:
        dlg.save_key_btn.setChecked(True)
        qtbot.waitUntil(lambda: dlg.key == "supersecret", timeout=1000)
        assert dlg.save_key_btn.isChecked()

        dlg.save_key_btn.setChecked(False)
        qtbot.waitUntil(lambda: dlg.key == "", timeout=1000)
        assert dlg.key == ""
    finally:
        timer.stop()


def test_change_key_mismatch_shows_error(qtbot, tmp_db_cfg, tmp_path, app):
    cfg = DBConfig(
        path=tmp_path / "iso.db",
        key="oldkey",
        idle_minutes=0,
        theme="light",
        move_todos=True,
    )
    db = DBManager(cfg)
    assert db.connect()
    db.save_new_version("2000-01-01", "seed", "seed")

    parent = QWidget()
    parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
    dlg = SettingsDialog(cfg, db, parent=parent)
    qtbot.addWidget(dlg)
    dlg.show()

    keys = ["one", "two"]

    def _pump_popups():
        for w in QApplication.topLevelWidgets():
            if isinstance(w, KeyPrompt):
                w.key_entry.setText(keys.pop(0) if keys else "zzz")
                w.accept()
            elif isinstance(w, QMessageBox):
                w.accept()

    timer = QTimer()
    timer.setInterval(10)
    timer.timeout.connect(_pump_popups)
    timer.start()
    try:
        dlg._change_key()
    finally:
        timer.stop()
    db.close()
    db2 = DBManager(cfg)
    assert db2.connect()
    db2.close()


def test_change_key_success(qtbot, tmp_path, app):
    cfg = DBConfig(
        path=tmp_path / "iso2.db",
        key="oldkey",
        idle_minutes=0,
        theme="light",
        move_todos=True,
    )
    db = DBManager(cfg)
    assert db.connect()
    db.save_new_version("2001-01-01", "seed", "seed")

    parent = QWidget()
    parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
    dlg = SettingsDialog(cfg, db, parent=parent)
    qtbot.addWidget(dlg)
    dlg.show()

    keys = ["newkey", "newkey"]

    def _pump():
        for w in QApplication.topLevelWidgets():
            if isinstance(w, KeyPrompt):
                w.key_entry.setText(keys.pop(0) if keys else "newkey")
                w.accept()
            elif isinstance(w, QMessageBox):
                w.accept()

    timer = QTimer()
    timer.setInterval(10)
    timer.timeout.connect(_pump)
    timer.start()
    try:
        dlg._change_key()
    finally:
        timer.stop()
    qtbot.wait(50)

    db.close()
    cfg.key = "newkey"
    db2 = DBManager(cfg)
    assert db2.connect()
    assert "seed" in db2.get_entry("2001-01-01")
    db2.close()


def test_settings_compact_error(qtbot, app, monkeypatch, tmp_db_cfg, fresh_db):
    # Parent with ThemeManager (dialog uses parent().themes.set(...))
    parent = QWidget()
    parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
    dlg = SettingsDialog(tmp_db_cfg, fresh_db, parent=parent)
    qtbot.addWidget(dlg)
    dlg.show()

    # Monkeypatch db.compact to raise
    def boom():
        raise RuntimeError("nope")

    dlg._db.compact = boom  # type: ignore

    called = {"critical": False, "title": None, "text": None}

    class DummyMB:
        @staticmethod
        def information(*args, **kwargs):
            return 0

        @staticmethod
        def critical(parent, title, text, *rest):
            called["critical"] = True
            called["title"] = title
            called["text"] = str(text)
            return 0

    # Swap QMessageBox used inside the dialog module so signature mismatch can't occur
    monkeypatch.setattr(sd, "QMessageBox", DummyMB, raising=True)

    # Invoke
    dlg._compact_btn_clicked()

    assert called["critical"]
    assert called["title"]
    assert called["text"]


class _Host(QWidget):
    def __init__(self, themes):
        super().__init__()
        self.themes = themes


def _make_host_and_dialog(tmp_db_cfg, fresh_db):
    # Create a real ThemeManager so we don't have to fake anything here
    from PySide6.QtWidgets import QApplication

    themes = ThemeManager(QApplication.instance(), ThemeConfig(theme=Theme.SYSTEM))
    host = _Host(themes)
    dlg = SettingsDialog(tmp_db_cfg, fresh_db, parent=host)
    return host, dlg


def _clear_qsettings_theme_to_system():
    """Make the radio-button default deterministic across the full suite."""
    s = get_settings()
    s.clear()
    s.setValue("ui/theme", "system")


def test_default_theme_radio_is_system_checked(qtbot, tmp_db_cfg, fresh_db):
    # Ensure no stray theme value from previous tests
    _clear_qsettings_theme_to_system()

    host, dlg = _make_host_and_dialog(tmp_db_cfg, fresh_db)
    qtbot.addWidget(host)
    qtbot.addWidget(dlg)
    # With fresh settings (system), the 'system' radio should be selected
    assert dlg.theme_system.isChecked()


def test_save_selects_system_when_no_explicit_choice(
    qtbot, tmp_db_cfg, fresh_db, monkeypatch
):
    host, dlg = _make_host_and_dialog(tmp_db_cfg, fresh_db)
    qtbot.addWidget(dlg)
    # Ensure neither dark nor light is checked so SYSTEM path is taken
    dlg.theme_dark.setChecked(False)
    dlg.theme_light.setChecked(False)
    # This should not raise
    dlg._save()


def test_save_with_dark_selected(qtbot, tmp_db_cfg, fresh_db):
    host, dlg = _make_host_and_dialog(tmp_db_cfg, fresh_db)
    qtbot.addWidget(dlg)
    dlg.theme_dark.setChecked(True)
    dlg._save()


def test_change_key_cancel_first_prompt(qtbot, tmp_db_cfg, fresh_db, monkeypatch):
    host, dlg = _make_host_and_dialog(tmp_db_cfg, fresh_db)
    qtbot.addWidget(dlg)

    class P1:
        def __init__(self, *a, **k):
            pass

        def exec(self):
            return QDialog.Rejected

        def key(self):
            return ""

    monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", P1)
    dlg._change_key()  # returns early


def test_change_key_cancel_second_prompt(qtbot, tmp_db_cfg, fresh_db, monkeypatch):
    host, dlg = _make_host_and_dialog(tmp_db_cfg, fresh_db)
    qtbot.addWidget(dlg)

    class P1:
        def __init__(self, *a, **k):
            pass

        def exec(self):
            return QDialog.Accepted

        def key(self):
            return "abc"

    class P2:
        def __init__(self, *a, **k):
            pass

        def exec(self):
            return QDialog.Rejected

        def key(self):
            return "abc"

    # First call yields P1, second yields P2
    seq = [P1, P2]

    def _factory(*a, **k):
        cls = seq.pop(0)
        return cls(*a, **k)

    monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", _factory)
    dlg._change_key()  # returns early


def test_change_key_empty_and_exception_paths(qtbot, tmp_db_cfg, fresh_db, monkeypatch):
    host, dlg = _make_host_and_dialog(tmp_db_cfg, fresh_db)
    qtbot.addWidget(dlg)

    # Timer that auto-accepts any modal QMessageBox so we don't hang.
    def _pump_boxes():
        # Try both the active modal and the general top-level enumeration
        m = QApplication.activeModalWidget()
        if isinstance(m, QMessageBox):
            m.accept()
        for w in QApplication.topLevelWidgets():
            if isinstance(w, QMessageBox):
                w.accept()

    timer = QTimer()
    timer.setInterval(10)
    timer.timeout.connect(_pump_boxes)
    timer.start()

    try:

        class P1:
            def __init__(self, *a, **k):
                pass

            def exec(self):
                return QDialog.Accepted

            def key(self):
                return ""

        class P2:
            def __init__(self, *a, **k):
                pass

            def exec(self):
                return QDialog.Accepted

            def key(self):
                return ""

        seq = [P1, P2, P1, P2]

        def _factory(*a, **k):
            cls = seq.pop(0)
            return cls(*a, **k)

        monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", _factory)
        # First run triggers empty-key warning path and return (auto-closed)
        dlg._change_key()

        # Now make rekey() raise to hit the except block (critical dialog)
        def boom(*a, **k):
            raise RuntimeError("nope")

        dlg._db.rekey = boom

        # Return a non-empty matching key twice
        class P3:
            def __init__(self, *a, **k):
                pass

            def exec(self):
                return QDialog.Accepted

            def key(self):
                return "secret"

        monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: P3())
        dlg._change_key()
    finally:
        timer.stop()


def test_save_key_btn_clicked_cancel_flow(qtbot, tmp_db_cfg, fresh_db, monkeypatch):
    host, dlg = _make_host_and_dialog(tmp_db_cfg, fresh_db)
    qtbot.addWidget(dlg)

    # Make sure we start with no key saved so it will prompt
    dlg.key = ""

    class P1:
        def __init__(self, *a, **k):
            pass

        def exec(self):
            return QDialog.Rejected

        def key(self):
            return ""

    monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", P1)

    dlg.save_key_btn.setChecked(True)  # toggles and calls handler
    # Handler should have undone the checkbox back to False
    assert not dlg.save_key_btn.isChecked()
