<?php

declare(strict_types=1);

namespace Srp\Tests;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

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

/**
 * Tests for redirect/functions.php — the stateless validation/sanitization
 * helpers extracted from the redirect hot path. Helpers that read $_SERVER are
 * isolated by snapshotting and restoring the superglobal around each test.
 */
final class FunctionsTest extends TestCase
{
    /** @var array<string, mixed> */
    private array $serverSnapshot;

    protected function setUp(): void
    {
        $this->serverSnapshot = $_SERVER;
    }

    protected function tearDown(): void
    {
        $_SERVER = $this->serverSnapshot;
    }

    // ── srp_sanitize_token ──────────────────────────────────────────────────

    public function testSanitizeTokenAcceptsUrlSafeWithinMax(): void
    {
        self::assertSame('Ab-1_2', srp_sanitize_token('  Ab-1_2  ', 16));
    }

    public function testSanitizeTokenRejectsEmptyOverlongOrIllegal(): void
    {
        self::assertNull(srp_sanitize_token('   ', 16));
        self::assertNull(srp_sanitize_token('abcdef', 3));
        self::assertNull(srp_sanitize_token('a.b/c', 16));
    }

    // ── srp_sanitize_title ──────────────────────────────────────────────────

    public function testSanitizeTitleCollapsesControlWhitespaceAndCaps(): void
    {
        self::assertSame('a b c', srp_sanitize_title("  a\r\nb\tc  "));
        self::assertSame(200, mb_strlen(srp_sanitize_title(str_repeat('x', 500)), 'UTF-8'));
    }

    // ── srp_sanitize_https_url ──────────────────────────────────────────────

    public function testSanitizeHttpsUrlAcceptsHttpsOnly(): void
    {
        self::assertSame('https://example.com/x', srp_sanitize_https_url('https://example.com/x'));
        self::assertNull(srp_sanitize_https_url('http://example.com/x'));
        self::assertNull(srp_sanitize_https_url('not a url'));
        self::assertNull(srp_sanitize_https_url(''));
        self::assertNull(srp_sanitize_https_url('https://' . str_repeat('a', 2048)));
    }

    // ── srp_normalize_lg ────────────────────────────────────────────────────

    /**
     * @return array<string, array{0: mixed, 1: string}>
     */
    public static function lgCases(): array
    {
        return [
            'landing word' => ['LANDING', 'landing'],
            'one' => ['1', 'landing'],
            'direct word' => ['direct', 'direct'],
            'unknown' => ['xyz', 'direct'],
            'non-scalar' => [['a'], 'direct'],
        ];
    }

    #[DataProvider('lgCases')]
    public function testNormalizeLg(mixed $input, string $expected): void
    {
        self::assertSame($expected, srp_normalize_lg($input));
    }

    // ── srp_device_type (MobileDetect-backed) ───────────────────────────────

    public function testDeviceTypeClassifiesDesktopMobileTablet(): void
    {
        $desktop = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36';
        $iphone = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';
        $ipad = 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';

        self::assertSame('WEB', srp_device_type(srp_mobile_detect($desktop)));
        self::assertSame('WAP', srp_device_type(srp_mobile_detect($iphone)));
        self::assertSame('TABLET', srp_device_type(srp_mobile_detect($ipad)));
    }

    // ── srp_is_social_bot ───────────────────────────────────────────────────

    public function testSocialBotDetection(): void
    {
        self::assertTrue(srp_is_social_bot('facebookexternalhit/1.1'));
        self::assertTrue(srp_is_social_bot('Mozilla/5.0 (compatible; Googlebot/2.1)'));
        self::assertTrue(srp_is_social_bot('TelegramBot (like TwitterBot)'));
        self::assertFalse(srp_is_social_bot(''));
        self::assertFalse(srp_is_social_bot('Mozilla/5.0 (Windows NT 10.0) Chrome/120'));
    }

    public function testFacebookInAppBrowserIsNotABot(): void
    {
        self::assertFalse(srp_is_social_bot('Mozilla/5.0 (iPhone) [FBAN/FBIOS;FBAV/400.0]'));
    }

    // ── srp_is_suspicious_mobile_probe ───────────────────────────────────────

    public function testSuspiciousMobileProbeDetectsSpoofedCheckers(): void
    {
        // Mobile token + automation / HTTP-client / checker signature → probe.
        self::assertTrue(srp_is_suspicious_mobile_probe(
            'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 python-requests/2.31',
        ));
        self::assertTrue(srp_is_suspicious_mobile_probe('okhttp/4.9.3 (Android 12)'));
        self::assertTrue(srp_is_suspicious_mobile_probe(
            'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) HeadlessChrome/120',
        ));
        self::assertTrue(srp_is_suspicious_mobile_probe('SomeLinkChecker/1.0 (compatible; Mobile)'));
        self::assertTrue(srp_is_suspicious_mobile_probe('Dalvik-free Android curl/8.0'));
    }

    public function testSuspiciousMobileProbeLeavesRealVisitorsAlone(): void
    {
        // Genuine mobile browsers must never be flagged.
        self::assertFalse(srp_is_suspicious_mobile_probe(
            'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
        ));
        self::assertFalse(srp_is_suspicious_mobile_probe(
            'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Mobile Safari/537.36',
        ));
        // Desktop checker (no mobile token) is out of scope for this mobile-only rule.
        self::assertFalse(srp_is_suspicious_mobile_probe('curl/8.0.1'));
        self::assertFalse(srp_is_suspicious_mobile_probe(''));
    }

    // ── srp_header_country_code ──────────────────────────────────────────────

    public function testHeaderCountryCodeUppercasesValidTwoLetter(): void
    {
        $_SERVER['HTTP_CF_IPCOUNTRY'] = 'id';
        self::assertSame('ID', srp_header_country_code('HTTP_CF_IPCOUNTRY'));
    }

    public function testHeaderCountryCodeRejectsSentinelsAndJunk(): void
    {
        $_SERVER['HTTP_CF_IPCOUNTRY'] = 'T1';
        self::assertNull(srp_header_country_code('HTTP_CF_IPCOUNTRY'));

        $_SERVER['HTTP_CF_IPCOUNTRY'] = 'XX';
        self::assertNull(srp_header_country_code('HTTP_CF_IPCOUNTRY'));

        $_SERVER['HTTP_CF_IPCOUNTRY'] = 'USA';
        self::assertNull(srp_header_country_code('HTTP_CF_IPCOUNTRY'));

        unset($_SERVER['HTTP_CF_IPCOUNTRY']);
        self::assertNull(srp_header_country_code('HTTP_CF_IPCOUNTRY'));
    }

    // ── srp_geo_country_code ─────────────────────────────────────────────────

    public function testGeoCountryCodeFallsBackToXx(): void
    {
        self::assertSame('US', srp_geo_country_code(['countryCode' => 'us']));
        self::assertSame('XX', srp_geo_country_code([]));
        self::assertSame('XX', srp_geo_country_code(['countryCode' => '']));
    }

    // ── srp_normalize_ip / srp_is_valid_ip ───────────────────────────────────

    public function testNormalizeIp(): void
    {
        self::assertSame('203.0.113.1', srp_normalize_ip('  203.0.113.1 '));
        self::assertSame('', srp_normalize_ip(''));
        self::assertSame('', srp_normalize_ip(str_repeat('a', 46)));
    }

    public function testIsValidIp(): void
    {
        self::assertTrue(srp_is_valid_ip('203.0.113.1'));
        self::assertTrue(srp_is_valid_ip('2001:db8::1'));
        self::assertFalse(srp_is_valid_ip('999.1.1.1'));
        self::assertFalse(srp_is_valid_ip('nope'));
    }

    // ── srp_is_https ─────────────────────────────────────────────────────────

    public function testIsHttpsDetectsForwardedProto(): void
    {
        unset($_SERVER['HTTPS'], $_SERVER['SERVER_PORT'], $_SERVER['HTTP_CF_VISITOR']);
        $_SERVER['HTTP_X_FORWARDED_PROTO'] = 'https';
        self::assertTrue(srp_is_https());
    }

    public function testIsHttpsDetectsCfVisitorAndPortAndHttpsFlag(): void
    {
        unset(
            $_SERVER['HTTP_X_FORWARDED_PROTO'],
            $_SERVER['HTTPS'],
            $_SERVER['SERVER_PORT'],
            $_SERVER['HTTP_CF_VISITOR'],
        );
        $_SERVER['HTTP_CF_VISITOR'] = '{"scheme":"https"}';
        self::assertTrue(srp_is_https());

        unset($_SERVER['HTTP_CF_VISITOR']);
        $_SERVER['SERVER_PORT'] = '443';
        self::assertTrue(srp_is_https());

        unset($_SERVER['SERVER_PORT']);
        $_SERVER['HTTPS'] = 'on';
        self::assertTrue(srp_is_https());
    }

    public function testIsHttpsFalseWhenNoSignals(): void
    {
        unset(
            $_SERVER['HTTP_X_FORWARDED_PROTO'],
            $_SERVER['HTTP_CF_VISITOR'],
            $_SERVER['HTTPS'],
            $_SERVER['SERVER_PORT'],
        );
        self::assertFalse(srp_is_https());
    }

    // ── srp_validate_host ────────────────────────────────────────────────────

    public function testValidateHost(): void
    {
        self::assertTrue(srp_validate_host('example.com'));
        self::assertTrue(srp_validate_host('sub.example.com:443'));
        self::assertFalse(srp_validate_host(''));
        self::assertFalse(srp_validate_host('bad host!'));
        self::assertFalse(srp_validate_host('example.com:99999x'));
    }

    // ── srp_e ────────────────────────────────────────────────────────────────

    public function testEscapeHtmlContext(): void
    {
        // ENT_QUOTES | ENT_HTML5 encodes the single quote as &apos;.
        self::assertSame('&lt;a&gt;&quot;&apos;', srp_e('<a>"\''));
    }
}
