<?php

use classes\licenseplaterecognizer;

class scanner_test_license_plate_recognizer extends licenseplaterecognizer
{
    public int $config_reads = 0;

    public function __construct(private readonly array $config_values)
    {
        parent::__construct(false);
    }

    public function exposedRuntimeConfig(): array
    {
        return $this->runtimeConfig();
    }

    protected function readRuntimeModuleConfig(): array
    {
        $this->config_reads++;

        return $this->config_values;
    }
}

class scanner_test_shared_cache_license_plate_recognizer extends licenseplaterecognizer
{
    public static int $config_reads = 0;
    public static array $config_values = [];
    public static ?scanner_test_runtime_config_cache_store $cache_store = null;

    public function __construct()
    {
        parent::__construct(false);
    }

    public function exposedRuntimeConfig(): array
    {
        return $this->runtimeConfig();
    }

    protected function shouldUseSharedRuntimeConfigCache(): bool
    {
        return true;
    }

    protected function readRuntimeModuleConfig(): array
    {
        self::$config_reads++;

        return self::$config_values;
    }

    protected function runtimeConfigCacheStore(): ?object
    {
        return self::$cache_store;
    }
}

class scanner_test_runtime_config_cache_store
{
    public array $store = [];
    public array $set_ex_calls = [];

    public function get(string $key): ?string
    {
        return $this->store[$key] ?? null;
    }

    public function setEx(string $key, string $value, int $expiration): void
    {
        $this->store[$key] = $value;
        $this->set_ex_calls[] = [
            'key' => $key,
            'value' => $value,
            'expiration' => $expiration,
        ];
    }
}

function reset_license_plate_recognizer_runtime_config_cache(): void
{
    $cacheProperty = new ReflectionProperty(licenseplaterecognizer::class, 'runtime_config_cache');
    $cacheProperty->setValue(null, null);
    scanner_test_shared_cache_license_plate_recognizer::$config_reads = 0;
    scanner_test_shared_cache_license_plate_recognizer::$config_values = [];
    scanner_test_shared_cache_license_plate_recognizer::$cache_store = null;
}

beforeEach(function (): void {
    reset_license_plate_recognizer_runtime_config_cache();
});

afterEach(function (): void {
    reset_license_plate_recognizer_runtime_config_cache();
});

it('normalizes license plate recognizer info responses decoded as arrays or objects', function (): void {
    $arrayInfo = new \licenseplaterecognizer\helpers\licenseplaterecognizer_info([
        'version' => '1.54.0',
        'usage' => ['calls' => '98'],
        'license_key' => 'license-key',
        'webhooks' => [['id' => 'webhook-1']],
        'total_calls' => '2500',
    ]);
    $objectInfo = new \licenseplaterecognizer\helpers\licenseplaterecognizer_info((object)[
        'version' => '1.55.0',
        'usage' => (object)['calls' => 7],
        'license_key' => 'license-key-2',
        'webhooks' => [],
        'total_calls' => 100,
    ]);

    expect($arrayInfo->version)->toBe('1.54.0');
    expect($arrayInfo->usage->calls)->toBe(98);
    expect($arrayInfo->license_key)->toBe('license-key');
    expect($arrayInfo->webhooks)->toHaveCount(1);
    expect($arrayInfo->total_calls)->toBe(2500);
    expect($objectInfo->version)->toBe('1.55.0');
    expect($objectInfo->usage->calls)->toBe(7);
    expect($objectInfo->total_calls)->toBe(100);
});

it('sends data URI images to Plate Recognizer as multipart bytes with fast mode enabled', function (): void {
    $reflection = new ReflectionClass(licenseplaterecognizer::class);
    $recognizer = $reflection->newInstanceWithoutConstructor();
    $method = $reflection->getMethod('buildPlateReaderPayload');

    $payload = $method->invoke($recognizer, 'data:image/jpeg;base64,' . base64_encode('jpeg-bytes'));

    expect($payload['upload'])->toBeInstanceOf(CURLStringFile::class);
    expect($payload['upload']->data)->toBe('jpeg-bytes');
    expect($payload['upload']->postname)->toBe('license-plate.jpg');
    expect($payload['upload']->mime)->toBe('image/jpeg');
    expect(json_decode($payload['config'], true))->toBe([
        'mode' => 'fast',
        'plates_per_vehicle' => 1,
        'zoom_in_vehicles' => 0,
    ]);
    expect($payload['regions'])->toBe('dk,de,se,no');
});

it('uses precomputed Plate Reader config strings on the scanner payload hot path', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));

    expect($source)->not->toBeFalse();
    expect($source)->toContain('PLATE_READER_CONFIG_JSON');
    expect($source)->toContain('RESULT_CACHE_CONTEXT');
    expect($source)->toContain("'config' => self::PLATE_READER_CONFIG_JSON");
    expect($source)->toContain('return self::RESULT_CACHE_CONTEXT;');
    expect($source)->not->toContain("'config' => json_encode(self::PLATE_READER_CONFIG");
    expect($source)->not->toContain("return json_encode([\n            'config' => self::PLATE_READER_CONFIG");
});

it('keeps already raw base64 upload data unchanged', function (): void {
    $reflection = new ReflectionClass(licenseplaterecognizer::class);
    $recognizer = $reflection->newInstanceWithoutConstructor();
    $method = $reflection->getMethod('buildPlateReaderPayload');

    $payload = $method->invoke($recognizer, 'abc123');

    expect($payload['upload'])->toBe('abc123');
});

it('does not retain raw upstream Plate Recognizer responses in compact scanner results', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));

    expect($source)->not->toBeFalse();
    expect($source)->not->toContain("'raw_response' => \$response_data");
    expect($source)->toContain("'license_plate_number' => \$response_data['results'][0]['plate'] ?? null");
    expect($source)->toContain("'confidence' => \$response_data['results'][0]['score'] ?? null");
    expect($source)->toContain("'message' => 'No license plate detected.'");
});

it('sends multipart route image bytes to Plate Recognizer without base64 wrapping', function (): void {
    $reflection = new ReflectionClass(licenseplaterecognizer::class);
    $recognizer = $reflection->newInstanceWithoutConstructor();
    $uploadMethod = $reflection->getMethod('buildUploadValueFromBytes');
    $payloadMethod = $reflection->getMethod('buildPlateReaderPayloadFromUpload');

    $upload = $uploadMethod->invoke($recognizer, 'jpeg-bytes', 'image/jpeg');
    $payload = $payloadMethod->invoke($recognizer, $upload);

    expect($payload['upload'])->toBeInstanceOf(CURLStringFile::class);
    expect($payload['upload']->data)->toBe('jpeg-bytes');
    expect($payload['upload']->postname)->toBe('license-plate.jpg');
    expect($payload['upload']->mime)->toBe('image/jpeg');
    expect(json_decode($payload['config'], true))->toBe([
        'mode' => 'fast',
        'plates_per_vehicle' => 1,
        'zoom_in_vehicles' => 0,
    ]);
    expect($payload['regions'])->toBe('dk,de,se,no');
});

it('sends multipart route upload files to Plate Recognizer without reading bytes into PHP memory', function (): void {
    $path = tempnam(sys_get_temp_dir(), 'lpr-upload-');
    expect($path)->not->toBeFalse();
    file_put_contents($path, 'jpeg-bytes');

    try {
        $reflection = new ReflectionClass(licenseplaterecognizer::class);
        $recognizer = $reflection->newInstanceWithoutConstructor();
        $uploadMethod = $reflection->getMethod('buildUploadValueFromFile');
        $payloadMethod = $reflection->getMethod('buildPlateReaderPayloadFromUpload');

        $upload = $uploadMethod->invoke($recognizer, $path, 'image/jpeg');
        $payload = $payloadMethod->invoke($recognizer, $upload);

        expect($payload['upload'])->toBeInstanceOf(CURLFile::class);
        expect($payload['upload']->getFilename())->toBe($path);
        expect($payload['upload']->getPostFilename())->toBe('license-plate.jpg');
        expect($payload['upload']->getMimeType())->toBe('image/jpeg');
        expect(json_decode($payload['config'], true))->toBe([
            'mode' => 'fast',
            'plates_per_vehicle' => 1,
            'zoom_in_vehicles' => 0,
        ]);
        expect($payload['regions'])->toBe('dk,de,se,no');
    } finally {
        @unlink($path);
    }
});

it('records Plate Recognizer processing time from the upstream response', function (): void {
    $reflection = new ReflectionClass(licenseplaterecognizer::class);
    $recognizer = $reflection->newInstanceWithoutConstructor();
    $method = $reflection->getMethod('recordResponseTimings');

    $method->invoke($recognizer, [
        'processing_time' => 58.184,
    ]);

    expect($recognizer->getLastTimings())->toHaveKey('upstream_processing', 58.184);
});

it('can skip config object setup and cache runtime config for scanner requests', function (): void {
    $recognizer = new scanner_test_license_plate_recognizer([
        'enabled' => 'true',
        'api_key' => 'test-key',
    ]);
    $configProperty = new ReflectionProperty(licenseplaterecognizer::class, 'config');

    expect($configProperty->isInitialized($recognizer))->toBeFalse();
    expect($recognizer->exposedRuntimeConfig())->toBe([
        'enabled' => true,
        'api_key' => 'test-key',
    ]);
    expect($recognizer->exposedRuntimeConfig())->toBe([
        'enabled' => true,
        'api_key' => 'test-key',
    ]);
    expect($recognizer->config_reads)->toBe(1);
});

it('reads runtime config once in the scanner recognition hot path', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));

    expect($source)->not->toBeFalse();
    expect($source)->toContain('$runtime_config = $this->runtimeConfig();');
    expect($source)->toContain('$api_key = $runtime_config[\'api_key\'];');
    expect($source)->not->toContain('$this->requireModuleEnabled();' . "\n            " . '$api_key = $this->runtimeConfig()[\'api_key\'];');
});

it('shares runtime config briefly across scanner recognizer instances', function (): void {
    scanner_test_shared_cache_license_plate_recognizer::$config_values = [
        'enabled' => 'true',
        'api_key' => 'cached-key',
    ];

    $first = new scanner_test_shared_cache_license_plate_recognizer();
    expect($first->exposedRuntimeConfig())->toBe([
        'enabled' => true,
        'api_key' => 'cached-key',
    ]);

    scanner_test_shared_cache_license_plate_recognizer::$config_values = [
        'enabled' => 'false',
        'api_key' => 'new-key',
    ];

    $second = new scanner_test_shared_cache_license_plate_recognizer();
    expect($second->exposedRuntimeConfig())->toBe([
        'enabled' => true,
        'api_key' => 'cached-key',
    ]);
    expect(scanner_test_shared_cache_license_plate_recognizer::$config_reads)->toBe(1);
});

it('refreshes the shared runtime config cache after the short scanner ttl', function (): void {
    $cacheProperty = new ReflectionProperty(licenseplaterecognizer::class, 'runtime_config_cache');
    $cacheProperty->setValue(null, [
        'values' => [
            'enabled' => true,
            'api_key' => 'stale-key',
        ],
        'cached_at' => microtime(true) - 20,
    ]);
    scanner_test_shared_cache_license_plate_recognizer::$config_values = [
        'enabled' => 'false',
        'api_key' => 'fresh-key',
    ];

    $recognizer = new scanner_test_shared_cache_license_plate_recognizer();

    expect($recognizer->exposedRuntimeConfig())->toBe([
        'enabled' => false,
        'api_key' => 'fresh-key',
    ]);
    expect(scanner_test_shared_cache_license_plate_recognizer::$config_reads)->toBe(1);
});

it('uses Redis-backed runtime config cache across scanner requests when available', function (): void {
    $cache = new scanner_test_runtime_config_cache_store();
    scanner_test_shared_cache_license_plate_recognizer::$cache_store = $cache;
    scanner_test_shared_cache_license_plate_recognizer::$config_values = [
        'enabled' => 'true',
        'api_key' => 'redis-key',
    ];

    $first = new scanner_test_shared_cache_license_plate_recognizer();
    expect($first->exposedRuntimeConfig())->toBe([
        'enabled' => true,
        'api_key' => 'redis-key',
    ]);
    expect(scanner_test_shared_cache_license_plate_recognizer::$config_reads)->toBe(1);
    expect($cache->set_ex_calls)->toHaveCount(1);
    expect($cache->set_ex_calls[0]['expiration'])->toBe(15);

    $cacheProperty = new ReflectionProperty(licenseplaterecognizer::class, 'runtime_config_cache');
    $cacheProperty->setValue(null, null);
    scanner_test_shared_cache_license_plate_recognizer::$config_values = [
        'enabled' => 'false',
        'api_key' => 'db-should-not-be-read',
    ];

    $second = new scanner_test_shared_cache_license_plate_recognizer();

    expect($second->exposedRuntimeConfig())->toBe([
        'enabled' => true,
        'api_key' => 'redis-key',
    ]);
    expect(scanner_test_shared_cache_license_plate_recognizer::$config_reads)->toBe(1);
});

it('treats false runtime config values as disabled', function (): void {
    $recognizer = new scanner_test_license_plate_recognizer([
        'enabled' => 'false',
        'api_key' => 'test-key',
    ]);

    expect($recognizer->exposedRuntimeConfig())->toBe([
        'enabled' => false,
        'api_key' => 'test-key',
    ]);
    expect(fn () => $recognizer->requireModuleEnabled())
        ->toThrow(Exception::class, 'licenseplaterecognizer module is not enabled.');
});

it('can point Plate Recognizer calls at a local measurement upstream', function (): void {
    $recognizer = new licenseplaterecognizer(false, 'http://127.0.0.1:18081/');
    $apiUrlProperty = new ReflectionProperty(licenseplaterecognizer::class, 'api_url');

    expect($apiUrlProperty->getValue($recognizer))->toBe('http://127.0.0.1:18081');
});

it('disables curl expect continue waits for large multipart uploads', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));

    expect($source)->not->toBeFalse();
    expect($source)->toContain("'Expect:'");
});

it('disables tcp write coalescing on scanner Plate Recognizer requests when curl supports it', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));

    expect($source)->not->toBeFalse();
    expect($source)->toContain('$curl_options = [');
    expect($source)->toContain("defined('CURLOPT_TCP_NODELAY')");
    expect($source)->toContain("\$curl_options[(int)constant('CURLOPT_TCP_NODELAY')] = true;");
    expect($source)->toContain('curl_setopt_array($ch, $curl_options);');
});

it('does not capture outgoing curl headers on the scanner hot path', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));

    expect($source)->not->toBeFalse();
    expect($source)->not->toContain('CURLINFO_HEADER_OUT');
    expect($source)->toContain('curl_setopt_array($ch');
});

it('records only selected curl timing fields on the scanner hot path', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));

    expect($source)->not->toBeFalse();
    expect($source)->toContain('$this->recordCurlTimings($ch);');
    expect($source)->not->toContain('$curl_info = curl_getinfo($ch);');
    expect($source)->toContain('CURLINFO_NAMELOOKUP_TIME');
    expect($source)->toContain('CURLINFO_TOTAL_TIME');
});

it('aborts upstream recognition transfers when the HTTP client disconnects', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));

    expect($source)->not->toBeFalse();
    expect($source)->toContain('CURLOPT_NOPROGRESS');
    expect($source)->toContain('CURLOPT_XFERINFOFUNCTION');
    expect($source)->toContain('connection_aborted() ? 1 : 0');
});

it('bounds Plate Recognizer scanner uploads with conservative curl timeouts', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));

    expect($source)->not->toBeFalse();
    expect($source)->toContain('PLATE_READER_CONNECT_TIMEOUT_MS = 1000');
    expect($source)->toContain('PLATE_READER_TOTAL_TIMEOUT_MS = 4500');
    expect($source)->toContain('CURLOPT_CONNECTTIMEOUT_MS => self::PLATE_READER_CONNECT_TIMEOUT_MS');
    expect($source)->toContain('CURLOPT_TIMEOUT_MS => self::PLATE_READER_TOTAL_TIMEOUT_MS');
    expect($source)->toContain('CURLOPT_NOSIGNAL => true');
});

it('uses a short exact-image result cache before calling Plate Recognizer', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));

    expect($source)->not->toBeFalse();
    expect($source)->toContain('RESULT_CACHE_TTL_SECONDS = 10');
    expect($source)->toContain('RESULT_CACHE_REDIS_KEY_PREFIX');
    expect($source)->toContain('$cached_result = $this->readRecognitionResultCache($result_cache, $result_cache_key);');
    expect($source)->toContain('return $this->completeRecognition($started_at, $cached_result);');
    expect($source)->toContain('$this->writeRecognitionResultCache($result_cache, $result_cache_key, $recognized_result);');
    expect($source)->toContain('$this->last_timings[\'cache\']');
    expect($source)->toContain('$this->last_timings[\'cache_hit\'] = 1;');
    expect($source)->toContain('$this->last_timings[\'cache_miss\'] = 1;');
});

it('does not hash multipart upload temp files for result-cache misses', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));

    expect($source)->not->toBeFalse();
    expect($source)->toContain('public function licenseplaterecognizerUploadFile');
    expect($source)->toContain('return $this->recognizePlate(');
    expect($source)->toContain('$this->buildUploadValueFromFile($image_path, $mime_type)');
    expect($source)->not->toContain('buildResultCacheKeyFromFile');
    expect($source)->not->toContain('hash_update_file($context, $image_path)');
    expect($source)->not->toContain('file_get_contents($image_path');
    expect($source)->not->toContain('hash_update($context, $chunk)');
});

it('does not hash raw scanner body bytes on the live camera upload path', function (): void {
    $source = file_get_contents(app_path('classes/licenseplaterecognizer.php'));
    $route = file_get_contents(app_path('routes/moduleScannerRoute.php'));

    expect($source)->not->toBeFalse();
    expect($route)->not->toBeFalse();
    expect($source)->toContain('public function licenseplaterecognizerUploadUncached');
    expect($source)->toContain('fn () => $this->buildPlateReaderPayloadFromUpload(');
    expect($route)->toContain('licenseplaterecognizerUploadUncached($raw_image_upload');
    expect($route)->not->toContain('licenseplaterecognizerUpload($raw_image_upload');
});

it('builds stable result cache keys from equivalent in-memory image bytes', function (): void {
    $reflection = new ReflectionClass(licenseplaterecognizer::class);
    $recognizer = $reflection->newInstanceWithoutConstructor();
    $bytesMethod = $reflection->getMethod('buildResultCacheKeyFromBytes');
    $uploadStringMethod = $reflection->getMethod('buildResultCacheKeyFromUploadString');

    $bytesKey = $bytesMethod->invoke($recognizer, 'jpeg-bytes');
    $dataUriKey = $uploadStringMethod->invoke($recognizer, 'data:image/jpeg;base64,' . base64_encode('jpeg-bytes'));
    $differentKey = $bytesMethod->invoke($recognizer, 'other-jpeg-bytes');

    expect($dataUriKey)->toBe($bytesKey);
    expect($differentKey)->not->toBe($bytesKey);
    expect($bytesKey)->toStartWith('licenseplaterecognizer:result:v1:');
});
