<?php

namespace Tests\Feature;

use App\Models\Asns;
use App\Models\Goliath\GoliathOrganisationSpecial;
use App\Models\Goliath\PrefixGoliathFlag;
use App\Models\Goliath\PrefixGoliathLoose;
use App\Models\Group;
use App\Models\Organisation;
use App\Models\PrefixCommunity;
use App\Models\Prefixes;
use App\Models\ProductRights;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Redis;
use Tests\TestCase;

/**
 *      @@@ TEST CERTIFICATION @@@
 *      This set of tests has been confirmed to be functioning as intended.
 *
 *      More tests will need to be added for:
 *          - CITECs Australia driven behaviour
 *          - Special functionality for GGG
 *
 *      Jesse Cunningham, 2026-04-21 4:00 PM AEST
 *
 *      Updated 2026-05-28: Expected prefix maps now reflect the post-crush
 *      output shape. The configuration crusher collapses adjacent/contained
 *      prefixes sharing identical configs and fragments containers around
 *      more-specific children that carry different configs. The expected
 *      maps below were derived by hand-tracing the crusher against this
 *      fixture; see the comments above each block for the reasoning.
 */
class GoliathConfigPullTest extends TestCase
{
    use RefreshDatabase;

    // ---------------------------------------------------------------------
    // Fixture identifiers
    // ---------------------------------------------------------------------

    private const REGION_1    = 1;
    private const REGION_2    = 2;
    private const REGION_3    = 3;

    private const SUBREGION_1 = 1;
    private const SUBREGION_2 = 2;
    private const SUBREGION_3 = 3;

    private const METRO_1     = 1;
    private const METRO_2     = 2;
    private const METRO_3     = 3;

    // Each prefix id carries a single descriptive name.
    private const PREFIX_NOT_SETUP          = 1;
    private const PREFIX_S_ON_H3_CHAINLESS  = 2;
    private const PREFIX_S_OFF_H2_CHAIN     = 3;
    private const PREFIX_FIVEM_CHAIN        = 4;
    private const PREFIX_FIVEM_CHAINLESS    = 5;
    private const PREFIX_MERCURY_CLAMP_MSS  = 6;
    private const PREFIX_UNANNOUNCED        = 7;
    private const PREFIX_H0                 = 8;
    private const PREFIX_H1                 = 9;
    private const PREFIX_H2_CHAIN_S_ON      = 10;
    private const PREFIX_H3_CHAIN_S_OFF     = 11;

    private const REDIS_KEYS = [
        'goliath-flags',
        'goliath-flags.SREG-1',
        'goliath-flags.SREG-2',
        'goliath-flags.SREG-3',
        'goliath-chains',
        'goliath-chains.SREG-1',
        'goliath-chains.SREG-2',
        'goliath-chains.SREG-3',
    ];

    private User $user;
    private Organisation $organisation;
    private Asns $asn;

    // ---------------------------------------------------------------------
    // Setup
    // ---------------------------------------------------------------------

    protected function setUp(): void
    {
        parent::setUp();

        // Keep environment safe — any outbound HTTP is stubbed.
        Http::fake(['*' => Http::response(['Fallback Error'], 500)]);

        $this->seedOrganisationsAndUser();
        $this->seedRegionalHierarchy();
        $this->seedPrefixes();

        // Add Mercury + BGP communities to prefix
        PrefixCommunity::create([
            'prefix_id' => self::PREFIX_MERCURY_CLAMP_MSS,
            'community' => '7578:91',
            'is_large'  => 0,
        ]);

        PrefixCommunity::create([
            'prefix_id' => self::PREFIX_MERCURY_CLAMP_MSS,
            'community' => '7578:92',
            'is_large'  => 0,
        ]);
    }

    private function seedOrganisationsAndUser(): void
    {
        $this->organisation = Organisation::create(['name' => '']);
        $group              = Group::create(['name' => 'Goliath']);
        $this->asn          = Asns::create([
            'asn'             => 137409,
            'organisation_id' => $this->organisation->id,
            'approved'        => 1,
        ]);

        $this->user = User::create([
            'name'            => 'Test McTest',
            'email'           => 'creatia@globalsecurelayer.com',
            'phone'           => null,
            'password'        => '',
            'group_id'        => $group->id,
            'organisation_id' => $this->organisation->id,
        ]);
    }

    private function seedRegionalHierarchy(): void
    {
        $regions = [
            [self::REGION_1, 'Region 1', 'REG-1'],
            [self::REGION_2, 'Region 2', 'REG-2'],
            [self::REGION_3, 'Region 3', 'REG-3'],
        ];
        foreach ($regions as [$id, $name, $telemetry]) {
            DB::table('goliath_regions')->insert([
                'id'                   => $id,
                'name'                 => $name,
                'telemetry_identifier' => $telemetry,
                'visible'              => 1,
            ]);
        }

        $subregions = [
            [self::SUBREGION_1, 'Subregion 1', 'SREG-1', self::REGION_1],
            [self::SUBREGION_2, 'Subregion 2', 'SREG-2', self::REGION_2],
            [self::SUBREGION_3, 'Subregion 3', 'SREG-3', self::REGION_3],
        ];
        foreach ($subregions as [$id, $name, $telemetry, $regionId]) {
            DB::table('goliath_subregions')->insert([
                'id'                   => $id,
                'name'                 => $name,
                'telemetry_identifier' => $telemetry,
                'visible'              => 1,
                'region_id'            => $regionId,
            ]);
        }

        $metros = [
            [self::METRO_1, 'Metro 1', 'MET-1', self::SUBREGION_1],
            [self::METRO_2, 'Metro 2', 'MET-2', self::SUBREGION_2],
            [self::METRO_3, 'Metro 3', 'MET-3', self::SUBREGION_3],
        ];
        foreach ($metros as [$id, $name, $telemetry, $subregionId]) {
            DB::table('goliath_metro')->insert([
                'id'                   => $id,
                'name'                 => $name,
                'telemetry_identifier' => $telemetry,
                'visible'              => 1,
                'subregion_id'         => $subregionId,
            ]);
        }
    }

    private function seedPrefixes(): void
    {
        // Columns that don't change across rows in this fixture.
        $common = [
            'seen_at'        => Carbon::now(),
            'created_source' => 'prefix-list-uncrushed',
        ];

        // [id, name, address, mask, still_announced, setup_required, org_kind]
        // org_kind: 'standard'
        $rows = [
            [self::PREFIX_NOT_SETUP,         'Keep it not setup',             '103.1.213.13', 24, 1, 1,    'standard'],
            [self::PREFIX_S_ON_H3_CHAINLESS, 'S-on H3 Chainless',             '103.1.213.14', 32, 1, 0,    'standard'],
            [self::PREFIX_S_OFF_H2_CHAIN,    'S-off H2 Chain',                '103.1.213.15', 32, 1, 0,    'standard'],
            [self::PREFIX_FIVEM_CHAIN,       'FiveM Chain',                   '103.1.213.16', 32, 1, 0,    'standard'],
            [self::PREFIX_FIVEM_CHAINLESS,   'FiveM Chainless',               '103.1.213.17', 32, 1, 0,    'standard'],
            [self::PREFIX_MERCURY_CLAMP_MSS, 'Mercury Clamp MSS',             '103.1.213.18', 32, 1, 0,    'standard'],
            [self::PREFIX_UNANNOUNCED,       'Unannounced',                   '103.1.213.19', 32, 0, 0,    'standard'],
            [self::PREFIX_H0,                'H0',                            '103.1.213.0',  28, 1, 0,    'standard'],
            [self::PREFIX_H1,                'H1',                            '103.1.213.0',  30, 1, null, 'standard'],
            [self::PREFIX_H2_CHAIN_S_ON,     'H2 Chain with Synproxy',        '208.10.10.0',  29, 1, null, 'standard'],
            [self::PREFIX_H3_CHAIN_S_OFF,    'H3 Chain w/o Synproxy',         '208.10.10.1',  32, 1, null, 'standard'],
        ];

        $orgMap = [
            'standard' => [$this->organisation->id, $this->asn->id]
        ];

        $inserts = [];
        foreach ($rows as [$id, $name, $address, $mask, $stillAnnounced, $setupRequired, $orgKind]) {
            [$orgId, $asnId] = $orgMap[$orgKind];
            $inserts[] = $common + [
                'id'              => $id,
                'name'            => $name,
                'address'         => $address,
                'mask'            => $mask,
                'still_announced' => $stillAnnounced,
                'setup_required'  => $setupRequired,
                'organisation_id' => $orgId,
                'asn_id'          => $asnId,
            ];
        }

        DB::table('prefixes')->insert($inserts);
    }

    // ---------------------------------------------------------------------
    // API helpers
    // ---------------------------------------------------------------------

    /**
     * PUT flags onto a prefix.
     * When $flags contains a 'heuristics' key we route to the admin endpoint,
     * matching the original test's split between user and admin flag APIs.
     */
    private function setFlags(int $prefixId, array $flags): void
    {
        $this->actingAs($this->user)->withoutMiddleware()->put("/api/admin/goliath/prefix/$prefixId/flag", $flags);
    }

    private function createChain(string $name, array $elements): int
    {
        $response = $this->actingAs($this->user)->withoutMiddleware()->post('/api/goliath/chain', [
            'name'           => $name,
            'default_action' => 'drop',
            'default_x'      => 0,
            'default_y'      => 200,
            'elements'       => $elements,
        ]);

        return $response->json()['chain_id'];
    }

    private function attachChain(int $prefixId, int $chainId, array $extra = [], bool $admin = false): void
    {
        $path = $admin
            ? "/api/admin/goliath/prefix/chain/$prefixId"
            : "/api/goliath/prefix/chain/$prefixId";

        $this->actingAs($this->user)->withoutMiddleware()->put($path, array_merge([
            'chain_id'       => $chainId,
            'make_duplicate' => false,
        ], $extra));
    }

    private function clearGoliathRedis(): void
    {
        foreach (self::REDIS_KEYS as $key) {
            Redis::delete($key);
        }
    }

    private function fetchConfig(?string $region = null, ?string $subregion = null, ?string $metro = null): array
    {
        $path = '/api/goliath/data-v4';
        if ($region !== null) {
            $path .= "?region=$region&subregion=$subregion&datacentre=$metro";
        }

        return $this->withoutMiddleware()->get($path)->json();
    }

    // ---------------------------------------------------------------------
    // Assertion helpers
    // ---------------------------------------------------------------------

    private function assertChainsMatch(array $actual, array $expected): void
    {
        $this->assertCount(count($expected), $actual, 'The number of chains should definitely align.');

        foreach ($expected as $key => $chain) {
            $this->assertArrayHasKey($key, $actual, "Chain $key should be returned.");
            $this->assertCount(count($chain), $actual[$key], "Chain $key returned a different amount of members.");

            foreach ($chain as $i => $member) {
                $this->assertEquals(
                    $member['bpfRule'] ?? null,
                    $actual[$key][$i]['bpfRule'] ?? null,
                    "Chain $key member $i returned an incorrect BPF rule."
                );
                $this->assertEquals(
                    $member['matchAction'],
                    $actual[$key][$i]['matchAction'],
                    "Chain $key member $i returned an incorrect action."
                );
            }
        }
    }

    private function assertPrefixesMatch(array $actual, array $expected): void
    {
        $this->assertCount(count($expected), $actual, 'The number of prefixes should definitely align.');

        $required = ['synproxy', 'heuristicsLevel', 'asn', 'organisation'];
        $optional = ['chainID', 'gameFilters', 'mercury', 'dedicatedBgp', 'dropTCPUnknown', 'dropUDPUnknown', 'srcLimitPps', 'srcLimitMbps', 'dropUnexpectedProtocols', 'dropNonCoreProtocols', 'dropUDPToTCPPorts', 'udpEstablishedLane'];

        foreach ($expected as $key => $prefix) {
            $this->assertArrayHasKey($key, $actual, "Prefix $key should be returned.");

            foreach ($required as $property) {
                $this->assertEquals(
                    $prefix[$property],
                    $actual[$key][$property],
                    "Prefix $key property '$property' mismatch."
                );
            }

            foreach ($optional as $property) {
                if (($prefix[$property] ?? null) === null) {
                    $this->assertArrayNotHasKey(
                        $property,
                        $actual[$key],
                        "Prefix $key property '$property' should not exist but it does."
                    );
                } else {
                    if(is_object($actual[$key][$property])) {
                        $this->assertObjectEquals(
                            $prefix[$property],
                            $actual[$key][$property],
                            "Prefix $key property '$property' mismatch."
                        );

                    }
                    else {
                        $this->assertEquals(
                            $prefix[$property],
                            $actual[$key][$property],
                            "Prefix $key property '$property' mismatch."
                        );
                    }

                }
            }
        }
    }

    /**
     * Pull the configuration for a given region tuple and verify chains + prefixes.
     * Pass an empty array to skip checking that dimension.
     */
    private function runRegionalAssertion(
        string $region,
        string $subregion,
        string $metro,
        array $expectedChains,
        array $expectedPrefixes
    ): void {
        $config = $this->fetchConfig($region, $subregion, $metro);

        if ($expectedChains !== []) {
            $this->assertChainsMatch(json_decode($config['chains'], true), $expectedChains);
        }

        if ($expectedPrefixes !== []) {
            $this->assertPrefixesMatch(json_decode($config['targets'], true), $expectedPrefixes);
        }
    }

    /**
     * Build a config row pre-filled with the shared base (organisation, asn,
     * and the optional fields defaulted to null) so each expected prefix only
     * has to specify what's distinctive.
     */
    private function baseCfg(array $overrides = []): array
    {
        return array_merge([
            'organisation'    => $this->organisation->id,
            'asn'             => $this->asn->asn,
            'synproxy'        => false,
            'heuristicsLevel' => 0,
            'chainID'         => null,
            'gameFilters'     => null,
            'mercury'         => null,
            'dedicatedBgp'    => null,
            'dropUDPUnknown'  => null,
            'dropTCPUnknown'  => null
        ], $overrides);
    }

    // ---------------------------------------------------------------------
    // Test: Standard config pull
    // ---------------------------------------------------------------------

    public function testGoliathPullConfigStandard(): void
    {

        $flags = [
            self::PREFIX_H0              => ['inline' => false, 'synproxy' => false, 'filters' => null, 'heuristics' => 0, 'loose' => false],
            self::PREFIX_H1              => ['inline' => false, 'synproxy' => false, 'filters' => null, 'heuristics' => 1,  'loose' => false],
            self::PREFIX_H2_CHAIN_S_ON   => ['inline' => true,  'synproxy' => true,  'filters' => null, 'heuristics' => 2,  'loose' => false],
            self::PREFIX_H3_CHAIN_S_OFF  => ['inline' => true,  'synproxy' => false, 'filters' => null, 'heuristics' => 3,  'loose' => false],
            self::PREFIX_S_ON_H3_CHAINLESS => ['inline' => false, 'synproxy' => true,  'filters' => null, 'heuristics' => 3, 'loose' => false],
            self::PREFIX_S_OFF_H2_CHAIN    => ['inline' => false, 'synproxy' => false, 'filters' => null, 'heuristics' => 2, 'loose' => false],
            self::PREFIX_FIVEM_CHAIN       => ['inline' => false, 'synproxy' => false, 'filters' => [ 'fivem' => ['1000','1010'], 'source1' => ['1001-1008'], 'rust' => ['1009'] ], 'heuristics' => 3, 'loose' => false],
            self::PREFIX_FIVEM_CHAINLESS   => ['inline' => false, 'synproxy' => false, 'filters' => [ 'fivem' => ['10010-30000'] ], 'heuristics' => 3, 'loose' => false],
            self::PREFIX_MERCURY_CLAMP_MSS => ['inline' => false, 'synproxy' => false, 'filters' => null, 'heuristics' => 2, 'loose' => false, 'block_udp' => true, 'block_tcp' => true], // The blocks should fail
            self::PREFIX_UNANNOUNCED       => ['inline' => true,  'synproxy' => true,  'filters' => [ 'source1' => ['20010-40000'] ], 'heuristics' => 3, 'loose' => false, 'block_udp' => true, 'block_tcp' => true]
        ];

        foreach ($flags as $prefixId => $x) {
            $this->setFlags($prefixId, $x);
        }

        // --- Chains -----------------------------------------------------
        $chainPassMost = $this->createChain('Pass Most', [
            ['bpf' => 'tcp and udp and icmp', 'action' => 'accept', 'x' => 0, 'y' => 0],
        ]);

        $chainDropUdpTcp = $this->createChain('Drop UDP then TCP pass ICMP drop other', [
            ['bpf' => 'udp',  'action' => 'drop',   'x' => 0, 'y' => 0],
            ['bpf' => 'tcp',  'action' => 'drop',   'x' => 0, 'y' => 0],
            ['bpf' => 'icmp', 'action' => 'accept', 'x' => 0, 'y' => 0],
        ]);

        // --- Chain attachments ------------------------------------------
        // Global attachments
        $this->attachChain(self::PREFIX_S_OFF_H2_CHAIN,  $chainPassMost);
        $this->attachChain(self::PREFIX_FIVEM_CHAIN,     $chainPassMost);
        $this->attachChain(self::PREFIX_H2_CHAIN_S_ON,   $chainDropUdpTcp);
        $this->attachChain(self::PREFIX_H3_CHAIN_S_OFF,  $chainDropUdpTcp);

        // Regional override: H3 chain uses chainPassMost inside REGION_2/SUBREGION_2
        $this->attachChain(self::PREFIX_H3_CHAIN_S_OFF, $chainPassMost, [
            'region_id'    => self::REGION_2,
            'subregion_id' => self::SUBREGION_2,
        ]);

        // --- Expected data ---------------------------------------------
        $expectedChains = [
            $chainPassMost => [
                ['bpfRule' => 'tcp and udp and icmp', 'matchAction' => 'accept'],
                ['matchAction' => 'drop'],
            ],
            $chainDropUdpTcp => [
                ['bpfRule' => 'udp',  'matchAction' => 'drop'],
                ['bpfRule' => 'tcp',  'matchAction' => 'drop'],
                ['bpfRule' => 'icmp', 'matchAction' => 'accept'],
                ['matchAction' => 'drop'],
            ],
        ];

        /*
         * Expected prefix map after crushing — REG-1 / SREG-1 perspective.
         *
         * Hand-traced from the fixture:
         *
         *   In 103.1.213.0/28 (.0–.15):
         *     .0/30   (H1)             — claims .0–.3
         *     .14/32  (S-on H3)        — claims .14
         *     .15/32  (S-off H2 chain) — claims .15
         *     .0/28   (H0)             — claims remainder .4–.13
         *
         *   H0's remainder .4–.13 collapses (via summarize_address_range) to:
         *     103.1.213.4/30   (.4–.7)
         *     103.1.213.8/30   (.8–.11)
         *     103.1.213.12/31  (.12–.13)
         *
         *   In 208.10.10.0/29 (.0–.7):
         *     .1/32 (H3)  — claims .1
         *     .0/29 (H2)  — claims .0, .2–.7
         *
         *   H2's remainder collapses to:
         *     208.10.10.0/32   (.0)
         *     208.10.10.2/31   (.2–.3)
         *     208.10.10.4/30   (.4–.7)
         *
         *   The isolated /32s (.16–.19) have no overlapping siblings, so they
         *   pass through unchanged.
         */
        $expectedPrefixes = [
            // Isolated /32s — pass through unchanged.
            '103.1.213.14/32' => $this->baseCfg(['synproxy' => true,  'heuristicsLevel' => 3]),
            '103.1.213.15/32' => $this->baseCfg(['heuristicsLevel' => 2, 'chainID' => $chainPassMost]),
            '103.1.213.16/32' => $this->baseCfg([
                'heuristicsLevel' => 3,
                'chainID'         => $chainPassMost,
                'gameFilters'     => ['1000' => 'fivem', '1001-1008' => 'source1', '1009' => 'rust', '1010' => 'fivem'],
            ]),
            '103.1.213.17/32' => $this->baseCfg([
                'heuristicsLevel' => 3,
                'gameFilters'     => ['10010-30000' => 'fivem'],
            ]),
            '103.1.213.18/32' => $this->baseCfg([
                'heuristicsLevel' => 2,
                'mercury'         => true,
                'dedicatedBgp'    => true,
                'dropUDPUnknown'  => null,
                'dropTCPUnknown'  => null
            ]),
            '103.1.213.19/32' => $this->baseCfg([
                'synproxy'        => true,
                'heuristicsLevel' => 3,
                'gameFilters'     => ['20010-40000' => 'source1'],
                'dropUDPUnknown'  => true,
                'dropTCPUnknown'  => true
            ]),

            // H1 /30 — survives unchanged.
            '103.1.213.0/30'  => $this->baseCfg(['heuristicsLevel' => 1]),

            // H0 /28 — fragments around H1's /30 and the four /32s above.
            '103.1.213.4/30'  => $this->baseCfg(['heuristicsLevel' => 0]),
            '103.1.213.8/30'  => $this->baseCfg(['heuristicsLevel' => 0]),
            '103.1.213.12/31' => $this->baseCfg(['heuristicsLevel' => 0]),

            // H2 /29 — fragments around H3's .1/32.
            '208.10.10.0/32'  => $this->baseCfg(['synproxy' => true, 'heuristicsLevel' => 2, 'chainID' => $chainDropUdpTcp]),
            '208.10.10.2/31'  => $this->baseCfg(['synproxy' => true, 'heuristicsLevel' => 2, 'chainID' => $chainDropUdpTcp]),
            '208.10.10.4/30'  => $this->baseCfg(['synproxy' => true, 'heuristicsLevel' => 2, 'chainID' => $chainDropUdpTcp]),

            // H3 /32 — survives.
            '208.10.10.1/32'  => $this->baseCfg(['heuristicsLevel' => 3, 'chainID' => $chainDropUdpTcp]),
        ];

        // REG-2 / SREG-2 only differs on the H3 chain override.
        // Fragmentation of the surrounding /29 is identical because the same
        // /32 is still carved out — only its chainID changes.
        $reg2Prefixes = $expectedPrefixes;
        $reg2Prefixes['208.10.10.1/32']['chainID'] = $chainPassMost;

        // --- Run test ---------------------------------------------------
        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $this->runRegionalAssertion('REG-1', 'SREG-1', 'MET-1', $expectedChains, $expectedPrefixes);
        $this->runRegionalAssertion('REG-2', 'SREG-2', 'MET-2', $expectedChains, $reg2Prefixes);

        // --- After deletions -------------------------------------------
        // Removing the .14 and .15 /32s opens up H0's claim: it now covers
        // .4–.15 instead of .4–.13. That collapses differently:
        //   103.1.213.4/30  (.4–.7)
        //   103.1.213.8/29  (.8–.15)
        Prefixes::whereIn('id', [
            self::PREFIX_NOT_SETUP,
            self::PREFIX_S_ON_H3_CHAINLESS,
            self::PREFIX_S_OFF_H2_CHAIN,
        ])->delete();

        $this->artisan('goliath:store-config', []);

        $afterDeletion = $expectedPrefixes;
        unset(
            $afterDeletion['103.1.213.14/32'],
            $afterDeletion['103.1.213.15/32'],
            $afterDeletion['103.1.213.4/30'],
            $afterDeletion['103.1.213.8/30'],
            $afterDeletion['103.1.213.12/31'],
        );
        $afterDeletion['103.1.213.4/30'] = $this->baseCfg(['heuristicsLevel' => 0]);
        $afterDeletion['103.1.213.8/29'] = $this->baseCfg(['heuristicsLevel' => 0]);

        $this->runRegionalAssertion('REG-1', 'SREG-1', 'MET-1', [], $afterDeletion);

        // --- Redis failure modes ---------------------------------------
        $assertFailsWhenMissing = function (): void {
            $this->assertEquals(
                500,
                $this->withoutMiddleware()->get('/api/goliath/data-v4')->status(),
                'If one of the entries are missing from Redis it needs to fail.'
            );
        };

        // Flags missing
        foreach (['goliath-flags', 'goliath-flags.SREG-1', 'goliath-flags.SREG-2', 'goliath-flags.SREG-3'] as $k) {
            Redis::delete($k);
        }
        $assertFailsWhenMissing();
        $this->artisan('goliath:store-config', []);

        // Chains missing
        foreach (['goliath-chains', 'goliath-chains.SREG-1', 'goliath-chains.SREG-2', 'goliath-chains.SREG-3'] as $k) {
            Redis::delete($k);
        }
        $assertFailsWhenMissing();
        $this->artisan('goliath:store-config', []);

        // Both missing
        $this->clearGoliathRedis();
        $assertFailsWhenMissing();
    }

    // ---------------------------------------------------------------------
    // Test: Loose config pull (subregional overrides)
    // ---------------------------------------------------------------------

    public function testGoliathPullConfigLoose(): void
    {
        /*
         * Expected criteria after crushing.
         *
         * Same fragmentation pattern as the standard test:
         *   - Six isolated /32s in 103.1.213.14–.19
         *   - 103.1.213.0/30  (H1) survives
         *   - H0 /28 fragments to .4/30, .8/30, .12/31
         *   - 208.10.10.0/29 fragments around .1/32 to .0/32, .2/31, .4/30
         *   - 208.10.10.1/32 survives
         *
         * The loose test only checks synproxy, heuristicsLevel, and
         * gameFilters — chainID and the org-special fields are not asserted.
         */
        $standardCriteria = [
            '103.1.213.14/32' => ['synproxy' => true,  'gameFilters' => null, 'heuristicsLevel' => 3],
            '103.1.213.15/32' => ['synproxy' => false, 'gameFilters' => null, 'heuristicsLevel' => 2],
            '103.1.213.16/32' => ['synproxy' => false, 'gameFilters' => [ '1000' => 'fivem', '1001-1008' => 'source1', '1009' => 'rust' ],  'heuristicsLevel' => 3],
            '103.1.213.17/32' => ['synproxy' => false, 'gameFilters' => [ '10010-30000' => 'fivem' ],  'heuristicsLevel' => 3],
            '103.1.213.18/32' => ['synproxy' => false, 'gameFilters' => null, 'heuristicsLevel' => 2],
            '103.1.213.19/32' => ['synproxy' => true,  'gameFilters' => [ '20010-40000' => 'source1' ],  'heuristicsLevel' => 3],
            '103.1.213.0/30'  => ['synproxy' => false, 'gameFilters' => null, 'heuristicsLevel' => 1],
            '103.1.213.4/30'  => ['synproxy' => false, 'gameFilters' => null, 'heuristicsLevel' => 0],
            '103.1.213.8/30'  => ['synproxy' => false, 'gameFilters' => null, 'heuristicsLevel' => 0],
            '103.1.213.12/31' => ['synproxy' => false, 'gameFilters' => null, 'heuristicsLevel' => 0],
            '208.10.10.0/32'  => ['synproxy' => true,  'gameFilters' => null, 'heuristicsLevel' => 2],
            '208.10.10.2/31'  => ['synproxy' => true,  'gameFilters' => null, 'heuristicsLevel' => 2],
            '208.10.10.4/30'  => ['synproxy' => true,  'gameFilters' => null, 'heuristicsLevel' => 2],
            '208.10.10.1/32'  => ['synproxy' => false, 'gameFilters' => null, 'heuristicsLevel' => 3],
        ];

        // In Subregion 1, loose overrides kick in. The loose overrides only
        // touch /32 entries (the same ones that survive crushing intact), so
        // the fragmentation of H0 and H2 is unchanged across regions.
        $subregion1Criteria = array_replace($standardCriteria, [
            '103.1.213.14/32' => ['synproxy' => true,  'gameFilters' => null, 'heuristicsLevel' => 2],
            '103.1.213.16/32' => ['synproxy' => false, 'gameFilters' => [ '1000' => 'fivem', '1001-1008' => 'source1', '1009' => 'rust' ],  'heuristicsLevel' => 2],
            '103.1.213.17/32' => ['synproxy' => false, 'gameFilters' => [ '10010-30000' => 'fivem' ],  'heuristicsLevel' => 2],
            '103.1.213.19/32' => ['synproxy' => true,  'gameFilters' => [ '20010-40000' => 'source1' ],  'heuristicsLevel' => 2],
        ]);

        $looseRequests = [
            [self::PREFIX_S_ON_H3_CHAINLESS, ['inline' => false, 'synproxy' => true,  'filters' => null, 'heuristics' => 3, 'loose' => true]],
            [self::PREFIX_S_OFF_H2_CHAIN,    ['inline' => false, 'synproxy' => false, 'filters' => null, 'heuristics' => 2, 'loose' => true]],
            [self::PREFIX_FIVEM_CHAIN,       ['inline' => false, 'synproxy' => false, 'filters' => [ 'fivem' => ['1000'], 'source1' => ['1001-1008'], 'rust' => ['1009'] ],  'heuristics' => 3, 'loose' => true]],
            [self::PREFIX_FIVEM_CHAINLESS,   ['inline' => false, 'synproxy' => false, 'filters' => [ 'fivem' => ['10010-30000'] ],  'heuristics' => 3, 'loose' => true]],
            [self::PREFIX_MERCURY_CLAMP_MSS, ['inline' => false, 'synproxy' => false, 'filters' => null, 'heuristics' => 2, 'loose' => true]],
            [self::PREFIX_UNANNOUNCED,       ['inline' => true,  'synproxy' => true,  'filters' => [ 'source1' => ['20010-40000'] ],  'heuristics' => 3, 'loose' => true]],
            [self::PREFIX_H0,                ['inline' => false, 'synproxy' => false, 'filters' => null, 'heuristics' => 0, 'loose' => true]],
            [self::PREFIX_H1,                ['inline' => false, 'synproxy' => false, 'filters' => null, 'heuristics' => 1, 'loose' => false]],
            [self::PREFIX_H2_CHAIN_S_ON,     ['inline' => true,  'synproxy' => true,  'filters' => null, 'heuristics' => 2, 'loose' => false]],
            [self::PREFIX_H3_CHAIN_S_OFF,    ['inline' => true,  'synproxy' => false, 'filters' => null, 'heuristics' => 3, 'loose' => false]],
        ];

        foreach ($looseRequests as [$prefixId, $flags]) {
            // Give each prefix a subregional identity.
            PrefixGoliathLoose::insert([[
                'prefix_id'      => $prefixId,
                'loose'          => 1,
                'subregion_info' => json_encode(['7578:199' => self::SUBREGION_1]),
            ]]);

            $this->setFlags($prefixId, $flags);
        }

        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $regionalVariants = [
            ['REG-1', 'SREG-1', 'MET-1'],
            ['REG-2', 'SREG-2', 'MET-2'],
            ['REG-3', 'SREG-3', 'MET-3'],
        ];

        foreach ($regionalVariants as $regionality) {
            $targets = json_decode(
                $this->fetchConfig(
                    $regionality[0] ?? null,
                    $regionality[1] ?? null,
                    $regionality[2] ?? null
                )['targets'],
                true
            );

            $criteria = ($regionality !== null && $regionality[0] === 'REG-1')
                ? $subregion1Criteria
                : $standardCriteria;

            $this->assertCount(
                count($criteria),
                $targets,
                'The number of prefixes returned is inaccurate.'
            );

            foreach ($targets as $prefix => $values) {
                $this->assertArrayHasKey($prefix, $criteria, "Unexpected prefix `$prefix` in output.");
                $this->assertEquals($criteria[$prefix]['synproxy'], $values['synproxy'], "Synproxy is incorrect for `$prefix`");
                $this->assertEquals($criteria[$prefix]['heuristicsLevel'], $values['heuristicsLevel'], "Heuristics is incorrect for `$prefix`");

                if($criteria[$prefix]['gameFilters'] === null) {
                    $this->assertFalse(isset($values['gameFilters']), "Filters are incorrect for `$prefix`");
                }
                else {
                    $this->assertEquals($criteria[$prefix]['gameFilters'], $values['gameFilters'], "Filters are incorrect for `$prefix`");
                }

            }
        }
    }

    // ---------------------------------------------------------------------
    // Test: block_fragments flag
    // ---------------------------------------------------------------------

    public function testGoliathPullConfigBlockFragments(): void
    {
        // Set up a base set of flags across the prefixes. Most get the flag
        // absent/false; two get it explicitly true, covering both the
        // "no filters" and "with filters" cases.
        $flags = [
            // No filters, block_fragments true — the key case this test exists for.
            self::PREFIX_S_ON_H3_CHAINLESS => [
                'inline'           => false,
                'synproxy'         => true,
                'filters'          => null,
                'heuristics'       => 3,
                'loose'            => false,
                'block_fragments'  => true,
            ],
            // With filters, block_fragments true — confirms it coexists with gameFilters.
            self::PREFIX_FIVEM_CHAINLESS => [
                'inline'           => false,
                'synproxy'         => false,
                'filters'          => ['fivem' => ['10010-30000']],
                'heuristics'       => 3,
                'loose'            => false,
                'block_fragments'  => true,
            ],
            // block_fragments explicitly false — key must be absent from output.
            self::PREFIX_S_OFF_H2_CHAIN => [
                'inline'           => false,
                'synproxy'         => false,
                'filters'          => null,
                'heuristics'       => 2,
                'loose'            => false,
                'block_fragments'  => false,
            ],
            // block_fragments absent entirely — key must also be absent from output.
            self::PREFIX_MERCURY_CLAMP_MSS => [
                'inline'           => false,
                'synproxy'         => false,
                'filters'          => null,
                'heuristics'       => 2,
                'loose'            => false,
            ],
        ];

        foreach ($flags as $prefixId => $x) {
            $this->setFlags($prefixId, $x);
        }

        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $config  = $this->fetchConfig('REG-1', 'SREG-1', 'MET-1');
        $targets = json_decode($config['targets'], true);

        // --- Prefix present with block_fragments true, no filters ----------
        $this->assertArrayHasKey(
            '103.1.213.14/32',
            $targets,
            'Prefix 103.1.213.14/32 (S-on H3 Chainless) should appear in output.'
        );
        $this->assertEquals(
            true,
            $targets['103.1.213.14/32']['dropFragments'],
            'dropFragments should be true when flag is set.'
        );
        // Confirm it coexists cleanly with a null-filters prefix (no gameFilters key).
        $this->assertArrayNotHasKey(
            'gameFilters',
            $targets['103.1.213.14/32'],
            'gameFilters should be absent when filters are null, even with dropFragments set.'
        );

        // --- Prefix present with block_fragments true AND filters ----------
        $this->assertArrayHasKey(
            '103.1.213.17/32',
            $targets,
            'Prefix 103.1.213.17/32 (FiveM Chainless) should appear in output.'
        );
        $this->assertEquals(
            true,
            $targets['103.1.213.17/32']['dropFragments'],
            'dropFragments should be true alongside gameFilters.'
        );
        $this->assertEquals(
            ['10010-30000' => 'fivem'],
            $targets['103.1.213.17/32']['gameFilters'],
            'gameFilters should be unaffected by dropFragments.'
        );

        // --- block_fragments false → key must be absent --------------------
        $this->assertArrayHasKey(
            '103.1.213.15/32',
            $targets,
            'Prefix 103.1.213.15/32 (S-off H2 Chain) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'dropFragments',
            $targets['103.1.213.15/32'],
            'dropFragments must not appear in output when the flag is false.'
        );

        // --- block_fragments absent → key must be absent ------------------
        $this->assertArrayHasKey(
            '103.1.213.18/32',
            $targets,
            'Prefix 103.1.213.18/32 (Mercury Clamp MSS) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'dropFragments',
            $targets['103.1.213.18/32'],
            'dropFragments must not appear in output when the flag was never set.'
        );
    }

    // ---------------------------------------------------------------------
    // Test: syn_cookies flag
    // ---------------------------------------------------------------------

    public function testGoliathPullConfigSynCookies(): void
    {
        // syn_cookies is standalone (not gated behind filters, unlike block_udp/tcp),
        // so cover it both with and without filters, plus the false/absent cases.
        $flags = [
            // No filters, syn_cookies true — the key case this test exists for.
            self::PREFIX_S_ON_H3_CHAINLESS => [
                'inline'       => false,
                'synproxy'     => true,
                'filters'      => null,
                'heuristics'   => 3,
                'loose'        => false,
                'syn_cookies'  => true,
            ],
            // With filters, syn_cookies true — confirms it coexists with gameFilters.
            self::PREFIX_FIVEM_CHAINLESS => [
                'inline'       => false,
                'synproxy'     => false,
                'filters'      => ['fivem' => ['10010-30000']],
                'heuristics'   => 3,
                'loose'        => false,
                'syn_cookies'  => true,
            ],
            // syn_cookies explicitly false — key must be absent from output.
            self::PREFIX_S_OFF_H2_CHAIN => [
                'inline'       => false,
                'synproxy'     => false,
                'filters'      => null,
                'heuristics'   => 2,
                'loose'        => false,
                'syn_cookies'  => false,
            ],
            // syn_cookies absent entirely — key must also be absent from output.
            self::PREFIX_MERCURY_CLAMP_MSS => [
                'inline'       => false,
                'synproxy'     => false,
                'filters'      => null,
                'heuristics'   => 2,
                'loose'        => false,
            ],
        ];

        foreach ($flags as $prefixId => $x) {
            $this->setFlags($prefixId, $x);
        }

        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $config  = $this->fetchConfig('REG-1', 'SREG-1', 'MET-1');
        $targets = json_decode($config['targets'], true);

        // --- Prefix present with syn_cookies true, no filters ---------------
        $this->assertArrayHasKey(
            '103.1.213.14/32',
            $targets,
            'Prefix 103.1.213.14/32 (S-on H3 Chainless) should appear in output.'
        );
        $this->assertEquals(
            true,
            $targets['103.1.213.14/32']['synCookies'],
            'synCookies should be true when flag is set.'
        );
        // Confirm it coexists cleanly with a null-filters prefix (no gameFilters key).
        $this->assertArrayNotHasKey(
            'gameFilters',
            $targets['103.1.213.14/32'],
            'gameFilters should be absent when filters are null, even with synCookies set.'
        );

        // --- Prefix present with syn_cookies true AND filters --------------
        $this->assertArrayHasKey(
            '103.1.213.17/32',
            $targets,
            'Prefix 103.1.213.17/32 (FiveM Chainless) should appear in output.'
        );
        $this->assertEquals(
            true,
            $targets['103.1.213.17/32']['synCookies'],
            'synCookies should be true alongside gameFilters.'
        );
        $this->assertEquals(
            ['10010-30000' => 'fivem'],
            $targets['103.1.213.17/32']['gameFilters'],
            'gameFilters should be unaffected by synCookies.'
        );

        // --- syn_cookies false → key must be absent ------------------------
        $this->assertArrayHasKey(
            '103.1.213.15/32',
            $targets,
            'Prefix 103.1.213.15/32 (S-off H2 Chain) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'synCookies',
            $targets['103.1.213.15/32'],
            'synCookies must not appear in output when the flag is false.'
        );

        // --- syn_cookies absent → key must be absent -----------------------
        $this->assertArrayHasKey(
            '103.1.213.18/32',
            $targets,
            'Prefix 103.1.213.18/32 (Mercury Clamp MSS) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'synCookies',
            $targets['103.1.213.18/32'],
            'synCookies must not appear in output when the flag was never set.'
        );
    }

    public function testGoliathPullConfigDropTCPOutOfState(): void
    {
        // drop_tcp_out_of_state is standalone (not gated behind filters), so
        // cover it both with and without filters, plus the false/absent cases.
        $flags = [
            // No filters, drop_tcp_out_of_state true — the key case this test exists for.
            self::PREFIX_S_ON_H3_CHAINLESS => [
                'inline'                => false,
                'synproxy'              => true,
                'filters'               => null,
                'heuristics'            => 3,
                'loose'                 => false,
                'drop_tcp_out_of_state' => true,
            ],
            // With filters, drop_tcp_out_of_state true — confirms it coexists with gameFilters.
            self::PREFIX_FIVEM_CHAINLESS => [
                'inline'                => false,
                'synproxy'              => false,
                'filters'               => ['fivem' => ['10010-30000']],
                'heuristics'            => 3,
                'loose'                 => false,
                'drop_tcp_out_of_state' => true,
            ],
            // drop_tcp_out_of_state explicitly false — key must be absent from output.
            self::PREFIX_S_OFF_H2_CHAIN => [
                'inline'                => false,
                'synproxy'              => false,
                'filters'               => null,
                'heuristics'            => 2,
                'loose'                 => false,
                'drop_tcp_out_of_state' => false,
            ],
            // drop_tcp_out_of_state absent entirely — key must also be absent from output.
            self::PREFIX_MERCURY_CLAMP_MSS => [
                'inline'     => false,
                'synproxy'   => false,
                'filters'    => null,
                'heuristics' => 2,
                'loose'      => false,
            ],
        ];

        foreach ($flags as $prefixId => $x) {
            $this->setFlags($prefixId, $x);
        }

        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $config  = $this->fetchConfig('REG-1', 'SREG-1', 'MET-1');
        $targets = json_decode($config['targets'], true);

        // --- Prefix present with drop_tcp_out_of_state true, no filters ----
        $this->assertArrayHasKey(
            '103.1.213.14/32',
            $targets,
            'Prefix 103.1.213.14/32 (S-on H3 Chainless) should appear in output.'
        );
        $this->assertEquals(
            true,
            $targets['103.1.213.14/32']['dropTCPOutOfState'],
            'dropTCPOutOfState should be true when flag is set.'
        );
        // Confirm it coexists cleanly with a null-filters prefix (no gameFilters key).
        $this->assertArrayNotHasKey(
            'gameFilters',
            $targets['103.1.213.14/32'],
            'gameFilters should be absent when filters are null, even with dropTCPOutOfState set.'
        );

        // --- Prefix present with drop_tcp_out_of_state true AND filters ----
        $this->assertArrayHasKey(
            '103.1.213.17/32',
            $targets,
            'Prefix 103.1.213.17/32 (FiveM Chainless) should appear in output.'
        );
        $this->assertEquals(
            true,
            $targets['103.1.213.17/32']['dropTCPOutOfState'],
            'dropTCPOutOfState should be true alongside gameFilters.'
        );
        $this->assertEquals(
            ['10010-30000' => 'fivem'],
            $targets['103.1.213.17/32']['gameFilters'],
            'gameFilters should be unaffected by dropTCPOutOfState.'
        );

        // --- drop_tcp_out_of_state false → key must be absent --------------
        $this->assertArrayHasKey(
            '103.1.213.15/32',
            $targets,
            'Prefix 103.1.213.15/32 (S-off H2 Chain) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'dropTCPOutOfState',
            $targets['103.1.213.15/32'],
            'dropTCPOutOfState must not appear in output when the flag is false.'
        );

        // --- drop_tcp_out_of_state absent → key must be absent -------------
        $this->assertArrayHasKey(
            '103.1.213.18/32',
            $targets,
            'Prefix 103.1.213.18/32 (Mercury Clamp MSS) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'dropTCPOutOfState',
            $targets['103.1.213.18/32'],
            'dropTCPOutOfState must not appear in output when the flag was never set.'
        );
    }

    // ---------------------------------------------------------------------
    // Test: app_filter_strict flag
    // ---------------------------------------------------------------------

    public function testGoliathPullConfigAppFilterStrict(): void
    {
        // app_filter_strict is gated behind application filters, both at the
        // API layer (never stored without filters) and at config assembly
        // (only emitted inside the gameFilters block).
        $flags = [
            // With filters, app_filter_strict true — the key case this test exists for.
            self::PREFIX_FIVEM_CHAINLESS => [
                'inline'            => false,
                'synproxy'          => false,
                'filters'           => ['fivem' => ['10010-30000']],
                'heuristics'        => 3,
                'loose'             => false,
                'app_filter_strict' => true,
            ],
            // No filters, app_filter_strict requested — must never reach the output.
            self::PREFIX_S_ON_H3_CHAINLESS => [
                'inline'            => false,
                'synproxy'          => true,
                'filters'           => null,
                'heuristics'        => 3,
                'loose'             => false,
                'app_filter_strict' => true,
            ],
            // With filters, app_filter_strict explicitly false — key must be absent.
            self::PREFIX_FIVEM_CHAIN => [
                'inline'            => false,
                'synproxy'          => false,
                'filters'           => ['fivem' => ['1000']],
                'heuristics'        => 3,
                'loose'             => false,
                'app_filter_strict' => false,
            ],
            // app_filter_strict absent entirely — key must also be absent.
            self::PREFIX_MERCURY_CLAMP_MSS => [
                'inline'     => false,
                'synproxy'   => false,
                'filters'    => null,
                'heuristics' => 2,
                'loose'      => false,
            ],
        ];

        foreach ($flags as $prefixId => $x) {
            $this->setFlags($prefixId, $x);
        }

        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $config  = $this->fetchConfig('REG-1', 'SREG-1', 'MET-1');
        $targets = json_decode($config['targets'], true);

        // --- Filters + strict → both keys present ---------------------------
        $this->assertArrayHasKey(
            '103.1.213.17/32',
            $targets,
            'Prefix 103.1.213.17/32 (FiveM Chainless) should appear in output.'
        );
        $this->assertEquals(
            true,
            $targets['103.1.213.17/32']['appFilterStrict'],
            'appFilterStrict should be true when set alongside filters.'
        );
        $this->assertEquals(
            ['10010-30000' => 'fivem'],
            $targets['103.1.213.17/32']['gameFilters'],
            'gameFilters should be unaffected by appFilterStrict.'
        );

        // --- Strict without filters → dropped by the API, absent here ------
        $this->assertArrayHasKey(
            '103.1.213.14/32',
            $targets,
            'Prefix 103.1.213.14/32 (S-on H3 Chainless) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'appFilterStrict',
            $targets['103.1.213.14/32'],
            'appFilterStrict must never be emitted for a filterless prefix.'
        );

        // --- Filters with strict false → key must be absent ----------------
        $this->assertArrayHasKey(
            '103.1.213.16/32',
            $targets,
            'Prefix 103.1.213.16/32 (FiveM Chain) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'appFilterStrict',
            $targets['103.1.213.16/32'],
            'appFilterStrict must not appear in output when the flag is false.'
        );
        $this->assertEquals(
            ['1000' => 'fivem'],
            $targets['103.1.213.16/32']['gameFilters'],
            'gameFilters should still be emitted when strict is off.'
        );

        // --- app_filter_strict absent → key must be absent -----------------
        $this->assertArrayHasKey(
            '103.1.213.18/32',
            $targets,
            'Prefix 103.1.213.18/32 (Mercury Clamp MSS) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'appFilterStrict',
            $targets['103.1.213.18/32'],
            'appFilterStrict must not appear in output when the flag was never set.'
        );
    }

    // ---------------------------------------------------------------------
    // Test: null_route flag
    // ---------------------------------------------------------------------

    public function testGoliathPullConfigNullRoute(): void
    {
        // nullRoute is not exposed through the API yet - the column exists and
        // the assembler emits it, so the flag is set directly here. It is an
        // opt-in kill switch: it must only ever reach the targets file when
        // the flag is explicitly true.
        $flags = [
            self::PREFIX_S_ON_H3_CHAINLESS => [
                'inline'     => false,
                'synproxy'   => true,
                'filters'    => null,
                'heuristics' => 3,
                'loose'      => false,
            ],
            self::PREFIX_S_OFF_H2_CHAIN => [
                'inline'     => false,
                'synproxy'   => false,
                'filters'    => null,
                'heuristics' => 2,
                'loose'      => false,
            ],
            self::PREFIX_MERCURY_CLAMP_MSS => [
                'inline'     => false,
                'synproxy'   => false,
                'filters'    => null,
                'heuristics' => 2,
                'loose'      => false,
            ],
        ];

        foreach ($flags as $prefixId => $x) {
            $this->setFlags($prefixId, $x);
        }

        // Set directly on the column - a query-builder update skips the model
        // events, so this does not emit a spurious audit entry.
        PrefixGoliathFlag::where('prefix_id', '=', self::PREFIX_S_ON_H3_CHAINLESS)->update(['flag_null_route' => 1]);
        PrefixGoliathFlag::where('prefix_id', '=', self::PREFIX_S_OFF_H2_CHAIN)->update(['flag_null_route' => 0]);

        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $config  = $this->fetchConfig('REG-1', 'SREG-1', 'MET-1');
        $targets = json_decode($config['targets'], true);

        // --- null_route true -> key present and true -----------------------
        $this->assertArrayHasKey(
            '103.1.213.14/32',
            $targets,
            'Prefix 103.1.213.14/32 (S-on H3 Chainless) should appear in output.'
        );
        $this->assertEquals(
            true,
            $targets['103.1.213.14/32']['nullRoute'],
            'nullRoute should be true when the flag is set.'
        );

        // --- null_route false -> key must be absent ------------------------
        $this->assertArrayHasKey(
            '103.1.213.15/32',
            $targets,
            'Prefix 103.1.213.15/32 (S-off H2 Chain) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'nullRoute',
            $targets['103.1.213.15/32'],
            'nullRoute must not appear in output when the flag is false.'
        );

        // --- null_route absent -> key must be absent -----------------------
        $this->assertArrayHasKey(
            '103.1.213.18/32',
            $targets,
            'Prefix 103.1.213.18/32 (Mercury Clamp MSS) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'nullRoute',
            $targets['103.1.213.18/32'],
            'nullRoute must not appear in output when the flag was never set.'
        );
    }

    // ---------------------------------------------------------------------
    // Test: syn_cookie_data_promote / syn_cookie_os_gate flags
    // ---------------------------------------------------------------------

    public function testGoliathPullConfigSynCookieOptions(): void
    {
        // synCookieDataPromote is emitted only when true; synCookieOsGate is
        // emitted verbatim whenever it is not null.
        $flags = [
            self::PREFIX_S_ON_H3_CHAINLESS => [
                'inline'                  => false,
                'synproxy'                => true,
                'filters'                 => null,
                'heuristics'              => 3,
                'loose'                   => false,
                'syn_cookie_data_promote' => true,
                'syn_cookie_os_gate'      => 'windows',
            ],
            self::PREFIX_S_OFF_H2_CHAIN => [
                'inline'                  => false,
                'synproxy'                => false,
                'filters'                 => null,
                'heuristics'              => 2,
                'loose'                   => false,
                'syn_cookie_data_promote' => false,
                'syn_cookie_os_gate'      => 'non-windows',
            ],
            self::PREFIX_MERCURY_CLAMP_MSS => [
                'inline'     => false,
                'synproxy'   => false,
                'filters'    => null,
                'heuristics' => 2,
                'loose'      => false,
            ],
        ];

        foreach ($flags as $prefixId => $x) {
            $this->setFlags($prefixId, $x);
        }

        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $config  = $this->fetchConfig('REG-1', 'SREG-1', 'MET-1');
        $targets = json_decode($config['targets'], true);

        // --- promote true + windows gate -> both keys present --------------
        $this->assertArrayHasKey(
            '103.1.213.14/32',
            $targets,
            'Prefix 103.1.213.14/32 (S-on H3 Chainless) should appear in output.'
        );
        $this->assertEquals(
            true,
            $targets['103.1.213.14/32']['synCookieDataPromote'],
            'synCookieDataPromote should be true when the flag is set.'
        );
        $this->assertEquals(
            'windows',
            $targets['103.1.213.14/32']['synCookieOsGate'],
            'synCookieOsGate should carry the stored enum value.'
        );

        // --- promote false + non-windows gate -> gate only -----------------
        $this->assertArrayHasKey(
            '103.1.213.15/32',
            $targets,
            'Prefix 103.1.213.15/32 (S-off H2 Chain) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'synCookieDataPromote',
            $targets['103.1.213.15/32'],
            'synCookieDataPromote must not appear in output when the flag is false.'
        );
        $this->assertEquals(
            'non-windows',
            $targets['103.1.213.15/32']['synCookieOsGate'],
            'synCookieOsGate should be emitted for the non-windows gate.'
        );

        // --- neither set -> neither key present ----------------------------
        $this->assertArrayHasKey(
            '103.1.213.18/32',
            $targets,
            'Prefix 103.1.213.18/32 (Mercury Clamp MSS) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'synCookieDataPromote',
            $targets['103.1.213.18/32'],
            'synCookieDataPromote must not appear in output when the flag was never set.'
        );
        $this->assertArrayNotHasKey(
            'synCookieOsGate',
            $targets['103.1.213.18/32'],
            'synCookieOsGate must not appear in output when the gate is null.'
        );
    }
    // ---------------------------------------------------------------------
    // Test: syn_challenge / syn_drop_first flags
    // ---------------------------------------------------------------------

    public function testGoliathPullConfigSynChallengeOptions(): void
    {
        // synChallenge is emitted verbatim whenever it is not null, and
        // synDropFirst only when it is true.
        $flags = [
            self::PREFIX_S_ON_H3_CHAINLESS => [
                'inline'         => false,
                'synproxy'       => true,
                'filters'        => null,
                'heuristics'     => 3,
                'loose'          => false,
                'syn_challenge'  => 'safe-reset',
                'syn_drop_first' => true,
            ],
            self::PREFIX_S_OFF_H2_CHAIN => [
                'inline'         => false,
                'synproxy'       => false,
                'filters'        => null,
                'heuristics'     => 2,
                'loose'          => false,
                'syn_challenge'  => 'cookie',
                'syn_drop_first' => false,
            ],
            self::PREFIX_MERCURY_CLAMP_MSS => [
                'inline'     => false,
                'synproxy'   => false,
                'filters'    => null,
                'heuristics' => 2,
                'loose'      => false,
            ],
        ];

        foreach ($flags as $prefixId => $x) {
            $this->setFlags($prefixId, $x);
        }

        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $config  = $this->fetchConfig('REG-1', 'SREG-1', 'MET-1');
        $targets = json_decode($config['targets'], true);

        // --- safe-reset + forced retransmit -> both keys present -----------
        $this->assertArrayHasKey(
            '103.1.213.14/32',
            $targets,
            'Prefix 103.1.213.14/32 (S-on H3 Chainless) should appear in output.'
        );
        $this->assertEquals(
            'safe-reset',
            $targets['103.1.213.14/32']['synChallenge'],
            'synChallenge should carry the stored enum value.'
        );
        $this->assertEquals(
            true,
            $targets['103.1.213.14/32']['synDropFirst'],
            'synDropFirst should be true when the flag is set.'
        );

        // --- cookie challenge, retransmit off -> gate only ------------------
        $this->assertArrayHasKey(
            '103.1.213.15/32',
            $targets,
            'Prefix 103.1.213.15/32 (S-off H2 Chain) should appear in output.'
        );
        $this->assertEquals(
            'cookie',
            $targets['103.1.213.15/32']['synChallenge'],
            'synChallenge should be emitted for the cookie mechanism.'
        );
        $this->assertArrayNotHasKey(
            'synDropFirst',
            $targets['103.1.213.15/32'],
            'synDropFirst must not appear in output when the flag is false.'
        );

        // --- neither set -> neither key present -----------------------------
        $this->assertArrayHasKey(
            '103.1.213.18/32',
            $targets,
            'Prefix 103.1.213.18/32 (Mercury Clamp MSS) should appear in output.'
        );
        $this->assertArrayNotHasKey(
            'synChallenge',
            $targets['103.1.213.18/32'],
            'synChallenge must not appear in output when the mechanism is null.'
        );
        $this->assertArrayNotHasKey(
            'synDropFirst',
            $targets['103.1.213.18/32'],
            'synDropFirst must not appear in output when the flag was never set.'
        );
    }

    // ---------------------------------------------------------------------
    // Test: per-source rate limit, protocol policy, UDP-to-TCP-port drop and
    // established UDP lane flags
    // ---------------------------------------------------------------------

    public function testGoliathPullConfigProtocolAndSourceLimitOptions(): void
    {
        // Each key is emitted only while its flag is set, and a limit carries its number.
        $flags = [
            self::PREFIX_S_ON_H3_CHAINLESS => [
                'inline'                    => false,
                'synproxy'                  => true,
                'filters'                   => null,
                'heuristics'                => 3,
                'loose'                     => false,
                'src_limit_pps'             => 12300,
                'drop_unexpected_protocols' => true,
                'drop_udp_to_tcp_ports'     => true,
                'udp_established_lane'      => true,
            ],
            self::PREFIX_S_OFF_H2_CHAIN => [
                'inline'                    => false,
                'synproxy'                  => false,
                'filters'                   => null,
                'heuristics'                => 2,
                'loose'                     => false,
                'src_limit_mbps'            => 250,
                'drop_non_core_protocols'   => true,
            ],
            self::PREFIX_MERCURY_CLAMP_MSS => [
                'inline'     => false,
                'synproxy'   => false,
                'filters'    => null,
                'heuristics' => 2,
                'loose'      => false,
            ],
        ];

        foreach ($flags as $prefixId => $x) {
            $this->setFlags($prefixId, $x);
        }

        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $targets = json_decode($this->fetchConfig('REG-1', 'SREG-1', 'MET-1')['targets'], true);

        // --- pps limit, lenient protocol policy, port drop and lane --------
        $this->assertArrayHasKey('103.1.213.14/32', $targets, 'Prefix 103.1.213.14/32 (S-on H3 Chainless) should appear in output.');
        $this->assertSame(12300, $targets['103.1.213.14/32']['srcLimitPps'], 'srcLimitPps should carry the stored number.');
        $this->assertArrayNotHasKey('srcLimitMbps', $targets['103.1.213.14/32'], 'srcLimitMbps must not appear beside a pps limit.');
        $this->assertTrue($targets['103.1.213.14/32']['dropUnexpectedProtocols'], 'dropUnexpectedProtocols should be true when the flag is set.');
        $this->assertArrayNotHasKey('dropNonCoreProtocols', $targets['103.1.213.14/32'], 'dropNonCoreProtocols must not appear when the flag is unset.');
        $this->assertTrue($targets['103.1.213.14/32']['dropUDPToTCPPorts'], 'dropUDPToTCPPorts should be true when the flag is set.');
        $this->assertTrue($targets['103.1.213.14/32']['udpEstablishedLane'], 'udpEstablishedLane should be true when the flag is set.');

        // --- Mbps limit and the strict protocol policy ----------------------
        $this->assertArrayHasKey('103.1.213.15/32', $targets, 'Prefix 103.1.213.15/32 (S-off H2 Chain) should appear in output.');
        $this->assertSame(250, $targets['103.1.213.15/32']['srcLimitMbps'], 'srcLimitMbps should carry the stored number.');
        $this->assertArrayNotHasKey('srcLimitPps', $targets['103.1.213.15/32'], 'srcLimitPps must not appear beside an Mbps limit.');
        $this->assertTrue($targets['103.1.213.15/32']['dropNonCoreProtocols'], 'dropNonCoreProtocols should be true when the flag is set.');

        foreach (['dropUnexpectedProtocols', 'dropUDPToTCPPorts', 'udpEstablishedLane'] as $key) {
            $this->assertArrayNotHasKey($key, $targets['103.1.213.15/32'], "$key must not appear when its flag is unset.");
        }

        // --- nothing set -> no key ------------------------------------------
        $this->assertArrayHasKey('103.1.213.18/32', $targets, 'Prefix 103.1.213.18/32 (Mercury Clamp MSS) should appear in output.');

        foreach (['srcLimitPps', 'srcLimitMbps', 'dropUnexpectedProtocols', 'dropNonCoreProtocols', 'dropUDPToTCPPorts', 'udpEstablishedLane'] as $key) {
            $this->assertArrayNotHasKey($key, $targets['103.1.213.18/32'], "$key must not appear when its flag was never set.");
        }

        // --- a row naming both protocol policies sends only the strict one --
        // Validation refuses the pair, so the row is written directly.
        PrefixGoliathFlag::where('prefix_id', '=', self::PREFIX_S_ON_H3_CHAINLESS)->update(['flag_drop_non_core_protocols' => 1]);

        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $targets = json_decode($this->fetchConfig('REG-1', 'SREG-1', 'MET-1')['targets'], true);

        $this->assertTrue($targets['103.1.213.14/32']['dropNonCoreProtocols'], 'The strict protocol policy should win.');
        $this->assertArrayNotHasKey('dropUnexpectedProtocols', $targets['103.1.213.14/32'], 'dropUnexpectedProtocols must not appear beside dropNonCoreProtocols.');
    }

    // ---------------------------------------------------------------------
    // Test: filter names
    // ---------------------------------------------------------------------

    public function testGoliathPullConfigTranslatesFilterNames(): void
    {
        // Creatia offers fivem_tcp_gated and fivemx_tcp_gated as the double handshake filters; targets.json must
        // still carry goliath's names, and every other filter keeps its own.
        $this->setFlags(self::PREFIX_S_ON_H3_CHAINLESS, [
            'inline'     => false,
            'synproxy'   => true,
            'heuristics' => 3,
            'loose'      => false,
            'filters'    => [
                'fivem_double_handshake'  => ['30120'],
                'fivemx_double_handshake' => ['30130-30131'],
                'openvpn'                 => ['1194'],
                'samp_stateless'          => ['7777'],
            ],
        ]);

        $this->clearGoliathRedis();
        $this->artisan('goliath:store-config', []);

        $targets = json_decode($this->fetchConfig('REG-1', 'SREG-1', 'MET-1')['targets'], true);

        $this->assertArrayHasKey('103.1.213.14/32', $targets, 'Prefix 103.1.213.14/32 (S-on H3 Chainless) should appear in output.');
        $this->assertEquals(
            ['30120' => 'fivem_tcp_gated', '30130-30131' => 'fivemx_tcp_gated', '1194' => 'openvpn', '7777' => 'samp_stateless'],
            $targets['103.1.213.14/32']['gameFilters'],
            'The double handshake filters should reach goliath under its names, and the rest unchanged.'
        );
    }
}
