import test from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:net';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { ApprovalChannel, should_request_approval } from './approval.ts';
import pager from './device-lab-pager.ts';

test('positive inspection grammar; mutations, composition and unknown tools need approval', () => {
  for (const name of ['read','grep','search','ls','list','find']) assert.equal(should_request_approval(name, {}), false);
  for (const command of ['pwd','git status --short','git diff','ls -la','cat /tmp/test']) assert.equal(should_request_approval('bash', {command}), false);
  for (const command of ['rm x','git push','systemctl restart x','ls; touch x','cat $(touch x)','echo hi > x','python3 script.py','find . -delete','git -c alias.x=!touch x']) assert.equal(should_request_approval('bash', {command}), true);
  for (const name of ['write','edit','apply_patch','delete','deploy','read_and_delete','custom']) assert.equal(should_request_approval(name, {}), true);
});

test('official Pi tool_call hook blocks until decision; approve/reject, timeout and runtime shutdown', async () => {
  const dir=mkdtempSync(join(tmpdir(),'pi-approval-test-'));
  const old=process.env.DEVICE_LAB_CONTROL_DIR;process.env.DEVICE_LAB_CONTROL_DIR=dir;
  const sockets:any[]=[];const requests:any[]=[];
  const server=createServer(s=>{sockets.push(s);s.on('data', d=>requests.push(JSON.parse(d.toString())));});
  await new Promise<void>(r=>server.listen(join(dir,'approvals.sock'),r));
  const handlers=new Map<string,any>();pager({on:(name:string,fn:any)=>handlers.set(name,fn)} as any);
  const ctx:any={cwd:dir,sessionManager:{getSessionId:()=> 'same-session',getSessionName:()=> 'Test'}};
  const call=handlers.get('tool_call');
  const waitRequest=async (n:number)=>{for(let i=0;i<100 && requests.length<n;i++)await new Promise(r=>setTimeout(r,5));assert.equal(requests.length,n);};
  try {
    assert.equal(await call({toolName:'read',input:{path:'x'},toolCallId:'read'},ctx),undefined);
    assert.equal(requests.length,0);
    let settled=false;
    const rejected=call({toolName:'write',input:{path:'x'},toolCallId:'one'},ctx).then((r:any)=>{settled=true;return r;});
    await waitRequest(1);assert.equal(settled,false);
    sockets[0].end(JSON.stringify({approval_id:'a',decision:'reject',state:'REJECTED'})+'\n');
    assert.deepEqual(await rejected,{block:true,reason:'Rejected from Device Lab'});
    const approved=call({toolName:'edit',input:{path:'x'},toolCallId:'two'},ctx);
    await waitRequest(2);sockets[1].end(JSON.stringify({approval_id:'b',decision:'approve',state:'APPROVED'})+'\n');
    assert.equal(await approved,undefined);
    const stopped=call({toolName:'write',input:{path:'x'},toolCallId:'three'},ctx);
    await waitRequest(3);await handlers.get('session_shutdown')({},ctx);
    assert.equal((await stopped).block,true);
    const channel=new ApprovalChannel(20,join(dir,'approvals.sock'));
    assert.equal(await channel.request({test:'timeout'}),false);
    channel.close();
    assert.equal(requests[0].session_id,requests[1].session_id);
    assert.notEqual(requests[0].tool_call_id,requests[1].tool_call_id);
  } finally {
    for(const s of sockets)s.destroy();
    await new Promise<void>(r=>server.close(()=>r()));
    if(old===undefined)delete process.env.DEVICE_LAB_CONTROL_DIR;else process.env.DEVICE_LAB_CONTROL_DIR=old;
    rmSync(dir,{recursive:true,force:true});
  }
});


test('accepted Pi approval waits past the old limit using a controlled clock', async t => {
  const dir=mkdtempSync(join(tmpdir(),'pi-indefinite-'));
  let peer: any;
  let accepted!: () => void;
  const ack=new Promise<void>(r=>accepted=r);
  const server=createServer(s=>{peer=s;s.on('data',()=>{s.write('{"state":"PENDING","approval_id":"long"}\n');accepted();});});
  await new Promise<void>(r=>server.listen(join(dir,'approvals.sock'),r));
  t.mock.timers.enable({apis:['setTimeout']});
  const channel=new ApprovalChannel(5000,join(dir,'approvals.sock'));
  let settled=false;
  const pending=channel.request({test:'wait'}).then(r=>{settled=true;return r;});
  try {
    await ack;
    await new Promise(r=>setImmediate(r));
    await new Promise(r=>setImmediate(r));
    t.mock.timers.tick(4*60*60*1000);
    assert.equal(settled,false);
    peer.end('{"approval_id":"long","state":"APPROVED","decision":"approve"}\n');
    assert.equal(await pending,true);
  } finally {
    channel.close();peer?.destroy();t.mock.timers.reset();
    await new Promise<void>(r=>server.close(()=>r()));rmSync(dir,{recursive:true,force:true});
  }
});
