"""
Regression tests for issue #1771: switching sessions with missing/stale model
metadata must not leave the composer model picker on the previously viewed
chat's model.

These tests execute the real static/ui.js syncTopbar() path in Node with a tiny
DOM/select shim so the behavioral contract is protected without needing a full
browser harness.
"""
import json
import shutil
import subprocess
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parents[1]
UI_JS_PATH = REPO_ROOT / "static" / "ui.js"
NODE = shutil.which("node")

pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")


_DRIVER_SRC = r"""
const fs = require('fs');
const ui = fs.readFileSync(process.argv[2], 'utf8');

function extractFunc(name, opts = {}) {
  const re = new RegExp('function\\s+' + name + '\\s*\\(');
  const start = ui.search(re);
  if (start < 0) {
    if (opts.optional) return '';
    throw new Error(name + ' not found');
  }
  let i = ui.indexOf('{', start);
  let depth = 1;
  i++;
  while (depth > 0 && i < ui.length) {
    if (ui[i] === '{') depth++;
    else if (ui[i] === '}') depth--;
    i++;
  }
  return ui.slice(start, i);
}

const calls = {syncModelChip: 0, renderModelDropdown: 0, positionModelDropdown: 0, fetches: []};
let modelSelect;
let dropdownOpen = false;
const dropdown = {classList: {contains: (name) => name === 'open' && dropdownOpen}};

function makeSelect(options, initialValue) {
  const sel = {id: 'modelSelect', options: [], selectedIndex: -1, selectedOptions: []};
  Object.defineProperty(sel, 'value', {
    get() { return this._value || ''; },
    set(v) {
      this._value = v;
      const idx = this.options.findIndex(o => o.value === v);
      this.selectedIndex = idx;
      this.selectedOptions = idx >= 0 ? [this.options[idx]] : [];
    }
  });
  sel.querySelector = function(_selector) { return this.options[0] || null; };
  for (const item of options) {
    const group = {tagName: 'OPTGROUP', dataset: {provider: item.provider || ''}};
    const opt = {value: item.value, textContent: item.label || item.value, parentElement: group, dataset: {}};
    sel.options.push(opt);
  }
  sel.value = initialValue || '';
  return sel;
}

function $(id) {
  if (id === 'modelSelect') return modelSelect;
  if (id === 'composerModelDropdown') return dropdown;
  return {textContent: '', style: {}, classList: {add(){}, remove(){}, toggle(){}, contains(){return false;}}, appendChild(){}, appendChildNode(){}};
}
function t(key) { return key; }
function syncModelChip() { calls.syncModelChip++; }
function renderModelDropdown() { calls.renderModelDropdown++; }
function _positionModelDropdown() { calls.positionModelDropdown++; }
function syncAppTitlebar() {}
function syncWorkspaceDisplays() {}
function syncReasoningChip() {}
function syncToolsetsChip() {}
function syncTerminalButton() {}
function _syncHermesPanelSessionActions() {}
function _latestGatewayRoutingForSession() { return null; }
function getModelLabel(v) { return v; }
function _formatGatewayModelLabel(_v, text) { return text; }
const _liveModelFetchPending = new Set();
const document = {
  title: '',
  baseURI: 'http://127.0.0.1/hermes/',
  createElement(tag) { return {tagName: tag.toUpperCase(), className: '', textContent: '', appendChild(){}}; },
  createTextNode(text) { return {textContent: text}; },
};
const window = { _botName: 'Hermes', _defaultModel: null, _activeProvider: null };
function fetch(url, opts) { calls.fetches.push({url: String(url), body: opts && opts.body || ''}); return Promise.resolve({ok: true}); }

for (const name of [
  'assistantDisplayName',
  '_topbarLoadedMessageCount', '_topbarMessageMetaText',
  '_getOptionProviderId', '_providerFromModelValue', '_modelStateForSelect',
  '_findModelInDropdown', '_refreshOpenModelDropdown', '_applyModelToDropdown',
  '_modelStateFromAppliedDropdown', '_persistSessionModelCorrection',
  '_applySessionModelFallback', 'syncTopbar'
]) {
  const src = extractFunc(name, {optional: name !== 'syncTopbar'});
  if (src) eval(src);
}

const args = JSON.parse(process.argv[3]);
modelSelect = makeSelect(args.options, args.initialValue);
dropdownOpen = !!args.dropdownOpen;
window._defaultModel = args.defaultModel || null;
window._activeProvider = args.activeProvider || null;
var S = {
  session: {
    session_id: 'session-b',
    id: 'session-b',
    title: 'Session B',
    model: args.sessionModel,
    model_provider: args.sessionProvider || null,
    messages: [],
    _modelResolutionDeferred: !!args.modelResolutionDeferred,
  },
  messages: [],
  activeProfile: 'default',
};

syncTopbar();

process.stdout.write(JSON.stringify({
  selectValue: modelSelect.value,
  sessionModel: S.session.model,
  sessionProvider: S.session.model_provider,
  calls,
}));
"""


@pytest.fixture(scope="module")
def driver_path(tmp_path_factory):
    p = tmp_path_factory.mktemp("issue1771_driver") / "driver.js"
    p.write_text(_DRIVER_SRC, encoding="utf-8")
    return str(p)


def _run_sync(driver_path, *, session_model, initial_value="@expensive:gpt-5.5", default_model="@safe:gpt-4o-mini", dropdown_open=False, model_resolution_deferred=False):
    payload = {
        "sessionModel": session_model,
        "sessionProvider": None,
        "initialValue": initial_value,
        "defaultModel": default_model,
        "activeProvider": "safe",
        "dropdownOpen": dropdown_open,
        "modelResolutionDeferred": model_resolution_deferred,
        "options": [
            {"provider": "expensive", "value": "@expensive:gpt-5.5", "label": "GPT-5.5"},
            {"provider": "safe", "value": "@safe:gpt-4o-mini", "label": "GPT-4o mini"},
        ],
    }
    result = subprocess.run(
        [NODE, driver_path, str(UI_JS_PATH), json.dumps(payload)],
        capture_output=True,
        text=True,
        timeout=10,
    )
    if result.returncode != 0:
        raise RuntimeError(f"node driver failed:\nSTDOUT={result.stdout}\nSTDERR={result.stderr}")
    return json.loads(result.stdout)


def test_sync_topbar_missing_model_falls_back_to_configured_default_not_previous_chat(driver_path):
    got = _run_sync(driver_path, session_model="")

    assert got["selectValue"] == "@safe:gpt-4o-mini"
    assert got["sessionModel"] == "@safe:gpt-4o-mini"
    assert got["sessionProvider"] == "safe"
    assert got["selectValue"] != "@expensive:gpt-5.5"


def test_sync_topbar_unknown_model_falls_back_to_configured_default_not_first_option(driver_path):
    got = _run_sync(driver_path, session_model="unknown")

    assert got["selectValue"] == "@safe:gpt-4o-mini"
    assert got["sessionModel"] == "@safe:gpt-4o-mini"
    assert got["sessionProvider"] == "safe"


def test_sync_topbar_rerenders_open_visible_model_dropdown_after_session_model_change(driver_path):
    got = _run_sync(driver_path, session_model="", dropdown_open=True)

    assert got["selectValue"] == "@safe:gpt-4o-mini"
    assert got["calls"]["renderModelDropdown"] >= 1
    assert got["calls"]["positionModelDropdown"] >= 1



def test_sync_topbar_does_not_persist_correction_while_model_resolution_deferred(driver_path):
    """Regression for stage-310 Opus review: the !hasSessionModel branch must
    skip the network write + state mutation while sessions.js has set
    _modelResolutionDeferred=true between the fast-path session render and
    the resolve_model=1 round-trip.

    Without this guard, every fast-path session view of an empty/unknown-model
    session fires a /api/session/update POST that races _resolveSessionModelForDisplaySoon
    and thrashes imported/read-only CLI sessions whose model field reads "unknown"
    (#1778 introduced exactly that surface in v0.51.16).
    """
    got_empty = _run_sync(driver_path, session_model="", model_resolution_deferred=True)
    # Visible UX still happens (sel.value gets the safe default) ...
    assert got_empty["selectValue"] == "@safe:gpt-4o-mini"
    # ... but session state is NOT mutated and NO POST is issued.
    assert got_empty["sessionModel"] == "", "S.session.model must not be mutated while resolution is deferred"
    update_calls = [c for c in got_empty["calls"]["fetches"] if "session" in c["url"] and "update" in c["url"]]
    assert update_calls == [], f"no /api/session/update POSTs while deferred (got {update_calls})"

    got_unknown = _run_sync(driver_path, session_model="unknown", model_resolution_deferred=True)
    assert got_unknown["selectValue"] == "@safe:gpt-4o-mini"
    assert got_unknown["sessionModel"] == "unknown"
    update_calls_u = [c for c in got_unknown["calls"]["fetches"] if "session" in c["url"] and "update" in c["url"]]
    assert update_calls_u == [], "imported/read-only CLI session with model=unknown must not be silently written"
