<?php

namespace Tests\Feature;

use App\Models\Asns;
use App\Models\AsSets;
use App\Models\Goliath\GoliathAppliance;
use App\Models\Goliath\GoliathMetro;
use App\Models\Goliath\GoliathRegion;
use App\Models\Goliath\GoliathRuleset;
use App\Models\Goliath\GoliathSubregion;
use App\Models\Goliath\RulesetAction;
use App\Models\Goliath\RulesetType;
use App\Models\Group;

use App\Models\Organisation;
use App\Models\Prefixes;
use App\Models\User;
use App\Models\Webhook;
use App\Rules\AdminAnyAsnIdentity;
use App\Rules\AdminAnyAsSetIdentity;
use App\Rules\AdminAnyPrefixIdentity;
use App\Rules\AdminAnyRulesetIdentity;
use App\Rules\AdminAnyUserIdentity;
use App\Rules\AdminGameFilters;
use App\Rules\AdminHeuristicsLevel;
use App\Rules\AdminRulesetType;
use App\Rules\AllowedPrefixIdentity;
use App\Rules\ApplianceIdentity;
use App\Rules\AsnFormat;
use App\Rules\AsSetFormat;
use App\Rules\BrandIdentity;
use App\Rules\ComputePeerIdentity;
use App\Rules\ExclusiveWith;
use App\Rules\ExternalApplication;
use App\Rules\Fragments\PagedTable;
use App\Rules\Fragments\Regionality;
use App\Rules\GroupIdentity;
use App\Rules\HeuristicsLevel;
use App\Rules\Interval;
use App\Rules\MetroIdentity;
use App\Rules\OrganisationIdentity;
use App\Rules\OwnedAsnIdentity;
use App\Rules\OwnedAsSetIdentity;
use App\Rules\OwnedRulesetIdentity;
use App\Rules\OwnedTunnelIdentity;
use App\Rules\OwnedUserIdentity;
use App\Rules\OwnedWebhookIdentity;
use App\Rules\PartialAddress;
use App\Rules\PartialTimestamp;
use App\Rules\RegionIdentity;
use App\Rules\RulesetActionType;
use App\Rules\RulesetClauseOperator;
use App\Rules\RulesetClauseType;
use App\Rules\RulesetIdentity;
use App\Rules\SafeName;
use App\Rules\SidHash;
use App\Rules\SrcLimit;
use App\Rules\SubregionIdentity;
use App\Rules\SynChallenge;
use App\Rules\SynCookieOsGate;
use App\Rules\UsageRight;
use App\Rules\GameFilters;
use App\Rules\WhoIs;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;

/**
 *      @@@ TEST CERTIFICATION @@@
 *      This set of tests has been confirmed to be functioning as intended.
 *
 *      Jesse Cunningham, 2026-04-21 4:00 PM AEST
 */

class ValidationRuleTest extends TestCase
{

    use RefreshDatabase;

    private $organisations = [];
    private $users = [];
    private $asns = [];
    private $as_sets = [];
    private $prefixes = [];
    private $rulesets = [];

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

        // Setup organisations
        $this->organisations = [
            'A' => Organisation::create([ 'id' => 1, 'name' => 'A' ]),
            'B' => Organisation::create([ 'id' => 2, 'name' => 'B' ]),
        ];

        // Setup groups and users
        $this->withoutMiddleware()->json('POST', '/api/admin/group', [ 'name' => 'Admin', 'scopes' => explode(' ', 'mercury.manage=1 network.redirect-view=1 network.redirect-write=1 goliath.l1.report=1 hook.view=1 hook.update=1 billing.admin.view=1 billing.admin.edit=1 provision.bgp.view=1 provision.bgp.write=1 goliath.l1.special=1 network.view=1 management.view=1 management.write=1 mitigation.view=1 mitigation.add=1 mitigation.edit=1 mitigation.management=1 mitigation.subtenancy=1 geoip.view=1 geoip.add=1 geoip.edit=1 geoip.management=1 geoip.subtenancy=1 goliath.edit-chain=1 goliath.view=1 goliath.management=1 goliath.edit-flags=1 goliath.edit-prefix=1 goliath.l1.view=1 goliath.l1.edit=1 goliath.l1.log=1 goliath.l1.manage=1 goliath.l2.view=1 goliath.l2.edit=1 goliath.l2.log=1 goliath.l2.manage=1 network.subtenant=1 network.rename=1 network.delete=1 network.manage=1 network.map=1 mercury.view=1 mercury.delete=1 compute.bgp.view=1 compute.bgp.tier1=1 compute.bgp.tier2=1 compute.bgp.tier3=1 compute.bgp.write=1 compute.bgp.delete=1 billing.view=1 billing.chart=1') ]);
        $this->withoutMiddleware()->json('POST', '/api/admin/group', [ 'name' => 'User', 'scopes' => explode(' ', 'goliath.l1.view=1 goliath.l1.edit=1 goliath.l1.log=1 goliath.l1.special=1 network.rename=1 network.map=1 goliath.l2.view=1 goliath.l2.edit=1 goliath.l2.log=1 network.redirect-view=1 network.redirect-write=1 organisation.manage=1 goliath.l1.report=1 mercury.view=1 mercury.delete=1 hook.view=1 hook.update=1 compute.bgp.view=1 compute.bgp.tier1=1 compute.bgp.tier2=1 compute.bgp.tier3=1 compute.bgp.write=1 compute.bgp.delete=1') ]);
        $this->withoutMiddleware()->json('POST', '/api/admin/group', [ 'name' => 'Special', 'scopes' => explode(' ', 'goliath.heuristics.0=1 goliath.heuristics.1=1 goliath.heuristics.6=1 goliath.l1.view=1 goliath.l1.edit=1 goliath.l1.log=1 goliath.l1.special=1 network.rename=1 network.map=1 goliath.l2.view=1 goliath.l2.edit=1 goliath.l2.log=1 network.redirect-view=1 network.redirect-write=1 organisation.manage=1 goliath.l1.report=1 mercury.view=1 mercury.delete=1 hook.view=1 hook.update=1 compute.bgp.view=1 compute.bgp.tier1=1 compute.bgp.tier2=1 compute.bgp.tier3=1 compute.bgp.write=1 compute.bgp.delete=1') ]);

        $default_user = [
            'phone'             => null,
            'password'          => ''
        ];

        $this->users = [
            'admin' => User::create([
                ...$default_user,
                'group_id'          => Group::where('name', '=', 'Admin')->get()->first()->id,
                'organisation_id'   => $this->organisations['B']->id,
                'name'              => 'Admin',
                'email'             => 'creatia+admin@globalsecurelayer.com'
            ]),
            'user' => User::create([
                ...$default_user,
                'group_id'          => Group::where('name', '=', 'User')->get()->first()->id,
                'organisation_id'   => $this->organisations['A']->id,
                'name'              => 'User',
                'email'             => 'creatia+user@globalsecurelayer.com'
            ]),
            'special' => User::create([
                ...$default_user,
                'group_id'          => Group::where('name', '=', 'Special')->get()->first()->id,
                'organisation_id'   => $this->organisations['A']->id,
                'name'              => 'Special',
                'email'             => 'creatia+special@globalsecurelayer.com'
            ])
        ];

        // Give some ASNs to the accounts
        $this->asns['A1'] = Asns::create([ 'organisation_id' => $this->organisations['A']->id, 'asn' => '12345', 'approved' => 1 ]);
        $this->asns['A2'] = Asns::create([ 'organisation_id' => $this->organisations['A']->id, 'asn' => '12346', 'approved' => 1 ]);
        $this->asns['A3'] = Asns::create([ 'organisation_id' => $this->organisations['A']->id, 'asn' => '12347', 'approved' => 0 ]);
        $this->asns['B1'] = Asns::create([ 'organisation_id' => $this->organisations['B']->id, 'asn' => '22345', 'approved' => 1 ]);
        $this->asns['B2'] = Asns::create([ 'organisation_id' => $this->organisations['B']->id, 'asn' => '22346', 'approved' => 1 ]);
        $this->asns['B3'] = Asns::create([ 'organisation_id' => $this->organisations['B']->id, 'asn' => '22347', 'approved' => 0 ]);

        $this->as_sets['A1'] = AsSets::create([ 'organisation_id' => $this->organisations['A']->id, 'as_set' => 'AS12345:AS-MERCURY', 'approved' => 1 ]);
        $this->as_sets['B1'] = AsSets::create([ 'organisation_id' => $this->organisations['B']->id, 'as_set' => 'AS22345:AS-GRE-AUTO', 'approved' => 0 ]);

        $this->prefixes = [
            'A1' => Prefixes::factory()->address('250.0.0.0', 20)->ownership($this->organisations['A'], $this->asns['A1'])->announced()->create(),
            'A2' => Prefixes::factory()->address('251.0.0.0', 20)->ownership($this->organisations['A'], $this->asns['A1'])->announced()->create(),
            'A3' => Prefixes::factory()->address('252.0.0.0', 20)->ownership($this->organisations['A'], $this->asns['A2'])->notAnnounced()->create(),
            'A4' => Prefixes::factory()->address('254.0.0.0', 20)->ownership($this->organisations['A'], $this->asns['A2'])->announced()->subtenant($this->organisations['B'])->create(),

            'B1' => Prefixes::factory()->address('150.0.0.0', 20)->ownership($this->organisations['B'], $this->asns['B1'])->announced()->create(),
            'B2' => Prefixes::factory()->address('151.0.0.0', 20)->ownership($this->organisations['B'], $this->asns['B1'])->announced()->create(),
            'B3' => Prefixes::factory()->address('152.0.0.0', 20)->ownership($this->organisations['B'], $this->asns['B2'])->notAnnounced()->create(),
            'B4' => Prefixes::factory()->address('154.0.0.0', 20)->ownership($this->organisations['B'], $this->asns['B2'])->announced()->subtenant($this->organisations['A'])->create(),
        ];

        $this->rulesets = [
            'A'   => GoliathRuleset::create(['name' => 'A', 'organisation_id' => $this->organisations['A']->id, 'type' => RulesetType::Universal->value, 'rules' => '[{"matchAction":"accept","x":0,"y":0,"bpfRule":"udp"},{"matchAction":"drop","x":0,"y":200}]']),
            'A_PERSONAL'   => GoliathRuleset::create(['name' => 'A', 'organisation_id' => $this->organisations['A']->id, 'type' => RulesetType::Reusable->value, 'rules' => '[{"matchAction":"accept","x":0,"y":0,"bpfRule":"udp"},{"matchAction":"drop","x":0,"y":200}]']),
            'B'    => GoliathRuleset::create(['name' => 'B', 'organisation_id' => $this->organisations['B']->id, 'type' => RulesetType::Reusable->value, 'rules' => '[{"matchAction":"accept","x":0,"y":0,"bpfRule":"udp"},{"matchAction":"drop","x":0,"y":200}]']),
        ];

        $this->app->instance('middleware.disable', false);

    }

    public function testAdminAnyAsnIdentity() {
        
        Auth::setUser($this->users['admin']);

        foreach([
            [ 'test' => [ [ 'x' => $this->asns['A1']->id ], [ 'x' => new AdminAnyAsnIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['A2']->id ], [ 'x' => new AdminAnyAsnIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['A3']->id ], [ 'x' => new AdminAnyAsnIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['B1']->id ], [ 'x' => new AdminAnyAsnIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['B2']->id ], [ 'x' => new AdminAnyAsnIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['B3']->id ], [ 'x' => new AdminAnyAsnIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['A1']->id ], [ 'x' => new AdminAnyAsnIdentity(true) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['A2']->id ], [ 'x' => new AdminAnyAsnIdentity(true) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['A3']->id ], [ 'x' => new AdminAnyAsnIdentity(true) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->asns['B1']->id ], [ 'x' => new AdminAnyAsnIdentity(true) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['B2']->id ], [ 'x' => new AdminAnyAsnIdentity(true) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['B3']->id ], [ 'x' => new AdminAnyAsnIdentity(true) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => new AdminAnyAsnIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => new AdminAnyAsnIdentity(true) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ $this->asns['A1']->id ] ], [ 'x' => new AdminAnyAsnIdentity ] ], 'fails' => true ]
        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }
        
    }

    public function testAdminAnyAsSetIdentity() {
        
        Auth::setUser($this->users['admin']);

        foreach([
            [ 'test' => [ [ 'x' => $this->as_sets['A1']->id ], [ 'x' => new AdminAnyAsSetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->as_sets['B1']->id ], [ 'x' => new AdminAnyAsSetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => new AdminAnyAsSetIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 10001 ], [ 'x' => new AdminAnyAsSetIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ $this->as_sets['A1']->id ] ], [ 'x' => new AdminAnyAsSetIdentity ] ], 'fails' => true ]
        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }
        
    }

    public function testAdminAnyPrefixIdentity() {

        Auth::setUser($this->users['admin']);

        foreach([
            [ 'test' => [ [ 'x' => $this->prefixes['A1']->id ], [ 'x' => new AdminAnyPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['A2']->id ], [ 'x' => new AdminAnyPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['A3']->id ], [ 'x' => new AdminAnyPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['A4']->id ], [ 'x' => new AdminAnyPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['B1']->id ], [ 'x' => new AdminAnyPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['B2']->id ], [ 'x' => new AdminAnyPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['B3']->id ], [ 'x' => new AdminAnyPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['B4']->id ], [ 'x' => new AdminAnyPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => new AdminAnyPrefixIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 10001 ], [ 'x' => new AdminAnyPrefixIdentity ] ], 'fails' => true ]
        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testWhoIsAsn() {

        // I'm lazy and writing a test that should only execute in development environments - This will break IF there's multiple endpoints configured for dev.
        Http::fake([
            'http://' . config('creatia.endpoint.whois_forward')[0] . '/asn?asn=AS137409' => Http::response([ 'result' => 'testing@test.com' ], 200),
            'http://' . config('creatia.endpoint.whois_forward')[0] . '/asn?asn=AS137410' => Http::response([ 'result' => '' ], 200),
            '*' => [ 'error' => 'Failed lookup' ]
        ]);

        foreach([
            [ 'test' => [ [ 'x' => [ 'AS137409', 'testing@test.com' ] ], [ 'x' => new WhoIs('asn', false)] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => [ 'AS137409', '' ] ], [ 'x' => new WhoIs('asn', false)] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ 'AS137409', 'testing1@test.com' ] ], [ 'x' => new WhoIs('asn', false)] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => [ 'AS137410', 'testing@test.com' ] ], [ 'x' => new WhoIs('asn', false)] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ 'AS137410', '' ] ], [ 'x' => new WhoIs('asn', false)] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ 'AS137410', 'testing1@test.com' ] ], [ 'x' => new WhoIs('asn', false)] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => [ 'AS1', 'testing@test.com' ] ], [ 'x' => new WhoIs('asn', true)] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ 'AS1', '' ] ], [ 'x' => new WhoIs('asn', true)] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ 'AS1', 'testing1@test.com' ] ], [ 'x' => new WhoIs('asn', true)] ], 'fails' => true ],
            
            [ 'test' => [ [ 'x' => [ 'AS137410', 'testing@test.com' ] ], [ 'x' => new WhoIs('asn', true)] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => [ 'AS137410', '' ] ], [ 'x' => new WhoIs('asn', true)] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => [ 'AS137410', 'testing1@test.com' ] ], [ 'x' => new WhoIs('asn', true)] ], 'fails' => false ],

        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testWhoIsAsSet() {

        Auth::setUser($this->users['user']);
        
        // I'm lazy and writing a test that should only execute in development environments - This will break IF there's multiple endpoints configured for dev.
        Http::fake([
            'http://' . config('creatia.endpoint.whois_forward')[0] . '/as_set?as_set=' . urlencode('AS12345:AS-MERCURY') => Http::response([ 'result' => 'as-set: AS12345:AS-MERCURY' ], 200),
            'http://' . config('creatia.endpoint.whois_forward')[0] . '/as_set?as_set=' . urlencode('AS12346:AS-GRE-AUTO') => Http::response([ 'result' => 'as-set: AS12346:AS-GRE-AUTO' ], 200),
            'http://' . config('creatia.endpoint.whois_forward')[0] . '/as_set?as_set=' . urlencode('AS12347:AS-GRE-AUTO') => Http::response([ 'result' => 'as-set: AS12347:AS-GRE-AUTO' ], 200),
            '*' => [ 'error' => 'Failed lookup' ]
        ]);

        foreach([
            [ 'test' => [ [ 'x' => 'AS12345:AS-MERCURY' ], [ 'x' => new WhoIs('as-set', false)] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'AS12346:AS-GRE-AUTO' ], [ 'x' => new WhoIs('as-set', false)] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'AS12347:AS-GRE-AUTO' ], [ 'x' => new WhoIs('as-set', false)] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 'AS12348:AS-GRE-AUTO' ], [ 'x' => new WhoIs('as-set', false)] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => 'AS12345:AS-MERCURY' ], [ 'x' => new WhoIs('as-set', true)] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'AS12346:AS-GRE-AUTO' ], [ 'x' => new WhoIs('as-set', true)] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'AS12347:AS-GRE-AUTO' ], [ 'x' => new WhoIs('as-set', true)] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'AS12348:AS-GRE-AUTO' ], [ 'x' => new WhoIs('as-set', true)] ], 'fails' => true ],
        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testOwnedAsnIdentity() {
        
        Auth::setUser($this->users['user']);

        foreach([
            [ 'test' => [ [ 'x' => $this->asns['A1']->id ], [ 'x' => new OwnedAsnIdentity() ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['A2']->id ], [ 'x' => new OwnedAsnIdentity() ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['A3']->id ], [ 'x' => new OwnedAsnIdentity() ] ], 'fails' => false ],

            [ 'test' => [ [ 'x' => $this->asns['A1']->id ], [ 'x' => new OwnedAsnIdentity(true) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['A2']->id ], [ 'x' => new OwnedAsnIdentity(true) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->asns['A3']->id ], [ 'x' => new OwnedAsnIdentity(true) ] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => $this->asns['B1']->id ], [ 'x' => new OwnedAsnIdentity() ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->asns['B2']->id ], [ 'x' => new OwnedAsnIdentity() ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->asns['B3']->id ], [ 'x' => new OwnedAsnIdentity() ] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => $this->asns['B1']->id ], [ 'x' => new OwnedAsnIdentity(true) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->asns['B2']->id ], [ 'x' => new OwnedAsnIdentity(true) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->asns['B3']->id ], [ 'x' => new OwnedAsnIdentity(true) ] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => 1001 ], [ 'x' => new OwnedAsnIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 1001 ], [ 'x' => new OwnedAsnIdentity(true) ] ], 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }
        
    }

    public function testOwnedAsSetIdentity() {
        
        Auth::setUser($this->users['user']);

        foreach([
            [ 'test' => [ [ 'x' => $this->as_sets['A1']->id ], [ 'x' => new OwnedAsSetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->as_sets['B1']->id ], [ 'x' => new OwnedAsSetIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 1001 ], [ 'x' => new OwnedAsSetIdentity ] ], 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }
        
    }

    public function testPartialTimestamp() {

        foreach([
            [ 'test' => '2025-01-01 00:00:00', 'fails' => false ],
            [ 'test' => '2025-01-01 00:00:0', 'fails' => false ],
            [ 'test' => '2025-01-01 00:00:', 'fails' => false ],
            [ 'test' => '2025-01-01 00:00', 'fails' => false ],
            [ 'test' => '2025-01-01 00:0', 'fails' => false ],
            [ 'test' => '2025-01-01 00:', 'fails' => false ],
            [ 'test' => '2025-01-01 00', 'fails' => false ],
            [ 'test' => '2025-01-01 0', 'fails' => false ],
            [ 'test' => '2025-01-01 ', 'fails' => false ],
            [ 'test' => '2025-01-01', 'fails' => false ],
            [ 'test' => '2025-01-0', 'fails' => false ],
            [ 'test' => '2025-01-', 'fails' => false ],
            [ 'test' => '2025-01', 'fails' => false ],
            [ 'test' => '2025-0', 'fails' => false ],
            [ 'test' => '2025-', 'fails' => false ],
            [ 'test' => '2025', 'fails' => false ],
            [ 'test' => '202', 'fails' => false ],
            [ 'test' => '20', 'fails' => false ],
            [ 'test' => '2', 'fails' => false ],

            [ 'test' => '25-01-01 00:00:00', 'fails' => false ],
            [ 'test' => '-01-01 00:00:00', 'fails' => false ],
            [ 'test' => '-01-01 00:', 'fails' => false ],

            [ 'test' => '2025-13-01 00:00:00', 'fails' => true ],
            [ 'test' => '2025-23-01', 'fails' => true ],

            [ 'test' => '00:10:00', 'fails' => false ],
            [ 'test' => '32:10:00', 'fails' => true ],

            [ 'test' => '31002:10:00', 'fails' => true ],

            [ 'test' => '2025-13-01T00:00:00Z', 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new PartialTimestamp()])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new PartialTimestamp()])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testUsageRight() {

        foreach([
            [ 'test' => [ [ 'x' => 'mercury' ], [ 'x' => new UsageRight ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'bgp' ], [ 'x' => new UsageRight ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'tier1' ], [ 'x' => new UsageRight ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'tier2' ], [ 'x' => new UsageRight ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'tier3' ], [ 'x' => new UsageRight ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'dedi-streamline' ], [ 'x' => new UsageRight ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'dedi-go-dedicated' ], [ 'x' => new UsageRight ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'ddos-advanced' ], [ 'x' => new UsageRight ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'special' ], [ 'x' => new UsageRight ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'ignorable' ], [ 'x' => new UsageRight ] ], 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testExternalApplication() {

        foreach([
            [ 'test' => [ [ 'x' => 'gsl' ], [ 'x' => new ExternalApplication ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'whmcs_godedicated' ], [ 'x' => new ExternalApplication ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'whmcs_streamline' ], [ 'x' => new ExternalApplication ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'whmcs_arbitrary' ], [ 'x' => new ExternalApplication ] ], 'fails' => true ],
        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }
 
    }

    public function testBrandIdentity() {

        foreach([
            [ 'test' => [ [ 'x' => 'gsl' ], [ 'x' => new BrandIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'go-dedicated' ], [ 'x' => new BrandIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'streamline' ], [ 'x' => new BrandIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'other' ], [ 'x' => new BrandIdentity ] ], 'fails' => true ],
        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }
 
    }

    public function testOwnedRulesetIdentity() {
        
        Auth::setUser($this->users['user']);

        foreach([
            [ 'test' => [ [ 'x' => $this->rulesets['A']->id ], [ 'x' => new OwnedRulesetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->rulesets['B']->id  ], [ 'x' => new OwnedRulesetIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 1001  ], [ 'x' => new OwnedRulesetIdentity ] ], 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }
        
    }

    public function testAdminAnyRulesetIdentity() {
        
        Auth::setUser($this->users['admin']);

        foreach([
            [ 'test' => [ [ 'x' => $this->rulesets['A_PERSONAL']->id ], [ 'x' => new AdminAnyRulesetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->rulesets['A']->id ], [ 'x' => new AdminAnyRulesetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->rulesets['B']->id  ], [ 'x' => new AdminAnyRulesetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 1001  ], [ 'x' => new AdminAnyRulesetIdentity ] ], 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }
     
    }

    public function testAdminRulesetType() {
        
        Auth::setUser($this->users['admin']);

        foreach([
            [ 'test' => [ [ 'x' => RulesetType::Reusable->value], [ 'x' => new AdminRulesetType ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => RulesetType::Universal->value], [ 'x' => new AdminRulesetType ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 1001  ], [ 'x' => new AdminRulesetType ] ], 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }
     
    }
    
    public function testRulesetIdentity() {
        
        Auth::setUser($this->users['admin']);

        foreach([
            [ 'test' => [ [ 'x' => $this->rulesets['A_PERSONAL']->id ], [ 'x' => new RulesetIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->rulesets['A']->id ], [ 'x' => new RulesetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->rulesets['B']->id  ], [ 'x' => new RulesetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 1001  ], [ 'x' => new RulesetIdentity ] ], 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

        Auth::setUser($this->users['user']);

        foreach([
            [ 'test' => [ [ 'x' => $this->rulesets['A_PERSONAL']->id ], [ 'x' => new RulesetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->rulesets['A']->id ], [ 'x' => new RulesetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->rulesets['B']->id  ], [ 'x' => new RulesetIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 1001  ], [ 'x' => new RulesetIdentity ] ], 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }
        
    }

    public function testPartialAddress() {

        foreach([
            [ 'test' => '103.1.1.1', 'fails' => false ],
            [ 'test' => '103.1.1.', 'fails' => false ],
            [ 'test' => '103.1.1', 'fails' => false ],
            [ 'test' => '103.1.', 'fails' => false ],
            [ 'test' => '103.1.', 'fails' => false ],
            [ 'test' => '103.1', 'fails' => false ],
            [ 'test' => '103.', 'fails' => false ],
            [ 'test' => '103', 'fails' => false ],
            [ 'test' => '10', 'fails' => false ],
            [ 'test' => '1', 'fails' => false ],
            [ 'test' => '03.1.1.1', 'fails' => false ],
            [ 'test' => '3.1.1.1', 'fails' => false ],
            [ 'test' => '.1.1.1', 'fails' => false ],
            [ 'test' => '1.1.1', 'fails' => false ],
            [ 'test' => '.1.1', 'fails' => false ],
            [ 'test' => '1.1', 'fails' => false ],
            [ 'test' => '.1', 'fails' => false ],
            [ 'test' => '.', 'fails' => false ],
            [ 'test' => '300.', 'fails' => true ],
            [ 'test' => '..', 'fails' => true ],

            [ 'test' => '2001::7334', 'fails' => false ],
            [ 'test' => '::', 'fails' => false ],
            [ 'test' => ':', 'fails' => false ],
            [ 'test' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334', 'fails' => false ],
            [ 'test' => '2001::', 'fails' => false ],
            [ 'test' => '2001:', 'fails' => false ],
            [ 'test' => ':7334', 'fails' => false ],
            [ 'test' => '::7334', 'fails' => false ],
            [ 'test' => '::Q', 'fails' => true ],
        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new PartialAddress()])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new PartialAddress()])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testHeuristicsLevel() {

        foreach([
            [ 'test' => 3, 'fails' => false ],
            [ 'test' => 2, 'fails' => false ],
            [ 'test' => 1, 'fails' => true ],
            [ 'test' => 0, 'fails' => true ],
            [ 'test' => 'null', 'fails' => true ],
            [ 'test' => 4, 'fails' => true ],
            [ 'test' => 6, 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new HeuristicsLevel()])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new HeuristicsLevel()])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

        Auth::setUser($this->users['special']);

        foreach([
            [ 'test' => 3, 'fails' => false ],
            [ 'test' => 2, 'fails' => false ],
            [ 'test' => 1, 'fails' => false ],
            [ 'test' => 0, 'fails' => false ],
            [ 'test' => null, 'fails' => true ],
            [ 'test' => 4, 'fails' => true ],
            [ 'test' => 6, 'fails' => false ],
            [ 'test' => 7, 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new HeuristicsLevel()])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new HeuristicsLevel()])->fails();

            $this->assertTrue( $result === $x['fails'], 'The special result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

        Auth::setUser($this->users['user']);

    }

    public function testAdminHeuristicsLevel() {

        foreach([
            [ 'test' => 3, 'fails' => false ],
            [ 'test' => 2, 'fails' => false ],
            [ 'test' => 1, 'fails' => false ],
            [ 'test' => 0, 'fails' => false ],
            [ 'test' => null, 'fails' => true ],
            [ 'test' => 4, 'fails' => false ],
            [ 'test' => 5, 'fails' => false ],
            [ 'test' => 6, 'fails' => false ],
            [ 'test' => 7, 'fails' => true ],
            [ 'test' => -1, 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new AdminHeuristicsLevel])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new AdminHeuristicsLevel])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testAllowedPrefixIdentity() {
        
        Auth::setUser($this->users['user']);

        foreach([
            [ 'test' => [ [ 'x' => $this->prefixes['A1']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['A2']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['A3']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['A4']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['B1']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->prefixes['B2']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->prefixes['B3']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->prefixes['B4']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => false ],

            [ 'test' => [ [ 'x' => 1001  ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

        Auth::setUser($this->users['admin']);

        foreach([
            [ 'test' => [ [ 'x' => $this->prefixes['A1']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->prefixes['A2']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->prefixes['A3']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->prefixes['A4']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['B1']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['B2']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['B3']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->prefixes['B4']->id ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => false ],

            [ 'test' => [ [ 'x' => 1001  ], [ 'x' => new AllowedPrefixIdentity ] ], 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }
    }

    public function testRulesetClauseType() {

        foreach([
            [ 'test' => 'Protocol', 'fails' => false ],
            [ 'test' => 'Source Port', 'fails' => false ],
            [ 'test' => 'Destination Port', 'fails' => false ],
            [ 'test' => 'Source Port Range', 'fails' => false ],
            [ 'test' => 'Destination Port Range', 'fails' => false ],
            [ 'test' => 'Source Address', 'fails' => false ],
            [ 'test' => 'Destination Address', 'fails' => false ],
            [ 'test' => 'Source Network', 'fails' => false ],
            [ 'test' => 'Destination Network', 'fails' => false ],
            [ 'test' => 'Test Other', 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new RulesetClauseType])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new RulesetClauseType])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testRulesetClauseOperator() {

        foreach([
            [ 'test' => 'start', 'fails' => false ],
            [ 'test' => 'or', 'fails' => false ],
            [ 'test' => 'and', 'fails' => false ],
            [ 'test' => 'or-before', 'fails' => false ],
            [ 'test' => 'and-before', 'fails' => false ],
            [ 'test' => 'nor', 'fails' => true ],
            [ 'test' => 'xor', 'fails' => true ],
            [ 'test' => 'not', 'fails' => true ],

        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new RulesetClauseOperator])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new RulesetClauseOperator])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testRulesetActionType() {

        foreach([
            [ 'test' => RulesetAction::Accept->value, 'fails' => false ],
            [ 'test' => RulesetAction::Drop->value, 'fails' => false ],
            [ 'test' => RulesetAction::Police->value, 'fails' => false ],
            [ 'test' => RulesetAction::Goto->value, 'fails' => false ],
            [ 'test' => 'jump', 'fails' => true ],
            [ 'test' => 'rand', 'fails' => true ]

        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new RulesetActionType])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new RulesetActionType])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testRegionIdentity() {

        $regions = [
            '1' => GoliathRegion::create([ 'name' => '1', 'telemetry_identifier' => '1', 'visible' => 1 ]),
            '2' => GoliathRegion::create([ 'name' => '2', 'telemetry_identifier' => '2', 'visible' => 1 ]),
            '3' => GoliathRegion::create([ 'name' => '3', 'telemetry_identifier' => '3', 'visible' => 1 ]),
        ];

        foreach([
            [ 'test' => $regions['1']->id, 'fails' => false ],
            [ 'test' => $regions['2']->id, 'fails' => false ],
            [ 'test' => $regions['3']->id, 'fails' => false ],
            [ 'test' => 1000, 'fails' => true ],
            [ 'test' => 1001, 'fails' => true ],
            [ 'test' => 2001, 'fails' => true ],
            [ 'test' => [ $regions['1']->id ], 'fails' => true ]

        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new RegionIdentity])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new RegionIdentity])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testSubregionIdentity() {

        $regions = [
            '1' => GoliathRegion::create([ 'name' => '1', 'telemetry_identifier' => '1', 'visible' => 1 ]),
            '2' => GoliathRegion::create([ 'name' => '2', 'telemetry_identifier' => '2', 'visible' => 1 ]),
            '3' => GoliathRegion::create([ 'name' => '3', 'telemetry_identifier' => '3', 'visible' => 1 ]),
        ];

        $subregions = [
            '1' => GoliathSubregion::create([ 'name' => '1.1', 'telemetry_identifier' => '1.1', 'visible' => 1, 'region_id' => $regions['1']->id ]),
            '2' => GoliathSubregion::create([ 'name' => '2.1', 'telemetry_identifier' => '2.1', 'visible' => 1, 'region_id' => $regions['2']->id ]),
            '3' => GoliathSubregion::create([ 'name' => '3.1', 'telemetry_identifier' => '3.1', 'visible' => 1, 'region_id' => $regions['3']->id ]),
        ];

        foreach([
            [ 'test' => $subregions['1']->id, 'fails' => false ],
            [ 'test' => $subregions['2']->id, 'fails' => false ],
            [ 'test' => $subregions['3']->id, 'fails' => false ],
            [ 'test' => 1000, 'fails' => true ],
            [ 'test' => 1001, 'fails' => true ],
            [ 'test' => 2001, 'fails' => true ],
            [ 'test' => [ $subregions['1']->id ], 'fails' => true ]

        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new SubregionIdentity])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new SubregionIdentity])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testSafeName() {

        foreach([
            [ 'test' => 'Testing Label #1', 'fails' => false ],
            [ 'test' => 'Can I label as this (Yes_I_CAN)', 'fails' => false ],
            [ 'test' => 'Can I label as this (No) \'--', 'fails' => true ],
            [ 'test' => 'Can I label as this (No) "--', 'fails' => true ],
            [ 'test' => 'Can I label as this (No) `--', 'fails' => true ],
            [ 'test' => '<>;:/\\=%*', 'fails' => true ],
        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new SafeName])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new SafeName])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testSafeGameFilters() {

        foreach([
            [ 'test' => [ 'fivem' => ['1'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem' => ['1-100'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem' => ['64000'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem' => ['100-101'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem' => ['100-101','102','90-99'] ] , 'fails' => false ],

            [ 'test' => [ 'fivem' => ['1'], 'source1' => ['2'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem' => ['1-100'], 'source1' => ['101-200'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem' => ['64000'], 'source1' => ['50000-63999'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem' => ['100-101'], 'source1' => ['90-99'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem' => ['100-101','80-89'], 'source1' => ['90-99','200-299'] ] , 'fails' => false ],

            [ 'test' => [ 'fivem' => ['1'], 'source1' => ['1'] ] , 'fails' => true ],
            [ 'test' => [ 'fivem' => ['1-100'], 'source1' => ['99-200'] ] , 'fails' => true ],
            [ 'test' => [ 'fivem' => ['64000'], 'source1' => ['50000-64001'] ] , 'fails' => true ],
            [ 'test' => [ 'fivem' => ['100-101'], 'source1' => ['90-110'] ] , 'fails' => true ],

            [ 'test' => [ 'fivem' => '-1' ] , 'fails' => true ],
            [ 'test' => [ 'fivem' => '0' ], 'fails' => true ],
            [ 'test' => [ 'fivem' => '70000' ], 'fails' => true ],
            [ 'test' => [ 'fivem' => '300-100' ], 'fails' => true ],
            [ 'test' => [ 'fivem' => '0-70000' ], 'fails' => true ],
            [ 'test' => [ 'fivem' => '70000-0' ], 'fails' => true ],

            [ 'test' => [ 'udp_tcp_gate' => ['30120'] ] , 'fails' => false ],
            [ 'test' => [ 'sip' => ['5060-5061'] ] , 'fails' => false ],
            [ 'test' => [ 'sip_stateless' => ['5060-5061'] ] , 'fails' => false ],
            [ 'test' => [ 'fivemx' => ['30120'] ] , 'fails' => false ],
            [ 'test' => [ 'fivemx_stateless' => ['30120'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem' => ['30120'], 'fivemx' => ['30125'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem' => ['30120'], 'fivemx' => ['30120'] ] , 'fails' => true ],

            // udp_tcp_gate has no stateless variant, and the stateless
            // Minecraft Java filter has been removed.
            [ 'test' => [ 'udp_tcp_gate_stateless' => ['30120'] ] , 'fails' => true ],
            [ 'test' => [ 'minecraft' => ['25565'] ] , 'fails' => false ],
            [ 'test' => [ 'minecraft_stateless' => ['25565'] ] , 'fails' => true ],

            // fivem_double_handshake (goliath's fivem_tcp_gated) and wireguard
            // are stateful-only, so neither has a stateless twin. Goliath's own
            // name for the filter is not accepted.
            [ 'test' => [ 'fivem_double_handshake' => ['30120'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem_double_handshake' => ['30120-30130'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem_double_handshake_stateless' => ['30120'] ] , 'fails' => true ],
            [ 'test' => [ 'fivem_tcp_gated' => ['30120'] ] , 'fails' => true ],
            [ 'test' => [ 'wireguard' => ['51820'] ] , 'fails' => false ],
            [ 'test' => [ 'wireguard' => ['51820-51830'] ] , 'fails' => false ],
            [ 'test' => [ 'wireguard_stateless' => ['51820'] ] , 'fails' => true ],
            [ 'test' => [ 'fivem' => ['30120'], 'fivem_double_handshake' => ['30121'] ] , 'fails' => false ],
            [ 'test' => [ 'fivem' => ['30120'], 'fivem_double_handshake' => ['30120'] ] , 'fails' => true ],

            // FiveM Enhanced's double handshake, the SA-MP and MTA:SA game
            // filters (each with a stateless twin), MTA:SA's query port, and
            // the Level 1 filters, which have no twin.
            [ 'test' => [ 'fivemx_double_handshake' => ['30120'] ] , 'fails' => false ],
            [ 'test' => [ 'fivemx_double_handshake_stateless' => ['30120'] ] , 'fails' => true ],
            [ 'test' => [ 'fivemx_tcp_gated' => ['30120'] ] , 'fails' => true ],
            [ 'test' => [ 'samp' => ['7777'], 'samp_stateless' => ['7778'] ] , 'fails' => false ],
            [ 'test' => [ 'mtasa' => ['22003'], 'mtasa_ase' => ['22126'] ] , 'fails' => false ],
            [ 'test' => [ 'mtasa_stateless' => ['22003'] ] , 'fails' => false ],
            [ 'test' => [ 'mtasa_ase_stateless' => ['22126'] ] , 'fails' => true ],
            [ 'test' => [ 'openvpn' => ['1194'], 'teamspeak3' => ['9987'], 'srt' => ['4000-4010'], 'ephemeral' => ['32768-60999'] ] , 'fails' => false ],
            [ 'test' => [ 'openvpn_stateless' => ['1194'] ] , 'fails' => true ],
            [ 'test' => [ 'teamspeak3_stateless' => ['9987'] ] , 'fails' => true ],
            [ 'test' => [ 'srt_stateless' => ['4000'] ] , 'fails' => true ],
            [ 'test' => [ 'ephemeral_stateless' => ['32768'] ] , 'fails' => true ],
            [ 'test' => [ 'openvpn' => ['1194'], 'udp_generic' => ['1194'] ] , 'fails' => true ],

            // GameSpy, with its stateless twin.
            [ 'test' => [ 'gamespy' => ['27900-27901'], 'gamespy_stateless' => ['29900'] ] , 'fails' => false ],

            // Administrators only.
            [ 'test' => [ 'bypass' => ['4321'] ] , 'fails' => true ],
            [ 'test' => [ 'icmp_anycast' => ['0-0'] ] , 'fails' => true ],
            [ 'test' => [ 'wireguard' => ['51820'], 'udp_generic' => ['51820'] ] , 'fails' => true ],

            // http_inbound, tls_inbound and ssh_inbound are stateful-only
            // despite the `_inbound` suffix the stateless filters also use.
            [ 'test' => [ 'http_inbound' => ['80'] ] , 'fails' => false ],
            [ 'test' => [ 'tls_inbound' => ['443'] ] , 'fails' => false ],
            [ 'test' => [ 'ssh_inbound' => ['22'] ] , 'fails' => false ],
            [ 'test' => [ 'http_inbound' => ['80', '8080-8090'] ] , 'fails' => false ],
            [ 'test' => [ 'http_inbound' => ['80'], 'tls_inbound' => ['443'], 'ssh_inbound' => ['22'] ] , 'fails' => false ],
            [ 'test' => [ 'http_inbound_stateless' => ['80'] ] , 'fails' => true ],
            [ 'test' => [ 'tls_inbound_stateless' => ['443'] ] , 'fails' => true ],
            [ 'test' => [ 'ssh_inbound_stateless' => ['22'] ] , 'fails' => true ],
            [ 'test' => [ 'http_inbound' => ['80'], 'tcp_inbound' => ['80'] ] , 'fails' => true ],

            [ 'test' => [ 'fivem' => ['-1'], 'rust' => ['1'] ] , 'fails' => true ],
            [ 'test' => [ 'fivem' => ['0'], 'source1' => ['1-100'] ], 'fails' => true ],
            [ 'test' => [ 'fivem' => ['70000'], 'source1' => ['64000'] ], 'fails' => true ],
            [ 'test' => [ 'fivem' => ['300-100'], 'source1' => ['100-101'] ], 'fails' => true ],
            [ 'test' => [ 'fivem' => ['1-50'], 'source1' => ['100-150','45'] ], 'fails' => true ],
            [ 'test' => [ 'fivem' => ['0-70000'], 'source1' => ['100-101'] ], 'fails' => true ],
            [ 'test' => [ 'fivem' => ['70000-0'], 'source1' => ['100-101'] ], 'fails' => true ],
        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new GameFilters])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new GameFilters])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testSynCookieOsGate() {

        foreach([
            [ 'test' => null, 'fails' => false ],
            [ 'test' => 'windows', 'fails' => false ],
            [ 'test' => 'non-windows', 'fails' => false ],
            [ 'test' => 'Windows', 'fails' => true ],
            [ 'test' => 'NON-WINDOWS', 'fails' => true ],
            [ 'test' => 'linux', 'fails' => true ],
            // Laravel skips non-implicit rules for empty strings, so this
            // passes validation - the controllers normalise it to null.
            [ 'test' => '', 'fails' => false ],
            [ 'test' => 0, 'fails' => true ],
            [ 'test' => true, 'fails' => true ],
            [ 'test' => ['windows'], 'fails' => true ],
        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => ['nullable', new SynCookieOsGate]])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => ['nullable', new SynCookieOsGate]])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testSynChallenge() {

        foreach([
            [ 'test' => null, 'fails' => false ],
            [ 'test' => 'cookie', 'fails' => false ],
            [ 'test' => 'safe-reset', 'fails' => false ],
            [ 'test' => 'Cookie', 'fails' => true ],
            [ 'test' => 'SAFE-RESET', 'fails' => true ],
            [ 'test' => 'safe_reset', 'fails' => true ],
            [ 'test' => 'reset', 'fails' => true ],
            // Laravel skips non-implicit rules for empty strings, so this
            // passes validation - the controllers normalise it to null.
            [ 'test' => '', 'fails' => false ],
            [ 'test' => 0, 'fails' => true ],
            [ 'test' => true, 'fails' => true ],
            [ 'test' => ['cookie'], 'fails' => true ],
        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => ['nullable', new SynChallenge]])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => ['nullable', new SynChallenge]])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testAdminGameFilters() {

        foreach([
            // Everything a customer may set, plus the administrator-only filters.
            [ 'test' => [ 'fivem_double_handshake' => ['30120'], 'fivemx_double_handshake' => ['30121'] ] , 'fails' => false ],
            [ 'test' => [ 'samp' => ['7777'], 'mtasa' => ['22003'], 'mtasa_ase' => ['22126'] ] , 'fails' => false ],
            [ 'test' => [ 'openvpn' => ['1194'], 'teamspeak3' => ['9987'], 'srt' => ['4000'], 'ephemeral' => ['32768-60999'] ] , 'fails' => false ],
            [ 'test' => [ 'gamespy' => ['27900-27901'], 'gamespy_stateless' => ['29900'] ] , 'fails' => false ],
            [ 'test' => [ 'bypass' => ['4321'] ] , 'fails' => false ],
            [ 'test' => [ 'icmp_anycast' => ['0-0'] ] , 'fails' => false ],

            // Goliath's names for the double handshake filters are not accepted here either.
            [ 'test' => [ 'fivem_tcp_gated' => ['30120'] ] , 'fails' => true ],
            [ 'test' => [ 'fivemx_tcp_gated' => ['30120'] ] , 'fails' => true ],
            [ 'test' => [ 'bypass_stateless' => ['4321'] ] , 'fails' => true ],
            [ 'test' => [ 'gamespy' => ['27900'], 'bypass' => ['27900'] ] , 'fails' => true ],
        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => new AdminGameFilters])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => new AdminGameFilters])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testSrcLimit() {

        foreach([
            [ 'unit' => 'pps', 'test' => null, 'fails' => false ],
            [ 'unit' => 'pps', 'test' => 100, 'fails' => false ],
            [ 'unit' => 'pps', 'test' => 999, 'fails' => false ],
            [ 'unit' => 'pps', 'test' => 1000, 'fails' => false ],
            [ 'unit' => 'pps', 'test' => 2500, 'fails' => false ],
            [ 'unit' => 'pps', 'test' => 12300, 'fails' => false ],
            [ 'unit' => 'pps', 'test' => 999000, 'fails' => false ],
            // Goliath would quietly round these to three significant digits.
            [ 'unit' => 'pps', 'test' => 1001, 'fails' => true ],
            [ 'unit' => 'pps', 'test' => 12345, 'fails' => true ],
            [ 'unit' => 'pps', 'test' => 12340, 'fails' => true ],
            [ 'unit' => 'pps', 'test' => 99, 'fails' => true ],
            [ 'unit' => 'pps', 'test' => 0, 'fails' => true ],
            [ 'unit' => 'pps', 'test' => -100, 'fails' => true ],
            [ 'unit' => 'pps', 'test' => 1000000, 'fails' => true ],
            [ 'unit' => 'pps', 'test' => '1000', 'fails' => true ],
            [ 'unit' => 'pps', 'test' => 1000.5, 'fails' => true ],
            [ 'unit' => 'pps', 'test' => true, 'fails' => true ],

            [ 'unit' => 'mbps', 'test' => null, 'fails' => false ],
            [ 'unit' => 'mbps', 'test' => 1, 'fails' => false ],
            [ 'unit' => 'mbps', 'test' => 250, 'fails' => false ],
            [ 'unit' => 'mbps', 'test' => 12300, 'fails' => false ],
            [ 'unit' => 'mbps', 'test' => 99900, 'fails' => false ],
            [ 'unit' => 'mbps', 'test' => 1234, 'fails' => true ],
            [ 'unit' => 'mbps', 'test' => 0, 'fails' => true ],
            [ 'unit' => 'mbps', 'test' => 100000, 'fails' => true ],
        ] as $x) {

            $reason = Validator::make(['x' => $x['test']], ['x' => ['nullable', new SrcLimit($x['unit'])]])->errors();
            $result = Validator::make(['x' => $x['test']], ['x' => ['nullable', new SrcLimit($x['unit'])]])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    public function testExclusiveWith() {

        foreach([
            [ 'test' => [ 'x' => true, 'y' => true ], 'fails' => true ],
            [ 'test' => [ 'x' => 1000, 'y' => 50 ], 'fails' => true ],
            [ 'test' => [ 'x' => '1', 'y' => 1 ], 'fails' => true ],
            [ 'test' => [ 'x' => true, 'y' => false ], 'fails' => false ],
            [ 'test' => [ 'x' => true, 'y' => null ], 'fails' => false ],
            [ 'test' => [ 'x' => true ], 'fails' => false ],
            [ 'test' => [ 'x' => 1000, 'y' => 0 ], 'fails' => false ],
            [ 'test' => [ 'x' => false, 'y' => true ], 'fails' => false ],
            [ 'test' => [ 'x' => null, 'y' => true ], 'fails' => false ],
            [ 'test' => [ 'x' => '0', 'y' => '1' ], 'fails' => false ],
        ] as $x) {

            $reason = Validator::make($x['test'], ['x' => ['nullable', new ExclusiveWith('y')]])->errors();
            $result = Validator::make($x['test'], ['x' => ['nullable', new ExclusiveWith('y')]])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    private function assertTable(array $cases) {

        foreach($cases as $x) {

            $reason = Validator::make(...$x['test'])->errors();
            $result = Validator::make(...$x['test'])->fails();

            $this->assertTrue( $result === $x['fails'], 'The result was ' . ($result ? 'true' : 'false') . ' when it needed to be ' . ($x['fails'] ? 'true' : 'false') . ". The test was:\n\n" . json_encode($x) . "\n\nProvided reasoning: \n\n" . json_encode($reason) );

        }

    }

    private function makeRegionality() {

        $region = GoliathRegion::create([ 'name' => '1', 'telemetry_identifier' => '1', 'visible' => 1 ]);
        $subregion = GoliathSubregion::create([ 'name' => '1.1', 'telemetry_identifier' => '1.1', 'visible' => 1, 'region_id' => $region->id ]);
        $metro = GoliathMetro::create([ 'name' => '1.1.1', 'telemetry_identifier' => '1.1.1', 'visible' => 1, 'subregion_id' => $subregion->id ]);

        return [ $region, $subregion, $metro ];
    }

    public function testMetroIdentity() {

        [ $region, $subregion, $metro ] = $this->makeRegionality();

        $this->assertTable([
            [ 'test' => [ [ 'x' => $metro->id ], [ 'x' => new MetroIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => new MetroIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 'garbage' ], [ 'x' => new MetroIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ $metro->id ] ], [ 'x' => new MetroIdentity ] ], 'fails' => true ],
        ]);

    }

    public function testApplianceIdentity() {

        [ $region, $subregion, $metro ] = $this->makeRegionality();

        $appliance = GoliathAppliance::create([ 'name' => '1', 'organisation_id' => $this->organisations['A']->id, 'registered_at' => Carbon::now(), 'region_id' => $region->id, 'subregion_id' => $subregion->id, 'metro_id' => $metro->id, 'authorised' => 1, 'address' => '126.0.0.1' ]);
        $trashed = GoliathAppliance::create([ 'name' => '2', 'organisation_id' => $this->organisations['A']->id, 'registered_at' => Carbon::now(), 'region_id' => $region->id, 'subregion_id' => $subregion->id, 'metro_id' => $metro->id, 'authorised' => 1, 'address' => '126.0.0.2' ]);
        $trashed->delete();

        $this->assertTable([
            [ 'test' => [ [ 'x' => $appliance->id ], [ 'x' => new ApplianceIdentity ] ], 'fails' => false ],

            [ 'test' => [ [ 'x' => $trashed->id ], [ 'x' => new ApplianceIdentity ] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => new ApplianceIdentity ] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => [ $appliance->id ] ], [ 'x' => new ApplianceIdentity ] ], 'fails' => true ],
        ]);

    }

    public function testOrganisationIdentity() {

        $trashed = Organisation::create([ 'name' => 'C' ]);
        $trashed->delete();

        $this->assertTable([
            [ 'test' => [ [ 'x' => $this->organisations['A']->id ], [ 'x' => new OrganisationIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->organisations['B']->id ], [ 'x' => new OrganisationIdentity ] ], 'fails' => false ],

            [ 'test' => [ [ 'x' => $trashed->id ], [ 'x' => new OrganisationIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $trashed->id ], [ 'x' => OrganisationIdentity::make(true) ] ], 'fails' => false ],

            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => new OrganisationIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => OrganisationIdentity::make(true) ] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => [ $this->organisations['A']->id ] ], [ 'x' => new OrganisationIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ $this->organisations['A']->id ] ], [ 'x' => OrganisationIdentity::make(true) ] ], 'fails' => true ],
        ]);

    }

    public function testGroupIdentity() {

        $group = Group::where('name', '=', 'Admin')->get()->first();

        $this->assertTable([
            [ 'test' => [ [ 'x' => $group->id ], [ 'x' => new GroupIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => new GroupIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ $group->id ] ], [ 'x' => new GroupIdentity ] ], 'fails' => true ],
        ]);

    }

    public function testAdminAnyUserIdentity() {

        Auth::setUser($this->users['admin']);

        $trashed = User::create([ 'phone' => null, 'password' => '', 'group_id' => $this->users['user']->group_id, 'organisation_id' => $this->organisations['A']->id, 'name' => 'Trashed', 'email' => 'creatia+trashed@globalsecurelayer.com' ]);
        $trashed->delete();

        $this->assertTable([
            [ 'test' => [ [ 'x' => $this->users['admin']->id ], [ 'x' => new AdminAnyUserIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->users['user']->id ], [ 'x' => new AdminAnyUserIdentity ] ], 'fails' => false ],

            [ 'test' => [ [ 'x' => $trashed->id ], [ 'x' => new AdminAnyUserIdentity ] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => new AdminAnyUserIdentity ] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => [ $this->users['admin']->id ] ], [ 'x' => new AdminAnyUserIdentity ] ], 'fails' => true ],
        ]);

    }

    public function testOwnedUserIdentity() {

        Auth::setUser($this->users['user']);

        $trashed = User::create([ 'phone' => null, 'password' => '', 'group_id' => $this->users['user']->group_id, 'organisation_id' => $this->organisations['A']->id, 'name' => 'Trashed', 'email' => 'creatia+trashed@globalsecurelayer.com' ]);
        $trashed->delete();

        $this->assertTable([
            [ 'test' => [ [ 'x' => $this->users['user']->id ], [ 'x' => new OwnedUserIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->users['special']->id ], [ 'x' => new OwnedUserIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->users['admin']->id ], [ 'x' => new OwnedUserIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $trashed->id ], [ 'x' => new OwnedUserIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => new OwnedUserIdentity ] ], 'fails' => true ],
        ]);

    }

    public function testOwnedWebhookIdentity() {

        Auth::setUser($this->users['user']);

        $webhooks = [
            'A1' => Webhook::create([ 'name' => 'A1', 'hook' => 'https://example.com/a1', 'hook_type' => 'slack', 'usage_type' => 'reporting', 'organisation_id' => $this->organisations['A']->id ]),
            'B1' => Webhook::create([ 'name' => 'B1', 'hook' => 'https://example.com/b1', 'hook_type' => 'slack', 'usage_type' => 'reporting', 'organisation_id' => $this->organisations['B']->id ]),
            'A2' => Webhook::create([ 'name' => 'A2', 'hook' => 'https://example.com/a2', 'hook_type' => 'slack', 'usage_type' => 'reporting', 'organisation_id' => $this->organisations['A']->id ]),
        ];

        $webhooks['A2']->delete();

        $this->assertTable([
            [ 'test' => [ [ 'x' => $webhooks['A1']->id ], [ 'x' => new OwnedWebhookIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $webhooks['B1']->id ], [ 'x' => new OwnedWebhookIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $webhooks['A2']->id ], [ 'x' => new OwnedWebhookIdentity ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => new OwnedWebhookIdentity ] ], 'fails' => true ],
        ]);

    }

    public function testOwnedTunnelIdentity() {

        Auth::setUser($this->users['user']);

        $key = 'U:' . $this->users['user']->organisation_id . ':TUNNEL:ROW:P:9001';

        Redis::setex($key, 60, json_encode([ 'endpoint_ip' => '126.0.0.1' ]));

        try {
            $this->assertTable([
                [ 'test' => [ [ 'x' => 9001 ], [ 'x' => new OwnedTunnelIdentity ] ], 'fails' => false ],
                [ 'test' => [ [ 'x' => 9002 ], [ 'x' => new OwnedTunnelIdentity ] ], 'fails' => true ],
            ]);

            // A tunnel keyed under a different organisation is not visible.
            Auth::setUser($this->users['admin']);

            $this->assertTable([
                [ 'test' => [ [ 'x' => 9001 ], [ 'x' => new OwnedTunnelIdentity ] ], 'fails' => true ],
            ]);
        }
        finally {
            Redis::del($key);
        }

    }

    public function testComputePeerIdentity() {

        Auth::setUser($this->users['user']);

        $key = 'U:' . $this->users['user']->id . ':COMPUTE:ROW:P:9001';

        Redis::setex($key, 60, json_encode([ 'peer' => 'test' ]));

        try {
            $this->assertTable([
                [ 'test' => [ [ 'x' => 9001 ], [ 'x' => new ComputePeerIdentity ] ], 'fails' => false ],
                [ 'test' => [ [ 'x' => 9002 ], [ 'x' => new ComputePeerIdentity ] ], 'fails' => true ],
            ]);

            // A peer keyed under a different user is not visible.
            Auth::setUser($this->users['admin']);

            $this->assertTable([
                [ 'test' => [ [ 'x' => 9001 ], [ 'x' => new ComputePeerIdentity ] ], 'fails' => true ],
            ]);
        }
        finally {
            Redis::del($key);
        }

    }

    public function testSidHash() {

        $this->assertTable([
            [ 'test' => [ [ 'x' => hash('sha512', 'test') ], [ 'x' => new SidHash ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => strtoupper(hash('sha512', 'test')) ], [ 'x' => new SidHash ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => substr(hash('sha512', 'test'), 0, 127) ], [ 'x' => new SidHash ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => hash('sha512', 'test') . 'a' ], [ 'x' => new SidHash ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => str_repeat('g', 128) ], [ 'x' => new SidHash ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ hash('sha512', 'test') ] ], [ 'x' => new SidHash ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 12345 ], [ 'x' => new SidHash ] ], 'fails' => true ],
        ]);

    }

    public function testAsnFormat() {

        $this->assertTable([
            [ 'test' => [ [ 'x' => 'AS12345' ], [ 'x' => new AsnFormat ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'ASN12345' ], [ 'x' => new AsnFormat ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => '12345' ], [ 'x' => new AsnFormat ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 12345 ], [ 'x' => new AsnFormat ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'not an asn at all' ], [ 'x' => new AsnFormat ] ], 'fails' => true ],
            // Anchored: trailing/embedded junk after the digits is rejected.
            [ 'test' => [ [ 'x' => 'AS12345 injected' ], [ 'x' => new AsnFormat ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => '12345:AS-EVIL' ], [ 'x' => new AsnFormat ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ '12345' ] ], [ 'x' => new AsnFormat ] ], 'fails' => true ],
        ]);

    }

    public function testAsSetFormat() {

        $this->assertTable([
            [ 'test' => [ [ 'x' => 'AS12345:AS-MERCURY' ], [ 'x' => AsSetFormat::make(true) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'as12345:as-mercury' ], [ 'x' => AsSetFormat::make(true) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'AS-MERCURY' ], [ 'x' => AsSetFormat::make(true) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 'AS12345:AS-MERCURY extra' ], [ 'x' => AsSetFormat::make(true) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => '12345' ], [ 'x' => AsSetFormat::make(true) ] ], 'fails' => true ],

            // The loose variant accepts an AS-SET-ish token (letters, digits,
            // colons, dashes) that carries at least one letter/colon/dash, but
            // is anchored so freeform text and stray characters are rejected.
            [ 'test' => [ [ 'x' => 'AS-MERCURY' ], [ 'x' => AsSetFormat::make(false) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'AS137409:AS-MERCURY' ], [ 'x' => AsSetFormat::make(false) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'anything with letters' ], [ 'x' => AsSetFormat::make(false) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 'AS-EVIL; drop' ], [ 'x' => AsSetFormat::make(false) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => '12345' ], [ 'x' => AsSetFormat::make(false) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ 'AS-MERCURY' ] ], [ 'x' => AsSetFormat::make(false) ] ], 'fails' => true ],
        ]);

    }

    public function testInterval() {

        $usage_chart = Interval::make(Interval::USAGE_CHART);

        $cases = [];

        // Every advertised usage chart window passes, as int and as string.
        foreach([ 300, 900, 1800, 3600, 7200, 21600, 43200, 86400, 172800 ] as $seconds) {
            $cases[] = [ 'test' => [ [ 'x' => $seconds ], [ 'x' => $usage_chart ] ], 'fails' => false ];
            $cases[] = [ 'test' => [ [ 'x' => (string) $seconds ], [ 'x' => $usage_chart ] ], 'fails' => false ];
        }

        $this->assertTable([
            ...$cases,
            [ 'test' => [ [ 'x' => 600 ], [ 'x' => $usage_chart ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 301 ], [ 'x' => $usage_chart ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => '300.5' ], [ 'x' => $usage_chart ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 0 ], [ 'x' => $usage_chart ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => true ], [ 'x' => $usage_chart ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 'garbage' ], [ 'x' => $usage_chart ] ], 'fails' => true ],

            // Configuration is respected - 10 minutes is valid when offered.
            [ 'test' => [ [ 'x' => 600 ], [ 'x' => Interval::make([ 10 ]) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 300 ], [ 'x' => Interval::make([ 10 ]) ] ], 'fails' => true ],
        ]);

    }

    public function testOwnedAsSetIdentityApprovedOnly() {

        Auth::setUser($this->users['user']);

        $unapproved = AsSets::create([ 'organisation_id' => $this->organisations['A']->id, 'as_set' => 'AS12346:AS-PENDING', 'approved' => 0 ]);

        $this->assertTable([
            [ 'test' => [ [ 'x' => $this->as_sets['A1']->id ], [ 'x' => OwnedAsSetIdentity::make(true) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $unapproved->id ], [ 'x' => new OwnedAsSetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $unapproved->id ], [ 'x' => OwnedAsSetIdentity::make(true) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => $this->as_sets['B1']->id ], [ 'x' => OwnedAsSetIdentity::make(true) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => OwnedAsSetIdentity::make(true) ] ], 'fails' => true ],
        ]);

    }

    public function testAdminAnyAsSetIdentityApprovedOnly() {

        Auth::setUser($this->users['admin']);

        // Fixture note: A1 is approved, B1 is not.
        $this->assertTable([
            [ 'test' => [ [ 'x' => $this->as_sets['A1']->id ], [ 'x' => AdminAnyAsSetIdentity::make(true) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->as_sets['B1']->id ], [ 'x' => new AdminAnyAsSetIdentity ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => $this->as_sets['B1']->id ], [ 'x' => AdminAnyAsSetIdentity::make(true) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => 10000 ], [ 'x' => AdminAnyAsSetIdentity::make(true) ] ], 'fails' => true ],
        ]);

    }

    public function testSafeNameCharsets() {

        $this->assertTable([
            // Default charset is unchanged - hashes remain permitted.
            [ 'test' => [ [ 'x' => 'Testing Label #1' ], [ 'x' => SafeName::make(SafeName::CHARSET_DEFAULT) ] ], 'fails' => false ],

            // The chain name charset drops the hash but keeps brackets.
            [ 'test' => [ [ 'x' => 'Chain (Primary) [v2]' ], [ 'x' => SafeName::make(SafeName::CHARSET_NO_HASH) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'Chain #1' ], [ 'x' => SafeName::make(SafeName::CHARSET_NO_HASH) ] ], 'fails' => true ],

            // The element name charset is words, digits, dashes and spaces only.
            [ 'test' => [ [ 'x' => 'element_name - 1' ], [ 'x' => SafeName::make(SafeName::CHARSET_SIMPLE) ] ], 'fails' => false ],
            [ 'test' => [ [ 'x' => 'element (1)' ], [ 'x' => SafeName::make(SafeName::CHARSET_SIMPLE) ] ], 'fails' => true ],

            [ 'test' => [ [ 'x' => '<>;:/\\=%*' ], [ 'x' => SafeName::make(SafeName::CHARSET_SIMPLE) ] ], 'fails' => true ],
            [ 'test' => [ [ 'x' => [ 'array' ] ], [ 'x' => new SafeName ] ], 'fails' => true ],
        ]);

    }

    public function testRegionalityFullFragment() {

        [ $region, $subregion, $metro ] = $this->makeRegionality();

        // Structure snapshot - any change to the fragment shape shows up here.
        $full = Regionality::full();

        $this->assertSame([ 'region_id', 'subregion_id', 'metro_id' ], array_keys($full));
        $this->assertInstanceOf(RegionIdentity::class, $full['region_id'][0]);
        $this->assertInstanceOf(SubregionIdentity::class, $full['subregion_id'][0]);
        $this->assertSame('exclude_without:region_id', $full['subregion_id'][1]);
        $this->assertInstanceOf(MetroIdentity::class, $full['metro_id'][0]);
        $this->assertSame('exclude_without:subregion_id', $full['metro_id'][1]);

        $this->assertTable([
            [ 'test' => [ [ 'region_id' => $region->id, 'subregion_id' => $subregion->id, 'metro_id' => $metro->id ], Regionality::full() ], 'fails' => false ],
            [ 'test' => [ [ 'region_id' => $region->id ], Regionality::full() ], 'fails' => false ],
            [ 'test' => [ [], Regionality::full() ], 'fails' => false ],

            [ 'test' => [ [ 'region_id' => 10000 ], Regionality::full() ], 'fails' => true ],
            [ 'test' => [ [ 'region_id' => $region->id, 'subregion_id' => 10000 ], Regionality::full() ], 'fails' => true ],
            [ 'test' => [ [ 'region_id' => $region->id, 'subregion_id' => $subregion->id, 'metro_id' => 10000 ], Regionality::full() ], 'fails' => true ],

            // The identity rule precedes exclude_without in the array (matching
            // the original inline sites), so it runs before the exclusion can
            // kick in - a bad narrower id fails even without its parent level.
            [ 'test' => [ [ 'subregion_id' => 10000 ], Regionality::full() ], 'fails' => true ],
            [ 'test' => [ [ 'metro_id' => 10000 ], Regionality::full() ], 'fails' => true ],

            // A valid narrower id without its parent level passes - the identity
            // check succeeds and the field is then merely excluded from the
            // validated-data set (controllers read via input(), so no change).
            [ 'test' => [ [ 'subregion_id' => $subregion->id ], Regionality::full() ], 'fails' => false ],
            [ 'test' => [ [ 'metro_id' => $metro->id ], Regionality::full() ], 'fails' => false ],
        ]);

    }

    public function testRegionalityNullablePairFragment() {

        [ $region, $subregion, $metro ] = $this->makeRegionality();

        $pair = Regionality::nullablePair();

        $this->assertSame([ 'region_id', 'subregion_id' ], array_keys($pair));
        $this->assertSame('nullable', $pair['region_id'][0]);
        $this->assertInstanceOf(RegionIdentity::class, $pair['region_id'][1]);
        $this->assertSame('nullable', $pair['subregion_id'][0]);
        $this->assertInstanceOf(SubregionIdentity::class, $pair['subregion_id'][1]);

        $this->assertTable([
            [ 'test' => [ [ 'region_id' => $region->id, 'subregion_id' => $subregion->id ], Regionality::nullablePair() ], 'fails' => false ],
            [ 'test' => [ [ 'region_id' => null, 'subregion_id' => null ], Regionality::nullablePair() ], 'fails' => false ],
            [ 'test' => [ [], Regionality::nullablePair() ], 'fails' => false ],
            [ 'test' => [ [ 'region_id' => 10000 ], Regionality::nullablePair() ], 'fails' => true ],
            [ 'test' => [ [ 'subregion_id' => 10000 ], Regionality::nullablePair() ], 'fails' => true ],
        ]);

    }

    public function testPagedTableFragment() {

        // Structure snapshot.
        $rules = PagedTable::rules([ 'community', 'description' ]);

        $this->assertSame([ 'draw', 'start', 'length', 'order_direction', 'order_column' ], array_keys($rules));
        $this->assertSame([ 'integer' ], $rules['draw']);
        $this->assertSame([ 'required', 'min:0' ], $rules['start']);
        $this->assertSame([ 'required', 'min:0' ], $rules['length']);

        $relaxed = PagedTable::rules([], false);

        $this->assertSame([ 'min:0' ], $relaxed['start']);
        $this->assertSame([ 'min:0' ], $relaxed['length']);

        $valid = [ 'draw' => 1, 'start' => 0, 'length' => 25, 'order_direction' => 'asc', 'order_column' => 'community' ];

        $this->assertTable([
            [ 'test' => [ $valid, PagedTable::rules([ 'community', 'description' ]) ], 'fails' => false ],
            [ 'test' => [ [ ...$valid, 'order_direction' => 'up' ], PagedTable::rules([ 'community', 'description' ]) ], 'fails' => true ],
            [ 'test' => [ [ ...$valid, 'order_column' => 'other' ], PagedTable::rules([ 'community', 'description' ]) ], 'fails' => true ],
            [ 'test' => [ [ 'draw' => 1 ], PagedTable::rules([ 'community', 'description' ]) ], 'fails' => true ],
            [ 'test' => [ [ 'draw' => 1 ], PagedTable::rules([ 'community', 'description' ], false) ], 'fails' => false ],
        ]);

    }

}
