<?php

/**
 * Pure Logic Unit Test
 *
 * Tests deterministic, I/O-free logic extracted from controllers:
 *
 * TfeController (via subclass to access protected methods):
 *   - buildMetaDescription()  — truncation at 160 chars, empty synopsis fallback
 *   - resolveOgImage()        — cover preferred over image, fallback chain
 *   - splitJuryByRole()       — correct binning of promoteur/lecteur/president/ulb
 *   - collectCaptionPaths()   — VTT files extracted in order
 *
 * ThesisCreateController (static / public helpers):
 *   - autofocusFieldForError()
 *   - detectFileType()        — via subclass to access private
 *   - generateAuthorSlug()    — accent stripping, uppercase, underscore
 *
 * ExportController:
 *   - CSV_HEADERS count matches exportAllTheses() row width
 *
 * SearchController:
 *   - handleSearch() return array always contains 'coverMap' key (regression for undefined var bug)
 */

putenv('DB_ENV=test');

if (!defined('APP_ROOT')) {
    define('APP_ROOT', dirname(__DIR__, 2));
}
if (!defined('STORAGE_ROOT')) {
    define('STORAGE_ROOT', APP_ROOT . '/storage');
}

require_once APP_ROOT . '/src/Database.php';
require_once APP_ROOT . '/src/RateLimit.php';
require_once APP_ROOT . '/src/Controllers/TfeController.php';
require_once APP_ROOT . '/src/Controllers/ThesisCreateController.php';
require_once APP_ROOT . '/src/Controllers/SearchController.php';
require_once APP_ROOT . '/src/Controllers/ExportController.php';

// ── Test harness helpers ──────────────────────────────────────────────────────

function plAssert(bool $cond, string $label): void
{
    if ($cond) {
        echo "  ✓ $label\n";
    } else {
        throw new RuntimeException("FAIL: $label");
    }
}

function plAssertEq(mixed $expected, mixed $actual, string $label): void
{
    if ($expected === $actual) {
        echo "  ✓ $label\n";
    } else {
        $e = var_export($expected, true);
        $a = var_export($actual, true);
        throw new RuntimeException("FAIL $label\n    expected: $e\n    actual:   $a");
    }
}

// ── Subclasses to expose protected / private methods ─────────────────────────

class TfeControllerTestable extends TfeController
{
    public function testBuildMetaDescription(string $synopsis): string
    {
        return $this->buildMetaDescription($synopsis);
    }

    public function testResolveOgImage(array $files): string
    {
        return $this->resolveOgImage($files);
    }

    public function testSplitJuryByRole(array $jury): array
    {
        return $this->splitJuryByRole($jury);
    }

    public function testCollectCaptionPaths(array $files): array
    {
        return $this->collectCaptionPaths($files);
    }
}

class ThesisCreateControllerTestable extends ThesisCreateController
{
    public function testDetectFileType(string $mimeType, string $ext): string
    {
        return $this->detectFileType($mimeType, $ext);
    }

    public function testGenerateAuthorSlug(string $name): string
    {
        return $this->generateAuthorSlug($name);
    }
}

// ── Setup ─────────────────────────────────────────────────────────────────────

echo "Pure Logic Unit Test\n";
echo "====================\n\n";

$db              = Database::getInstance();
$tfe             = new TfeControllerTestable($db);
$createCtrl      = new ThesisCreateControllerTestable($db);

try {

    // =========================================================================
    // SECTION A: TfeController helpers
    // =========================================================================

    // ── A1: buildMetaDescription ──────────────────────────────────────────────
    echo "A1: buildMetaDescription — normal synopsis\n";
    $desc = $tfe->testBuildMetaDescription('A short synopsis.');
    plAssertEq('A short synopsis.', $desc, 'short synopsis returned as-is');
    echo "\n";

    echo "A2: buildMetaDescription — synopsis over 160 chars truncated\n";
    $long = str_repeat('a', 200);
    $desc = $tfe->testBuildMetaDescription($long);
    plAssert(strlen($desc) <= 160, 'length <= 160');
    plAssert(str_ends_with($desc, '…'), 'ends with ellipsis');
    echo "\n";

    echo "A3: buildMetaDescription — exactly 160 chars not truncated\n";
    $exact = str_repeat('b', 160);
    $desc = $tfe->testBuildMetaDescription($exact);
    plAssertEq($exact, $desc, '160-char synopsis not truncated');
    echo "\n";

    echo "A4: buildMetaDescription — empty synopsis returns fallback\n";
    $desc = $tfe->testBuildMetaDescription('');
    plAssert($desc !== '', 'non-empty fallback returned');
    plAssert(strlen($desc) <= 160, 'fallback length <= 160');
    echo "\n";

    echo "A5: buildMetaDescription — HTML tags stripped\n";
    $desc = $tfe->testBuildMetaDescription('<p>Hello <strong>world</strong></p>');
    plAssertEq('Hello world', $desc, 'HTML tags stripped');
    echo "\n";

    // ── A6: resolveOgImage ────────────────────────────────────────────────────
    echo "A6: resolveOgImage — cover preferred over image files\n";
    $files = [
        ['file_type' => 'image',  'file_path' => 'theses/2025/img.jpg'],
        ['file_type' => 'cover',  'file_path' => 'covers/cover.jpg'],
        ['file_type' => 'main',   'file_path' => 'theses/2025/main.pdf'],
    ];
    $url = $tfe->testResolveOgImage($files);
    plAssert(str_contains($url, rawurlencode('covers/cover.jpg')), 'cover used when available');
    echo "\n";

    echo "A7: resolveOgImage — falls back to first image when no cover\n";
    $files = [
        ['file_type' => 'main',  'file_path' => 'theses/2025/main.pdf'],
        ['file_type' => 'image', 'file_path' => 'theses/2025/img.jpg'],
        ['file_type' => 'image', 'file_path' => 'theses/2025/img2.png'],
    ];
    $url = $tfe->testResolveOgImage($files);
    plAssert(str_contains($url, rawurlencode('theses/2025/img.jpg')), 'first image file used as fallback');
    plAssert(!str_contains($url, rawurlencode('img2.png')), 'second image not used');
    echo "\n";

    echo "A8: resolveOgImage — empty string when no image at all\n";
    $files = [
        ['file_type' => 'main', 'file_path' => 'theses/2025/main.pdf'],
        ['file_type' => 'audio', 'file_path' => 'theses/2025/audio.mp3'],
    ];
    $url = $tfe->testResolveOgImage($files);
    plAssertEq('', $url, 'empty string when no image');
    echo "\n";

    echo "A9: resolveOgImage — empty files array returns empty string\n";
    plAssertEq('', $tfe->testResolveOgImage([]), 'empty array → empty string');
    echo "\n";

    // ── A10: splitJuryByRole ──────────────────────────────────────────────────
    echo "A10: splitJuryByRole — all roles correctly binned\n";
    $jury = [
        ['name' => 'Alice',   'role' => 'president',  'is_external' => 0, 'is_ulb' => 0],
        ['name' => 'Bob',     'role' => 'promoteur',  'is_external' => 0, 'is_ulb' => 0],
        ['name' => 'Carol',   'role' => 'promoteur',  'is_external' => 1, 'is_ulb' => 1],
        ['name' => 'Dave',    'role' => 'promoteur',  'is_external' => 1, 'is_ulb' => 0],
        ['name' => 'Eve',     'role' => 'lecteur',    'is_external' => 0, 'is_ulb' => 0],
        ['name' => 'Frank',   'role' => 'lecteur',    'is_external' => 1, 'is_ulb' => 0],
    ];
    $split = $tfe->testSplitJuryByRole($jury);

    plAssertEq(['Alice'],   $split['presidents'],        'president');
    plAssertEq(['Bob'],     $split['internes'],          'interne promoteur');
    plAssertEq(['Carol'],   $split['ulb'],               'ulb promoteur');
    plAssertEq(['Dave'],    $split['externes'],          'externe promoteur (non-ULB)');
    plAssertEq(['Eve'],     $split['lecteurs_internes'], 'lecteur interne');
    plAssertEq(['Frank'],   $split['lecteurs_externes'], 'lecteur externe');
    echo "\n";

    echo "A11: splitJuryByRole — empty name skipped\n";
    $jury = [['name' => '', 'role' => 'president', 'is_external' => 0, 'is_ulb' => 0]];
    $split = $tfe->testSplitJuryByRole($jury);
    plAssertEq([], $split['presidents'], 'empty name not added');
    echo "\n";

    echo "A12: splitJuryByRole — empty jury returns all-empty arrays\n";
    $split = $tfe->testSplitJuryByRole([]);
    plAssertEq([], $split['presidents'],        'presidents empty');
    plAssertEq([], $split['internes'],          'internes empty');
    plAssertEq([], $split['ulb'],               'ulb empty');
    plAssertEq([], $split['externes'],          'externes empty');
    plAssertEq([], $split['lecteurs_internes'], 'lecteurs_internes empty');
    plAssertEq([], $split['lecteurs_externes'], 'lecteurs_externes empty');
    echo "\n";

    // ── A13: collectCaptionPaths ──────────────────────────────────────────────
    echo "A13: collectCaptionPaths — VTT files extracted in order\n";
    $files = [
        ['mime_type' => 'application/pdf', 'file_path' => 'main.pdf'],
        ['mime_type' => 'text/vtt',        'file_path' => 'captions1.vtt'],
        ['mime_type' => 'video/mp4',       'file_path' => 'video.mp4'],
        ['mime_type' => 'text/plain',      'file_path' => 'captions2.vtt'],
    ];
    $captions = $tfe->testCollectCaptionPaths($files);
    plAssertEq(['captions1.vtt', 'captions2.vtt'], $captions, 'both VTT files returned in order');
    echo "\n";

    echo "A14: collectCaptionPaths — .vtt extension without mime match\n";
    $files = [
        ['mime_type' => 'application/octet-stream', 'file_path' => 'sub.vtt'],
        ['mime_type' => 'video/mp4',                'file_path' => 'video.mp4'],
    ];
    $captions = $tfe->testCollectCaptionPaths($files);
    plAssertEq(['sub.vtt'], $captions, '.vtt extension matches even with generic mime');
    echo "\n";

    echo "A15: collectCaptionPaths — no VTT returns empty array\n";
    $files = [
        ['mime_type' => 'video/mp4', 'file_path' => 'video.mp4'],
    ];
    plAssertEq([], $tfe->testCollectCaptionPaths($files), 'empty array when no VTT');
    echo "\n";

    // =========================================================================
    // SECTION B: ThesisCreateController helpers
    // =========================================================================

    // ── B1: autofocusFieldForError ────────────────────────────────────────────
    echo "B1: autofocusFieldForError — known error messages map to fields\n";
    $cases = [
        ["Titre du mémoire",             'titre'],
        ["Nom/Prénom/Pseudo",            'auteurice'],
        ["Synopsis",                     'synopsis'],
        ["Année invalide",               'année'],
        ["orientation",                  'orientation'],
        ["Atelier Pratique",             'ap'],
        ["finalité",                     'finality'],
        ["langue",                       'languages'],
        ["promoteur",                    'jury_promoteur'],
        ["lecteur·ice interne",          'jury_lecteur_interne[]'],
        ["lecteur·ice externe",          'jury_lecteur_externe[]'],
        ["format",                       'formats'],
        ["licence",                      'license_id'],
        ["Lien URL",                     'lien'],
    ];
    foreach ($cases as [$message, $expected]) {
        $actual = ThesisCreateController::autofocusFieldForError($message);
        plAssertEq($expected, $actual, "\"$message\" → $expected");
    }
    echo "\n";

    echo "B2: autofocusFieldForError — unknown message returns null\n";
    plAssertEq(null, ThesisCreateController::autofocusFieldForError('completely unknown'), 'null for unknown');
    echo "\n";

    // ── B3: detectFileType ────────────────────────────────────────────────────
    echo "B3: detectFileType — mime-based detection\n";
    $cases = [
        ['text/vtt',         'vtt',  'caption'],
        ['audio/mpeg',       'mp3',  'audio'],
        ['audio/ogg',        'ogg',  'audio'],
        ['video/mp4',        'mp4',  'video'],
        ['video/webm',       'webm', 'video'],
        ['application/pdf',  'pdf',  'main'],
        ['image/jpeg',       'jpg',  'image'],
        ['image/png',        'png',  'image'],
        ['application/zip',  'zip',  'other'],
    ];
    foreach ($cases as [$mime, $ext, $expected]) {
        $actual = $createCtrl->testDetectFileType($mime, $ext);
        plAssertEq($expected, $actual, "$mime / $ext → $expected");
    }
    echo "\n";

    echo "B4: detectFileType — extension-based fallback\n";
    // application/octet-stream with known extensions
    $cases = [
        ['application/octet-stream', 'mp3',  'audio'],
        ['application/octet-stream', 'mp4',  'video'],
        ['application/octet-stream', 'pdf',  'main'],
        ['application/octet-stream', 'webp', 'image'],
        ['application/octet-stream', 'vtt',  'caption'],
    ];
    foreach ($cases as [$mime, $ext, $expected]) {
        $actual = $createCtrl->testDetectFileType($mime, $ext);
        plAssertEq($expected, $actual, "octet-stream + .$ext → $expected");
    }
    echo "\n";

    // ── B5: generateAuthorSlug ────────────────────────────────────────────────
    echo "B5: generateAuthorSlug — basic ASCII\n";
    plAssertEq('JANE_DOE', $createCtrl->testGenerateAuthorSlug('Jane Doe'), 'spaces to underscores, uppercase');
    plAssertEq('AUTHOR',   $createCtrl->testGenerateAuthorSlug(''),         'empty → AUTHOR');
    echo "\n";

    echo "B6: generateAuthorSlug — French accents stripped\n";
    plAssertEq('ELEONORE_DUPONT', $createCtrl->testGenerateAuthorSlug('Éléonore Dupont'), 'accents stripped');
    plAssertEq('FRANCOISE',       $createCtrl->testGenerateAuthorSlug('Françoise'),       'ç → C');
    echo "\n";

    echo "B7: generateAuthorSlug — multiple authors comma-separated\n";
    $slug = $createCtrl->testGenerateAuthorSlug('Alice Martin, Bob Durand');
    plAssert(str_contains($slug, 'ALICE'), 'contains ALICE');
    plAssert(str_contains($slug, 'BOB'),   'contains BOB');
    echo "\n";

    // =========================================================================
    // SECTION C: ExportController — CSV column count consistency
    // =========================================================================

    echo "C1: ExportController — CSV_HEADERS count matches row column count\n";
    $export = new ExportController($db);
    $rows   = $export->exportAllTheses();
    $headerCount = count(ExportController::CSV_HEADERS);

    plAssert($headerCount > 0, 'CSV_HEADERS is non-empty');

    if (!empty($rows)) {
        foreach ($rows as $i => $row) {
            if (count($row) !== $headerCount) {
                throw new RuntimeException(
                    "FAIL: row $i has " . count($row) . " columns, expected $headerCount"
                );
            }
        }
        echo "  ✓ all " . count($rows) . " rows have $headerCount columns matching CSV_HEADERS\n";
    } else {
        echo "  ✓ no rows to check (empty export) — header count is $headerCount\n";
    }
    echo "\n";

    // =========================================================================
    // SECTION D: SearchController — coverMap key always present (regression)
    // =========================================================================

    echo "D1: SearchController::handleSearch() — coverMap key always in return array\n";
    // Simulate $_GET for the method (it reads from $_GET directly via collectSearchParams)
    $_GET = ['query' => ''];
    $rateLimit = new RateLimit(1000, 60);
    $searchCtrl = new SearchController($db, $rateLimit);
    $vars = $searchCtrl->handleSearch();
    plAssert(array_key_exists('coverMap', $vars), 'coverMap key present in handleSearch() return');
    plAssert(is_array($vars['coverMap']),          'coverMap is an array');
    $_GET = [];
    echo "\n";

    echo "✅ All pure logic tests passed!\n";
    $result = true;

} catch (Exception $e) {
    echo '❌ FAIL: ' . $e->getMessage() . "\n";
    $result = false;
}

return $result ?? false;
