<?php
declare(strict_types=1);

if (PHP_SAPI !== 'cli') {
    http_response_code(404);
    exit;
}

require_once dirname(__DIR__) . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'bootstrap.php';
app_bootstrap();

use App\Admin\AdminRepository;
use App\Audit\AuditLogger;
use App\Security\HtmlSanitizer;
use App\Support\Translator;
use App\Widgets\PublicWidgetRenderer;
use App\Database\Connection;
use App\Database\SchemaHealthService;
use App\Languages\LanguageRepository;

$writeMode = in_array('--write', $argv ?? [], true);
$results = [];
$failed = 0;

$run = static function (string $name, callable $callback) use (&$results, &$failed): mixed {
    try {
        $value = $callback();
        $results[] = ['ok' => true, 'name' => $name, 'detail' => is_int($value) ? (string) $value : 'OK'];
        return $value;
    } catch (Throwable $throwable) {
        $failed++;
        $results[] = ['ok' => false, 'name' => $name, 'detail' => $throwable->getMessage()];
        return null;
    }
};

if (!extension_loaded('pdo_mysql')) {
    fwrite(STDERR, "PDO MySQL is required. Enable pdo_mysql in the PHP CLI used for this command.\n");
    exit(2);
}

try {
    $pdo = Connection::connect(app_config('database'));
} catch (Throwable $throwable) {
    fwrite(STDERR, 'Connection failed: ' . $throwable->getMessage() . PHP_EOL);
    exit(2);
}

try {
    $health = (new SchemaHealthService($pdo, app_root_path('database', 'migrations')))->inspect();
    if (!$health['healthy']) {
        fwrite(STDERR, "Database schema is not ready for admin smoke tests.\n");
        foreach ($health['pending'] as $migration) {
            fwrite(STDERR, '- Pending migration: ' . $migration . PHP_EOL);
        }
        foreach ($health['issues'] as $issue) {
            fwrite(STDERR, '- ' . $issue . PHP_EOL);
        }
        fwrite(STDERR, "Run: php bin/migrate.php\n");
        exit(3);
    }
} catch (Throwable $throwable) {
    fwrite(STDERR, 'Schema health check failed: ' . $throwable->getMessage() . PHP_EOL);
    exit(3);
}

$repository = new AdminRepository($pdo);
$languageRepository = null;
try {
    $languageRepository = new LanguageRepository($pdo);
    $languageRepository->enabled();
} catch (Throwable) {
    $languageRepository = null;
}

$run('Dashboard statistics', static fn () => count($repository->stats()));
$run('Questions list', static fn () => count($repository->questions()));
$run('Answers list', static fn () => count($repository->answers()));
$run('Comments list', static fn () => count($repository->comments()));
$run('Categories list + hierarchy/SEO columns', static function () use ($repository): int {
    $rows = $repository->categories();
    foreach ($rows as $row) {
        foreach (['parent_id', 'seo_title', 'seo_description', 'canonical_url', 'seo_index'] as $key) {
            if (!array_key_exists($key, $row)) throw new RuntimeException('Missing category management column: ' . $key);
        }
    }
    return count($rows);
});
$run('Tags list', static fn () => count($repository->tags()));
$run('Users list', static fn () => count($repository->users()));
$run('Reports list + target context', static function () use ($repository): int {
    $rows = $repository->reports();
    foreach ($rows as $row) {
        foreach (['reportable_type', 'reportable_id', 'question_title', 'question_slug', 'target_content'] as $key) {
            if (!array_key_exists($key, $row)) throw new RuntimeException('Missing report context column: ' . $key);
        }
    }
    return count($rows);
});
$run('Admin topbar notifications query', static function () use ($repository, $pdo): int {
    $adminId = (int) $pdo->query("SELECT id FROM users WHERE role='admin' AND status='active' ORDER BY id ASC LIMIT 1")->fetchColumn();
    if ($adminId <= 0) throw new RuntimeException('No active administrator exists for notification smoke test.');
    return count($repository->recentUnreadNotifications($adminId, 5));
});
$run('Contact messages list', static fn () => count($repository->contactMessages()));
$run('Widget/layout manager', static function () use ($repository): int {
    $widgets = $repository->widgets();
    return count($widgets['layouts']) + count($widgets['sections']) + count($widgets['widgets']);
});
$run('Settings repository', static fn () => count($repository->settings()->all()));

if ($languageRepository instanceof LanguageRepository) {
    $run('Installed languages', static fn () => count($languageRepository->all()));
    $run('Enabled languages', static fn () => count($languageRepository->enabled()));
} else {
    $failed++;
    $results[] = ['ok' => false, 'name' => 'Language management', 'detail' => 'Migration 012 is not applied. Run: php bin/migrate.php'];
}

if ($writeMode && $failed === 0 && $languageRepository instanceof LanguageRepository) {
    $pdo->beginTransaction();
    try {
        $enabled = $languageRepository->enabled();
        $locale = (string) ($enabled[0]['code'] ?? 'ar');
        $suffix = strtolower(bin2hex(random_bytes(4)));
        $parentId = (int) $run('Write: create parent category', static fn () => $repository->saveCategory([
            'name' => 'Smoke Parent ' . $suffix,
            'slug' => 'smoke-parent-' . $suffix,
            'content_locale' => $locale,
            'sort_order' => 9990,
        ]));
        $childId = (int) $run('Write: create child category', static fn () => $repository->saveCategory([
            'name' => 'Smoke Child ' . $suffix,
            'slug' => 'smoke-child-' . $suffix,
            'content_locale' => $locale,
            'parent_id' => $parentId,
            'sort_order' => 9991,
        ]));
        $run('Write: category cycle protection', static function () use ($repository, $parentId, $childId, $locale, $suffix): int {
            try {
                $repository->saveCategory([
                    'id' => $parentId,
                    'name' => 'Smoke Parent ' . $suffix,
                    'slug' => 'smoke-parent-' . $suffix,
                    'content_locale' => $locale,
                    'parent_id' => $childId,
                    'sort_order' => 9990,
                ]);
            } catch (InvalidArgumentException) {
                return 1;
            }
            throw new RuntimeException('Cycle protection did not reject a parent -> child -> parent loop.');
        });
        $movableId = (int) $run('Write: create movable root category', static fn () => $repository->saveCategory([
            'name' => 'Smoke Movable ' . $suffix,
            'slug' => 'smoke-movable-' . $suffix,
            'content_locale' => $locale,
            'sort_order' => 9992,
        ]));
        $run('Write: convert existing root to child', static function () use ($repository, $movableId, $parentId, $locale, $suffix): int {
            $repository->saveCategory([
                'id' => $movableId,
                'name' => 'Smoke Movable ' . $suffix,
                'slug' => 'smoke-movable-' . $suffix,
                'content_locale' => $locale,
                'parent_id' => $parentId,
                'sort_order' => 9992,
            ]);
            foreach ($repository->categories() as $category) {
                if ((int) ($category['id'] ?? 0) === $movableId) {
                    return (int) ($category['parent_id'] ?? 0) === $parentId ? 1 : 0;
                }
            }
            throw new RuntimeException('Reassigned category was not returned by the repository.');
        });
        $sourceTagId = (int) $run('Write: create source tag', static fn () => $repository->saveTag([
            'name' => 'smoke-source-' . $suffix,
            'slug' => 'smoke-source-' . $suffix,
            'content_locale' => $locale,
            'synonyms' => 'smoke-source-alias-' . $suffix,
        ]));
        $targetTagId = (int) $run('Write: create target tag', static fn () => $repository->saveTag([
            'name' => 'smoke-target-' . $suffix,
            'slug' => 'smoke-target-' . $suffix,
            'content_locale' => $locale,
        ]));
        $run('Write: merge tags and preserve source name', static function () use ($repository, $pdo, $sourceTagId, $targetTagId, $suffix): int {
            $repository->mergeTags($sourceTagId, $targetTagId, true);
            $sourceCheck = $pdo->prepare('SELECT COUNT(*) FROM tags WHERE id = :id');
            $sourceCheck->execute(['id' => $sourceTagId]);
            if ((int) $sourceCheck->fetchColumn() !== 0) throw new RuntimeException('Source tag still exists after merge.');
            $synonymCheck = $pdo->prepare('SELECT COUNT(*) FROM tag_synonyms WHERE tag_id = :tag_id AND synonym = :synonym');
            $synonymCheck->execute(['tag_id' => $targetTagId, 'synonym' => 'smoke-source-' . $suffix]);
            return (int) $synonymCheck->fetchColumn() > 0 ? 1 : 0;
        });
        $layoutId = (int) $run('Write: create layout', static fn () => $repository->saveLayout([
            'name' => 'Smoke Layout ' . $suffix,
            'slug' => 'smoke-layout-' . $suffix,
            'status' => 'draft',
        ]));
        $sectionId = (int) $run('Write: create layout section', static fn () => $repository->saveSection([
            'layout_id' => $layoutId,
            'section_key' => 'smoke_' . $suffix,
            'name' => 'Smoke Section',
            'sort_order' => 9990,
        ]));
        if ($sectionId <= 0) {
            $statement = $pdo->prepare('SELECT id FROM layout_sections WHERE layout_id = :layout_id AND section_key = :section_key LIMIT 1');
            $statement->execute(['layout_id' => $layoutId, 'section_key' => 'smoke_' . $suffix]);
            $sectionId = (int) $statement->fetchColumn();
        }
        $widgetId = (int) $run('Write: create safe widget', static fn () => $repository->saveWidget([
            'layout_section_id' => $sectionId,
            'type' => 'latest_questions',
            'title' => 'Smoke Widget',
            'config' => ['limit' => 5, 'category_id' => null],
        ]));
        $publicLayoutId = (int) $run('Write: create published homepage layout', static fn () => $repository->saveLayout([
            'name' => 'Smoke Public Layout ' . $suffix,
            'slug' => 'smoke-public-layout-' . $suffix,
            'status' => 'published',
        ]));
        $publicSectionId = (int) $run('Write: create homepage_main section', static fn () => $repository->saveSection([
            'layout_id' => $publicLayoutId,
            'section_key' => 'homepage_main',
            'name' => 'Homepage main',
            'sort_order' => 1,
        ]));
        if ($publicSectionId <= 0) {
            $statement = $pdo->prepare('SELECT id FROM layout_sections WHERE layout_id = :layout_id AND section_key = :section_key LIMIT 1');
            $statement->execute(['layout_id' => $publicLayoutId, 'section_key' => 'homepage_main']);
            $publicSectionId = (int) $statement->fetchColumn();
        }
        $run('Write: create public categories widget', static fn () => $repository->saveWidget([
            'layout_section_id' => $publicSectionId,
            'type' => 'categories',
            'title' => 'Smoke Categories',
            'config' => [],
            'sort_order' => 1,
            'is_enabled' => 1,
        ]));
        $run('Write: render published homepage widgets', static function () use ($pdo): int {
            $renderer = new PublicWidgetRenderer($pdo, new HtmlSanitizer());
            $html = $renderer->renderHomepage(new Translator('en', 'en'), static fn (string $path = ''): string => '/' . ltrim($path, '/'));
            if (!str_contains($html, 'Smoke Categories')) throw new RuntimeException('Published homepage widget did not render.');
            return 1;
        });
        $run('Write: edit widget', static function () use ($repository, $widgetId, $sectionId): int {
            $savedId = $repository->saveWidget([
                'id' => $widgetId,
                'layout_section_id' => $sectionId,
                'type' => 'categories',
                'title' => 'Smoke Widget Updated',
                'config' => [],
                'sort_order' => 9991,
                'is_enabled' => 0,
            ]);
            return $savedId === $widgetId ? 1 : 0;
        });
        $run('Write: settings round-trip', static function () use ($repository, $suffix): int {
            $repository->settings()->put('smoke.admin.' . $suffix, 'ok', 'string');
            return $repository->settings()->get('smoke.admin.' . $suffix) === 'ok' ? 1 : 0;
        });

        $temporaryPassword = password_hash('Quaeriva-Smoke-User-2026!', PASSWORD_DEFAULT);
        if (!is_string($temporaryPassword)) throw new RuntimeException('Could not hash the smoke-test user password.');
        $temporaryEmail = 'smoke-' . $suffix . '@example.test';
        $insertTemporaryUser = $pdo->prepare("INSERT INTO users (name, email, password_hash, role, status, preferred_locale, email_verified_at, created_at, updated_at) VALUES (:name, :email, :password_hash, 'member', 'active', :locale, NOW(), NOW(), NOW())");
        $insertTemporaryUser->execute(['name' => 'Smoke User', 'email' => $temporaryEmail, 'password_hash' => $temporaryPassword, 'locale' => $locale]);
        $temporaryUserId = (int) $pdo->lastInsertId();
        $run('Write: user suspend/activate', static function () use ($repository, $temporaryUserId): int {
            $repository->setUserStatus($temporaryUserId, 'suspended');
            $repository->setUserStatus($temporaryUserId, 'active');
            return 1;
        });
        $run('Write: delete temporary user', static function () use ($repository, $pdo, $temporaryUserId): int {
            $repository->deleteUser($temporaryUserId);
            $statement = $pdo->prepare('SELECT COUNT(*) FROM users WHERE id = :id');
            $statement->execute(['id' => $temporaryUserId]);
            return (int) $statement->fetchColumn() === 0 ? 1 : 0;
        });

        $adminId = (int) $pdo->query("SELECT id FROM users WHERE role = 'admin' AND status = 'active' ORDER BY id ASC LIMIT 1")->fetchColumn();
        $run('Write: translation override', static function () use ($languageRepository, $locale, $adminId, $suffix): int {
            $key = 'admin.smoke.' . $suffix;
            $languageRepository->saveOverride($locale, $key, 'smoke', $adminId);
            return ($languageRepository->overrides($locale)[$key] ?? null) === 'smoke' ? 1 : 0;
        });
        if ($adminId > 0) {
            $run('Write: admin locale preference', static function () use ($languageRepository, $locale, $adminId): int {
                $languageRepository->setAdminLocaleForUser($adminId, $locale);
                return $languageRepository->adminLocaleForUser($adminId) === $locale ? 1 : 0;
            });
            $run('Write: audit log persistence', static function () use ($pdo, $adminId): int {
                (new AuditLogger($pdo))->log($adminId, 'smoke.audit', 'system', 1, null, ['ok' => true], '127.0.0.1');
                $statement=$pdo->prepare("SELECT COUNT(*) FROM audit_log WHERE actor_user_id=:actor AND action='smoke.audit'");
                $statement->execute(['actor'=>$adminId]);
                return (int)$statement->fetchColumn() > 0 ? 1 : 0;
            });
        }
    } finally {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
    }
}

fwrite(STDOUT, 'Quaeriva ' . app_version() . ' admin smoke test' . ($writeMode ? ' (transactional write mode)' : ' (read-only mode)') . PHP_EOL);
foreach ($results as $result) {
    fwrite(STDOUT, sprintf("[%s] %-38s %s\n", $result['ok'] ? 'OK' : 'FAIL', $result['name'], $result['detail']));
}
if ($writeMode) {
    fwrite(STDOUT, "All write-mode changes are rolled back before exit.\n");
}
exit($failed === 0 ? 0 : 1);
