<?php

declare(strict_types=1);

namespace Srp\Tests;

use PHPUnit\Framework\TestCase;
use Srp\Redirect\GeoResolver;

require_once __DIR__ . '/../redirect/GeoResolver.php';

/**
 * Tests for redirect/GeoResolver.php — IP → country lookup with a 24h file
 * cache. The invalid-input, cache-read, and TTL-expiry paths are exercised
 * without the MMDB; one integration case hits the real GeoLite2 DB when it is
 * present (skipped on a fresh checkout that only ships PLACE_GEOIP_HERE.txt).
 */
final class GeoResolverTest extends TestCase
{
    private const MMDB = __DIR__ . '/../redirect/databases/GeoLite2-Country.mmdb';

    private string $cacheSubDir;
    private string $cacheDir;

    protected function setUp(): void
    {
        $this->cacheSubDir = 'srp_test_' . bin2hex(random_bytes(6));
        $this->cacheDir = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR)
            . DIRECTORY_SEPARATOR . $this->cacheSubDir;
    }

    protected function tearDown(): void
    {
        if (!is_dir($this->cacheDir)) {
            return;
        }
        foreach (glob($this->cacheDir . DIRECTORY_SEPARATOR . '*.json') ?: [] as $file) {
            @unlink($file);
        }
        @rmdir($this->cacheDir);
    }

    public function testInvalidIpReturnsEmptyWithoutTouchingDb(): void
    {
        // Non-existent MMDB path proves these short-circuit before any lookup.
        $resolver = new GeoResolver('/no/such/file.mmdb', $this->cacheSubDir);

        self::assertSame([], $resolver->resolve(''));
        self::assertSame([], $resolver->resolve('not-an-ip'));
        self::assertSame([], $resolver->resolve('999.999.999.999'));
    }

    public function testFreshCacheIsReturnedWithoutDbLookup(): void
    {
        $ip = '203.0.113.50'; // TEST-NET-3, not in any real GeoIP DB
        @mkdir($this->cacheDir, 0700, true);
        file_put_contents(
            $this->cacheDir . DIRECTORY_SEPARATOR . 'geo_' . md5($ip) . '.json',
            json_encode(['countryCode' => 'ZZ', 'countryName' => 'Zedland']),
        );

        // MMDB path is bogus; a non-empty result can only come from the cache.
        $resolver = new GeoResolver('/no/such/file.mmdb', $this->cacheSubDir);

        self::assertSame(['countryCode' => 'ZZ', 'countryName' => 'Zedland'], $resolver->resolve($ip));
    }

    public function testExpiredCacheIsIgnored(): void
    {
        $ip = '203.0.113.51';
        @mkdir($this->cacheDir, 0700, true);
        file_put_contents(
            $this->cacheDir . DIRECTORY_SEPARATOR . 'geo_' . md5($ip) . '.json',
            json_encode(['countryCode' => 'ZZ', 'countryName' => 'Zedland']),
        );

        // TTL 0 makes every cache entry stale; falls through to lookup, which
        // for a TEST-NET IP (or a missing DB) yields no country → [].
        $resolver = new GeoResolver('/no/such/file.mmdb', $this->cacheSubDir, 0);

        self::assertSame([], $resolver->resolve($ip));
    }

    public function testResolvesKnownPublicIpAgainstRealDb(): void
    {
        if (!is_file(self::MMDB)) {
            self::markTestSkipped('GeoLite2-Country.mmdb not present (fresh checkout).');
        }

        $resolver = new GeoResolver(self::MMDB, $this->cacheSubDir);
        $result = $resolver->resolve('8.8.8.8');

        self::assertArrayHasKey('countryCode', $result);
        // Assert shape, not a specific country, so the test survives DB updates.
        self::assertMatchesRegularExpression('/^[A-Z]{2}$/', $result['countryCode']);
        self::assertNotSame('', $result['countryName'] ?? '');
    }

    public function testResolveWritesCacheFile(): void
    {
        if (!is_file(self::MMDB)) {
            self::markTestSkipped('GeoLite2-Country.mmdb not present (fresh checkout).');
        }

        $ip = '8.8.4.4';
        $resolver = new GeoResolver(self::MMDB, $this->cacheSubDir);
        $resolver->resolve($ip);

        self::assertFileExists($this->cacheDir . DIRECTORY_SEPARATOR . 'geo_' . md5($ip) . '.json');
    }
}
