"""Regression tests for the Codex gpt-5.x autoraise notice gate.

Covers two layers:

1. The config display gate (``compression.codex_gpt55_autoraise_notice``) —
   suppresses the banner without disabling the threshold autoraise.
2. The per-profile dedupe marker (#54432) — the notice must show at most once
   per profile/config state. Before the fix it re-fired on every agent init,
   and because the gateway rebuilds the agent per inbound message it spammed
   Discord etc. The gate persists a marker under ``$HERMES_HOME``
   (profile-scoped, isolated to a tempdir by the conftest autouse fixture)
   keyed on the model slug + displayed from→to percentages, so an unchanged
   threshold stays silent across restarts while a changed threshold (or a
   different autoraised Codex model) re-notifies once.
"""

from __future__ import annotations

import contextlib
import io
from pathlib import Path

import pytest

from hermes_constants import get_hermes_home
from hermes_state import SessionDB
from run_agent import AIAgent

from agent.agent_init import (
    _codex_gpt55_autoraise_notice_marker,
    _codex_gpt55_autoraise_notice_seen,
    _codex_gpt55_autoraise_notice_state,
    _record_codex_gpt55_autoraise_notice,
)

# The dict agent_init stashes when the Codex gpt-5.5 override fires.
AUTORAISE = {"model": "gpt-5.5", "from": 0.50, "to": 0.85}


def _config(*, show_notice: bool) -> dict:
    return {
        "compression": {
            "enabled": True,
            "threshold": 0.50,
            "target_ratio": 0.20,
            "protect_first_n": 3,
            "protect_last_n": 20,
            "codex_gpt55_autoraise": True,
            "codex_gpt55_autoraise_notice": show_notice,
        },
        "prompt_caching": {"cache_ttl": "5m"},
        "sessions": {},
        "bedrock": {},
    }


def _make_codex_agent(monkeypatch, tmp_path: Path, *, show_notice: bool):
    """Construct a real Codex gpt-5.5 agent under an isolated config."""
    from hermes_cli import config as config_mod

    monkeypatch.setattr(config_mod, "load_config", lambda: _config(show_notice=show_notice))
    db = SessionDB(db_path=tmp_path / "state.db")
    stdout = io.StringIO()

    with contextlib.redirect_stdout(stdout):
        agent = AIAgent(
            base_url="https://chatgpt.com/backend-api/codex",
            api_key="test-key",
            provider="openai-codex",
            model="gpt-5.5",
            enabled_toolsets=[],
            disabled_toolsets=[],
            quiet_mode=False,
            skip_memory=True,
            session_db=db,
            session_id="codex-notice-test",
        )

    return agent, stdout.getvalue()


def _threshold_ratio(agent: AIAgent) -> float:
    compressor = getattr(agent, "context_compressor")
    return round(compressor.threshold_tokens / compressor.context_length, 2)


# ── config display gate ──────────────────────────────────────────────────────


def test_codex_gpt55_autoraise_notice_enabled_by_default(monkeypatch, tmp_path):
    agent, stdout = _make_codex_agent(monkeypatch, tmp_path, show_notice=True)

    assert _threshold_ratio(agent) == 0.85
    warning = getattr(agent, "_compression_warning")
    assert warning is not None
    assert "auto-compaction was raised" in warning
    assert "auto-compaction was raised" in stdout


def test_codex_gpt55_autoraise_notice_can_be_suppressed_without_disabling_autoraise(
    monkeypatch, tmp_path
):
    agent, stdout = _make_codex_agent(monkeypatch, tmp_path, show_notice=False)

    assert _threshold_ratio(agent) == 0.85
    assert getattr(agent, "_compression_warning") is None
    assert "auto-compaction was raised" not in stdout


def test_codex_gpt55_autoraise_notice_deduped_across_agent_inits(monkeypatch, tmp_path):
    # Gateway spam scenario (#54432): the gateway rebuilds the agent per
    # inbound message. The first init shows the notice; the second stays
    # silent because the per-profile marker was recorded.
    agent1, stdout1 = _make_codex_agent(monkeypatch, tmp_path, show_notice=True)
    assert "auto-compaction was raised" in stdout1
    assert getattr(agent1, "_compression_warning") is not None

    agent2, stdout2 = _make_codex_agent(monkeypatch, tmp_path, show_notice=True)
    assert _threshold_ratio(agent2) == 0.85  # autoraise still applies
    assert "auto-compaction was raised" not in stdout2
    assert getattr(agent2, "_compression_warning") is None


# ── per-profile dedupe marker (#54432) ───────────────────────────────────────


def test_marker_lives_under_hermes_home() -> None:
    marker = _codex_gpt55_autoraise_notice_marker()
    assert marker.parent == get_hermes_home()
    assert marker.name == ".codex_gpt55_autoraise_notice"


def test_state_keyed_on_model_and_displayed_percentages() -> None:
    # Same percentages the notice text renders (int(round(ratio * 100))),
    # prefixed with the bare model slug.
    assert _codex_gpt55_autoraise_notice_state(AUTORAISE) == "gpt-5.5:50:85"
    assert (
        _codex_gpt55_autoraise_notice_state(
            {"model": "openai/gpt-5.4", "from": 0.75, "to": 0.85}
        )
        == "gpt-5.4:75:85"
    )


def test_unseen_before_anything_is_recorded() -> None:
    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False


def test_seen_after_record() -> None:
    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False
    _record_codex_gpt55_autoraise_notice(AUTORAISE)
    # A "restart" is just another call: the marker persists on disk.
    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is True


def test_changed_threshold_renotifies_once() -> None:
    _record_codex_gpt55_autoraise_notice(AUTORAISE)
    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is True
    # User raises their global threshold -> "from" changes -> notice re-fires.
    changed = {"model": "gpt-5.5", "from": 0.60, "to": 0.85}
    assert _codex_gpt55_autoraise_notice_seen(changed) is False
    _record_codex_gpt55_autoraise_notice(changed)
    assert _codex_gpt55_autoraise_notice_seen(changed) is True
    # And the old state is now considered unseen (marker moved forward).
    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False


def test_changed_model_renotifies_once() -> None:
    # Switching to a different autoraised Codex model re-fires the notice
    # (the banner names the model, so it displays new information).
    _record_codex_gpt55_autoraise_notice(AUTORAISE)
    other_model = {"model": "gpt-5.4", "from": 0.50, "to": 0.85}
    assert _codex_gpt55_autoraise_notice_seen(other_model) is False
    _record_codex_gpt55_autoraise_notice(other_model)
    assert _codex_gpt55_autoraise_notice_seen(other_model) is True


def test_record_is_idempotent() -> None:
    _record_codex_gpt55_autoraise_notice(AUTORAISE)
    _record_codex_gpt55_autoraise_notice(AUTORAISE)
    assert (
        _codex_gpt55_autoraise_notice_marker().read_text(encoding="utf-8")
        == "gpt-5.5:50:85"
    )


def test_malformed_marker_reads_as_unseen() -> None:
    marker = _codex_gpt55_autoraise_notice_marker()
    marker.parent.mkdir(parents=True, exist_ok=True)
    marker.write_text("not-a-state", encoding="utf-8")
    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False


@pytest.mark.parametrize("bad", [{}, {"from": 0.5}, {"from": None, "to": None}])
def test_seen_tolerates_malformed_autoraise(bad) -> None:
    # Never raises even if the stashed dict is missing/garbage keys.
    assert _codex_gpt55_autoraise_notice_seen(bad) is False


def test_full_init_gate_shows_once_then_stays_silent() -> None:
    # Mirror the decision agent_init makes on each build:
    #   show = bool(autoraise) and compression_enabled and not seen(autoraise)
    def decide(compression_enabled: bool) -> bool:
        show = (
            bool(AUTORAISE)
            and compression_enabled
            and not _codex_gpt55_autoraise_notice_seen(AUTORAISE)
        )
        if show:
            _record_codex_gpt55_autoraise_notice(AUTORAISE)
        return show

    # First init (any surface) shows; every subsequent init in this profile
    # stays silent — the gateway spam scenario from the issue.
    assert decide(compression_enabled=True) is True
    assert decide(compression_enabled=True) is False
    assert decide(compression_enabled=True) is False
