<?php

/**
 * ErrorHandler Unit Test
 *
 * Tests that ErrorHandler correctly:
 *  - Maps PDO FK constraint errors to precise field names
 *  - Falls back to generic messages when table cannot be identified
 *  - Passes through domain exceptions (DuplicateThesisException, RuntimeException)
 *  - Generates structured log entries without crashing
 *  - Handles edge cases: empty message, null values, unknown exception types
 */

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/ErrorHandler.php';
require_once APP_ROOT . '/src/DuplicateThesisException.php';

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

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

function ehAssertContains(string $needle, string $haystack, string $label): void
{
    if (str_contains($haystack, $needle)) {
        echo "  ✓ $label\n";
    } else {
        throw new RuntimeException("FAIL: $label\n    expected to contain: $needle\n    actual: $haystack");
    }
}

function ehAssertEq(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");
    }
}

function ehAssertNotContains(string $needle, string $haystack, string $label): void
{
    if (!str_contains($haystack, $needle)) {
        echo "  ✓ $label\n";
    } else {
        throw new RuntimeException("FAIL: $label\n    expected NOT to contain: $needle\n    actual: $haystack");
    }
}

// ── Helper: create a PDOException with a given SQLite error message ──────────
// PDOException's constructor takes (message, code, previous).
// The message is what SQLite would produce.
function makeFkException(string $sqliteMessage): PDOException
{
    return new PDOException($sqliteMessage, 787); // 787 = SQLITE_CONSTRAINT_FOREIGNKEY
}

function makeUniqueException(): PDOException
{
    return new PDOException('UNIQUE constraint failed: thesis_tags.tag_id, thesis_tags.thesis_id', 2067);
}

function makeNotNullException(): PDOException
{
    return new PDOException('NOT NULL constraint failed: theses.title', 1299);
}

function makeGenericPdoException(): PDOException
{
    return new PDOException('database disk image is malformed', 11);
}

// ── Start ────────────────────────────────────────────────────────────────────

echo "ErrorHandler Unit Test\n";
echo "======================\n\n";

try {

    // =========================================================================
    // SECTION A: FK constraint — precise field extraction
    // =========================================================================

    echo "A: FK constraint — table name extracted from INSERT INTO\n";

    echo "A1: theses table (orientation_id FK) — lists all possible FK fields\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO theses (title, orientation_id) VALUES (?,?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Orientation', $user, 'mentions Orientation (one of the possible FK fields on theses)');
    ehAssertContains('AP', $user, 'mentions AP');
    ehAssertContains('Licence', $user, 'mentions Licence');
    ehAssertNotContains('FOREIGN KEY', $user, 'raw SQL not leaked');

    echo "A2: ap_programs\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO theses (ap_program_id) VALUES (?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('AP', $user, 'identifies AP field');

    echo "A3: finality_types\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO theses (finality_id) VALUES (?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Finalité', $user, 'identifies Finalité field');

    echo "A4: thesis_languages\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?,?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Langue(s)', $user, 'identifies Langue(s) field');

    echo "A5: thesis_formats\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?,?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Format(s)', $user, 'identifies Format(s) field');

    echo "A6: thesis_tags\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_tags (thesis_id, tag_id) VALUES (?,?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Mots-clés', $user, 'identifies Mots-clés field');

    echo "A7: thesis_supervisors\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_supervisors (thesis_id, supervisor_id) VALUES (?,?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Composition du jury', $user, 'identifies jury field');

    echo "A8: access_types\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO theses (access_type_id) VALUES (?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains("Type d'accès", $user, 'identifies access type');

    echo "A9: license_types\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO theses (license_id) VALUES (?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Licence', $user, 'identifies Licence field');

    echo "A10: authors\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_authors (thesis_id, author_id) VALUES (?,?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Auteur·ice', $user, 'identifies Auteur·ice field');

    echo "\n";

    // =========================================================================
    // SECTION B: FK constraint — \"table\" pattern (SQLite 3.37+)
    // =========================================================================

    echo "B: FK constraint — \"table\" pattern (SQLite 3.37+)\n";

    echo "B1: quoted table name\n";
    $msg = makeFkException('FOREIGN KEY constraint failed (table "orientations")');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Orientation', $user, 'extracts from quoted table name');

    echo "B2: quoted table — languages\n";
    $msg = makeFkException('FOREIGN KEY constraint failed (table "languages")');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Langue(s)', $user, 'maps languages → Langue(s)');

    echo "B3: quoted table — format_types\n";
    $msg = makeFkException('FOREIGN KEY constraint failed (table "format_types")');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Format(s)', $user, 'maps format_types → Format(s)');

    echo "\n";

    // =========================================================================
    // SECTION C: FK constraint — REFERENCES pattern
    // =========================================================================

    echo "C: FK constraint — REFERENCES pattern\n";

    echo "C1: REFERENCES tags\n";
    $msg = makeFkException('FOREIGN KEY constraint failed (REFERENCES tags(id))');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Mots-clés', $user, 'maps REFERENCES tags → Mots-clés');

    echo "C2: REFERENCES orientations\n";
    $msg = makeFkException('FOREIGN KEY constraint failed (REFERENCES orientations(id))');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Orientation', $user, 'maps REFERENCES orientations → Orientation');

    echo "\n";

    // =========================================================================
    // SECTION D: FK constraint — unknown table falls back to generic
    // =========================================================================

    echo "D: FK constraint — unknown table → generic fallback\n";

    echo "D1: unknown table in INSERT\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO unknown_table VALUES (?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('contrainte de référence est invalide', $user, 'generic FK message');
    ehAssertNotContains('unknown_table', $user, 'table name not leaked');

    echo "D2: empty message with FK keywords\n";
    $msg = makeFkException('FOREIGN KEY constraint failed');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('contrainte de référence est invalide', $user, 'generic FK for unparseable message');

    echo "\n";

    // =========================================================================
    // SECTION E: UNIQUE constraint\n
    // =========================================================================

    echo "E: UNIQUE constraint\n";

    echo "E1: UNIQUE constraint failed\n";
    $msg = makeUniqueException();
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('valeur en double', $user, 'mentions duplicate value');
    ehAssertNotContains('UNIQUE', $user, 'raw SQL not leaked');
    ehAssertNotContains('thesis_tags', $user, 'table name not leaked');

    echo "\n";

    // =========================================================================
    // SECTION F: NOT NULL constraint\n
    // =========================================================================

    echo "F: NOT NULL constraint\n";

    echo "F1: NOT NULL constraint failed\n";
    $msg = makeNotNullException();
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('champ obligatoire est manquant', $user, 'mentions required field');
    ehAssertNotContains('NOT NULL', $user, 'raw SQL not leaked');

    echo "\n";

    // =========================================================================
    // SECTION G: Generic PDO errors\n
    // =========================================================================

    echo "G: Generic PDO errors\n";

    echo "G1: disk image malformed\n";
    $msg = makeGenericPdoException();
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Une erreur de base de données est survenue', $user, 'generic DB message');
    ehAssertNotContains('disk image', $user, 'raw SQL not leaked');
    ehAssertNotContains('malformed', $user, 'raw error text not leaked');

    echo "\n";

    // =========================================================================
    // SECTION H: Domain exceptions pass through\n
    // =========================================================================

    echo "H: Domain exceptions pass through with original message\n";

    echo "H1: DuplicateThesisException\n";
    $dup = new DuplicateThesisException(42, '2025-ABC12345', 'Test titre', 'Auteur', 2025);
    $user = ErrorHandler::userMessage($dup);
    ehAssertContains('2025-ABC12345', $user, 'identifier in message');
    ehAssertContains('Auteur', $user, 'author in message');

    echo "H2: RuntimeException (validation)\n";
    $val = new RuntimeException('Le titre est requis.');
    $user = ErrorHandler::userMessage($val);
    ehAssertEq('Le titre est requis.', $user, 'validation message passes through');

    echo "\n";

    // =========================================================================
    // SECTION I: Unknown exception types → generic fallback\n
    // =========================================================================

    echo "I: Unknown exception types → generic fallback\n";

    echo "I1: generic Exception\n";
    $gen = new Exception('Something went wrong');
    $user = ErrorHandler::userMessage($gen);
    ehAssertContains('Une erreur inattendue est survenue', $user, 'generic message');
    ehAssertNotContains('Something went wrong', $user, 'raw message not leaked');

    echo "I2: TypeError\n";
    $typeErr = new TypeError('htmlspecialchars(): Argument #1 must be string, array given');
    $user = ErrorHandler::userMessage($typeErr);
    ehAssertContains('Une erreur inattendue est survenue', $user, 'generic message for TypeError');
    ehAssertNotContains('htmlspecialchars', $user, 'internal function name not leaked');

    echo "\n";

    // =========================================================================
    // SECTION J: Log method — does not crash, captures all context\n
    // =========================================================================

    echo "J: ErrorHandler::log() — structured logging without errors\n";

    // Suppress actual error_log output during test; verify no exception thrown.
    echo "J1: log with extra context\n";
    try {
        ErrorHandler::log('test_context', new Exception('test message'), [
            'thesis_id' => 42,
            'slug' => '20250101-TEST1234',
            'author' => 'Test Author',
        ]);
        echo "  ✓ log() completed without exception\n";
    } catch (Throwable $e) {
        throw new RuntimeException("FAIL: log() threw: " . $e->getMessage());
    }

    echo "J2: log with null values in extra\n";
    ErrorHandler::log('test_null', new PDOException('test'), ['id' => null, 'name' => 'foo']);
    echo "  ✓ log() handles null values\n";

    echo "J3: log with empty extra array\n";
    ErrorHandler::log('test_empty', new RuntimeException('bare'));
    echo "  ✓ log() handles empty extra\n";

    echo "\n";

    // =========================================================================
    // SECTION K: Keyword normalization logic (tag normalization)
    // =========================================================================

    echo "K: Keyword normalization logic\n";

    // Test the normalization regex used in controllers and JS:
    //   strtolower(trim(preg_replace('/\s+/', ' ', $t)))
    $normalize = fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));

    echo "K1: basic trimming and casing\n";
    ehAssertEq('hello', $normalize('Hello'), 'uppercase → lowercase');
    ehAssertEq('hello', $normalize('  hello  '), 'trimmed');
    ehAssertEq('hello world', $normalize('Hello World'), 'two words lowercased');

    echo "K2: multiple spaces collapsed\n";
    ehAssertEq('a b c', $normalize('a  b   c'), 'double spaces → single');
    ehAssertEq('x y', $normalize(' x    y '), 'leading/trailing + multiple → clean');

    echo "K3: tabs and newlines collapsed to space\n";
    ehAssertEq('word1 word2', $normalize("word1\tword2"), 'tab → space');
    ehAssertEq('line1 line2', $normalize("line1\nline2"), 'newline → space');
    ehAssertEq('mixed spaces', $normalize("mixed  \t \n spaces"), 'mixed whitespace → single space');

    echo "K4: French accents preserved\n";
    ehAssertEq('très précis', $normalize('Très Précis'), 'accents preserved in lowercase');

    echo "K5: empty string\n";
    ehAssertEq('', $normalize(''), 'empty stays empty');
    ehAssertEq('', $normalize('   '), 'whitespace-only becomes empty');

    echo "K6: special characters not mangled\n";
    ehAssertEq("c++", $normalize("C++"), 'symbols preserved');
    ehAssertEq("c#", $normalize("C#"), 'hash preserved');

    echo "\n";

    // =========================================================================
    // SECTION L: Deduplication on normalize (case-insensitive)
    // =========================================================================

    echo "L: Deduplication after normalization\n";

    $dedup = function(array $tags): array {
        return array_values(array_unique(array_map(
            fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t))),
            $tags
        )));
    };

    echo "L1: case-insensitive dedup\n";
    ehAssertEq(['hello'], $dedup(['Hello', 'hello', 'HELLO']), 'case variations → one entry');

    echo "L2: whitespace-insensitive dedup\n";
    ehAssertEq(['hello world'], $dedup(['hello  world', 'Hello World', 'hello world']), 'whitespace + case → one entry');

    echo "L3: empty strings filtered\n";
    $filtered = array_values(array_filter($dedup(['', '  ', 'valid']), fn($t) => $t !== ''));
    ehAssertEq(['valid'], $filtered, 'empty/whitespace-only removed');

    echo "L4: mixed valid and empty\n";
    $result = array_values(array_filter($dedup(['Alpha', '', '  ', 'BETA', 'alpha']), fn($t) => $t !== ''));
    ehAssertEq(['alpha', 'beta'], $result, 'deduplicated and empties filtered');

    echo "\n";

    // =========================================================================
    // SECTION M: Minimum/maximum tag count enforcement\n
    // =========================================================================

    echo "M: Tag count constraints\n";

    echo "M1: 3 tags is valid\n";
    $valid = ['one', 'two', 'three'];
    ehAssert(count($valid) >= 3, '3 tags ≥ minimum 3');
    ehAssert(count($valid) <= 10, '3 tags ≤ maximum 10');

    echo "M2: < 3 tags triggers error\n";
    $tooFew = ['one'];
    ehAssert(count($tooFew) < 3, '1 tag < minimum 3');

    echo "M3: > 10 tags triggers error\n";
    $tooMany = ['a','b','c','d','e','f','g','h','i','j','k'];
    ehAssert(count($tooMany) > 10, '11 tags > maximum 10');

    echo "M4: empty array\n";
    ehAssert(count([]) < 3, 'empty array < minimum 3');

    echo "\n";

    // =========================================================================
    // SECTION N: Real SQLite FK error message formats
    // =========================================================================

    echo "N: Real-world SQLite FK error message patterns\n";

    // These are actual error messages observed in the wild.
    echo "N1: typical INSERT INTO with VALUES\n";
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_formats ("thesis_id", "format_id") VALUES (?, ?)');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Format(s)', $user, 'quoted column names handled');

    echo "N2: UPDATE statement\n";
    $msg = makeFkException('FOREIGN KEY constraint failed UPDATE theses SET orientation_id = ? WHERE id = ?');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Orientation', $user, 'UPDATE statement parsed');

    echo "N3: long FK message with multiple table references\n";
    // Only the first match should be used (the INSERT target table)
    $msg = makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?) REFERENCES format_types');
    $user = ErrorHandler::userMessage($msg);
    ehAssertContains('Format(s)', $user, 'first table match used');

    echo "\n";

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

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

return $result ?? false;
