<?php

use App\Jobs\EnrichQrCodeScan;
use App\Models\QrCode;
use App\Models\QrCodeScan;
use App\Services\QrCode\IpAnonymizer;
use Illuminate\Support\Facades\Queue;

// Redirect behaviour

test('redirect sends a 302 to the target url and creates exactly one scan row', function () {
    Queue::fake();

    $qrCode = QrCode::factory()->url()->create(['target_url' => 'https://example.com/landing']);

    $response = $this->get('/q/'.$qrCode->short_code);

    $response->assertStatus(302);
    $response->assertRedirect('https://example.com/landing');

    expect(QrCodeScan::query()->where('qr_code_id', $qrCode->id)->count())->toBe(1);
});

test('redirect response is 302, never 301', function () {
    Queue::fake();

    $qrCode = QrCode::factory()->url()->create();

    $response = $this->get('/q/'.$qrCode->short_code);

    expect($response->getStatusCode())->toBe(302)
        ->and($response->getStatusCode())->not->toBe(301);
});

test('redirect sends no-store cache headers so browsers never cache the 302', function () {
    Queue::fake();

    $qrCode = QrCode::factory()->url()->create();

    $response = $this->get('/q/'.$qrCode->short_code);

    expect($response->headers->get('Cache-Control'))->toContain('no-store');
});

test('redirect increments scan_count and sets last_scanned_at', function () {
    Queue::fake();

    $qrCode = QrCode::factory()->url()->create(['scan_count' => 5, 'last_scanned_at' => null]);

    $this->get('/q/'.$qrCode->short_code);

    $qrCode->refresh();

    expect($qrCode->scan_count)->toBe(6)
        ->and($qrCode->last_scanned_at)->not->toBeNull();
});

test('redirect dispatches the enrichment job for the created scan', function () {
    Queue::fake();

    $qrCode = QrCode::factory()->url()->create();

    $this->get('/q/'.$qrCode->short_code);

    $scan = QrCodeScan::query()->where('qr_code_id', $qrCode->id)->firstOrFail();

    Queue::assertPushed(EnrichQrCodeScan::class, fn (EnrichQrCodeScan $job): bool => $job->qrCodeScanId === $scan->id);
});

test('inactive qr code returns 410 gone, not 404', function () {
    $qrCode = QrCode::factory()->url()->inactive()->create();

    $response = $this->get('/q/'.$qrCode->short_code);

    $response->assertStatus(410);
});

test('unknown short code returns 404', function () {
    $response = $this->get('/q/does-not-exist');

    $response->assertNotFound();
});

test('a static (non-dynamic) qr code 404s on the redirect route', function () {
    $qrCode = QrCode::factory()->static()->create();

    $response = $this->get('/q/'.$qrCode->short_code);

    $response->assertNotFound();
});

// IpAnonymizer

test('truncate zeroes the last octet of an ipv4 address', function () {
    expect(IpAnonymizer::truncate('203.0.113.42'))->toBe('203.0.113.0');
});

test('truncate zeroes the last 80 bits of an ipv6 address', function () {
    expect(IpAnonymizer::truncate('2001:db8:85a3::8a2e:370:7334'))->toBe('2001:db8:85a3::')
        ->and(IpAnonymizer::truncate('::1'))->toBe('::');
});

test('truncate returns unparseable input unchanged', function () {
    expect(IpAnonymizer::truncate('not-an-ip'))->toBe('not-an-ip');
});

test('isPrivateOrReserved detects private, loopback and link-local ranges', function () {
    expect(IpAnonymizer::isPrivateOrReserved('127.0.0.1'))->toBeTrue()
        ->and(IpAnonymizer::isPrivateOrReserved('10.0.0.5'))->toBeTrue()
        ->and(IpAnonymizer::isPrivateOrReserved('172.16.0.5'))->toBeTrue()
        ->and(IpAnonymizer::isPrivateOrReserved('192.168.1.1'))->toBeTrue()
        ->and(IpAnonymizer::isPrivateOrReserved('::1'))->toBeTrue()
        ->and(IpAnonymizer::isPrivateOrReserved('8.8.8.8'))->toBeFalse();
});

test('visitorHash is stable for the same ip, user agent and day', function () {
    $date = now();

    $hash1 = IpAnonymizer::visitorHash('203.0.113.42', 'ua-1', $date, 'salt');
    $hash2 = IpAnonymizer::visitorHash('203.0.113.42', 'ua-1', $date, 'salt');

    expect($hash1)->toBe($hash2);
});

test('visitorHash differs when the ip, user agent or salt differs', function () {
    $date = now();
    $base = IpAnonymizer::visitorHash('203.0.113.42', 'ua-1', $date, 'salt');

    expect(IpAnonymizer::visitorHash('203.0.113.99', 'ua-1', $date, 'salt'))->not->toBe($base)
        ->and(IpAnonymizer::visitorHash('203.0.113.42', 'ua-2', $date, 'salt'))->not->toBe($base)
        ->and(IpAnonymizer::visitorHash('203.0.113.42', 'ua-1', $date, 'other-salt'))->not->toBe($base);
});

// IP storage on the redirect path

test('scan stores a truncated ipv4 address by default (store_full_ip = false)', function () {
    Queue::fake();
    config(['qr.store_full_ip' => false]);

    $qrCode = QrCode::factory()->url()->create();

    $this->withServerVariables(['REMOTE_ADDR' => '203.0.113.42'])->get('/q/'.$qrCode->short_code);

    $scan = QrCodeScan::query()->where('qr_code_id', $qrCode->id)->firstOrFail();

    expect($scan->ip_address)->toBe('203.0.113.0');
});

test('scan stores the full ipv4 address when store_full_ip is true', function () {
    Queue::fake();
    config(['qr.store_full_ip' => true]);

    $qrCode = QrCode::factory()->url()->create();

    $this->withServerVariables(['REMOTE_ADDR' => '203.0.113.42'])->get('/q/'.$qrCode->short_code);

    $scan = QrCodeScan::query()->where('qr_code_id', $qrCode->id)->firstOrFail();

    expect($scan->ip_address)->toBe('203.0.113.42');
});

test('scan stores a truncated ipv6 address by default (store_full_ip = false)', function () {
    Queue::fake();
    config(['qr.store_full_ip' => false]);

    $qrCode = QrCode::factory()->url()->create();

    $this->withServerVariables(['REMOTE_ADDR' => '2001:db8:85a3::8a2e:370:7334'])
        ->get('/q/'.$qrCode->short_code);

    $scan = QrCodeScan::query()->where('qr_code_id', $qrCode->id)->firstOrFail();

    expect($scan->ip_address)->toBe('2001:db8:85a3::');
});

test('scan stores the full ipv6 address when store_full_ip is true', function () {
    Queue::fake();
    config(['qr.store_full_ip' => true]);

    $qrCode = QrCode::factory()->url()->create();

    $this->withServerVariables(['REMOTE_ADDR' => '2001:db8:85a3::8a2e:370:7334'])
        ->get('/q/'.$qrCode->short_code);

    $scan = QrCodeScan::query()->where('qr_code_id', $qrCode->id)->firstOrFail();

    expect($scan->ip_address)->toBe('2001:db8:85a3::8a2e:370:7334');
});

test('visitor_hash is computed from the full ip even when store_full_ip is false', function () {
    Queue::fake();
    config(['qr.store_full_ip' => false]);

    $qrCode = QrCode::factory()->url()->create();

    $this->withServerVariables(['REMOTE_ADDR' => '203.0.113.42', 'HTTP_USER_AGENT' => 'test-agent'])
        ->get('/q/'.$qrCode->short_code);

    $scan = QrCodeScan::query()->where('qr_code_id', $qrCode->id)->firstOrFail();

    $expected = IpAnonymizer::visitorHash(
        '203.0.113.42',
        'test-agent',
        $scan->scanned_at,
        (string) config('qr.visitor_hash_salt'),
    );

    expect($scan->visitor_hash)->toBe($expected)
        ->and($scan->ip_address)->toBe('203.0.113.0');
});

// Retention (qr:prune-scans)

test('qr:prune-scans deletes only scans past the retention period', function () {
    config(['qr.scan_retention_days' => 30]);

    $qrCode = QrCode::factory()->url()->create();

    $old = QrCodeScan::factory()->for($qrCode)->scannedAt(now()->subDays(45))->create();
    $recent = QrCodeScan::factory()->for($qrCode)->scannedAt(now()->subDays(10))->create();

    $this->artisan('qr:prune-scans')->assertSuccessful();

    expect(QrCodeScan::query()->whereKey($old->id)->exists())->toBeFalse()
        ->and(QrCodeScan::query()->whereKey($recent->id)->exists())->toBeTrue();
});
