<?php
declare(strict_types=1);
// Own one loopback PHP server on a dynamically selected port. Never stop another process.
$root=dirname(__DIR__);
$socket=stream_socket_server('tcp://127.0.0.1:0',$errno,$error);
if($socket===false) throw new RuntimeException('Cannot reserve a local test port');
$address=stream_socket_get_name($socket,false);fclose($socket);
$port=(int)substr($address,strrpos($address,':')+1);
$stdout=tmpfile();$stderr=tmpfile();$env=getenv();unset($env['PHP_CLI_SERVER_WORKERS']);
$pipes=[];
$process=proc_open([PHP_BINARY,'-S','127.0.0.1:'.$port,'-t',$root.'/public'],[0=>['pipe','r'],1=>$stdout,2=>$stderr],$pipes,$root,$env);
if(!is_resource($process)) throw new RuntimeException('Cannot start own PHP test server');
fclose($pipes[0]);$checks=[];$failure=null;
function httpRequest(int $port,string $method,array $headers,string $body=''): array {
    $context=stream_context_create(['http'=>['method'=>$method,'header'=>implode("\r\n",$headers),'content'=>$body,'ignore_errors'=>true,'timeout'=>5]]);
    $response=@file_get_contents('http://127.0.0.1:'.$port.'/convert.php',false,$context);
    if($response===false) throw new RuntimeException('Local HTTP request failed');
    $status=0;
    $responseHeaders=function_exists('http_get_last_response_headers')?http_get_last_response_headers():($http_response_header??[]);
    foreach($responseHeaders as $line) if(preg_match('/^HTTP\/\S+ (\d+)/',$line,$m)) $status=(int)$m[1];
    return [$status,json_decode($response,true,32,JSON_THROW_ON_ERROR),$responseHeaders];
}
function expectHttp(string $name,int $expected,?array $data=null,string $method='POST',?string $raw=null,array $headers=['Content-Type: application/json']): void {
    global $port,$checks;
    $body=$raw??($data===null?'':json_encode($data,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
    [$status,$json,$responseHeaders]=httpRequest($port,$method,$headers,$body);
    $ok=$status===$expected && isset($json['ok']) && $json['ok']===($expected===200);
    if($expected===200) $ok=$ok && isset($json['results'][0]['html']);
    $checks[]=['name'=>$name,'expected'=>$expected,'actual'=>$status,'passed'=>$ok];
    if(!$ok) throw new RuntimeException($name.': unexpected HTTP contract');
}
try {
    $ready=false;
    for($i=0;$i<50;$i++) {
        if(!proc_get_status($process)['running']) throw new RuntimeException('Own PHP server exited before readiness');
        try { [$status]=httpRequest($port,'GET',[]);if($status===405){$ready=true;break;} }
        catch(Throwable) { usleep(100000); }
    }
    if(!$ready) throw new RuntimeException('Own PHP server did not become ready');
    $fixtures=json_decode(file_get_contents($root.'/public/fixtures.json'),true,32,JSON_THROW_ON_ERROR);
    foreach($fixtures as $i=>$fixture) {
        $data=['profile'=>$fixture['profile']];
        if($fixture['batch']??false) $data['documents']=preg_split('/^---DOC---\s*$/m',$fixture['markdown']);
        else $data['markdown']=$fixture['markdown'];
        expectHttp('fixture_'.($i+1),200,$data);
    }
    expectHttp('GET rejected',405,method:'GET');
    expectHttp('Content-Type rejected',415,raw:'{}',headers:['Content-Type: text/plain']);
    expectHttp('Cross-site rejected',403,['markdown'=>'x'],headers:['Content-Type: application/json','Sec-Fetch-Site: cross-site']);
    expectHttp('Malformed JSON',400,raw:'{');
    expectHttp('Invalid UTF8',400,raw:"{\"markdown\":\"\xFF\"}");
    expectHttp('JSON body limit',413,raw:str_repeat('x',65537));
    expectHttp('Markdown byte limit',413,['markdown'=>str_repeat('x',8193)]);
    expectHttp('Unknown field',422,['markdown'=>'x','path'=>'not-read']);
    expectHttp('HTML allow disallowed',422,['markdown'=>'x','htmlPolicy'=>'allow']);
    expectHttp('Batch size limit',422,['documents'=>array_fill(0,6,'x')]);
    expectHttp('Both inputs',422,['markdown'=>'x','documents'=>['x']]);
    expectHttp('Null documents',422,['markdown'=>'x','documents'=>null]);
    expectHttp('Null profile',422,['markdown'=>'x','profile'=>null]);
    expectHttp('Null policy',422,['markdown'=>'x','htmlPolicy'=>null]);
} catch(Throwable $e) { $failure=$e->getMessage(); }
finally {
    if(proc_get_status($process)['running']) proc_terminate($process);
    for($i=0;$i<20 && proc_get_status($process)['running'];$i++) usleep(100000);
    if(proc_get_status($process)['running']) proc_terminate($process,9);
    proc_close($process);fclose($stdout);fclose($stderr);
}
$passed=count(array_filter($checks,fn($check)=>$check['passed']));
echo json_encode(['php'=>PHP_VERSION,'os'=>PHP_OS_FAMILY,'checks'=>$checks,'passed'=>$passed,'total'=>count($checks),'failure'=>$failure,'ownServerClosed'=>true],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE).PHP_EOL;
exit($failure===null && $passed===22?0:1);
