"""Behavioral coverage for #68545's centralized journal-mode setting."""

from __future__ import annotations

import sqlite3

import pytest

import hermes_state_wal
import hermes_yaml as yaml


def _write_config(monkeypatch: pytest.MonkeyPatch, tmp_path, config: object) -> None:
    home = tmp_path / "hermes-home"
    home.mkdir(exist_ok=True)
    monkeypatch.setenv("HERMES_HOME", str(home))
    (home / "config.yaml").write_text(
        yaml.safe_dump(config),
        encoding="utf-8",
    )


def _configure_mode(monkeypatch: pytest.MonkeyPatch, tmp_path, mode: object) -> None:
    _write_config(monkeypatch, tmp_path, {"database": {"journal_mode": mode}})


def _disable_vulnerable_gate(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        "hermes_state_wal.is_sqlite_wal_reset_vulnerable",
        lambda **kwargs: False,
    )


@pytest.fixture(autouse=True)
def _reset_configured_delete_override_warned_paths():
    """Reset the configured-delete-override warned-paths set so the
    once-per-process-per-db_label dedup doesn't leak between tests."""

    hermes_state_wal._delete_overridden_warned_paths.clear()
    yield
    hermes_state_wal._delete_overridden_warned_paths.clear()


def test_wal_probe_unknown_never_emits_set_pragma(monkeypatch, tmp_path, caplog):
    """Probe failure (None) on the configured-WAL path must not reach any journal-mode
    pragma: the file may be held by a sibling whose -wal/-shm sidecars WAL-init would
    unlink. The DELETE branch already refused; this binds the WAL branch to the same rule,
    and ``require_wal=True`` must raise instead of reporting an unverified "wal"."""
    import logging

    from hermes_state_wal import WalUnsupportedError, apply_wal_with_fallback

    _configure_mode(monkeypatch, tmp_path, "wal")
    _disable_vulnerable_gate(monkeypatch)
    hermes_state_wal._wal_probe_unknown_paths.clear()

    class _SpyConnection(sqlite3.Connection):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.pragmas: list[str] = []

        def execute(self, sql, *args, **kwargs):  # type: ignore[override]
            if str(sql).lstrip().lower().startswith("pragma"):
                self.pragmas.append(str(sql))
            return super().execute(sql, *args, **kwargs)

    db_path = tmp_path / "probe-unknown.db"
    sibling = sqlite3.connect(str(db_path))
    try:
        assert sibling.execute("PRAGMA journal_mode=WAL").fetchone()[0].lower() == "wal"
        monkeypatch.setattr("hermes_state_wal._on_disk_journal_mode", lambda _conn: None)
        conn = sqlite3.connect(str(db_path), factory=_SpyConnection)
        try:
            with caplog.at_level(logging.WARNING, logger="hermes_state_wal"):
                assert apply_wal_with_fallback(conn, db_label="probe-unknown.db") == "wal"
            assert conn.pragmas == []  # nothing touched while ownership is unproven
            assert sibling.execute("PRAGMA journal_mode").fetchone()[0].lower() == "wal"
            assert any("could not verify the on-disk journal mode" in r.getMessage() for r in caplog.records)
            with pytest.raises(WalUnsupportedError, match="could not verify the on-disk journal mode"):
                apply_wal_with_fallback(conn, db_label="probe-unknown.db", require_wal=True)
            assert conn.pragmas == []
        finally:
            conn.close()
    finally:
        sibling.close()




def test_resolve_journal_mode_uses_real_database_config(monkeypatch, tmp_path):
    from hermes_state_wal import resolve_journal_mode

    _configure_mode(monkeypatch, tmp_path, "DELETE")
    assert resolve_journal_mode() == "delete"


def test_new_nonsecret_hermes_env_override_is_not_exposed(monkeypatch, tmp_path):
    from hermes_state_wal import resolve_journal_mode

    _configure_mode(monkeypatch, tmp_path, "wal")
    monkeypatch.setenv("HERMES_JOURNAL_MODE", "delete")
    assert resolve_journal_mode() == "wal"


@pytest.mark.parametrize("value", ["bogus", "truncate", None, 42, {"bad": "shape"}])
def test_invalid_config_value_falls_back_to_wal(monkeypatch, tmp_path, value):
    from hermes_state_wal import resolve_journal_mode

    _configure_mode(monkeypatch, tmp_path, value)
    assert resolve_journal_mode() == "wal"


@pytest.mark.parametrize("database", [[], "delete", 42, None])
def test_malformed_database_section_falls_back_to_wal(
    monkeypatch, tmp_path, database
):
    from hermes_state_wal import resolve_journal_mode

    _write_config(monkeypatch, tmp_path, {"database": database})
    assert resolve_journal_mode() == "wal"


def test_apply_wal_with_fallback_honors_delete_config(monkeypatch, tmp_path):
    from hermes_state_wal import apply_wal_with_fallback

    _configure_mode(monkeypatch, tmp_path, "delete")
    _disable_vulnerable_gate(monkeypatch)
    conn = sqlite3.connect(tmp_path / "configured.db")
    try:
        assert apply_wal_with_fallback(conn, db_label="configured.db") == "delete"
        assert conn.execute("PRAGMA journal_mode").fetchone()[0].lower() == "delete"
    finally:
        conn.close()


def test_apply_wal_with_fallback_defaults_to_wal(monkeypatch, tmp_path):
    from hermes_state_wal import apply_wal_with_fallback

    _configure_mode(monkeypatch, tmp_path, "wal")
    _disable_vulnerable_gate(monkeypatch)
    conn = sqlite3.connect(tmp_path / "default.db")
    try:
        assert apply_wal_with_fallback(conn, db_label="default.db") == "wal"
        assert conn.execute("PRAGMA journal_mode").fetchone()[0].lower() == "wal"
    finally:
        conn.close()


def test_configured_delete_validates_vulnerable_sqlite_result(monkeypatch, tmp_path):
    """The safety gate must not report DELETE when SQLite returns MEMORY."""
    from hermes_state_wal import apply_wal_with_fallback

    _configure_mode(monkeypatch, tmp_path, "delete")
    monkeypatch.setattr(
        "hermes_state_wal.is_sqlite_wal_reset_vulnerable",
        lambda **kwargs: True,
    )
    conn = sqlite3.connect(":memory:")
    try:
        with pytest.raises(sqlite3.OperationalError, match="configured.*delete"):
            apply_wal_with_fallback(conn, db_label="memory-configured.db")
        assert conn.execute("PRAGMA journal_mode").fetchone()[0].lower() == "memory"
    finally:
        conn.close()


def test_configured_delete_never_live_downgrades_existing_wal(monkeypatch, tmp_path, caplog):
    """Keeping WAL is correct, but the operator must be told their configured
    delete had no effect (otherwise the DB silently stays WAL and the protection
    they configured never applies)."""
    from hermes_state_wal import apply_wal_with_fallback

    _configure_mode(monkeypatch, tmp_path, "delete")
    db_path = tmp_path / "existing-wal.db"
    conn = sqlite3.connect(db_path)
    try:
        assert conn.execute("PRAGMA journal_mode=WAL").fetchone()[0].lower() == "wal"
        monkeypatch.setattr(
            "hermes_state_wal.is_sqlite_wal_reset_vulnerable",
            lambda **kwargs: True,
        )
        with caplog.at_level("ERROR", logger="hermes_state"):
            assert apply_wal_with_fallback(conn, db_label="existing-wal.db") == "wal"
        assert conn.execute("PRAGMA journal_mode").fetchone()[0].lower() == "wal"
        assert any(
            "database.journal_mode=delete is configured" in r.message
            and "on-disk" in r.message
            for r in caplog.records
        ), "expected a warning that configured delete was overridden by on-disk WAL"
    finally:
        conn.close()


def test_configured_delete_overridden_warns_on_non_vulnerable_runtime_too(monkeypatch, tmp_path, caplog):
    """When the SQLite runtime is NOT WAL-reset-vulnerable (e.g. after a
    3.51.3+ upgrade), the on-disk WAL + configured-delete case reaches the
    read-only probe path instead of the vulnerability path. That path used to
    return WAL with no signal at all; it must emit the same override warning."""
    from hermes_state_wal import apply_wal_with_fallback

    _configure_mode(monkeypatch, tmp_path, "delete")
    _disable_vulnerable_gate(monkeypatch)
    db_path = tmp_path / "existing-wal.db"
    conn = sqlite3.connect(db_path)
    try:
        assert conn.execute("PRAGMA journal_mode=WAL").fetchone()[0].lower() == "wal"
        with caplog.at_level("ERROR", logger="hermes_state"):
            assert apply_wal_with_fallback(conn, db_label="existing-wal.db") == "wal"
            assert apply_wal_with_fallback(conn, db_label="existing-wal.db") == "wal"
        assert conn.execute("PRAGMA journal_mode").fetchone()[0].lower() == "wal"
        warnings = [
            r for r in caplog.records
            if "database.journal_mode=delete is configured" in r.message
        ]
        assert len(warnings) == 1, (
            "probe path must warn when configured delete is overridden by on-disk WAL, "
            "and dedup to one per process per db_label"
        )
    finally:
        conn.close()


def test_configured_delete_overridden_warning_fires_once_per_db(monkeypatch, tmp_path, caplog):
    """The override warning is deduped per process per db_label (same discipline
    as the WAL-fallback warning), so repeated connections don't flood the log."""
    from hermes_state_wal import apply_wal_with_fallback

    _configure_mode(monkeypatch, tmp_path, "delete")
    monkeypatch.setattr(
        "hermes_state_wal.is_sqlite_wal_reset_vulnerable",
        lambda **kwargs: True,
    )
    db_path = tmp_path / "existing-wal.db"
    conn = sqlite3.connect(db_path)
    try:
        conn.execute("PRAGMA journal_mode=WAL").fetchone()
        with caplog.at_level("ERROR", logger="hermes_state"):
            assert apply_wal_with_fallback(conn, db_label="once.db") == "wal"
            assert apply_wal_with_fallback(conn, db_label="once.db") == "wal"
            assert apply_wal_with_fallback(conn, db_label="once.db") == "wal"
        warnings = [
            r for r in caplog.records
            if "database.journal_mode=delete is configured" in r.message
        ]
        assert len(warnings) == 1, "override warning must fire once per process per db_label"
    finally:
        conn.close()


def test_configured_delete_with_require_wal_and_existing_wal_returns_wal(monkeypatch, tmp_path, caplog):
    """Pin the require_wal=True + configured=delete + on-disk WAL edge case: the
    existing-WAL probe branch returns "wal" unconditionally (require_wal only
    governs the WAL-refusal fallback paths), so the override warning fires and no
    WalUnsupportedError is raised."""
    from hermes_state_wal import apply_wal_with_fallback

    _configure_mode(monkeypatch, tmp_path, "delete")
    _disable_vulnerable_gate(monkeypatch)
    db_path = tmp_path / "existing-wal.db"
    conn = sqlite3.connect(db_path)
    try:
        assert conn.execute("PRAGMA journal_mode=WAL").fetchone()[0].lower() == "wal"
        with caplog.at_level("ERROR", logger="hermes_state"):
            result = apply_wal_with_fallback(
                conn, db_label="existing-wal.db", require_wal=True
            )
        assert result == "wal"
        assert any(
            "database.journal_mode=delete is configured" in r.message
            for r in caplog.records
        )
    finally:
        conn.close()


def test_real_db_openers_honor_configured_delete(monkeypatch, tmp_path):
    """All helper-routed file-backed openers must behaviorally use DELETE."""
    _configure_mode(monkeypatch, tmp_path, "delete")
    _disable_vulnerable_gate(monkeypatch)

    from agent import verification_evidence
    from cron import executions
    from gateway import delivery_ledger
    from gateway.platforms.api_server import ResponseStore
    from hermes_cli import projects_db
    from hermes_cli import kanban_db_connect as kbc
    from hermes_state import SessionDB
    from plugins.memory.holographic.store import MemoryStore
    from plugins.platforms.discord.recovery import DiscordRecoveryStore
    from tools import async_delegation

    observed: dict[str, str] = {}

    for name, connect in (
        ("async_delegation", async_delegation._connect),
        ("delivery_ledger", delivery_ledger._connect),
        ("verification_evidence", verification_evidence._connect),
    ):
        conn = connect()
        try:
            observed[name] = conn.execute("PRAGMA journal_mode").fetchone()[0].lower()
        finally:
            conn.close()

    monkeypatch.setattr(executions, "EXECUTIONS_FILE", tmp_path / "cron" / "executions.db")
    executions.EXECUTIONS_FILE.parent.mkdir(parents=True, exist_ok=True)
    cron_conn = executions._connect()
    try:
        executions._initialize_schema(cron_conn)
        observed["cron_executions"] = cron_conn.execute(
            "PRAGMA journal_mode"
        ).fetchone()[0].lower()
    finally:
        cron_conn.close()

    discord = DiscordRecoveryStore(hermes_home=tmp_path)
    observed["discord_recovery"] = discord.call(
        lambda conn: conn.execute("PRAGMA journal_mode").fetchone()[0].lower()
    )

    session_db = SessionDB(db_path=tmp_path / "state.db")
    try:
        observed["session_db"] = session_db._conn.execute(
            "PRAGMA journal_mode"
        ).fetchone()[0].lower()
    finally:
        session_db.close()

    kanban_conn = kbc.connect(db_path=tmp_path / "kanban.db")
    try:
        observed["kanban"] = kanban_conn.execute(
            "PRAGMA journal_mode"
        ).fetchone()[0].lower()
    finally:
        kanban_conn.close()

    projects_conn = projects_db.connect(db_path=tmp_path / "projects.db")
    try:
        observed["projects"] = projects_conn.execute(
            "PRAGMA journal_mode"
        ).fetchone()[0].lower()
    finally:
        projects_conn.close()

    holographic = MemoryStore(db_path=tmp_path / "memory_store.db")
    try:
        observed["holographic"] = holographic._conn.execute(
            "PRAGMA journal_mode"
        ).fetchone()[0].lower()
    finally:
        holographic.close()

    response_store = ResponseStore(db_path=str(tmp_path / "response_store.db"))
    try:
        observed["response_store"] = response_store._conn.execute(
            "PRAGMA journal_mode"
        ).fetchone()[0].lower()
    finally:
        response_store.close()

    assert observed == {
        "async_delegation": "delete",
        "delivery_ledger": "delete",
        "verification_evidence": "delete",
        "cron_executions": "delete",
        "discord_recovery": "delete",
        "session_db": "delete",
        "kanban": "delete",
        "projects": "delete",
        "holographic": "delete",
        "response_store": "delete",
    }
