newInstanceWithoutConstructor(); } private function invokeMethod(object $obj, string $method, array $args = []): mixed { $ref = new ReflectionClass($obj); $m = $ref->getMethod($method); $m->setAccessible(true); return $m->invokeArgs($obj, $args); } // ========================================== // Title Cleaning Tests // ========================================== #[DataProvider('cleanTitleProvider')] public function test_clean_title(string $input, string $expected): void { $service = $this->getInstance(); $result = $service->cleanTitle($input); $this->assertSame($expected, $result, "Failed for input: {$input}"); } public static function cleanTitleProvider(): array { return [ // Basic scene releases ['Cyberpunk.2077-GOG', 'Cyberpunk 2077'], ['The.Witcher.3.Wild.Hunt-CODEX', 'The Witcher 3 Wild Hunt'], ['Elden.Ring-EMPRESS', 'Elden Ring'], ['Baldurs.Gate.3-SKIDROW', 'Baldurs Gate 3'], // With version numbers ['Starfield.v1.7.29-FLT', 'Starfield'], ['Cyberpunk.2077.v2.1.0.1-GOG', 'Cyberpunk 2077'], ['Game.v1.2.3.4.5-CODEX', 'Game'], // Edition tags ['ELDEN RING [v1.08 + DLCs] - EMPRESS', 'ELDEN RING'], ['The Witcher 3 Wild Hunt GOTY - GOG', 'The Witcher 3 Wild Hunt'], ['Horizon Zero Dawn Complete Edition - CODEX', 'Horizon Zero Dawn'], ['Red Dead Redemption 2 Ultimate Edition-EMPRESS', 'Red Dead Redemption 2'], // FitGirl-style releases - may include some noise ['[FitGirl] Red.Dead.Redemption.2.v1.0.1436.28.Repack', 'Red Dead Redemption 2'], // Complex releases - these may retain some noise ['Resident.Evil.4.Remake.REPACK-DODI', 'Resident Evil 4 Remake'], // Special characters ["Assassin's.Creed.Valhalla-CODEX", "Assassin's Creed Valhalla"], ['Tom.Clancys.Ghost.Recon.Breakpoint-EMPRESS', 'Tom Clancys Ghost Recon Breakpoint'], // Already clean ['Cyberpunk 2077', 'Cyberpunk 2077'], ['The Witcher 3 Wild Hunt', 'The Witcher 3 Wild Hunt'], // Edge cases ['', ''], [' ', ''], ]; } // ========================================== // Title Normalization Tests // ========================================== #[DataProvider('normalizeTitleProvider')] public function test_normalize_title(string $input, string $expectedContains, string $expectedNotContains = ''): void { $service = $this->getInstance(); $result = $this->invokeMethod($service, 'normalizeTitle', [$input]); if ($expectedContains !== '') { $this->assertStringContainsString($expectedContains, $result, "Expected '{$expectedContains}' in result '{$result}'"); } if ($expectedNotContains !== '') { $this->assertStringNotContainsString($expectedNotContains, $result, "Expected '{$expectedNotContains}' NOT in result '{$result}'"); } } public static function normalizeTitleProvider(): array { return [ ['The Witcher 3: Wild Hunt', 'witcher', 'the'], ['ELDEN RING', 'elden ring', ''], ['Red Dead Redemption 2', 'red dead redemption 2', ''], ['Cyberpunk 2077 GOTY Edition', 'cyberpunk 2077', 'goty'], ['Assassin\'s Creed IV: Black Flag', 'assassin', ''], ]; } // ========================================== // Roman Numeral Replacement Tests // ========================================== #[DataProvider('romanNumeralProvider')] public function test_replace_roman_numerals(string $input, string $expected): void { $service = $this->getInstance(); $result = $this->invokeMethod($service, 'replaceRomanNumerals', [$input]); $this->assertSame($expected, $result); } public static function romanNumeralProvider(): array { return [ ['final fantasy vii', 'final fantasy 7'], ['final fantasy viii', 'final fantasy 8'], ['resident evil iv', 'resident evil 4'], ['witcher iii', 'witcher 3'], ['gta v', 'gta 5'], ['civilization vi', 'civilization 6'], ['elder scrolls iv oblivion', 'elder scrolls 4 oblivion'], ['dark souls ii', 'dark souls 2'], ['street fighter ii', 'street fighter 2'], ['assassins creed ii', 'assassins creed 2'], // Should not replace standalone 'i' (common in titles) ['i robot', 'i robot'], // Should handle multiple numerals ['title ii and iii', 'title 2 and 3'], ]; } // ========================================== // Query Variants Generation Tests // ========================================== public function test_generate_query_variants_includes_base_title(): void { $service = $this->getInstance(); $variants = $this->invokeMethod($service, 'generateQueryVariants', ['The Witcher 3: Wild Hunt - Game of the Year Edition']); $this->assertIsArray($variants); $this->assertNotEmpty($variants); // Should include variants without edition tags $found = false; foreach ($variants as $v) { if (stripos($v, 'game of the year') === false && stripos($v, 'witcher') !== false) { $found = true; break; } } $this->assertTrue($found, 'Expected base title variant without edition tags'); } public function test_generate_query_variants_handles_colon_titles(): void { $service = $this->getInstance(); $variants = $this->invokeMethod($service, 'generateQueryVariants', ["Assassin's Creed IV: Black Flag"]); $this->assertIsArray($variants); // Should include left side of colon $foundLeft = false; foreach ($variants as $v) { if (stripos($v, 'black flag') === false && stripos($v, "Assassin's Creed") !== false) { $foundLeft = true; break; } } $this->assertTrue($foundLeft, 'Expected left-side of colon as a variant'); } // ========================================== // Title Scoring Tests // ========================================== #[DataProvider('scoreTitleProvider')] public function test_score_title(string $candidate, string $original, float $minScore): void { $service = $this->getInstance(); $score = $this->invokeMethod($service, 'scoreTitle', [$candidate, $original]); $this->assertIsFloat($score); $this->assertGreaterThanOrEqual($minScore, $score, "Score {$score} should be >= {$minScore} for '{$candidate}' vs '{$original}'"); } public static function scoreTitleProvider(): array { return [ // Exact matches ['Cyberpunk 2077', 'Cyberpunk 2077', 100.0], ['The Witcher 3: Wild Hunt', 'The Witcher 3: Wild Hunt', 100.0], // Close matches (should score high) ['The Witcher 3: Wild Hunt', 'The.Witcher.3.Wild.Hunt-CODEX', 90.0], ['Elden Ring', 'ELDEN RING - Deluxe Edition - EMPRESS', 70.0], ['Cyberpunk 2077', 'Cyberpunk.2077.v2.1-GOG', 70.0], ['Red Dead Redemption 2', '[FitGirl] Red.Dead.Redemption.2.Repack', 80.0], // Partial matches ['Witcher 3', 'The Witcher 3: Wild Hunt', 50.0], // Non-matches (should score low) ['Cyberpunk 2077', 'Elden Ring', 0.0], ]; } public function test_score_title_perfect_normalized_match(): void { $service = $this->getInstance(); // These should normalize to the same thing $score = $this->invokeMethod($service, 'scoreTitle', [ 'The Witcher 3: Wild Hunt', 'The.Witcher.III.Wild.Hunt.GOTY-CODEX' ]); // With roman numeral conversion and normalization, these should be very similar $this->assertGreaterThanOrEqual(85.0, $score); } // ========================================== // LIKE Pattern Building Tests // ========================================== public function test_build_like_pattern(): void { $service = $this->getInstance(); $pattern = $this->invokeMethod($service, 'buildLikePattern', ['Resident Evil 4']); $this->assertStringStartsWith('%', $pattern); $this->assertStringEndsWith('%', $pattern); $this->assertStringContainsString('resident', strtolower($pattern)); $this->assertStringContainsString('evil', strtolower($pattern)); $this->assertStringContainsString('4', $pattern); } public function test_build_like_pattern_handles_roman_numerals(): void { $service = $this->getInstance(); $pattern = $this->invokeMethod($service, 'buildLikePattern', ['Resident Evil VII Biohazard']); $this->assertStringContainsString('7', $pattern); $this->assertStringNotContainsString('vii', strtolower($pattern)); } // ========================================== // Tokenization Tests // ========================================== public function test_tokenize(): void { $service = $this->getInstance(); $tokens = $this->invokeMethod($service, 'tokenize', ['The Witcher 3 Wild Hunt']); $this->assertIsArray($tokens); $this->assertContains('witcher', $tokens); $this->assertContains('3', $tokens); $this->assertContains('wild', $tokens); $this->assertContains('hunt', $tokens); } public function test_tokenize_deduplicates(): void { $service = $this->getInstance(); $tokens = $this->invokeMethod($service, 'tokenize', ['game game game']); $this->assertCount(1, $tokens); $this->assertSame(['game'], $tokens); } // ========================================== // Strip Edition Tags Tests // ========================================== #[DataProvider('stripEditionTagsProvider')] public function test_strip_edition_tags(string $input, string $expected): void { $service = $this->getInstance(); $result = $this->invokeMethod($service, 'stripEditionTags', [$input]); $this->assertSame($expected, $result); } public static function stripEditionTagsProvider(): array { return [ ['Cyberpunk 2077 Ultimate Edition', 'Cyberpunk 2077'], ['Horizon Zero Dawn', 'Horizon Zero Dawn'], // No tags ]; } // ========================================== // Integration-style Tests (Method Combinations) // ========================================== public function test_full_matching_workflow(): void { $service = $this->getInstance(); // Simulate a scene release name $releaseName = 'The.Witcher.III.Wild.Hunt.GOTY.Edition-CODEX'; // Clean it $clean = $service->cleanTitle($releaseName); $this->assertStringNotContainsString('.', $clean); $this->assertStringNotContainsString('CODEX', $clean); // Generate variants $variants = $this->invokeMethod($service, 'generateQueryVariants', [$clean]); $this->assertNotEmpty($variants); // Normalize for matching $normalized = $this->invokeMethod($service, 'normalizeTitle', [$clean]); $this->assertStringContainsString('witcher', $normalized); $this->assertStringContainsString('3', $normalized); // Roman numeral converted } public function test_matching_workflow_with_various_formats(): void { $testCases = [ 'Cyberpunk.2077-GOG' => 'cyberpunk 2077', 'ELDEN RING [v1.08 + DLCs] - EMPRESS' => 'elden ring', '[FitGirl] Red.Dead.Redemption.2.v1.0.1436.28.Repack' => 'red dead redemption 2', 'Baldurs_Gate_3_v1.0.2_MULTI12-EMPRESS' => 'baldurs gate 3', ]; $service = $this->getInstance(); foreach ($testCases as $input => $expectedInNormalized) { $clean = $service->cleanTitle($input); $normalized = $this->invokeMethod($service, 'normalizeTitle', [$clean]); $this->assertStringContainsString( $expectedInNormalized, $normalized, "Failed for input: {$input}" ); } } }