"""Regression coverage for process-wakeup transcript rendering.

A background-process wakeup is stored as a synthetic user turn
(`_source: "process_wakeup"`).  It must be visible by default, but it must not
look like a human-authored chat bubble.  Keeping it in the visible message list
also preserves the user-turn boundary between the assistant response that
preceded the notification and the assistant response produced by the wakeup.
"""

import json
import re
import shutil
import subprocess
from pathlib import Path

import pytest


ROOT = Path(__file__).resolve().parents[1]
UI_JS_PATH = ROOT / "static" / "ui.js"
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
NODE = shutil.which("node")

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


_DRIVER = r"""
const fs = require('fs');
const src = fs.readFileSync(process.argv[1], 'utf8');
function extractFunc(name){
  const start = src.indexOf('function ' + name);
  if(start === -1) throw new Error(name + ' not found');
  const params = src.indexOf('(', start);
  let depth = 0, close = -1;
  for(let i=params; i<src.length; i++){
    if(src[i] === '(') depth++;
    else if(src[i] === ')'){
      depth--;
      if(depth === 0){ close = i; break; }
    }
  }
  const brace = src.indexOf('{', close);
  depth = 0;
  for(let i=brace; i<src.length; i++){
    if(src[i] === '{') depth++;
    else if(src[i] === '}'){
      depth--;
      if(depth === 0) return src.slice(start, i + 1);
    }
  }
  throw new Error(name + ' body did not close');
}
function msgContent(m){
  if(!m) return '';
  if(typeof m.content === 'string') return m.content;
  if(Array.isArray(m.content)) return m.content.map(p => (p && (p.text || p.content)) || '').join('\n');
  return String(m.content || '');
}
function _isContextCompactionMessage(){ return false; }
function _isPreservedCompressionTaskListMessage(){ return false; }
function _isRecoveryControlMessage(){ return false; }
function _messageHasReasoningPayload(){ return false; }
function _assistantMessageHasVisibleContent(m){ return !!String(msgContent(m)).trim(); }

global.window = {};
global.S = {messages: []};
let _visWithIdxCache = null;
let _visWithIdxCacheLen = 0;
let _visWithIdxCacheSrc = null;

const heightStart = src.indexOf('const MESSAGE_RENDER_WINDOW_DEFAULT');
const heightEnd = src.indexOf('let _messageVirtualMeasurementSeenKeys', heightStart);
if(heightStart !== -1 && heightEnd !== -1) eval(src.slice(heightStart, heightEnd));
if(src.indexOf('function _isProcessWakeupMessage') !== -1) eval(extractFunc('_isProcessWakeupMessage'));
eval(extractFunc('_stripWorkspaceDisplayPrefix'));
eval(extractFunc('_stripAttachedFilesMarkerForDisplay'));
eval(extractFunc('_messageIsRenderable'));
eval(extractFunc('_getVisibleMessagesWithIdx'));
eval(extractFunc('_messageVirtualRoleForEntry'));

const wakeup = {
  role: 'user',
  content: '[IMPORTANT: Background process proc_123 completed (exit_code=0).\nCommand: sleep 1\nOutput:\ndone]',
  _source: 'process_wakeup',
  timestamp: 1783405253.72,
};
S.messages = [
  {role: 'assistant', content: 'previous assistant report', timestamp: 1783405252.05},
  wakeup,
  {role: 'assistant', content: 'assistant response to wakeup', timestamp: 1783405254.10},
];

const visible = _getVisibleMessagesWithIdx();
const turns = [];
let current = [];
for(const entry of visible){
  const source = entry.m._source || '';
  if(entry.m.role === 'user'){
    if(current.length) turns.push(current);
    turns.push(['user:' + source + ':' + String(entry.m.content).slice(0, 35)]);
    current = [];
  }else if(entry.m.role === 'assistant'){
    current.push('assistant:' + entry.m.content);
  }
}
if(current.length) turns.push(current);
const virtualRole = _messageVirtualRoleForEntry({m: wakeup});
const virtualHeight = typeof _messageVirtualDefaultHeightForRole === 'function'
  ? _messageVirtualDefaultHeightForRole(virtualRole)
  : null;
const attachmentOnlyWakeup = {
  role: 'user',
  content: '',
  _source: 'process_wakeup',
  attachments: [{name: 'result.txt'}],
};
const markerWakeupContent = [
  '[Workspace::v1: /tmp/hermes]',
  'Visible wakeup text',
  '',
  '[Attached files: result.txt]',
].join(String.fromCharCode(10));

process.stdout.write(JSON.stringify({
  visible: visible.map(e => ({rawIdx: e.rawIdx, role: e.m.role, source: e.m._source || '', text: String(e.m.content).slice(0, 32)})),
  turns,
  virtualRole,
  virtualHeight,
  attachmentOnlyRenderable: _messageIsRenderable(attachmentOnlyWakeup),
  strippedWakeupDisplay: _stripAttachedFilesMarkerForDisplay(_stripWorkspaceDisplayPrefix(markerWakeupContent)),
}));
"""


def _run_driver():
    assert NODE is not None
    proc = subprocess.run(
        [NODE, "-e", _DRIVER, str(UI_JS_PATH)],
        text=True,
        capture_output=True,
        timeout=30,
        check=False,
    )
    assert proc.returncode == 0, proc.stderr
    return json.loads(proc.stdout)


def test_process_wakeup_message_stays_visible_and_preserves_turn_boundary():
    result = _run_driver()

    assert result["visible"] == [
        {"rawIdx": 0, "role": "assistant", "source": "", "text": "previous assistant report"},
        {"rawIdx": 1, "role": "user", "source": "process_wakeup", "text": "[IMPORTANT: Background process p"},
        {"rawIdx": 2, "role": "assistant", "source": "", "text": "assistant response to wakeup"},
    ]
    assert result["turns"] == [
        ["assistant:previous assistant report"],
        ["user:process_wakeup:[IMPORTANT: Background process proc"],
        ["assistant:assistant response to wakeup"],
    ]


def test_process_wakeup_has_its_own_virtual_height_role():
    result = _run_driver()

    assert result["virtualRole"] == "process_wakeup"
    assert isinstance(result["virtualHeight"], int)
    assert 1 <= result["virtualHeight"] <= 120


def test_attachment_only_process_wakeup_is_visible_and_display_markers_are_stripped():
    result = _run_driver()

    assert result["attachmentOnlyRenderable"] is True
    assert result["strippedWakeupDisplay"] == "Visible wakeup text"


def test_process_wakeup_uses_compact_status_row_not_normal_user_bubble():
    ui = UI_JS_PATH.read_text(encoding="utf-8")
    marker = "const isProcessWakeup="
    marker_idx = ui.find(marker)
    assert marker_idx != -1, "render loop must classify process-wakeup messages"
    process_branch_idx = ui.find("if(isProcessWakeup)", marker_idx)
    user_branch_idx = ui.find("if(isUser)", marker_idx)

    assert process_branch_idx != -1, "process-wakeup render branch missing"
    assert user_branch_idx != -1, "normal user render branch missing"
    assert process_branch_idx < user_branch_idx, (
        "process-wakeup rows must render through the compact status branch "
        "before the normal user-bubble branch"
    )
    process_branch = ui[process_branch_idx:user_branch_idx]
    assert "process-wakeup-row" in process_branch
    assert "process-wakeup-notice" in process_branch
    assert "data-role='process_wakeup'" in process_branch or "dataset.role='process_wakeup'" in process_branch
    assert "${filesHtml}" in process_branch
    assert "t('process_wakeup_label')" in process_branch
    assert "Background wakeup" not in process_branch
    assert "const rowDisplayContent=displayContent;" in ui
    assert "const rowDisplayContent=isProcessWakeup?content:displayContent;" not in ui

    assert ".process-wakeup-row" in STYLE_CSS
    assert ".process-wakeup-notice" in STYLE_CSS
    assert ".process-wakeup-text" in STYLE_CSS
    notice_rule = STYLE_CSS[
        STYLE_CSS.index(".process-wakeup-notice{") : STYLE_CSS.index(".process-wakeup-label{")
    ]
    assert "margin:8px 0 8px var(--msg-rail)" in notice_rule
    assert "max-width:min(var(--msg-max),760px)" in notice_rule
    assert "margin-left:30px" not in notice_rule
    assert "max-width:680px" not in notice_rule
    assert "@media(max-width:700px){.process-wakeup-notice{margin-left:0;}}" in STYLE_CSS


def test_process_wakeup_label_key_exists_in_all_locales():
    locale_pattern = re.compile(
        r"^\s{2}(?:'(?P<quoted>[A-Za-z0-9-]+)'|(?P<plain>[A-Za-z0-9-]+))\s*:\s*\{",
        re.MULTILINE,
    )
    locale_matches = list(locale_pattern.finditer(I18N_JS))
    assert locale_matches, "expected at least the English locale"
    for idx, match in enumerate(locale_matches):
        name = match.group("quoted") or match.group("plain")
        start = match.end()
        end = locale_matches[idx + 1].start() if idx + 1 < len(locale_matches) else I18N_JS.find("\n};", start)
        block = I18N_JS[start:end]
        assert re.search(r"\bprocess_wakeup_label\s*:", block), (
            f"process_wakeup_label missing from locale {name}"
        )
    assert "process_wakeup_label:'Background wakeup'" in I18N_JS
