import { expect, test } from 'bun:test'; import { createWaker, POKE } from './waker.js';

function harness(o:any={}){
  const sent:any[]=[]; const wakes:Record<string,number>={...(o.wakes??{})}; const wakeAttempts:Record<string,number>={...(o.wakeAttempts??{})}; const escalated:string[]=[];
  const store={
    staleUndelivered:()=>o.stale??[{id:'m1',sessionId:'s1',wakeAttempts:wakeAttempts.m1??0}],
    markWakeAttempt:(id:string)=>wakeAttempts[id]=(wakeAttempts[id]??0)+1,
    markEscalated:(id:string)=>{escalated.push(id)},
    getPane:(s:string)=>(o.panes??{s1:{socket:'/sock',pane:'%7',panePid:900}})[s]??null,
    lastWake:(s:string)=>wakes[s]??0,
    markWake:(s:string,at:number)=>{wakes[s]=at},
  };
  const waker=createWaker({
    store,
    listPanes:o.listPanes??(()=>[{pane:'%7',command:'bwrap',panePid:900}]),
    paneRunsClaude:o.paneRunsClaude??(()=>true),
    sendKeys:(socket,pane,text)=>{if(o.sendThrows)throw new Error('boom');sent.push({socket,pane,text})},
    now:()=>o.now??1_000_000,
    log:()=>{},
    ...(o.opts??{}),
  });
  return {waker,sent,wakes,wakeAttempts,escalated};
}

test('an idle claude pane gets a neutral poke, never the message text',()=>{
  const h=harness();
  expect(h.waker()).toBe(1);
  expect(h.sent).toEqual([{socket:'/sock',pane:'%7',text:POKE}]);
});

// A sandboxed session shows as bwrap, so the visible command must not decide this.
test('a sandboxed pane whose tree holds claude is still woken',()=>{
  const h=harness({listPanes:()=>[{pane:'%7',command:'bwrap',panePid:900}],paneRunsClaude:()=>true});
  expect(h.waker()).toBe(1);
});

test('a pane with no claude in its tree is never typed into',()=>{
  const h=harness({listPanes:()=>[{pane:'%7',command:'vim',panePid:900}],paneRunsClaude:()=>false});
  expect(h.waker()).toBe(0);
  expect(h.sent).toHaveLength(0);
});

test('a pane-tree inspection failure refuses rather than guesses',()=>{
  const h=harness({paneRunsClaude:()=>{throw new Error('no proc')}});
  expect(h.waker()).toBe(0);
  expect(h.sent).toHaveLength(0);
});

test('a recycled pane (same id, new pid) is never typed into',()=>{
  const h=harness({listPanes:()=>[{pane:'%7',command:'bwrap',panePid:4242}]});
  expect(h.waker()).toBe(0);
  expect(h.sent).toHaveLength(0);
});

test('a vanished pane is skipped',()=>{
  const h=harness({listPanes:()=>[]});
  expect(h.waker()).toBe(0);
});

test('a session with no pane on record is left undelivered rather than guessed at',()=>{
  const h=harness({panes:{}});
  expect(h.waker()).toBe(0);
  expect(h.sent).toHaveLength(0);
});

// Repeated pokes would turn one unread message into a stream of typed lines.
test('a session poked recently is not poked again inside the cooldown',()=>{
  const h=harness({wakes:{s1:1_000_000-1_000}});
  expect(h.waker()).toBe(0);
});

test('several stale messages for one session produce one poke',()=>{
  const h=harness({stale:[{sessionId:'s1'},{sessionId:'s1'},{sessionId:'s1'}]});
  expect(h.waker()).toBe(1);
  expect(h.sent).toHaveLength(1);
});

test('a tmux failure never marks the session woken',()=>{
  const h=harness({sendThrows:true});
  expect(h.waker()).toBe(0);
  expect(h.wakes.s1).toBeUndefined();
});

test('a message that survives two pokes is escalated instead of waking forever',()=>{
  const h=harness({wakeAttempts:{m1:2}});
  expect(h.waker()).toBe(0);
  expect(h.sent).toHaveLength(0);
  expect(h.escalated).toEqual(['m1']);
});

test('listPanes throwing is contained',()=>{
  const h=harness({listPanes:()=>{throw new Error('no server')}});
  expect(h.waker()).toBe(0);
});
