<?php

use App\Jobs\EnrichQrCodeScan;
use App\Models\QrCode;
use App\Models\QrCodeScan;
use App\Services\QrCode\Contracts\GeoLocator;
use App\Services\QrCode\HttpGeoLocator;
use Illuminate\Support\Facades\Http;

test('enrichment marks a known crawler user agent as a bot', function () {
    $qrCode = QrCode::factory()->url()->create();
    $scan = QrCodeScan::factory()->for($qrCode)->create([
        'user_agent' => 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)',
        'ip_address' => '127.0.0.1',
        'is_bot' => false,
        'enriched_at' => null,
    ]);

    (new EnrichQrCodeScan($scan->id))->handle(app(GeoLocator::class));

    $scan->refresh();

    expect($scan->is_bot)->toBeTrue()
        ->and($scan->enriched_at)->not->toBeNull();
});

test('enrichment flags link-preview crawlers DeviceDetector treats as apps', function (string $userAgent) {
    $qrCode = QrCode::factory()->url()->create();
    $scan = QrCodeScan::factory()->for($qrCode)->create([
        'user_agent' => $userAgent,
        'ip_address' => '127.0.0.1',
        'is_bot' => false,
        'enriched_at' => null,
    ]);

    (new EnrichQrCodeScan($scan->id))->handle(app(GeoLocator::class));

    expect($scan->refresh()->is_bot)->toBeTrue();
})->with([
    'whatsapp' => ['WhatsApp/2.23.20.0'],
    'whatsapp android' => ['WhatsApp/2.19.81 A'],
    'discord' => ['Discordbot/2.0 (+https://discordapp.com)'],
    'skype' => ['SkypeUriPreview Preview/0.5'],
]);

test('enrichment does not flag a real browser as a bot', function () {
    $qrCode = QrCode::factory()->url()->create();
    $scan = QrCodeScan::factory()->for($qrCode)->create([
        'user_agent' => 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
        'ip_address' => '127.0.0.1',
        'is_bot' => false,
        'enriched_at' => null,
    ]);

    (new EnrichQrCodeScan($scan->id))->handle(app(GeoLocator::class));

    expect($scan->refresh()->is_bot)->toBeFalse();
});

test('enrichment fills device_type, browser and platform from the user agent', function () {
    $qrCode = QrCode::factory()->url()->create();
    $scan = QrCodeScan::factory()->for($qrCode)->create([
        'user_agent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1',
        'ip_address' => '127.0.0.1',
        'device_type' => null,
        'browser' => null,
        'platform' => null,
    ]);

    (new EnrichQrCodeScan($scan->id))->handle(app(GeoLocator::class));

    $scan->refresh();

    expect($scan->device_type)->toBe('smartphone')
        ->and($scan->browser)->not->toBeNull()
        ->and($scan->platform)->toBe('iOS');
});

test('enrichment skips the geo lookup entirely for private/reserved ip addresses', function () {
    $qrCode = QrCode::factory()->url()->create();
    $scan = QrCodeScan::factory()->for($qrCode)->create([
        'ip_address' => '127.0.0.1',
        'country_code' => null,
        'country' => null,
    ]);

    $geoLocator = Mockery::mock(GeoLocator::class);
    $geoLocator->shouldNotReceive('locate');

    (new EnrichQrCodeScan($scan->id))->handle($geoLocator);

    $scan->refresh();

    expect($scan->country_code)->toBeNull()
        ->and($scan->enriched_at)->not->toBeNull();
});

test('enrichment fills geo data from the bound geo locator for public ip addresses', function () {
    $qrCode = QrCode::factory()->url()->create();
    $scan = QrCodeScan::factory()->for($qrCode)->create([
        'ip_address' => '203.0.113.5',
        'country_code' => null,
    ]);

    $geoLocator = Mockery::mock(GeoLocator::class);
    $geoLocator->shouldReceive('locate')->once()->with('203.0.113.5')->andReturn([
        'country_code' => 'IN',
        'country' => 'India',
        'region' => 'Gujarat',
        'city' => 'Surat',
        'latitude' => '21.1702',
        'longitude' => '72.8311',
    ]);

    (new EnrichQrCodeScan($scan->id))->handle($geoLocator);

    $scan->refresh();

    expect($scan->country_code)->toBe('IN')
        ->and($scan->city)->toBe('Surat');
});

test('enrichment leaves geo fields null and still sets enriched_at when the locator fails soft', function () {
    $qrCode = QrCode::factory()->url()->create();
    $scan = QrCodeScan::factory()->for($qrCode)->create([
        'ip_address' => '203.0.113.5',
        'country_code' => null,
    ]);

    $geoLocator = Mockery::mock(GeoLocator::class);
    $geoLocator->shouldReceive('locate')->once()->andReturn(null);

    (new EnrichQrCodeScan($scan->id))->handle($geoLocator);

    $scan->refresh();

    expect($scan->country_code)->toBeNull()
        ->and($scan->enriched_at)->not->toBeNull();
});

test('enrichment is safe to re-run and refreshes enriched_at', function () {
    $qrCode = QrCode::factory()->url()->create();
    $scan = QrCodeScan::factory()->for($qrCode)->create([
        'ip_address' => '127.0.0.1',
    ]);

    $job = new EnrichQrCodeScan($scan->id);
    $job->handle(app(GeoLocator::class));
    $firstEnrichedAt = $scan->refresh()->enriched_at;

    $this->travel(1)->minutes();

    $job->handle(app(GeoLocator::class));
    $secondEnrichedAt = $scan->refresh()->enriched_at;

    expect($secondEnrichedAt->greaterThan($firstEnrichedAt))->toBeTrue();
});

test('enrichment silently no-ops when the scan no longer exists', function () {
    expect(fn () => (new EnrichQrCodeScan(999999))->handle(app(GeoLocator::class)))
        ->not->toThrow(Exception::class);
});

test('the job retries up to 3 times', function () {
    $job = new EnrichQrCodeScan(1);

    expect($job->tries)->toBe(3);
});

test('HttpGeoLocator fails soft and returns null on an http error', function () {
    Http::fake([
        'ip-api.com/*' => Http::response(null, 500),
    ]);

    $result = (new HttpGeoLocator)->locate('203.0.113.5');

    expect($result)->toBeNull();
});

test('HttpGeoLocator parses a successful ip-api.com response', function () {
    Http::fake([
        'ip-api.com/*' => Http::response([
            'status' => 'success',
            'countryCode' => 'IN',
            'country' => 'India',
            'regionName' => 'Gujarat',
            'city' => 'Surat',
            'lat' => 21.1702,
            'lon' => 72.8311,
        ]),
    ]);

    $result = (new HttpGeoLocator)->locate('203.0.113.5');

    expect($result)->toBe([
        'country_code' => 'IN',
        'country' => 'India',
        'region' => 'Gujarat',
        'city' => 'Surat',
        'latitude' => '21.1702',
        'longitude' => '72.8311',
    ]);
});

test('NullGeoLocator is bound in the testing environment', function () {
    expect(app(GeoLocator::class))->toBeInstanceOf(\App\Services\QrCode\NullGeoLocator::class);
});
