"""Regression coverage for #3460 cron session unread badges."""

from __future__ import annotations

import io
import json
import shutil
import sqlite3
import subprocess
import sys
import types
from pathlib import Path
from types import SimpleNamespace

import pytest


REPO = Path(__file__).resolve().parents[1]
SESSIONS_JS_PATH = REPO / "static" / "sessions.js"
PANELS_JS_PATH = REPO / "static" / "panels.js"
NODE = shutil.which("node")

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


class _JSONHandler:
    def __init__(self):
        self.status = None
        self.headers = {}
        self.response_headers = []
        self.wfile = io.BytesIO()

    def send_response(self, status):
        self.status = status

    def send_header(self, key, value):
        self.response_headers.append((key, value))

    def end_headers(self):
        pass


def _payload(handler):
    return json.loads(handler.wfile.getvalue().decode("utf-8"))


def _run_node(script: str) -> dict:
    assert NODE is not None
    result = subprocess.run(
        [NODE, "-e", script],
        check=True,
        capture_output=True,
        text=True,
        timeout=30,
    )
    return json.loads(result.stdout)


def test_cron_recent_returns_latest_session_id_for_job(monkeypatch, tmp_path):
    import api.routes as routes

    db_path = tmp_path / "state.db"
    with sqlite3.connect(str(db_path)) as conn:
        conn.execute(
            """
            CREATE TABLE sessions (
                id TEXT PRIMARY KEY,
                source TEXT,
                started_at REAL
            )
            """
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at) VALUES (?, ?, ?)",
            ("cron_job3460_20260610_060000", "cron", 100.0),
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at) VALUES (?, ?, ?)",
            ("cron_job3460_20260610_070000", "cron", 200.0),
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at) VALUES (?, ?, ?)",
            ("telegram_job3460_20260610_080000", "telegram", 300.0),
        )

    cron_pkg = types.ModuleType("cron")
    cron_pkg.__path__ = []
    cron_jobs = types.ModuleType("cron.jobs")
    cron_jobs.list_jobs = lambda include_disabled=True: [
        {
            "id": "job3460",
            "name": "Morning Briefing",
            "last_run_at": 250,
            "last_status": "success",
        }
    ]
    monkeypatch.setattr(routes, "_active_state_db_path", lambda: db_path)
    monkeypatch.setitem(sys.modules, "cron", cron_pkg)
    monkeypatch.setitem(sys.modules, "cron.jobs", cron_jobs)

    handler = _JSONHandler()
    routes._handle_cron_recent(handler, SimpleNamespace(query="since=0"))

    body = _payload(handler)
    assert handler.status == 200
    assert body["completions"][0]["job_id"] == "job3460"
    assert body["completions"][0]["session_id"] == "cron_job3460_20260610_070000"
    assert body["completions"][0].get("message_count") is None


def test_cron_recent_falls_back_to_id_order_when_started_at_missing(monkeypatch, tmp_path):
    import api.routes as routes

    db_path = tmp_path / "state.db"
    with sqlite3.connect(str(db_path)) as conn:
        conn.execute(
            """
            CREATE TABLE sessions (
                id TEXT PRIMARY KEY,
                source TEXT
            )
            """
        )
        conn.execute(
            "INSERT INTO sessions(id, source) VALUES (?, ?)",
            ("cron_job3460_20260610_060000", "cron"),
        )
        conn.execute(
            "INSERT INTO sessions(id, source) VALUES (?, ?)",
            ("cron_job3460_20260610_070000", "cron"),
        )

    cron_pkg = types.ModuleType("cron")
    cron_pkg.__path__ = []
    cron_jobs = types.ModuleType("cron.jobs")
    cron_jobs.list_jobs = lambda include_disabled=True: [
        {
            "id": "job3460",
            "name": "Morning Briefing",
            "last_run_at": 250,
            "last_status": "success",
        }
    ]
    monkeypatch.setattr(routes, "_active_state_db_path", lambda: db_path)
    monkeypatch.setitem(sys.modules, "cron", cron_pkg)
    monkeypatch.setitem(sys.modules, "cron.jobs", cron_jobs)

    handler = _JSONHandler()
    routes._handle_cron_recent(handler, SimpleNamespace(query="since=0"))

    body = _payload(handler)
    assert handler.status == 200
    assert body["completions"][0]["session_id"] == "cron_job3460_20260610_070000"


def test_cron_recent_escapes_like_wildcards_in_job_id(monkeypatch, tmp_path):
    import api.routes as routes

    db_path = tmp_path / "state.db"
    with sqlite3.connect(str(db_path)) as conn:
        conn.execute(
            """
            CREATE TABLE sessions (
                id TEXT PRIMARY KEY,
                source TEXT,
                started_at REAL,
                message_count INTEGER
            )
            """
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at, message_count) VALUES (?, ?, ?, ?)",
            ("cron_job_3460%_20260610_090000", "cron", 300.0, 11),
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at, message_count) VALUES (?, ?, ?, ?)",
            ("cron_jobA3460x_20260610_100000", "cron", 400.0, 99),
        )

    cron_pkg = types.ModuleType("cron")
    cron_pkg.__path__ = []
    cron_jobs = types.ModuleType("cron.jobs")
    cron_jobs.list_jobs = lambda include_disabled=True: [
        {
            "id": "job_3460%",
            "name": "Wildcard Job",
            "last_run_at": 500,
            "last_status": "success",
        }
    ]
    monkeypatch.setattr(routes, "_active_state_db_path", lambda: db_path)
    monkeypatch.setitem(sys.modules, "cron", cron_pkg)
    monkeypatch.setitem(sys.modules, "cron.jobs", cron_jobs)

    handler = _JSONHandler()
    routes._handle_cron_recent(handler, SimpleNamespace(query="since=0"))

    body = _payload(handler)
    assert handler.status == 200
    assert body["completions"][0]["session_id"] == "cron_job_3460%_20260610_090000"
    assert body["completions"][0]["message_count"] == 11


def test_cron_recent_does_not_cross_match_shared_job_prefixes(monkeypatch, tmp_path):
    import api.routes as routes

    db_path = tmp_path / "state.db"
    with sqlite3.connect(str(db_path)) as conn:
        conn.execute(
            """
            CREATE TABLE sessions (
                id TEXT PRIMARY KEY,
                source TEXT,
                started_at REAL,
                message_count INTEGER
            )
            """
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at, message_count) VALUES (?, ?, ?, ?)",
            ("cron_backup_20260610_090000", "cron", 100.0, 4),
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at, message_count) VALUES (?, ?, ?, ?)",
            ("cron_backup_full_20260610_100000", "cron", 200.0, 8),
        )

    cron_pkg = types.ModuleType("cron")
    cron_pkg.__path__ = []
    cron_jobs = types.ModuleType("cron.jobs")
    cron_jobs.list_jobs = lambda include_disabled=True: [
        {
            "id": "backup",
            "name": "Backup",
            "last_run_at": 150,
            "last_status": "success",
        },
        {
            "id": "backup_full",
            "name": "Backup Full",
            "last_run_at": 250,
            "last_status": "success",
        },
    ]
    monkeypatch.setattr(routes, "_active_state_db_path", lambda: db_path)
    monkeypatch.setitem(sys.modules, "cron", cron_pkg)
    monkeypatch.setitem(sys.modules, "cron.jobs", cron_jobs)

    handler = _JSONHandler()
    routes._handle_cron_recent(handler, SimpleNamespace(query="since=0"))

    body = _payload(handler)
    assert handler.status == 200
    by_id = {item["job_id"]: item for item in body["completions"]}
    assert by_id["backup"]["session_id"] == "cron_backup_20260610_090000"
    assert by_id["backup"]["message_count"] == 4
    assert by_id["backup_full"]["session_id"] == "cron_backup_full_20260610_100000"
    assert by_id["backup_full"]["message_count"] == 8


def test_cron_recent_does_not_steal_older_long_prefix_history_for_short_job(
    monkeypatch, tmp_path
):
    import api.routes as routes

    db_path = tmp_path / "state.db"
    with sqlite3.connect(str(db_path)) as conn:
        conn.execute(
            """
            CREATE TABLE sessions (
                id TEXT PRIMARY KEY,
                source TEXT,
                started_at REAL,
                message_count INTEGER
            )
            """
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at, message_count) VALUES (?, ?, ?, ?)",
            ("cron_backup_full_20260610_110000", "cron", 300.0, 12),
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at, message_count) VALUES (?, ?, ?, ?)",
            ("cron_backup_full_20260610_100000", "cron", 200.0, 8),
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at, message_count) VALUES (?, ?, ?, ?)",
            ("cron_backup_20260610_090000", "cron", 100.0, 4),
        )

    cron_pkg = types.ModuleType("cron")
    cron_pkg.__path__ = []
    cron_jobs = types.ModuleType("cron.jobs")
    cron_jobs.list_jobs = lambda include_disabled=True: [
        {
            "id": "backup",
            "name": "Backup",
            "last_run_at": 150,
            "last_status": "success",
        },
        {
            "id": "backup_full",
            "name": "Backup Full",
            "last_run_at": 50,
            "last_status": "success",
        },
    ]
    monkeypatch.setattr(routes, "_active_state_db_path", lambda: db_path)
    monkeypatch.setitem(sys.modules, "cron", cron_pkg)
    monkeypatch.setitem(sys.modules, "cron.jobs", cron_jobs)

    handler = _JSONHandler()
    routes._handle_cron_recent(handler, SimpleNamespace(query="since=0"))

    body = _payload(handler)
    assert handler.status == 200
    assert body["completions"][0]["job_id"] == "backup"
    assert body["completions"][0]["session_id"] == "cron_backup_20260610_090000"
    assert body["completions"][0]["message_count"] == 4


def test_cron_recent_does_not_cross_match_newer_long_prefix_session_when_only_short_job_completed(
    monkeypatch, tmp_path
):
    import api.routes as routes

    db_path = tmp_path / "state.db"
    with sqlite3.connect(str(db_path)) as conn:
        conn.execute(
            """
            CREATE TABLE sessions (
                id TEXT PRIMARY KEY,
                source TEXT,
                started_at REAL,
                message_count INTEGER
            )
            """
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at, message_count) VALUES (?, ?, ?, ?)",
            ("cron_backup_full_20260610_110000", "cron", 300.0, 12),
        )
        conn.execute(
            "INSERT INTO sessions(id, source, started_at, message_count) VALUES (?, ?, ?, ?)",
            ("cron_backup_20260610_090000", "cron", 100.0, 4),
        )

    cron_pkg = types.ModuleType("cron")
    cron_pkg.__path__ = []
    cron_jobs = types.ModuleType("cron.jobs")
    cron_jobs.list_jobs = lambda include_disabled=True: [
        {
            "id": "backup",
            "name": "Backup",
            "last_run_at": 250,
            "last_status": "success",
        },
        {
            "id": "backup_full",
            "name": "Backup Full",
            "last_run_at": 150,
            "last_status": "success",
        },
    ]
    monkeypatch.setattr(routes, "_active_state_db_path", lambda: db_path)
    monkeypatch.setitem(sys.modules, "cron", cron_pkg)
    monkeypatch.setitem(sys.modules, "cron.jobs", cron_jobs)

    handler = _JSONHandler()
    routes._handle_cron_recent(handler, SimpleNamespace(query="since=200"))

    body = _payload(handler)
    assert handler.status == 200
    assert body["completions"] == [
        {
            "job_id": "backup",
            "name": "Backup",
            "status": "success",
            "completed_at": 250.0,
            "toast_notifications": True,
            "session_id": "cron_backup_20260610_090000",
            "message_count": 4,
        }
    ]


def test_sessions_helper_marks_background_completion_with_existing_snapshot():
    script = f"""
const fs = require('fs');
const src = fs.readFileSync({json.dumps(str(SESSIONS_JS_PATH))}, 'utf8');
function extractFunc(name) {{
  const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\(');
  const start = src.search(re);
  if (start < 0) throw new Error(name + ' not found');
  let i = src.indexOf('{{', start);
  let depth = 1; i++;
  while (depth > 0 && i < src.length) {{
    if (src[i] === '{{') depth++;
    else if (src[i] === '}}') depth--;
    i++;
  }}
  return src.slice(start, i);
}}
let _sessionListSnapshotById = new Map([['cron_1', {{message_count: 7}}]]);
let _allSessions = [];
let viewed = [];
let unread = [];
let renders = 0;
function _isSessionActivelyViewedForList() {{ return false; }}
function _setSessionViewedCount(sid, count) {{ viewed.push([sid, count]); }}
function _markSessionCompletionUnread(sid, count) {{ unread.push([sid, count]); }}
function renderSessionListFromCache() {{ renders += 1; }}
global.window = {{}};
    eval(extractFunc('_markSessionCompletionUnreadIfBackground'));
    const result = _markSessionCompletionUnreadIfBackground('cron_1');
    console.log(JSON.stringify({{result, viewed, unread, renders}}));
"""
    payload = _run_node(script)

    assert payload == {
        "result": True,
        "viewed": [],
        "unread": [["cron_1", 7]],
        "renders": 1,
    }


def test_sessions_helper_marks_actively_viewed_completion_as_read():
    script = f"""
const fs = require('fs');
const src = fs.readFileSync({json.dumps(str(SESSIONS_JS_PATH))}, 'utf8');
function extractFunc(name) {{
  const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\(');
  const start = src.search(re);
  if (start < 0) throw new Error(name + ' not found');
  let i = src.indexOf('{{', start);
  let depth = 1; i++;
  while (depth > 0 && i < src.length) {{
    if (src[i] === '{{') depth++;
    else if (src[i] === '}}') depth--;
    i++;
  }}
  return src.slice(start, i);
}}
let _sessionListSnapshotById = new Map();
let _allSessions = [{{session_id: 'cron_2', message_count: 9}}];
let viewed = [];
let unread = [];
let renders = 0;
function _isSessionActivelyViewedForList(sid) {{ return sid === 'cron_2'; }}
function _setSessionViewedCount(sid, count) {{ viewed.push([sid, count]); }}
function _markSessionCompletionUnread(sid, count) {{ unread.push([sid, count]); }}
function renderSessionListFromCache() {{ renders += 1; }}
global.window = {{}};
eval(extractFunc('_markSessionCompletionUnreadIfBackground'));
const result = _markSessionCompletionUnreadIfBackground('cron_2');
console.log(JSON.stringify({{result, viewed, unread, renders}}));
"""
    payload = _run_node(script)

    assert payload == {
        "result": False,
        "viewed": [["cron_2", 9]],
        "unread": [],
        "renders": 1,
    }


def test_cron_polling_marks_sidebar_unread_without_needing_toast():
    script = f"""
const fs = require('fs');
const src = fs.readFileSync({json.dumps(str(PANELS_JS_PATH))}, 'utf8');
function extractFunc(name) {{
  const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\(');
  const start = src.search(re);
  if (start < 0) throw new Error(name + ' not found');
  let i = src.indexOf('{{', start);
  let depth = 1; i++;
  while (depth > 0 && i < src.length) {{
    if (src[i] === '{{') depth++;
    else if (src[i] === '}}') depth--;
    i++;
  }}
  return src.slice(start, i);
}}
let _cronPollSince = 10;
let _cronPollTimer = null;
let _cronUnreadCount = 0;
let _cronPollGeneration = 0;
const _cronNewJobIds = new Set();
const markCalls = [];
const toastCalls = [];
let badgeUpdates = 0;
global.document = {{ hidden: false }};
global.setInterval = (fn, _ms) => {{ global.__tick = fn; return 1; }};
async function api(_url) {{
  return {{
    completions: [{{
      job_id: 'job3460',
      session_id: 'cron_job3460_20260610_070000',
      message_count: 12,
      name: 'Morning Briefing',
      status: 'success',
      completed_at: 25,
      toast_notifications: false
    }}]
  }};
}}
function showToast(...args) {{ toastCalls.push(args); }}
function t(...args) {{ return args.join('|'); }}
function updateCronBadge() {{ badgeUpdates += 1; }}
function _markSessionCompletionUnreadIfBackground(sid, count) {{ markCalls.push([sid, count]); }}
eval(extractFunc('startCronPolling'));
(async() => {{
  startCronPolling();
  await global.__tick();
  console.log(JSON.stringify({{
    since: _cronPollSince,
    unreadJobs: Array.from(_cronNewJobIds),
    markCalls,
    toastCalls,
    badgeUpdates
  }}));
}})().catch(err => {{
  console.error(err);
  process.exit(1);
}});
"""
    payload = _run_node(script)

    assert payload == {
        "since": 25,
        "unreadJobs": ["job3460"],
        "markCalls": [["cron_job3460_20260610_070000", 12]],
        "toastCalls": [],
        "badgeUpdates": 1,
    }
