"""Regression coverage for latency-bounded recent-session browsing."""

import sqlite3
import time

import pytest

from hermes_state import SessionDB


@pytest.fixture
def db(tmp_path):
    return SessionDB(tmp_path / "state.db")


def _set_activity(db, session_id, when):
    db._conn.execute(
        "UPDATE sessions SET last_activity_at = ? WHERE id = ?",
        (when, session_id),
    )
    db._conn.commit()




def test_writable_startup_reconciles_legacy_activity_column_before_index(tmp_path):
    """A pre-last_activity_at store must heal through the real startup path."""
    path = tmp_path / "legacy-state.db"
    original = SessionDB(path)
    original.close()

    conn = sqlite3.connect(path)
    try:
        conn.execute("DROP INDEX IF EXISTS idx_sessions_effective_activity")
        conn.execute("ALTER TABLE sessions DROP COLUMN last_activity_at")
        conn.commit()
    finally:
        conn.close()

    healed = SessionDB(path)
    try:
        columns = {
            row[1] for row in healed._conn.execute("PRAGMA table_info(sessions)")
        }
        indexes = {
            row[0]
            for row in healed._conn.execute(
                "SELECT name FROM sqlite_master WHERE type = 'index'"
            )
        }
        assert "last_activity_at" in columns
        assert "idx_sessions_effective_activity" in indexes
        assert healed.list_recent_sessions_bounded(limit=1) == []
    finally:
        healed.close()


def test_bounded_recent_orders_by_durable_activity_and_shapes_preview(db):
    now = time.time()
    db.create_session("older", source="cli")
    db.append_message("older", role="user", content="older preview")
    db.create_session("newer", source="cli")
    db.append_message("newer", role="user", content="newer preview")
    _set_activity(db, "older", now - 20)
    _set_activity(db, "newer", now - 10)

    rows = db.list_recent_sessions_bounded(limit=2)

    assert [row["id"] for row in rows] == ["newer", "older"]
    assert rows[0]["preview"] == "newer preview"


def test_bounded_recent_maps_recent_compression_tip_to_logical_root(db):
    now = time.time()
    db.create_session("root", source="cli")
    db.append_message("root", role="user", content="root preview")
    db.end_session("root", "compression")
    db.create_session("tip", source="cli", parent_session_id="root")
    db.append_message("tip", role="user", content="tip preview")
    _set_activity(db, "root", now - 1000)
    _set_activity(db, "tip", now)

    rows = db.list_recent_sessions_bounded(limit=1)

    assert rows[0]["id"] == "tip"
    assert rows[0]["_lineage_root_id"] == "root"
    assert rows[0]["preview"] == "tip preview"


def test_bounded_recent_keeps_reset_child_user_visible(db):
    now = time.time()
    db.create_session("before-reset", source="cli", session_key="cli:one")
    db.end_session("before-reset", "session_reset")
    db.create_session(
        "after-reset",
        source="cli",
        parent_session_id="before-reset",
        session_key="cli:one",
    )
    db.append_message("after-reset", role="user", content="fresh conversation")
    _set_activity(db, "after-reset", now)

    rows = db.list_recent_sessions_bounded(limit=5)

    assert "after-reset" in [row["id"] for row in rows]


def test_bounded_recent_keeps_branch_separate_from_compression_parent(db):
    now = time.time()
    db.create_session("branch-parent", source="cli")
    db.end_session("branch-parent", "compression")
    db.create_session(
        "branch-child",
        source="cli",
        parent_session_id="branch-parent",
        model_config={"_branched_from": "branch-parent"},
    )
    db.append_message("branch-child", role="user", content="branch preview")
    _set_activity(db, "branch-child", now)

    rows = db.list_recent_sessions_bounded(limit=5)

    branch = next(row for row in rows if row["id"] == "branch-child")
    assert branch.get("_lineage_root_id") is None


def test_bounded_recent_excludes_delegated_children_and_sources(db):
    now = time.time()
    db.create_session("visible", source="cli")
    db.append_message("visible", role="user", content="visible")
    _set_activity(db, "visible", now - 1)
    db.create_session(
        "delegated",
        source="cli",
        model_config={"_delegate_from": "parent"},
    )
    db.append_message("delegated", role="user", content="hidden delegate")
    _set_activity(db, "delegated", now)
    db.create_session("hidden-source", source="cron")
    db.append_message("hidden-source", role="user", content="hidden source")
    _set_activity(db, "hidden-source", now + 1)

    rows = db.list_recent_sessions_bounded(
        limit=5,
        exclude_sources=["cron"],
    )

    assert [row["id"] for row in rows] == ["visible"]


def test_bounded_recent_omits_deep_lineage_when_traversal_cap_is_reached(db):
    now = time.time()
    parent = None
    for i in range(40):
        sid = f"deep-{i}"
        db.create_session(sid, source="cli", parent_session_id=parent)
        if parent is not None:
            db.end_session(parent, "compression")
        _set_activity(db, sid, now + i)
        parent = sid
    db.create_session("visible-deep-peer", source="cli")
    _set_activity(db, "visible-deep-peer", now + 100)

    rows = db.list_recent_sessions_bounded(
        limit=5,
        candidate_limit=8,
        lineage_limit=8,
    )

    assert [row["id"] for row in rows] == ["visible-deep-peer"]


def test_bounded_recent_omits_branching_lineage_at_total_row_cap(db):
    now = time.time()
    db.create_session("fanout-root", source="cli")
    db.end_session("fanout-root", "compression")
    for i in range(40):
        sid = f"fanout-{i}"
        db.create_session(sid, source="cli", parent_session_id="fanout-root")
        _set_activity(db, sid, now + i)
    db.create_session("visible-fanout-peer", source="cli")
    _set_activity(db, "visible-fanout-peer", now + 100)

    rows = db.list_recent_sessions_bounded(
        limit=5,
        candidate_limit=8,
        lineage_limit=8,
    )

    assert [row["id"] for row in rows] == ["visible-fanout-peer"]


def test_bounded_recent_cycle_is_deduplicated_and_omitted(db):
    now = time.time()
    db.create_session("cycle-a", source="cli")
    db.create_session("cycle-b", source="cli", parent_session_id="cycle-a")
    db.end_session("cycle-a", "compression")
    db.end_session("cycle-b", "compression")
    db._conn.execute(
        "UPDATE sessions SET parent_session_id = ? WHERE id = ?",
        ("cycle-b", "cycle-a"),
    )
    _set_activity(db, "cycle-a", now)
    _set_activity(db, "cycle-b", now + 1)
    db.create_session("visible-cycle-peer", source="cli")
    _set_activity(db, "visible-cycle-peer", now + 2)

    rows = db.list_recent_sessions_bounded(
        limit=5,
        candidate_limit=8,
        lineage_limit=8,
    )

    assert [row["id"] for row in rows] == ["visible-cycle-peer"]


def test_bounded_recent_deadline_interrupts_sqlite(db):
    for i in range(40):
        sid = f"session-{i}"
        db.create_session(sid, source="cli")
        db.append_message(sid, role="user", content=f"message {i}")

    with pytest.raises(TimeoutError, match="recent-session browse exceeded"):
        db.list_recent_sessions_bounded(
            limit=20,
            candidate_limit=300,
            timeout_seconds=0.0,
        )

    # The progress handler is removed in finally: the same connection remains
    # usable after cancellation instead of poisoning subsequent gateway reads.
    assert db.get_session("session-0")["id"] == "session-0"