<?php
// Standalone check for create_support_bundle: php -d zend.assertions=1 tests/support-bundle.php
// No WordPress, no framework. A bundle that drops the failure, or that arrives megabytes long, is
// a bundle nobody can use — so this pins what survives the filter and what bounds it.

function __( $s, $d = null ) { return $s; }
function is_wp_error( $t ) { return false; }
function size_format( $b ) { return $b . 'B'; }
function is_multisite() { return false; }
function wp_get_upload_dir() { return array( 'basedir' => sys_get_temp_dir() ); }
function current_time( $f, $gmt = 0 ) { return gmdate( 'Y-m-d H:i:s' ); }
function wp_generate_uuid4() { return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
	mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0x0fff ) | 0x4000,
	mt_rand( 0, 0x3fff ) | 0x8000, mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) ); }
$wp_version = '6.8';

class FakeWpdb {
	public $prefix = 'wp_';
	public $last_error = '';
	public $inserted = array();
	public $queries = array();
	public function db_version() { return '8.0.36'; }
	public function esc_like( $t ) { return $t; }
	public function prepare( $sql, ...$a ) { if ( count($a)===1 && is_array($a[0]) ) $a=$a[0];
		$o=''; $i=0; foreach ( preg_split('/(%[sd])/',$sql,-1,PREG_SPLIT_DELIM_CAPTURE) as $p ) {
			if ($p==='%s') $o.="'".$a[$i++]."'"; else if ($p==='%d') $o.=(int)$a[$i++]; else $o.=$p; } return $o; }
	public function get_var( $sql ) { return "wp_mclean_support"; } // the table exists
	public function insert( $t, $data ) { $this->inserted[] = $data; return 1; }
	public function query( $sql ) { $this->queries[] = $sql; return 1; }
}

$wpdb = new FakeWpdb();
require __DIR__ . '/../classes/core.php';

$log = tempnam( sys_get_temp_dir(), 'wpmc-bundle-' );

class Probe extends Meow_WPMC_Core {
	public $log_path;
	function get_logs_path() { return $this->log_path; }
	function is_debug() { return true; }
	function is_multilingual() { return false; }
	public function get_request_time_budget() { return 24.0; }
	public function arm( $path ) {
		$this->log_path = $path;
		$p = new ReflectionProperty( 'Meow_WPMC_Core', 'current_method' );
		$p->setAccessible( true );
		$p->setValue( $this, 'media' );
	}
}

function bundle( $log, $lines ) {
	global $wpdb;
	file_put_contents( $log, implode( "\n", $lines ) . "\n" );
	$wpdb->inserted = array();
	$p = ( new ReflectionClass( 'Probe' ) )->newInstanceWithoutConstructor();
	$p->arm( $log );
	$id = $p->create_support_bundle( 'Post #12 cannot be scanned', array( 'Code' => 'wpmc_failed', 'Phase' => 'extractReferences' ) );
	return array( $id, $wpdb->inserted[0]['logs'] );
}

// Reference spam is dropped; everything that explains the failure survives.
$noise = array();
for ( $i = 0; $i < 5000; $i++ ) {
	$noise[] = "2026-09-21 03:26:22: ＋ 2020/10/MD-Crutch-Bild-$i-450x300-1.jpg (as URL)";
	$noise[] = "2026-09-21 03:26:22: ＋ Media #$i (as ID)";
}
$signal = array(
	'2026-09-21 03:26:22: 🏁 Extracting refs from content...',
	'2026-09-21 03:26:30: ▶ wpmc_scan_once_smartslider3 (wpmc_scan_once)',
	'2026-09-21 03:26:53: 🐌 wpmc_scan_once_smartslider3 (wpmc_scan_once) took 51.20s, 90 queries',
	'2026-09-21 03:26:53: 😵 Timeout! Some info for debug:',
);
list( $id, $logs ) = bundle( $log, array_merge( $noise, $signal, array( '2026-09-21 03:26:53: Flushing remaining 3 references to the database...' ) ) );

assert( strlen( $id ) === 12 && ctype_alnum( $id ), 'the support ID is short enough to read out: ' . $id );
assert( strpos( $logs, 'Support ID: ' . $id ) !== false, 'the bundle names its own ID' );
foreach ( $signal as $line ) {
	assert( strpos( $logs, substr( $line, 21 ) ) !== false, 'kept: ' . $line );
}
assert( strpos( $logs, '＋' ) === false, 'every reference line is dropped' );
assert( strpos( $logs, 'Flushing remaining' ) === false, 'and so are the flush counters' );
assert( strpos( $logs, 'Post #12 cannot be scanned' ) !== false, 'the error itself is in the bundle' );
assert( strpos( $logs, 'wpmc_failed' ) !== false, 'with its code' );

// Support reads the environment before it reads a log, so it travels with it.
foreach ( array( 'PHP:', 'max_execution_time:', 'Work budget:', 'Method: media', 'Multilingual: no' ) as $needle ) {
	assert( strpos( $logs, $needle ) !== false, 'environment includes ' . $needle );
}

// A bundle is pasted into a thread by a human, so it stays small even when the log did not.
assert( strlen( $logs ) < 300 * 1024, 'the bundle is bounded: ' . strlen( $logs ) );

// 10,000 noise lines must not push the failure out of the bundle: the tail is what is kept.
assert( strpos( $logs, '😵 Timeout!' ) !== false, 'the newest lines survive a huge log' );

// With debug logs off there is nothing to collect, and the bundle has to say so rather than look
// like the failure left no trace.
class QuietProbe extends Probe { function is_debug() { return false; } }
file_put_contents( $log, '' );
$wpdb->inserted = array();
$q = ( new ReflectionClass( 'QuietProbe' ) )->newInstanceWithoutConstructor();
$q->arm( $log );
$q->create_support_bundle( 'Something failed', array() );
assert( strpos( $wpdb->inserted[0]['logs'], 'Debug Logs were off' ) !== false, 'an empty log explains itself' );

// A fatal leaves no response, so the bundle is the only record of it. It has to name the step and
// the item, because "deletion stopped" and "deletion stopped in wp_delete_attachment on #123" are
// not the same report.
file_put_contents( $log, implode( "\n", array(
	'2026-09-21 03:26:22: 🗑 delete #4821 2019/05/photo.jpg (+ 16 thumbnails)',
	'2026-09-21 03:26:22: → forget_stale_trash_rows',
	'2026-09-21 03:26:23: → wp_delete_attachment',
) ) . "\n" );
$wpdb->inserted = array();
$f = ( new ReflectionClass( 'Probe' ) )->newInstanceWithoutConstructor();
$f->arm( $log );
$f->create_support_bundle( 'Allowed memory size of 268435456 bytes exhausted', array(
	'Code' => 'wpmc_fatal_error', 'Item' => '#4821 2019/05/photo.jpg', 'Phase' => 'cleanup_delete',
) );
$fatal = $wpdb->inserted[0]['logs'];
assert( strpos( $fatal, 'Allowed memory size' ) !== false, 'the fatal is the headline' );
assert( strpos( $fatal, '#4821 2019/05/photo.jpg' ) !== false, 'the item is named' );
assert( strpos( $fatal, 'cleanup_delete' ) !== false, 'and the phase' );
assert( strpos( $fatal, 'wp_delete_attachment' ) !== false, 'and the step it died in survives the filter' );

// Old bundles are pruned rather than accumulating forever.
assert( count( $wpdb->queries ) > 0 && stripos( $wpdb->queries[0], 'DELETE FROM' ) === 0, 'old bundles are pruned' );

unlink( $log );
echo "ok\n";
