<?php

declare(strict_types=1);

namespace Atk4\Data\Repro;

class Connection
{
    public ?\mysqli $mysqli = null;

    public function __construct(string $dbHost, int $dbPort, string $dbUser, string $dbPassword, string $dbDatabase)
    {
        mysqli_report(\MYSQLI_REPORT_OFF);

        $this->mysqli = new \mysqli($dbHost, $dbUser, $dbPassword, $dbDatabase, $dbPort);
    }

    public function sendQuery(string $sql): void
    {
        echo "\n\n" . 'query: ' . $sql . "\n";

        $this->mysqli->query($sql, \MYSQLI_ASYNC | \MYSQLI_STORE_RESULT);
    }

    public function hasMoreData(): bool
    {
        $read = [$this->mysqli];
        $error = $read;
        $reject = $read;
        $poolRes = mysqli_poll($read, $error, $reject, 0, 100);
        assert($poolRes !== false);
        assert($error === []); // @phpstan-ignore identical.alwaysFalse, function.impossibleType
        assert($reject === []); // @phpstan-ignore identical.alwaysFalse, function.impossibleType

        return $read !== []; // @phpstan-ignore notIdentical.alwaysTrue
    }

    /**
     * @return list<array<string|int, scalar>>
     */
    public function readResult(): array
    {
        echo '  reading result' . "\n";

        while (!$this->hasMoreData());

        $mysqliRes = @$this->mysqli->reap_async_query();
        $rows = is_bool($mysqliRes)
            ? []
            : $mysqliRes->fetch_all(\MYSQLI_ASSOC);

        if ($mysqliRes === false) {
            $error = 'ERROR ' . $this->mysqli->errno . ' (' . $this->mysqli->sqlstate . '): ' . $this->mysqli->error;
            echo '    query error: ' . $error . "\n";

            throw new \Exception($error, $this->mysqli->errno);
        }

        return $rows;
    }
}

class Test
{
    protected function createConnection(): Connection
    {
        return new Connection('127.0.0.1', 4057, 'root', 'r', 'd');
    }

    protected function createTestTable(): void
    {
        $conn = $this->createConnection();

        $conn->sendQuery('DROP TABLE IF EXISTS $TTT');
        $conn->readResult();

        $conn->sendQuery(<<<'EOD'
            CREATE TABLE $TTT (
              `name` varchar(50) CHARACTER SET ascii NOT NULL,
              `value` bigint UNSIGNED NOT NULL,
              PRIMARY KEY (`name`)
            ) ENGINE=InnoDB
            EOD);
        $conn->readResult();

        $conn->sendQuery('insert into $TTT values (\'a\', 100), (\'b\', 200)');
        $conn->readResult();
    }

    /**
     * @param mixed $expected
     * @param mixed $actual
     */
    protected static function assertSame($expected, $actual): void
    {
        if ($actual !== $expected) {
            throw new \Exception('Not same - expected: ' . var_export($expected, true) . ', actual: ' . var_export($actual, true));
        }
    }

    public function testIssueKillWithoutErrorAffectsTransaction(): void
    {
        $this->createTestTable();

        for ($i = 0; $i < 250_000; ++$i) {
            $connA = $this->createConnection();
            $connB = $this->createConnection();

            $connA->sendQuery('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED');
            $connA->readResult();

            $connA->sendQuery('start transaction');
            $connA->readResult();

            $connA->sendQuery('update $TTT set value = 10 where name != \'b\'');
            $connA->readResult();

            $connA->sendQuery('update $TTT set value = 20 where name = \'a\'');

            $connB->sendQuery('kill query ' . $connA->mysqli->thread_id);
            $e = null;
            try {
                $connA->readResult();
            } catch (\Exception $e) {
                self::assertSame(1317, $e->getCode());
            }
            $connB->readResult();

            // for MySQL to prevent 1317 error sometimes in the next select query
            $connA->sendQuery('select * from $TTT where name = \'a\' for update');
            try {
                $connA->readResult();
            } catch (\Exception $e2) {
                self::assertSame(1317, $e2->getCode());
            }

            $connA->sendQuery('select * from $TTT where name = \'a\' for update');
            $res = $connA->readResult();
            self::assertSame($e === null ? 20 : 10, (int) $res[0]['value']);

            // prevent too many connections
            gc_collect_cycles();
            ob_start();
            do {
                $connB->sendQuery('show status where `variable_name` = \'Threads_connected\'');
                $res = $connB->readResult();
                $activeConnections = (int) $res[0]['Value'];
            } while ($activeConnections > 50);
            ob_end_clean();

            echo "\n\n";
        }
    }
}

$test = new Test();
$test->testIssueKillWithoutErrorAffectsTransaction();
