<?php
// Pure tokenizer tests: no application bootstrap or database connection.
require __DIR__ . '/inventory.php';

$cases = 0;
function checkInventory($condition, $label) {
    global $cases;
    $cases++;
    if (!$condition) { throw new RuntimeException($label); }
}

$source = <<<'PHP'
<?php
namespace app\demo;
use think\Db as Database;
use app\common\model\User as Person;
class Service {
    public function run() {
        // Db::query('secret'); $fake->save();
        $literal = "Db::query('not executed') {$fake->save()}";
        $nowdoc = <<<'TXT'
        Db::execute('not executed');
        TXT;
        $rows = Database::name('user')->where('id', 1)->select();
        $person = Person::where('id', 1)->find();
        $person->save();
        $pdo = new \PDO('secret-dsn', 'secret-user', 'secret-password');
        $statement = $pdo->prepare('SELECT secret FROM private');
        $statement->execute();
        $statement->fetchAll();
        db('user')->count();
        $callback = function () { Database::query('SELECT 1'); };
    }
}
function outside() { Database::execute('SELECT 1'); }
PHP;
$rows = inventorySource($source, 'fixture', 'app/Service.php');
$methods = array_column($rows, 'method');
checkInventory(count(array_filter($methods, function ($m) { return $m === 'query'; })) === 1, 'Strings, interpolated strings, heredoc and comments are not executable callsites');
checkInventory(count(array_filter($methods, function ($m) { return $m === 'execute'; })) === 2, 'Only statement execute and outside facade execute');
$select = array_values(array_filter($rows, function ($r) { return $r['method'] === 'select'; }))[0];
checkInventory($select['class'] === 'app\\demo\\Service' && $select['function'] === 'run', 'Class/function lexical scope');
checkInventory($select['line'] === 12 && $select['call_kind'] === 'facade_chain_terminal', 'Aliased facade chain and exact line: ' . json_encode($select));
$find = array_values(array_filter($rows, function ($r) { return $r['method'] === 'find'; }))[0];
checkInventory($find['call_kind'] === 'model_chain_terminal', 'Imported model chain');
$query = array_values(array_filter($rows, function ($r) { return $r['method'] === 'query'; }))[0];
checkInventory(strpos($query['function'], '{closure@') === 0, 'Closure lexical scope');
$outside = end($rows);
checkInventory($outside['class'] === '' && $outside['function'] === 'outside', 'Scope exits class correctly');
checkInventory(strpos(json_encode($rows), 'secret') === false, 'No SQL values, DSNs or literal payloads in inventory');
checkInventory(count(array_filter($rows, function ($r) { return $r['status'] !== 'potential'; })) === 0, 'Never mark static candidates tested');
checkInventory(count(array_unique(array_column($rows, 'callsite_id'))) === count($rows), 'Callsite IDs unique');

$other = inventorySource('<?php use Other\\Db; Db::query("x"); $writer->save("output.xlsx"); $thing->$method(); $pdo->{"query"}("x");', 'fixture', 'other.php');
checkInventory($other[0]['call_kind'] === 'static_terminal_candidate', 'Unrelated Db alias not misclassified as ThinkPHP facade');
checkInventory($other[1]['call_kind'] === 'object_terminal_candidate', 'Unknown save receiver remains ambiguous candidate');
checkInventory(count(array_filter($other, function ($r) { return $r['call_kind'] === 'dynamic_call_unresolved'; })) === 2, 'Dynamic methods retained as unresolved candidates');

$model = inventorySource('<?php namespace app\\common\\model; class User extends \\think\\Model { function x(){ return self::where("id",1)->find(); } }', 'fixture', 'model/User.php');
checkInventory(end($model)['call_kind'] === 'model_chain_terminal', 'Self chain inside model namespace');
$sameLine = inventorySource('<?php Db::query("x"); Db::query("y");', 'fixture', 'same.php');
checkInventory($sameLine[0]['column'] !== $sameLine[1]['column'] && $sameLine[0]['callsite_id'] !== $sameLine[1]['callsite_id'], 'Same-line calls remain separate');
$properties = inventorySource('<?php $thing->$property; $thing->{"name"}; Thing::$property; Thing::{$property};', 'fixture', 'properties.php');
checkInventory(count($properties) === 0, 'Dynamic properties are not method callsites');
checkInventory($select['phase'] === 'execution_candidate' && $select['operation'] === 'read', 'Execution candidates separate from builders');
$groups = inventorySource('<?php use think\\{Db as Q, Model}; Q::table("x")->select();', 'fixture', 'groups.php');
checkInventory(end($groups)['call_kind'] === 'facade_chain_terminal', 'Grouped import aliases supported');
echo "PASS: {$cases} inventory assertions\n";
