<?php
declare(strict_types=1);

require_once __DIR__ . '/TypedStatement.php';

final class FakeTypedStatement extends TypedStatement
{
    private $rows;
    private $sourceMetadata;
    private $position = 0;
    public $metadataReads = 0;

    public function __construct(array $rows, array $metadata)
    {
        $this->rows = $rows;
        $this->sourceMetadata = $metadata;
    }

    protected function rawFetch($orientation, $offset)
    {
        return $this->rows[$this->position++] ?? false;
    }

    protected function rawColumnCount(): int
    {
        return count($this->sourceMetadata);
    }

    protected function rawColumnMeta(int $index): array
    {
        ++$this->metadataReads;
        return $this->sourceMetadata[$index];
    }

    protected function rawSetFetchMode($mode): bool
    {
        return true;
    }

    protected function rawExecute($parameters = null): bool
    {
        $this->position = 0;
        return true;
    }
}

$checks = 0;
$same = static function ($actual, $expected, string $label) use (&$checks): void {
    if ($actual !== $expected) {
        throw new RuntimeException($label . ': expected ' . var_export($expected, true)
            . ', got ' . var_export($actual, true));
    }
    ++$checks;
};
$throws = static function (callable $callback, string $label) use (&$checks): void {
    try {
        $callback();
    } catch (InvalidArgumentException $error) {
        ++$checks;
        return;
    }
    throw new RuntimeException($label . ': expected InvalidArgumentException');
};
$metadata = [
    ['name' => 'INT_VALUE', 'odbc:sql_type' => 4, 'odbc:column_size' => 10, 'scale' => 0],
    ['name' => 'DEC_VALUE', 'odbc:sql_type' => 2, 'odbc:column_size' => 30, 'scale' => 2],
    ['name' => 'FLOAT_VALUE', 'odbc:sql_type' => 8, 'odbc:column_size' => 53, 'scale' => 0],
    ['name' => 'COUNT_VALUE', 'odbc:sql_type' => -5, 'odbc:column_size' => 19, 'scale' => 0],
    ['name' => 'TIME_VALUE', 'odbc:sql_type' => 93, 'odbc:column_size' => 26, 'scale' => 6],
    ['name' => 'WIDE_ID', 'odbc:sql_type' => 2, 'odbc:column_size' => 20, 'scale' => 0],
];
$makeRow = static function (string $int, string $decimal, string $floating,
    string $count, string $time, string $wide): array {
    $values = [$int, $decimal, $floating, $count, $time, $wide];
    $names = ['int_value', 'dec_value', 'float_value', 'count_value', 'time_value', 'wide_id'];
    $row = [];
    foreach ($values as $index => $value) {
        $row[$index] = $value;
        $row[$names[$index]] = $value;
    }
    return $row;
};
$rawRows = [
    $makeRow('7', '1.2', '1.25', '1', '2026-10-01 12:34:56.000000', '9007199254740993'),
    $makeRow('8', '2.5', '3.5', '2', '2026-10-02 12:34:56.000000', '9007199254740994'),
];
$firstAssoc = [
    'int_value' => 7, 'dec_value' => '1.20', 'float_value' => 1.25,
    'count_value' => 1, 'time_value' => '2026-10-01 12:34:56.000000',
    'wide_id' => '9007199254740993',
];
$firstNumeric = array_values($firstAssoc);

$statement = new FakeTypedStatement($rawRows, $metadata);
$same($statement->fetch(PDO::FETCH_ASSOC), $firstAssoc, 'fetch ASSOC restores type and preserves raw key casing');
$same($statement->metadataReads, 6, 'fetch reads each column metadata once');
$same($statement->fetch(PDO::FETCH_NUM),
    [8, '2.50', 3.5, 2, '2026-10-02 12:34:56.000000', '9007199254740994'],
    'fetch NUM restores by ordinal');
$same($statement->metadataReads, 6, 'second fetch uses cached metadata');
$same($statement->fetch(PDO::FETCH_ASSOC), false, 'fetch EOF remains false');

$statement = new FakeTypedStatement([$rawRows[0]], $metadata);
$both = $statement->fetch(PDO::FETCH_BOTH);
foreach ($firstNumeric as $index => $value) {
    $same($both[$index], $value, 'BOTH numeric ' . $index);
}
foreach ($firstAssoc as $key => $value) {
    $same($both[$key], $value, 'BOTH associative ' . $key);
}
$same($statement->fetch(PDO::FETCH_COLUMN), false, 'fetch COLUMN at EOF');

$statement = new FakeTypedStatement($rawRows, $metadata);
$same($statement->fetchAll(PDO::FETCH_ASSOC), [
    $firstAssoc,
    ['int_value' => 8, 'dec_value' => '2.50', 'float_value' => 3.5,
        'count_value' => 2, 'time_value' => '2026-10-02 12:34:56.000000',
        'wide_id' => '9007199254740994'],
], 'fetchAll ASSOC');
$same($statement->metadataReads, 6, 'fetchAll caches metadata across rows');
$reused = $statement;
$statement = new FakeTypedStatement($rawRows, $metadata);
$same($statement->fetchAll(PDO::FETCH_NUM), [
    $firstNumeric,
    [8, '2.50', 3.5, 2, '2026-10-02 12:34:56.000000', '9007199254740994'],
], 'fetchAll NUM restores numeric values');
$statement = new FakeTypedStatement([$rawRows[0]], $metadata);
$bothRows = $statement->fetchAll(PDO::FETCH_BOTH);
$same($bothRows[0][0], 7, 'fetchAll BOTH numeric side');
$same($bothRows[0]['int_value'], 7, 'fetchAll BOTH associative side');
$same($statement->metadataReads, 6, 'fetchAll BOTH reads each metadata once');
$same($reused->execute(), true, 're-execute succeeds');
$same($reused->fetchColumn(0), 7, 'fetchColumn integer');
$same($reused->metadataReads, 12, 're-execute clears metadata cache');

$statement = new FakeTypedStatement($rawRows, $metadata);
$same($statement->fetchAll(PDO::FETCH_COLUMN, 1), ['1.20', '2.50'],
    'fetchAll COLUMN with explicit ordinal');
$statement = new FakeTypedStatement($rawRows, $metadata);
$same($statement->fetchAll(PDO::FETCH_COLUMN), [7, 8],
    'fetchAll COLUMN defaults to first column');
$statement = new FakeTypedStatement($rawRows, $metadata);
$same($statement->fetchColumn(5), '9007199254740993',
    'DECIMAL20 remains exact string and original type unresolved');
$throws(static function () use ($statement): void {
    $statement->fetchColumn(99);
}, 'invalid fetchColumn ordinal rejects explicitly');

$statement = new FakeTypedStatement([$rawRows[0]], $metadata);
$same($statement->setFetchMode(PDO::FETCH_NUM), true, 'setFetchMode NUM');
$same($statement->fetch(), $firstNumeric, 'default mode tracks setFetchMode');
$statement = new FakeTypedStatement([$rawRows[0]], $metadata);
$same($statement->fetch(PDO::FETCH_COLUMN), 7, 'fetch COLUMN first column');
$statement = new FakeTypedStatement([], $metadata);
$same($statement->fetchAll(PDO::FETCH_NUM), [], 'empty fetchAll remains empty array');

$nullable = [['name' => 'MAYBE', 'odbc:sql_type' => 4]];
$statement = new FakeTypedStatement([[0 => null, 'maybe' => null]], $nullable);
$same($statement->fetch(PDO::FETCH_ASSOC), ['maybe' => null], 'SQL NULL remains PHP null');

foreach (['fetch' => 3, 'fetchAll' => 3, 'fetchColumn' => 1] as $method => $count) {
    $same((new ReflectionMethod(TypedStatement::class, $method))->getNumberOfParameters(),
        $count, $method . ' matches PHP7.4 PDOStatement parameter count');
}

$statement = new FakeTypedStatement([$rawRows[0]], $metadata);
$throws(static function () use ($statement): void {
    $statement->fetch(PDO::FETCH_OBJ);
}, 'unsupported object mode rejects explicitly');
$throws(static function () use ($statement): void {
    $statement->fetchAll(PDO::FETCH_COLUMN, 99);
}, 'invalid fetchAll column rejects explicitly');
$throws(static function () use ($statement): void {
    $statement->setFetchMode(PDO::FETCH_CLASS);
}, 'unsupported default mode rejects explicitly');

$duplicate = [
    ['name' => 'SAME', 'odbc:sql_type' => 4],
    ['name' => 'same', 'odbc:sql_type' => 8],
];
$statement = new FakeTypedStatement([[0 => '7', 'same' => '1.25', 1 => '1.25']], $duplicate);
try {
    $statement->fetch(PDO::FETCH_ASSOC);
    throw new RuntimeException('ambiguous names should fail instead of guessing');
} catch (UnexpectedValueException $error) {
    ++$checks;
}

$statement = new FakeTypedStatement([[0 => '7', 'int_value' => '8']], [$metadata[0]]);
try {
    $statement->fetch(PDO::FETCH_ASSOC);
    throw new RuntimeException('mismatched PDO numeric/associative copies should fail');
} catch (UnexpectedValueException $error) {
    ++$checks;
}

$statement = new FakeTypedStatement([[0 => '7', 'int_value' => '7', 9 => '9']], [$metadata[0]]);
try {
    $statement->fetch(PDO::FETCH_BOTH);
    throw new RuntimeException('extra numeric key should fail without undefined-index warning');
} catch (UnexpectedValueException $error) {
    ++$checks;
}

echo 'ODBC_TYPED_STATEMENT_PURE_PASS: ' . $checks
    . ' checks; no database or extension' . PHP_EOL;
