// Shared fake process model for the two Case host-script tests. No real sudo.
// Call case_sudo_run RECORD_DIR COMMAND... after the caller's argv checks.
// Missing session-mode preserves the original exec-style fake.
import { spawn } from 'node:child_process';
import { readFileSync, readdirSync } from 'node:fs';

export const CASE_SUDO_SESSION = String.raw`
case_sudo_entry() {
    local record="$1" here sid; shift
    here="$BASHPID"
    read -r sid < <(/bin/ps -o sid= -p "$here")
    printf "%s %s\n" "$here" "$sid" >> "$record/session-pids"
    if { [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
       { [ -f "$record/stall-probe" ] && [[ "$*" == *SO_PEERCRED* ]]; }; then
        trap '' TERM HUP
        [ ! -f "$record/stall-output" ] || /bin/cat "$record/stall-output"
        exec /bin/sleep 60
    fi
    exec "$@"
}
case_sudo_run() {
    local record="$1" child="" rc=0 mode=exec; shift
    [ ! -f "$record/session-mode" ] || mode="$(<"$record/session-mode")"
    if [ "$mode" != hop ]; then case_sudo_entry "$record" "$@"; return "$?"; fi
    # The fake stays as parent, just as sudo does on its pty path. KILL cannot
    # be caught or forwarded. The child creates the separate terminal session.
    trap '[ -z "$child" ] || kill -TERM -- "-$child" 2>/dev/null || :' TERM
    trap '[ -z "$child" ] || kill -HUP -- "-$child" 2>/dev/null || :' HUP
    trap '[ -z "$child" ] || kill -INT -- "-$child" 2>/dev/null || :' INT
    export -f case_sudo_entry
    /usr/bin/setsid /bin/bash -c 'case_sudo_entry "$@"' -- "$record" "$@" &
    child=$!
    # An interrupted wait returns while the child still runs; keep waiting.
    while :; do
        wait "$child"; rc=$?
        kill -0 "$child" 2>/dev/null || break
    done
    return "$rc"
}
`;

export function liveRecordedSessions(recordFile, readFile, procDirs) {
  // Exec fake entries share the test runner's session. Only self-led sessions
  // are isolated and safe to scan here; exec coverage also checks exact PIDs.
  const sessions = new Set(readFile(recordFile).trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number)).filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
  const live=[];
  for(const entry of procDirs()){
    if(!/^\d+$/.test(entry))continue;
    try{
      const stat=readFile(`/proc/${entry}/stat`), fields=stat.slice(stat.lastIndexOf(') ')+2).split(' ');
      if(sessions.has(Number(fields[3])) && fields[0]!=='Z') live.push(Number(entry));
    }catch{}
  }
  return live;
}

export function recordedPids(file) {
  try{return readFileSync(file,'utf8').trim().split('\n').filter(Boolean).map(line=>line.trim().split(/\s+/).map(Number));}
  catch(error){if(error.code==='ENOENT')return [];throw error;}
}

export function recordedLivePids(file) {
  return sessionPairs(recordedPids(file)).filter(([pid,sid])=>{
    try{
      const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
      return Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X';
    }catch(error){if(error.code==='ENOENT'||error.code==='ESRCH')return false;throw error;}
  }).map(([pid])=>pid);
}

export function killRecordedPids(file) {
  killPairs(recordedPids(file));
}

function killPairs(pairs) {
  for(const [pid,sid] of sessionPairs(pairs)){
    try{
      const raw=readFileSync(`/proc/${pid}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' ');
      if(Number(fields[3])===sid && fields[0]!=='Z' && fields[0]!=='X')process.kill(pid,'SIGKILL');
    }catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
  }
}

function sessionPairs(pairs) {
  // Only self-led records authorize a session scan. Exec-mode records belong
  // to the runner's session and authorize checking their exact PIDs only.
  const sessions=new Set(pairs.filter(([pid,sid])=>pid===sid).map(([,sid])=>sid));
  const found=new Map(pairs.map(pair=>[pair[0],pair]));
  if(sessions.size)for(const entry of readdirSync('/proc')){
    if(!/^\d+$/.test(entry))continue;
    try{
      const raw=readFileSync(`/proc/${entry}/stat`,'utf8'),fields=raw.slice(raw.lastIndexOf(') ')+2).split(' '),sid=Number(fields[3]);
      if(sessions.has(sid))found.set(Number(entry),[Number(entry),sid]);
    }catch(error){if(error.code!=='ENOENT'&&error.code!=='ESRCH')throw error;}
  }
  return [...found.values()];
}

// Safety net for a removed-outer-bound change. It is fixture cleanup, never
// product supervision. The guard reads only fixture records and checks sid
// immediately before every kill. Normal outer-bound cases return before it.
export function fixtureGuard(t,files,milliseconds=9500) {
  const program=`const fs=require('node:fs');setTimeout(()=>{for(const file of JSON.parse(process.argv[1])){let rows=[];try{rows=fs.readFileSync(file,'utf8').trim().split(String.fromCharCode(10))}catch{}for(const row of rows){const [pid,sid]=row.trim().split(' ').map(Number);if(!(pid>0&&sid>0))continue;try{const raw=fs.readFileSync('/proc/'+pid+'/stat','utf8'),f=raw.slice(raw.lastIndexOf(') ')+2).split(' ');if(Number(f[3])===sid&&f[0]!=='Z')process.kill(pid,'SIGKILL')}catch{}}}},Number(process.argv[2]));`;
  const child=spawn(process.execPath,['-e',program,JSON.stringify(files),String(milliseconds)],{stdio:'ignore'});
  const stop=async()=>{
    if(child.exitCode!==null||child.signalCode!==null)return;
    const exited=new Promise(resolve=>child.once('exit',resolve));
    child.kill('SIGKILL');await exited;
  };
  t.after(stop);return stop;
}

export function cleanupFixture(t,files) {
  let captured=[];
  const capture=()=>{captured=files.flatMap(recordedPids);};
  // tmp(t)'s directory-removal hook may run first. Retain PID/SID pairs in
  // memory before assertions, then verify the kernel sid again before killing.
  t.after(()=>killPairs([...captured,...files.flatMap(recordedPids)]));
  return capture;
}

export async function pollFixtureGone(files) {
  for(let n=0;n<50;n++){
    if(files.every(file=>recordedLivePids(file).length===0))return [];
    await new Promise(resolve=>setTimeout(resolve,10));
  }
  return files.flatMap(recordedLivePids);
}

export function blockingQuerySource(records,kind='signals',member,proof) {
  const py=`import os,sys\nos.setpgid(0,0)\nwith open(sys.argv[2],"a") as f:f.write(str(os.getpid())+" "+str(os.getsid(0))+chr(10))\nos.execv(sys.argv[1],[sys.argv[1],"60"])`;
  return `import fs from 'node:fs';import {spawn} from 'node:child_process';
const records=${JSON.stringify(records)};
function record(pid){const s=fs.readFileSync('/proc/'+pid+'/stat','utf8');const sid=Number(s.slice(s.lastIndexOf(') ')+2).split(' ')[3]);fs.appendFileSync(records,pid+' '+sid+String.fromCharCode(10));}
record(process.pid);
export function loadSystem(){
${kind==='sleep'?`const c=spawn('/usr/bin/sleep',['60'],{stdio:'inherit'});record(c.pid);`:''}
${kind==='signals'?`process.on('SIGTERM',()=>{});process.on('SIGHUP',()=>{});`:''}
${kind==='member'?`const c=spawn('/usr/bin/python3',['-I','-S','-c',${JSON.stringify(py)},${JSON.stringify(member)},records],{stdio:'inherit'});for(let n=0;n<200;n++){try{const s=fs.readFileSync('/proc/'+c.pid+'/stat','utf8');if(s.includes('('+${JSON.stringify(member?.split('/').at(-1))}+') ')){fs.writeFileSync(${JSON.stringify(proof)},s);break;}}catch{}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,5);}`:''}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
}`;
}
