#!/usr/bin/env php
<?php

/*
 * artisan — a vendor-free stand-in for Laravel's console entry point
 * (STORY-334 T-PB-334f).
 *
 * WHY a stand-in: an example the engine's package CI runs must work offline
 * and in seconds, and a real Laravel application cannot run `artisan test`
 * without a `composer install` of ~80 packages. So this example keeps the
 * repository facts the stack-laravel pack keys on (this file, composer.json
 * requiring laravel/framework, app/ routes/ tests/Unit tests/Feature) and
 * implements exactly the command contract the pack's gates and suites call:
 *
 *   php artisan test [--testsuite=Unit|Feature] [--log-junit=<file>]
 *       runs every public test* method of every tests/<Suite>/*Test.php class,
 *       prints PHPUnit's summary ("OK (N tests, M assertions)" or
 *       "FAILURES! / Tests: N, Assertions: M, Failures: F."), writes a JUnit
 *       log when asked, and exits non-zero on any failure.
 *   php artisan --version
 *
 * A real Laravel application replaces this file with the framework's own; the
 * pack's commands are unchanged.
 */

declare(strict_types=1);

const SUITES = ['Unit', 'Feature'];

spl_autoload_register(static function (string $class): void {
    $map = ['App\\' => __DIR__ . '/app/', 'Tests\\' => __DIR__ . '/tests/'];
    foreach ($map as $prefix => $dir) {
        if (str_starts_with($class, $prefix)) {
            $file = $dir . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
            if (is_file($file)) {
                require $file;
            }
            return;
        }
    }
});

/** @return array{suite: ?string, junit: ?string} */
function parse_test_options(array $args): array
{
    $opts = ['suite' => null, 'junit' => null];
    foreach ($args as $arg) {
        if (str_starts_with($arg, '--testsuite=')) {
            $opts['suite'] = substr($arg, strlen('--testsuite='));
        } elseif (str_starts_with($arg, '--log-junit=')) {
            $opts['junit'] = substr($arg, strlen('--log-junit='));
        } else {
            fwrite(STDERR, "artisan test: unsupported option '{$arg}'\n");
            exit(2);
        }
    }
    if ($opts['suite'] !== null && !in_array($opts['suite'], SUITES, true)) {
        fwrite(STDERR, "artisan test: unknown test suite '{$opts['suite']}'\n");
        exit(2);
    }
    return $opts;
}

/** @return list<array{suite: string, class: string, file: string}> */
function discover_test_classes(?string $only): array
{
    $found = [];
    foreach ($only !== null ? [$only] : SUITES as $suite) {
        $files = glob(__DIR__ . "/tests/{$suite}/*Test.php") ?: [];
        sort($files);
        foreach ($files as $file) {
            $found[] = ['suite' => $suite, 'class' => "Tests\\{$suite}\\" . basename($file, '.php'), 'file' => $file];
        }
    }
    return $found;
}

/** @return list<array{suite: string, class: string, name: string, file: string, assertions: int, failure: ?string}> */
function run_test_classes(array $classes): array
{
    $results = [];
    foreach ($classes as $c) {
        if (!class_exists($c['class'])) {
            $results[] = ['suite' => $c['suite'], 'class' => $c['class'], 'name' => '(class)', 'file' => $c['file'],
                'assertions' => 0, 'failure' => "class {$c['class']} not found in {$c['file']}"];
            continue;
        }
        foreach (get_class_methods($c['class']) as $method) {
            if (!str_starts_with($method, 'test')) {
                continue;
            }
            $case = new $c['class']();
            $failure = null;
            try {
                $case->{$method}();
            } catch (\Throwable $e) {
                $failure = get_class($e) . ': ' . $e->getMessage();
            }
            $results[] = ['suite' => $c['suite'], 'class' => $c['class'], 'name' => $method, 'file' => $c['file'],
                'assertions' => $case->assertions, 'failure' => $failure];
            echo $failure === null ? '.' : 'F';
        }
    }
    echo "\n\n";
    return $results;
}

function write_junit(string $path, array $results): void
{
    $x = static fn (string $s): string => htmlspecialchars($s, ENT_XML1 | ENT_QUOTES);
    $bySuite = [];
    foreach ($results as $r) {
        $bySuite[$r['suite']][] = $r;
    }
    $xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<testsuites>\n";
    foreach ($bySuite as $suite => $rs) {
        $failures = count(array_filter($rs, static fn (array $r): bool => $r['failure'] !== null));
        $assertions = array_sum(array_column($rs, 'assertions'));
        $xml .= sprintf("  <testsuite name=\"%s\" tests=\"%d\" assertions=\"%d\" errors=\"0\" failures=\"%d\" skipped=\"0\" time=\"0\">\n",
            $x($suite), count($rs), $assertions, $failures);
        foreach ($rs as $r) {
            $xml .= sprintf('    <testcase name="%s" class="%s" file="%s" assertions="%d" time="0"',
                $x($r['name']), $x($r['class']), $x($r['file']), $r['assertions']);
            $xml .= $r['failure'] === null ? "/>\n"
                : ">\n      <failure type=\"AssertionFailed\">" . $x($r['failure']) . "</failure>\n    </testcase>\n";
        }
        $xml .= "  </testsuite>\n";
    }
    $xml .= "</testsuites>\n";
    if (!is_dir(dirname($path)) || file_put_contents($path, $xml) === false) {
        fwrite(STDERR, "artisan test: cannot write the JUnit log to {$path}\n");
        exit(2);
    }
}

function command_test(array $args): int
{
    $opts = parse_test_options($args);
    $results = run_test_classes(discover_test_classes($opts['suite']));
    if ($opts['junit'] !== null) {
        write_junit($opts['junit'], $results);
    }
    $tests = count($results);
    $assertions = array_sum(array_column($results, 'assertions'));
    $failed = array_values(array_filter($results, static fn (array $r): bool => $r['failure'] !== null));
    if ($tests === 0) {
        // PHPUnit's own behaviour: an empty run exits 0. The engine's
        // zero-tests guard, not the test runner, is what fails it.
        echo "No tests executed!\n";
        return 0;
    }
    if ($failed === []) {
        printf("OK (%d test%s, %d assertion%s)\n", $tests, $tests === 1 ? '' : 's', $assertions, $assertions === 1 ? '' : 's');
        return 0;
    }
    foreach ($failed as $i => $r) {
        printf("%d) %s::%s\n%s\n\n", $i + 1, $r['class'], $r['name'], $r['failure']);
    }
    printf("FAILURES!\nTests: %d, Assertions: %d, Failures: %d.\n", $tests, $assertions, count($failed));
    return 1;
}

$argv = $_SERVER['argv'];
$command = $argv[1] ?? 'list';
exit(match ($command) {
    'test' => command_test(array_slice($argv, 2)),
    '--version', '-V' => (static function (): int { echo "Laravel Framework (vendor-free example stand-in)\n"; return 0; })(),
    default => (static function () use ($command): int {
        fwrite(STDERR, "artisan: this example stand-in implements only `test` and `--version` (got '{$command}')\n");
        return 2;
    })(),
});
