<?php
// scripts/test-coraza-rule-manager.php — smoke tests for coraza_rule_manager.
// Uses an isolated temp DB so it can run on dev machines without touching prod state.
// Run with: php scripts/test-coraza-rule-manager.php
// Exits 0 on success, 1 on failure.

require_once __DIR__ . '/../web-files/configs/config.php';

// Override db_path + regen_script for tests by patching the source in-memory.
$tmp_db = tempnam(sys_get_temp_dir(), 'coraza-test-') . '.db';
$src = file_get_contents(__DIR__ . '/../web-files/libs/coraza_rule_manager.php');
$src = str_replace(
    "private string \$db_path = '/docker/whp/sql/security.db';",
    "private string \$db_path = '$tmp_db';",
    $src
);
$src = str_replace(
    "private string \$regen_script = '/root/whp/scripts/regenerate-coraza-overrides.sh';",
    "private string \$regen_script = '/bin/true';",  // no-op for tests
    $src
);
eval('?>' . $src);

// Initialize test DB schema.
$pdo = new SQLite3($tmp_db);
$pdo->exec("CREATE TABLE coraza_rule_overrides (rule_id INTEGER PRIMARY KEY, state TEXT NOT NULL, note TEXT, updated_at INTEGER NOT NULL, updated_by TEXT NOT NULL);");
$pdo->exec("CREATE TABLE coraza_rule_host_exceptions (rule_id INTEGER, hostname TEXT, note TEXT, updated_at INTEGER NOT NULL, updated_by TEXT NOT NULL, PRIMARY KEY (rule_id, hostname));");
$pdo->exec("CREATE TABLE coraza_rule_audit_log (id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER NOT NULL, actor TEXT NOT NULL, rule_id INTEGER NOT NULL, action TEXT NOT NULL, payload TEXT NOT NULL);");

$failures = 0;
function assertEq($expected, $actual, string $label): void {
    global $failures;
    if ($expected !== $actual) {
        echo "  FAIL  $label\n    expected: " . var_export($expected, true) . "\n    actual:   " . var_export($actual, true) . "\n";
        $failures++;
    } else {
        echo "  PASS  $label\n";
    }
}
function assertContains(string $needle, $haystack, string $label): void {
    global $failures;
    if (!is_array($haystack) && !is_string($haystack)) { echo "  FAIL  $label (haystack wrong type)\n"; $failures++; return; }
    $found = is_array($haystack) ? in_array($needle, $haystack, true) : (strpos($haystack, $needle) !== false);
    if (!$found) { echo "  FAIL  $label (missing: $needle)\n"; $failures++; }
    else { echo "  PASS  $label\n"; }
}

$m = new coraza_rule_manager();

echo "--- get_rule_state on unset returns null ---\n";
assertEq(null, $m->get_rule_state(913100), 'unset rule returns null');

echo "--- set_rule_state inserts and audits ---\n";
$m->set_rule_state(913100, 'off', 'josh', 'mastodon FP');
$st = $m->get_rule_state(913100);
assertEq('off', $st['state'], 'state stored');
assertEq('josh', $st['updated_by'], 'actor stored');
$audit = $m->recent_audit(10);
assertEq(1, count($audit), 'one audit row');
assertEq('set_state', $audit[0]['action'], 'audit action');
assertEq('off', $audit[0]['payload']['new'], 'audit payload new');

echo "--- set_rule_state upserts ---\n";
$m->set_rule_state(913100, 'detect', 'josh');
assertEq('detect', $m->get_rule_state(913100)['state'], 'upsert overwrote state');
assertEq(2, count($m->recent_audit(10)), 'two audit rows after upsert');

echo "--- invalid state rejected ---\n";
try {
    $m->set_rule_state(913100, 'banana', 'josh');
    echo "  FAIL  invalid state should throw\n"; $failures++;
} catch (InvalidArgumentException $e) {
    echo "  PASS  invalid state throws\n";
}

echo "--- clear_rule_state removes the row ---\n";
$m->set_rule_state(942100, 'enforce', 'josh');
$m->clear_rule_state(942100, 'josh');
assertEq(null, $m->get_rule_state(942100), 'rule cleared returns null');

echo "--- add_host_exception + list ---\n";
$m->add_host_exception(913100, 'hackerpublicradio.org', 'josh', 'mastodon');
$m->add_host_exception(913100, 'greggfranklin.com', 'josh');
$hosts = $m->list_host_exceptions(913100);
assertEq(2, count($hosts), 'two hosts listed');
assertContains('hackerpublicradio.org', array_column($hosts, 'hostname'), 'hpr present');
assertContains('greggfranklin.com', array_column($hosts, 'hostname'), 'gregg present');

echo "--- invalid hostname rejected ---\n";
try {
    $m->add_host_exception(913100, "evil; rm -rf /", 'josh');
    echo "  FAIL  invalid hostname should throw\n"; $failures++;
} catch (InvalidArgumentException $e) {
    echo "  PASS  invalid hostname throws\n";
}

echo "--- remove_host_exception ---\n";
$m->remove_host_exception(913100, 'hackerpublicradio.org', 'josh');
assertEq(1, count($m->list_host_exceptions(913100)), 'one host after removal');

echo "--- list_all_host_exceptions shape ---\n";
$all = $m->list_all_host_exceptions();
assertEq(['greggfranklin.com'], $all[913100], 'all-exceptions structure');

unlink($tmp_db);

echo "\n";
if ($failures > 0) { echo "FAILED: $failures\n"; exit(1); }
echo "OK\n";
exit(0);
