<?PHP
/**
 * Test for functions for looking up entry IDs by their (exact) names.
 *
 * @author Joshua Ramon Enslin <joshua@museum-digital.de>
 */
declare(strict_types = 1);

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Medium;

require_once __DIR__ . '/../../MDMysqli/test_connections.conf.php';

/**
 * Test for functions for looking up entry IDs by their (exact) names.
 */
#[Medium]
#[CoversClass(\NodaIDGetter::class)]
final class NodaIDGetterTest extends TestCase {

    private MDMysqli $_mysqli;

    /**
     * Quasi-constructor connects to DB.
     *
     * @return void
     */
    protected function setUp():void {

        $this->_mysqli = md_noda_mysqli_connect();

    }

    /**
     * Gets string and integer value from a mysql query.
     *
     * @param MDMysqli $mysqli DB connection.
     * @param string   $query  DB query.
     *
     * @return array{0: string, 1: integer}
     */
    private static function _getNameAndIdFromDbQuery(MDMysqli $mysqli, string $query):array {

        $result = $mysqli->do_read_query($query . "
            LIMIT 1");

        if (!($cur = $result->fetch_row())) {
            throw new Exception("Failed to get result for query: " . $query);
        }
        $result->close();

        return [
            (string)$cur[0],
            (int)$cur[1],
        ];

    }

    /**
     * Replaces vowels in string with variants with diacritics for testing
     * exact matching.
     *
     * @param string $input Input string.
     *
     * @return string
     */
    public static function _replaceVowelsWithDiacriticVariants(string $input):string {

        return strtr($input, [
            'a' => 'ä',
            'o' => 'ö',
            'u' => 'ü',
            'i' => 'ï',
            'e' => 'è',
        ]);

    }

    /**
     * Runs regular tests on the term:
     * - Matching by exact same name works.
     * - Matching by same name with different capitalization works.
     * - Matching by same name with diacritics replacing non-diacritic vowels fails
     *   (counter to MySQL's _ci-type notations rules).
     *
     * @param string  $function    NodaIDGetter's callback function for identification.
     * @param string  $name        Name of the entry.
     * @param integer $expected_id Expected target ID.
     *
     * @return void
     */
    private function runRegularComparisonTests(string $function, string $name, int $expected_id):void {

        self::assertEquals($expected_id,
            NodaIDGetter::$function($this->_mysqli, "de", $name),
            "Entry " . $name . " is not matched in exact lookup. Expected ID: " . $expected_id);

        // Ensure that different capitalization does not influence the results
        self::assertEquals($expected_id,
            NodaIDGetter::$function($this->_mysqli, "de", strtoupper($name)));
        self::assertEquals($expected_id,
            NodaIDGetter::$function($this->_mysqli, "de", strtolower($name)));

        // Ensure that diacritics will stop the entry from matching
        $nameReplaced = self::_replaceVowelsWithDiacriticVariants($name);
        self::assertNotEquals($expected_id,
            NodaIDGetter::$function($this->_mysqli, "de", $nameReplaced));

    }

    // PersinstIDByName

    /**
     * Returns a test actor name.
     *
     * @return array<array{0: string, 1: integer}>
     */
    public static function persinstByNameProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $persinstByNameSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `persinst_name`, `persinst_id`
            FROM `persinst`
            WHERE INSTR(`persinst_name`, 'i')");
        $persinstByDisplayNameSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `persinst_anzeigename`, `persinst_id`
            FROM `persinst`
            WHERE INSTR(`persinst_anzeigename`, 'i')
                AND `persinst_sterbejahr` != ''");
        $persinstByDisplayNameSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `trans_name`, `persinst_id`
            FROM `persinst_translation`
            WHERE INSTR(`trans_name`, 'i')
                AND `trans_language` = 'de'");
        $mysqli->close();

        return [
            'Persinst ID by name: ' . $persinstByNameSimple[0] => $persinstByNameSimple,
            'Persinst ID by display name: ' . $persinstByDisplayNameSimple[0] => $persinstByDisplayNameSimple,
            'Persinst ID by translated name: ' . $persinstByDisplayNameSimple[0] => $persinstByDisplayNameSimple,
        ];

    }

    /**
     * Returns a test actor name and life dates.
     *
     * @return array<array{0: string, 1: integer, 2: string, 3: string}>
     */
    public static function persinstByNameAndLifeDatesProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $result = $mysqli->do_read_query("SELECT `persinst_name_en`, `persinst_name`, `persinst_id`, `persinst_geburtsjahr`, `persinst_sterbejahr`
            FROM `persinst`
            WHERE INSTR(`persinst_name_en`, 'i')
                AND `persinst_geburtsjahr` != ''
                AND `persinst_sterbejahr` != ''
            LIMIT 1");
        if (!$cur = $result->fetch_row()) {
            throw new Exception("Error");
        }
        $result->close();
        $mysqli->close();

        return [
            'Persinst ID by name: ' . implode(' - ', $cur) => [
                (string)$cur[0], (int)$cur[2], (string)$cur[3], (string)$cur[4],
                (string)$cur[1], (int)$cur[2], (string)$cur[3], (string)$cur[4],
            ]
        ];

    }

    /**
     * Test getting persinst by name works.
     *
     * @param string  $name        Name of the entry.
     * @param integer $expected_id Expected target ID.
     *
     * @return void
     */
    #[DataProvider('persinstByNameProvider')]
    public function testGetPersinstIdByNameWorks(string $name, int $expected_id):void {

        $this->runRegularComparisonTests("getPersinstIDByName", $name, $expected_id);

    }

    /**
     * Test getting persinst by name and life dates works.
     *
     * @param string  $name        Name of the entry.
     * @param integer $expected_id Expected target ID.
     * @param string  $birth_year  Birth year.
     * @param string  $death_year  Death year.
     *
     * @return void
     */
    #[DataProvider('persinstByNameAndLifeDatesProvider')]
    public function testGetPersinstIdByNameAndLifeDatesWorks(string $name, int $expected_id, string $birth_year, string $death_year):void {

        self::assertEquals($expected_id,
            NodaIDGetter::getPersinstIDByNamePlusYears($this->_mysqli, "de", $name, $birth_year, $death_year),
            "Entry " . $name . " is not matched in exact lookup. Expected ID: " . $expected_id);

    }

    // PersinstIDByRewrite

    /**
     * Returns a test actor name.
     *
     * @return array<array{0: string, 1: integer}>
     */
    public static function persinstByRewriteProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $persinstByRewriteSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `input_name`, `persinst_id`
            FROM `persinst_rewriting`
            WHERE INSTR(`input_name`, 'i')");
        $mysqli->close();

        return [
            'Persinst ID by rewrite: ' . $persinstByRewriteSimple[0] => $persinstByRewriteSimple,
        ];

    }

    /**
     * Test getting persinst by rewrite works.
     *
     * @param string  $name        Name of the entry.
     * @param integer $expected_id Expected target ID.
     *
     * @return void
     */
    #[DataProvider('persinstByRewriteProvider')]
    public function testGetPersinstIdByRewriteWorks(string $name, int $expected_id):void {

        $this->runRegularComparisonTests("getPersinstIDByRewrite", $name, $expected_id);

    }

    // PlaceIDByName

    /**
     * Returns a test place name.
     *
     * @return array<array{0: string, 1: integer}>
     */
    public static function placeByNameProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $placeByNameSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `ort_name`, `ort_id`
            FROM `orte`
            WHERE INSTR(`ort_name`, 'i')");
        $placeByDisplayNameSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `trans_name`, `ort_id`
            FROM `ort_translation`
            WHERE INSTR(`trans_name`, 'i')
                AND `trans_language` = 'de'");
        $mysqli->close();

        return [
            'Place ID by name: ' . $placeByNameSimple[0] => $placeByNameSimple,
            'Place ID by translated name: ' . $placeByDisplayNameSimple[0] => $placeByDisplayNameSimple,
        ];

    }

    /**
     * Test getting persinst by name works.
     *
     * @param string  $name        Name of the entry.
     * @param integer $expected_id Expected target ID.
     *
     * @return void
     */
    #[DataProvider('placeByNameProvider')]
    public function testGetPlaceIdByNameWorks(string $name, int $expected_id):void {

        $this->runRegularComparisonTests("getPlaceIDByName", $name, $expected_id);
        $this->runRegularComparisonTests("getPlaceIDByNamesAndRewrites", $name, $expected_id);

    }

    // PlaceIDByRewrite

    /**
     * Returns a test place name registered for rewriting.
     *
     * @return array<array{0: string, 1: integer}>
     */
    public static function placeByRewriteProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $placeByRewriteSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `input_name`, `ort_id`
            FROM `ort_rewriting`
            WHERE INSTR(`input_name`, 'i')");
        $mysqli->close();

        return [
            'Place ID by rewrite: ' . $placeByRewriteSimple[0] => $placeByRewriteSimple,
        ];

    }

    /**
     * Test getting places by rewrite works.
     *
     * @param string  $name        Name of the entry.
     * @param integer $expected_id Expected target ID.
     *
     * @return void
     */
    #[DataProvider('placeByRewriteProvider')]
    public function testGetPlaceIdByRewriteWorks(string $name, int $expected_id):void {

        $this->runRegularComparisonTests("getPlaceIDByRewrite", $name, $expected_id);
        $this->runRegularComparisonTests("getPlaceIDByNamesAndRewrites", $name, $expected_id);

    }

    // TagIDByName

    /**
     * Returns a test tag name.
     *
     * @return array<array{0: string, 1: integer}>
     */
    public static function tagByNameProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $tagByNameSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `tag_name`, `tag_id`
            FROM `tag`
            WHERE INSTR(`tag_name`, 'i')");
        $tagByDisplayNameSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `trans_name`, `tag_id`
            FROM `tag_translation`
            WHERE INSTR(`trans_name`, 'i')
                AND `trans_language` = 'de'");
        $mysqli->close();

        return [
            'Tag ID by name: ' . $tagByNameSimple[0] => $tagByNameSimple,
            'Tag ID by translated name: ' . $tagByDisplayNameSimple[0] => $tagByDisplayNameSimple,
        ];

    }

    /**
     * Test getting tags by name works.
     *
     * @param string  $name        Name of the entry.
     * @param integer $expected_id Expected target ID.
     *
     * @return void
     */
    #[DataProvider('tagByNameProvider')]
    public function testGetTagIdByNameWorks(string $name, int $expected_id):void {

        $this->runRegularComparisonTests("getTagIDByName", $name, $expected_id);
        $this->runRegularComparisonTests("getTagIDByNamesAndRewrites", $name, $expected_id);

    }

    // TagIDByRewrite

    /**
     * Returns a test tag name registered for rewriting.
     *
     * @return array<array{0: string, 1: integer}>
     */
    public static function tagByRewriteProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $tagByRewriteSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `input_name`, `tag_id`
            FROM `tag_rewriting`
            WHERE INSTR(`input_name`, 'i')");
        $mysqli->close();

        return [
            'Tag ID by rewrite: ' . $tagByRewriteSimple[0] => $tagByRewriteSimple,
        ];

    }

    /**
     * Test getting tags by rewrite works.
     *
     * @param string  $name        Name of the entry.
     * @param integer $expected_id Expected target ID.
     *
     * @return void
     */
    #[DataProvider('tagByRewriteProvider')]
    public function testGetTagIdByRewriteWorks(string $name, int $expected_id):void {

        self::assertContains($expected_id, NodaIDGetter::getTagIDByRewrite($this->_mysqli, "de", $name));

        // Ensure that different capitalization does not influence the results
        self::assertContains($expected_id, NodaIDGetter::getTagIDByRewrite($this->_mysqli, "de", strtoupper($name)));
        self::assertContains($expected_id, NodaIDGetter::getTagIDByRewrite($this->_mysqli, "de", strtolower($name)));

        // Ensure that diacritics will stop the entry from matching
        $nameReplaced = self::_replaceVowelsWithDiacriticVariants($name);
        self::assertFalse(in_array($expected_id, NodaIDGetter::getTagIDByRewrite($this->_mysqli, "de", $nameReplaced), true));

    }

    // TimeIDByName

    /**
     * Returns a test time name.
     *
     * @return array<array{0: string, 1: integer}>
     */
    public static function timeByNameProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $timeByNameSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `zeit_name`, `zeit_id`
            FROM `zeiten`
            WHERE INSTR(`zeit_name`, 'i')");
        $timeByDisplayNameSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `trans_name`, `zeit_id`
            FROM `zeit_translation`
            WHERE INSTR(`trans_name`, 'i')
                AND `trans_language` = 'de'");
        $mysqli->close();

        return [
            'Time ID by name: ' . $timeByNameSimple[0] => $timeByNameSimple,
            'Time ID by translated name: ' . $timeByDisplayNameSimple[0] => $timeByDisplayNameSimple,
        ];

    }

    /**
     * Test getting persinst by name works.
     *
     * @param string  $name        Name of the entry.
     * @param integer $expected_id Expected target ID.
     *
     * @return void
     */
    #[DataProvider('timeByNameProvider')]
    public function testGetTimeIdByNameWorks(string $name, int $expected_id):void {

        $this->runRegularComparisonTests("getTimeIDByName", $name, $expected_id);
        $this->runRegularComparisonTests("getTimeIDByNamesAndRewrites", $name, $expected_id);

    }

    // timeIDByRewrite

    /**
     * Returns a test time name registered for rewriting.
     *
     * @return array<array{0: string, 1: integer}>
     */
    public static function timeByRewriteProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $timeByRewriteSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `input_name`, `zeit_id`
            FROM `zeit_rewriting`
            WHERE INSTR(`input_name`, 'i')
                AND `language` = 'de'");
        $mysqli->close();

        return [
            'Time ID by rewrite: ' . $timeByRewriteSimple[0] => $timeByRewriteSimple,
        ];

    }

    /**
     * Test getting times by rewrite works.
     *
     * @param string  $name        Name of the entry.
     * @param integer $expected_id Expected target ID.
     *
     * @return void
     */
    #[DataProvider('timeByRewriteProvider')]
    public function testGetTimeIdByRewriteWorks(string $name, int $expected_id):void {

        $this->runRegularComparisonTests("getTimeIDByRewrite", $name, $expected_id);
        $this->runRegularComparisonTests("getTimeIDByNamesAndRewrites", $name, $expected_id);

    }

    // Entity getters by norm data references

    /**
     * Data provider for norm data reference for actors.
     *
     * @return array<array{0: MDNodaLink, 1: integer}>
     */
    public static function nodaRefForActorsProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $nodaSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `noda_link`, `persinst_id`
            FROM `noda`
            WHERE `noda_source` = 'Wikidata'
                AND `noda_link` != ''");
        $mysqli->close();

        return [
            'Valid actor ID by norm data reference: ' . $nodaSimple[0] => [
                new MDNodaLink(MDNodaRepository::wikidata, $nodaSimple[0]),
                $nodaSimple[1],
            ],
            'Invalid, non-existing Wikidata ID' => [
                new MDNodaLink(MDNodaRepository::wikidata, 'Q11111111111111'),
                0,
            ],
        ];

    }

    /**
     * Test getting actor ID by norm data reference.
     *
     * @param MDNodaLink $link        Noda link.
     * @param integer    $expected_id Expected target ID.
     *
     * @return void
     */
    #[DataProvider('nodaRefForActorsProvider')]
    public function testGetPersinstByNodaLinkWorks(MDNodaLink $link, int $expected_id):void {

        self::assertEquals($expected_id, NodaIDGetter::getPersinstIDByNodaLink($this->_mysqli, $link));

    }

    /**
     * Data provider for norm data reference for places.
     *
     * @return array<array{0: MDNodaLink, 1: integer}>
     */
    public static function nodaRefForPlacesProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $nodaSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `noda_link`, `ort_id`
            FROM `noda_orte`
            WHERE `noda_source` = 'Wikidata'
                AND `noda_link` != ''");
        $mysqli->close();

        return [
            'Valid place ID by norm data reference: ' . $nodaSimple[0] => [
                new MDNodaLink(MDNodaRepository::wikidata, $nodaSimple[0]),
                $nodaSimple[1],
            ],
            'Invalid, non-existing Wikidata ID' => [
                new MDNodaLink(MDNodaRepository::wikidata, 'Q11111111111111'),
                0,
            ],
        ];

    }

    /**
     * Test getting place ID by norm data reference.
     *
     * @param MDNodaLink $link        Noda link.
     * @param integer    $expected_id Expected target ID.
     *
     * @return void
     */
    #[DataProvider('nodaRefForPlacesProvider')]
    public function testGetPlaceByNodaLinkWorks(MDNodaLink $link, int $expected_id):void {

        self::assertEquals($expected_id, NodaIDGetter::getPlaceIDByNodaLink($this->_mysqli, $link));

    }

    /**
     * Data provider for norm data reference for tags.
     *
     * @return array<array{0: MDNodaLink, 1: integer}>
     */
    public static function nodaRefForTagsProvider():array {

        $mysqli = md_noda_mysqli_connect();
        $nodaSimple = self::_getNameAndIdFromDbQuery($mysqli, "SELECT `noda_link`, `tag_id`
            FROM `noda_tag`
            WHERE `noda_source` = 'Wikidata'
                AND `noda_link` != ''");
        $mysqli->close();

        return [
            'Valid tag ID by norm data reference: ' . $nodaSimple[0] => [
                new MDNodaLink(MDNodaRepository::wikidata, $nodaSimple[0]),
                $nodaSimple[1],
            ],
            'Invalid, non-existing Wikidata ID' => [
                new MDNodaLink(MDNodaRepository::wikidata, 'Q11111111111111'),
                0,
            ],
        ];

    }

    /**
     * Test getting tag ID by norm data reference.
     *
     * @param MDNodaLink $link        Noda link.
     * @param integer    $expected_id Expected target ID.
     *
     * @return void
     */
    #[DataProvider('nodaRefForTagsProvider')]
    public function testGetTagByNodaLinkWorks(MDNodaLink $link, int $expected_id):void {

        self::assertEquals($expected_id, NodaIDGetter::getTagIDByNodaLink($this->_mysqli, $link));

    }
}
