<?php

namespace Tests\Feature;

use App\Models\Asns;
use App\Models\Goliath\GoliathAppliance;
use App\Models\Goliath\GoliathAttack;
use App\Models\Goliath\GoliathMetro;
use App\Models\Goliath\GoliathRegion;
use App\Models\Goliath\GoliathSubregion;
use App\Models\Goliath\ReportAwareness;
use App\Models\Group;
use App\Models\Organisation;
use App\Models\Prefixes;
use App\Models\ReportOrganisationAttribute;
use App\Models\Suborganisation;
use App\Models\User;
use App\Repositories\ReportRepository;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Redis;
use Tests\TestCase;

class ReportingV2Test extends TestCase
{

    use RefreshDatabase;

    private $organisations = [];
    private $users = [];
    private $asns = [];
    private $appliance = null;

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

        $this->organisations = [
            '1' => Organisation::create([ 'id' => 1, 'name' => 'Organisation #1' ]),
            '2' => Organisation::create([ 'id' => 2, 'name' => 'Organisation #2' ]),
            '3' => Organisation::create([ 'id' => 3, 'name' => 'Organisation #3' ]),
            '4' => Organisation::create([ 'id' => 4, 'name' => 'Organisation #4' ])  // This should always be a subtenant on Organisation #1
        ];

        ReportOrganisationAttribute::create([
            'organisation_id'           => $this->organisations['1']->id,
            'send_start'                => true,
            'send_finish'               => true,
            'send_webhook'              => true,
            'send_individual'           => true,
            'send_carpet'               => true,
            'send_daily'                => true,
            'send_weekly'               => true,
            'send_monthly'              => true,
            'send_quarterly'            => true,
            'minimum_threshold_gbps'    => 6,
            'minimum_threshold_mpps'    => 6
        ]);

        ReportOrganisationAttribute::create([
            'organisation_id'           => $this->organisations['2']->id,
            'send_start'                => true,
            'send_finish'               => true,
            'send_webhook'              => true,
            'send_individual'           => true,
            'send_carpet'               => true,
            'send_daily'                => true,
            'send_weekly'               => true,
            'send_monthly'              => true,
            'send_quarterly'            => true,
            'minimum_threshold_gbps'    => 6,
            'minimum_threshold_mpps'    => 6
        ]);

        ReportOrganisationAttribute::create([
            'organisation_id'           => $this->organisations['3']->id,
            'send_start'                => true,
            'send_finish'               => true,
            'send_webhook'              => true,
            'send_individual'           => true,
            'send_carpet'               => true,
            'send_daily'                => true,
            'send_weekly'               => true,
            'send_monthly'              => true,
            'send_quarterly'            => true,
            'minimum_threshold_gbps'    => 6,
            'minimum_threshold_mpps'    => 6
        ]);

        ReportOrganisationAttribute::create([
            'organisation_id'           => $this->organisations['4']->id,
            'send_start'                => true,
            'send_finish'               => true,
            'send_webhook'              => true,
            'send_individual'           => true,
            'send_carpet'               => true,
            'send_daily'                => true,
            'send_weekly'               => true,
            'send_monthly'              => true,
            'send_quarterly'            => true,
            'minimum_threshold_gbps'    => 6,
            'minimum_threshold_mpps'    => 6
        ]);

        Suborganisation::insert([
            [ 'parent_id' => 1 , 'child_id' => 4 ]
        ]);

        $this->asns = [
            '1' => Asns::create([ 'asn' => 64512, 'organisation_id' => $this->organisations['1']->id, 'approved' => 1 ]),
            '2' => Asns::create([ 'asn' => 64513, 'organisation_id' => $this->organisations['2']->id, 'approved' => 1 ]),
            '3' => Asns::create([ 'asn' => 64514, 'organisation_id' => $this->organisations['3']->id, 'approved' => 1 ]),
            '4' => Asns::create([ 'asn' => 64515, 'organisation_id' => $this->organisations['4']->id, 'approved' => 1 ])
        ];

        // Setup users
        $group = Group::create([ 'name' => 'User' ]);

        $default_user = [
            'phone'             => null,
            'password'          => '',
            'group_id'          => $group->id,
        ];

        $this->users = [
            '1' => User::create([ ...$default_user, 'organisation_id' => $this->organisations['1']->id, 'name' => 'User #1', 'email' => 'creatia+1@globalsecurelayer.com' ]),
            '2' => User::create([ ...$default_user, 'organisation_id' => $this->organisations['2']->id, 'name' => 'User #2', 'email' => 'creatia+2@globalsecurelayer.com' ]),
            '3' => User::create([ ...$default_user, 'organisation_id' => $this->organisations['3']->id, 'name' => 'User #3', 'email' => 'creatia+3@globalsecurelayer.com' ]),
            '4' => User::create([ ...$default_user, 'organisation_id' => $this->organisations['4']->id, 'name' => 'User #4', 'email' => 'creatia+4@globalsecurelayer.com' ])
        ];

        // Clear things up in Redis
        $keys = [
            "report-singular-automation-list","report-singular-automation-1-list","report-singular-automation-2-list","report-singular-automation-3-list","report-singular-automation-4-list","report-singular-admin-list","report-singular-admin-list-start_at-asc","report-singular-admin-list-gbps-asc","report-singular-admin-list-mpps-asc","report-singular-admin-list-start_at-desc","report-singular-admin-list-gbps-desc","report-singular-admin-list-mpps-desc","report-singular-1-list","report-singular-2-list","report-singular-3-list","report-singular-4-list","report-1-10","report-1-11","report-1-12","report-1-13","report-2-20","report-2-21","report-2-22","report-2-23","report-3-30","report-3-31","report-3-32","report-3-33","report-4-40","report-4-41"
        ];

        foreach($keys as $key) {
            Redis::del($key);
        }

        // Setup regions and appliance
        $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, 'region_id' => $region->id, 'subregion_id' => $subregion->id ]);
        
        $this->appliance = GoliathAppliance::create([ 'name' => '1', 'organisation_id' => $this->organisations['1']->id, 'registered_at' => Carbon::now(), 'region_id' => $region->id, 'subregion_id' => $subregion->id, 'metro_id' => $metro->id, 'authorised' => 1, 'address' => '126.0.0.1' ]);
        
        // Create prefixes that are relevant for tests
        Prefixes::factory()->address('10.0.0.0', 29)->ownership($this->organisations['1'], $this->asns['1'])->subtenant($this->organisations['4'], Carbon::now()->subDay())->create(); // 10.0.0.0 - 10.0.0.7
        Prefixes::factory()->address('10.0.1.0', 29)->ownership($this->organisations['2'], $this->asns['2'])->create(); // 10.0.1.0 - 10.0.1.7
        Prefixes::factory()->address('10.0.3.0', 29)->ownership($this->organisations['3'], $this->asns['3'])->create(); // 10.0.2.0 - 10.0.2.7
        Prefixes::factory()->address('10.0.4.0', 29)->ownership($this->organisations['4'], $this->asns['4'])->create(); // 10.0.3.0 - 10.0.3.7

        GoliathAttack::factory()->id(10)->address('10.0.0.1')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(5))->appliance($this->appliance)->collected()->create();
        GoliathAttack::factory()->id(11)->address('10.0.0.2')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(1))->appliance($this->appliance)->create();
        GoliathAttack::factory()->id(12)->address('10.0.0.3')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(0))->appliance($this->appliance)->create();
        GoliathAttack::factory()->id(13)->address('10.0.0.4')->start(Carbon::now()->subMinutes(40))->finish(Carbon::now()->subMinutes(10))->appliance($this->appliance)->collected()->create();
        GoliathAttack::factory()->id(14)->address('10.0.0.10')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(10))->appliance($this->appliance)->collected()->create();

        GoliathAttack::factory()->id(20)->address('10.0.1.1')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(5))->appliance($this->appliance)->collected()->create();
        GoliathAttack::factory()->id(21)->address('10.0.1.2')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(1))->appliance($this->appliance)->create();
        GoliathAttack::factory()->id(22)->address('10.0.1.3')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(0))->appliance($this->appliance)->create();
        GoliathAttack::factory()->id(23)->address('10.0.1.4')->start(Carbon::now()->subMinutes(40))->finish(Carbon::now()->subMinutes(10))->appliance($this->appliance)->collected()->create();

        GoliathAttack::factory()->id(30)->address('10.0.3.1')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(5))->appliance($this->appliance)->collected()->create();
        GoliathAttack::factory()->id(31)->address('10.0.3.2')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(1))->appliance($this->appliance)->create();
        GoliathAttack::factory()->id(32)->address('10.0.3.3')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(2))->appliance($this->appliance)->collected()->create();
        GoliathAttack::factory()->id(33)->address('10.0.3.4')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(5))->appliance($this->appliance)->create();

        GoliathAttack::factory()->id(40)->address('10.0.4.1')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(5))->appliance($this->appliance)->collected()->create();
        GoliathAttack::factory()->id(41)->address('10.0.4.2')->start(Carbon::now()->subMinutes(10))->finish(Carbon::now()->subMinutes(1))->appliance($this->appliance)->create();

    }

    /**
     *  Automation Tests:
     *      - Destination Table Caching
     *      - Destination Report Caching
     *      - Attack Start Tracking
     *      - Attack Completion Tracking
     */

    public function testDestinationTableCache(): void
    {
        /**
         * Scenarios:
         *      - Test that the admin table generates, and all sorting configurations generate as well.
         *      - Each organisation has the appropriate table.
         */

        $admin_hash = md5(json_encode(json_decode(file_get_contents(app_path("Development/Reporting/Tables/OrganisationReports-Admin.json")), true)));
        $organisation_1_hash = md5(json_encode(json_decode(file_get_contents(app_path("Development/Reporting/Tables/OrganisationReports-1.json")), true)));
        $organisation_2_hash = md5(json_encode(json_decode(file_get_contents(app_path("Development/Reporting/Tables/OrganisationReports-2.json")), true)));
        $organisation_3_hash = md5(json_encode(json_decode(file_get_contents(app_path("Development/Reporting/Tables/OrganisationReports-3.json")), true)));
        $organisation_4_hash = md5(json_encode(json_decode(file_get_contents(app_path("Development/Reporting/Tables/OrganisationReports-4.json")), true)));

        $hashes = [
            "report-singular-admin-list"                    => $admin_hash,
            "report-singular-admin-list-start_at-asc"       => $admin_hash,
            "report-singular-admin-list-gbps-asc"           => $admin_hash,
            "report-singular-admin-list-mpps-asc"           => $admin_hash,
            "report-singular-admin-list-start_at-desc"      => $admin_hash,
            "report-singular-admin-list-gbps-desc"          => $admin_hash,
            "report-singular-admin-list-mpps-desc"          => $admin_hash,
            "report-singular-1-list"                        => $organisation_1_hash,
            "report-singular-2-list"                        => $organisation_2_hash,
            "report-singular-3-list"                        => $organisation_3_hash,
            "report-singular-4-list"                        => $organisation_4_hash
        ];

        $keys = [
            "report-singular-admin-list",
            "report-singular-admin-list-start_at-asc",
            "report-singular-admin-list-gbps-asc",
            "report-singular-admin-list-mpps-asc",
            "report-singular-admin-list-start_at-desc",
            "report-singular-admin-list-gbps-desc",
            "report-singular-admin-list-mpps-desc",
            "report-singular-1-list",
            "report-singular-2-list",
            "report-singular-3-list",
            "report-singular-4-list"
        ];

        foreach($keys as $key) {
            Redis::del($key);
        }

        $this->artisan('report:cache-destination-table');

        foreach($keys as $key) {
            $this->assertEquals($hashes[$key], md5(Redis::get($key)), "`$key` does not have matching hashes.");
        }

    }

    public function testDestinationReportCache(): void
    {
        /**
         * Scenarios:
         *      - Test that the reports all generate to the appropriate organisation.
         */

        $this->testDestinationTableCache();

        $organisation_1_hash = md5(file_get_contents(app_path("Development/Reporting/Reports/OrganisationReport-1.pdf")));
        $organisation_2_hash = md5(file_get_contents(app_path("Development/Reporting/Reports/OrganisationReport-2.pdf")));
        $organisation_3_hash = md5(file_get_contents(app_path("Development/Reporting/Reports/OrganisationReport-3.pdf")));

        $hashes = [
            "report-1-10"   => $organisation_1_hash,
            "report-1-11"   => $organisation_1_hash,
            "report-1-12"   => $organisation_1_hash,
            "report-1-13"   => $organisation_1_hash,
            "report-2-20"   => $organisation_2_hash,
            "report-2-21"   => $organisation_2_hash,
            "report-2-22"   => $organisation_2_hash,
            "report-2-23"   => $organisation_2_hash,
            "report-3-30"   => $organisation_3_hash,
            "report-3-31"   => $organisation_3_hash,
            "report-3-32"   => $organisation_3_hash,
            "report-3-33"   => $organisation_3_hash,
            "report-4-40"   => null,
            "report-4-41"   => null
        ];

        foreach(array_keys($hashes) as $key) {
            Redis::del($key);
        }

        $this->artisan('report:cache-destination-report');

        foreach(array_keys($hashes) as $key) {

            if($hashes[$key] === null) {
                $this->assertEquals($hashes[$key], null, "`$key` should be null.");                
            }
            else {
                $this->assertEquals($hashes[$key], md5(base64_decode(gzinflate(Redis::get($key)))), "`$key` does not have matching hashes.");
            }
        }
    }

    public function testAttackTrackStart(): void
    {

        /**
         * The following aspects of this need to be tested:
         *      - Awareness is only ever tracked in such a way that an organisation can only ever have awareness generated for it's own attacks.
         *      - Awareness will never be generated a second time. 
         *      - Awareness contains accurate metrics.
         * 
         * This will involve running the caching process first, as everything should fail without a cached report being in place.
         * 
         * Once this is done, we will then need to test the following across each organisation:
         *      - #1: 10.0.0.x
         *          + 1: Enough GBPS, MPPS | Collected | Recent - Succeed
         *          + 2: Enough GBPS, MPPS | Uncollected | Recent - Fail
         *          + 3: Small | Uncollected | Recent - Fail
         *          + 4: Enough MPPS | Collected | Stale - Fail 
         *          + 5: Enough MPPS | Collected | Recent - Fail as it's not a valid owned prefix
         *      - #2: 10.0.1.x
         *          + 1: Enough MPPS | Collected | Recent - Succeed
         *          + 2: Enough MPPS | Uncollected | Recent - Fail
         *          + 3: Small | Uncollected | Recent - Fail
         *          + 4: Enough GBPS | Collected | Stale - Fail
         *      - #3: 10.0.3.x
         *          + 1: Empty - Failure
         *          + 2: Error - Fail
         *          + 3: Enough GBPS | Collected | Recent - Succeed
         *          + 4: Enough GBPS | Uncollected | Recent - Fail
         *      - #4: 10.0.4.x
         *          + 1: Enough GBPS, MPPS | Collected | Recent - Succeed
         *          + 2: Enough GBPS, MPPS | Uncollected | Recent - Fail
         *          + All the successful conditions for #1
         * 
         *          (Enough is defined as > 10)
         * 
         * After all of this, it should be verified that:
         *      - #1 has exactly 1 awareness entry for 10.0.0.1
         *      - #2 has exactly 1 awareness entry for 10.0.1.1
         *      - #3 has exactly 1 awareness entry for 10.0.3.3
         *      - #4 has exactly 2 awareness entries for 10.0.0.1, 10.0.4.1
         * 
         * Then it should be checked that notifications don't get made a second time.
         */

        $this->artisan('goliath:monitor-attacks', ['--thread-pool' => 1, '--minutes' => 1]);

        /**
         * Check that nothing is generated when nothing is yet cached.
         */

        $this->artisan('attack-track:start');

        $this->assertEquals(0, ReportAwareness::count(), 'Nothing should be tracked yet when there is nothing cached.');

        /**
         * Seed the automation list the tracker reads directly, in the shape attack-track:start consumes.
         * Finishes are recent so they clear the 15-minute recency gate, except 10.0.0.4 / 10.0.1.4 which
         * are deliberately stale so that gate drops them. Addresses line up with the notification mock.
         */
        $recent = static fn() => Carbon::now()->subMinutes(5);
        $stale  = static fn() => Carbon::now()->subMinutes(50);
        $attack = static fn($report_id, $destination, $subtenants, $finish) => [
            'report_id'   => $report_id,
            'destination' => $destination,
            'subtenants'  => $subtenants,
            'completed'   => 1,
            'start_at'    => $finish->copy()->subMinutes(10),
            'finish_at'   => $finish,
            'gbps'        => 1,
            'mpps'        => 1,
        ];

        Redis::set('report-singular-automation-list', json_encode([ 'data' => [
            $attack(10, '10.0.0.1',  [1, 4], $recent()),
            $attack(11, '10.0.0.2',  [1, 4], $recent()),
            $attack(12, '10.0.0.3',  [1, 4], $recent()),
            $attack(13, '10.0.0.4',  [1, 4], $stale()),
            $attack(14, '10.0.0.10', [1, 4], $recent()),
            $attack(20, '10.0.1.1',  [2],    $recent()),
            $attack(21, '10.0.1.2',  [2],    $recent()),
            $attack(22, '10.0.1.3',  [2],    $recent()),
            $attack(23, '10.0.1.4',  [2],    $stale()),
            $attack(30, '10.0.3.1',  [3],    $recent()),
            $attack(31, '10.0.3.2',  [3],    $recent()),
            $attack(32, '10.0.3.3',  [3],    $recent()),
            $attack(33, '10.0.3.4',  [3],    $recent()),
            $attack(40, '10.0.4.1',  [4],    $recent()),
            $attack(41, '10.0.4.2',  [4],    $recent()),
        ] ]));

        /**
         * Begin testing.
         */
        $this->artisan('attack-track:start');

        $this->assertEquals(5, ReportAwareness::count(), 'There should only be 5 entries for the report awareness being tracked.');

        $this->assertEquals(1, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['1']->id ] ])->count(), 'There should be only 1 entry for Organisation #1.');
        $this->assertEquals(1, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['2']->id ] ])->count(), 'There should be only 1 entry for Organisation #2.');
        $this->assertEquals(1, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['3']->id ] ])->count(), 'There should be only 1 entry for Organisation #3.');
        $this->assertEquals(2, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['4']->id ] ])->count(), 'There should be only 1 entry for Organisation #4.');

        $this->assertEquals(1, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['1']->id ], [ 'address', '=', '10.0.0.1' ] ])->count(), 'There should be `10.0.0.1` for Organisation #1.');
        $this->assertEquals(1, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['2']->id ], [ 'address', '=', '10.0.1.1' ] ])->count(), 'There should be `10.0.1.1` for Organisation #2.');
        $this->assertEquals(1, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['3']->id ], [ 'address', '=', '10.0.3.3' ] ])->count(), 'There should be `10.0.3.3` for Organisation #3.');
        $this->assertEquals(1, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['4']->id ], [ 'address', '=', '10.0.0.1' ] ])->count(), 'There should be `10.0.0.1` for Organisation #4.');
        $this->assertEquals(1, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['4']->id ], [ 'address', '=', '10.0.4.1' ] ])->count(), 'There should be `10.0.4.1` for Organisation #4.');

        $this->assertEquals(0, ReportAwareness::where([ [ 'address', '=', '10.0.0.10' ] ])->count(), 'There should never be a `10.0.0.10` report.');

        $this->artisan('attack-track:start');

        $this->assertEquals(5, ReportAwareness::count(), 'The report entries should not change after a subsequent run.');

    }

    public function testAttackTrackFinish(): void
    {
        /**
         * The following aspects of this need to be tested:
         *      - Awareness is only ever tracked in such a way that an organisation can only ever have awareness generated for it's own attacks.
         *      - Awareness will never be generated a second time. 
         *      - Awareness contains accurate metrics.
         *      - Awareness correctly generates for all start notifications.
         * 
         * This will involve running the caching process first, as everything should fail without a cached report being in place.
         * 
         * We can specifically just test forward from some start awareness entries we should manually craft. We need to test the following scenarios:
         *      - 1: There is a start awareness report, and the cached automation table should have it marked as complete, finished 10 minutes ago. Organisation #1, #4. Generates 2.
         *      - 2: There is a start awareness report, and the cached automation table should have it marked as complete, finished 50 minutes ago. Organisation #1, #4. Generates 2.
         *      - 3: There is a start awareness report, and the cached automation table should have it marked as not complete, finished 10 minutes ago. Organisation #2. Fails.
         *      - 4: There is a start awareness report, and the cached automation table should have it marked as not complete, finished 50 minutes ago. Organisation #2. Generates 1.
         * 
         *      - 5: There is not a start awareness report, and the cached automation table should have it marked as complete, finished 10 minutes ago. Organisation #2. Fails.
         *      - 6: There is not a start awareness report, and the cached automation table should have it marked as complete, finished 50 minutes ago. Organisation #2. Fails.
         *      - 7: There is not a start awareness report, and the cached automation table should have it marked as not complete, finished 10 minutes ago. Organisation #3. Fails.
         *      - 8: There is not a start awareness report, and the cached automation table should have it marked as not complete, finished 50 minutes ago. Organisation #3. Fails.
         * 
         *      - 9: There is already a start and finish awareness entry, and the cached automation table has it marked as complete and finished 10 minutes ago. Organisation #1. Nothing happens.
         *      - 10: There is already a start and finish awareness entry, and the cached automation table has it marked as not complete and finished 50 minutes ago. Organisation #1. Nothing happens.
         * 
         */

        // Cache reports
        Redis::setex('report-singular-automation-list', 172800, json_encode((new ReportRepository())->get_list_automation_singular(2, [], 0, 500, 'start_at', 'DESC')));

        // Setup start awareness entries.
        foreach([
            // #1
            [ Carbon::now()->subMinutes(10), 10, 1, '10.0.0.1' ],
            [ Carbon::now()->subMinutes(10), 10, 4, '10.0.0.1' ],
            // #2
            [ Carbon::now()->subMinutes(50), 11, 1, '10.0.0.2' ],
            [ Carbon::now()->subMinutes(50), 11, 4, '10.0.0.2' ],
            // # 3
            [ Carbon::now()->subMinutes(10), 20, 2, '10.0.1.1' ],
            // #4
            [ Carbon::now()->subMinutes(50), 21, 2, '10.0.1.2' ],
            // #9, 10
            [ Carbon::now()->subMinutes(10), 15, 1, '10.0.0.1' ],
            [ Carbon::now()->subMinutes(10), 16, 1, '10.0.0.2' ],
        ] as $x) {
            ReportAwareness::insert([
                'created_at'      => Carbon::now(),
                'updated_at'      => Carbon::now(),
                'detected_at'     => $x[0]->subMinute(10),
                'type'            => 'start',
                'status'          => 'pending',
                'report_id'       => $x[1],
                'organisation_id' => $x[2],
                'gbps'            => 1,
                'mpps'            => 0.1,
                'address'         => $x[3]
            ]);
        }

        // Setup finish awareness entries
        foreach([
            [ Carbon::now()->subMinutes(10), 15, 1, '10.0.0.1' ], // #1
            [ Carbon::now()->subMinutes(50), 16, 1, '10.0.0.2' ] // #2
        ] as $x) {
            ReportAwareness::insert([
                'created_at'      => Carbon::now(),
                'updated_at'      => Carbon::now(),
                'detected_at'     => $x[0],
                'type'            => 'finish',
                'status'          => 'pending',
                'report_id'       => $x[1],
                'organisation_id' => $x[2],
                'gbps'            => 1,
                'mpps'            => 0.1,
                'address'         => $x[3]
            ]);
        }

        // Run the actual command.
        $this->artisan('attack-track:completion');

        $this->assertEquals(7, ReportAwareness::where('type', '=', 'finish')->count(), 'There should only be 7 entries for the report awareness being tracked - Two of which already existed.');

        $this->assertEquals(2, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['1']->id ], [ 'type', '=', 'finish' ] ])->whereNotIn('report_id', [ 15, 16 ])->count(), 'There should be only 2 entry for Organisation #1.');
        $this->assertEquals(1, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['2']->id ], [ 'type', '=', 'finish' ] ])->count(), 'There should be only 1 entry for Organisation #2.');
        $this->assertEquals(0, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['3']->id ], [ 'type', '=', 'finish' ] ])->count(), 'There should be no entries for Organisation #3.');
        $this->assertEquals(2, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['4']->id ], [ 'type', '=', 'finish' ] ])->count(), 'There should be only 2 entry for Organisation #4.');

        // Run it again to ensure it's idempotent

        $this->artisan('attack-track:completion');

        $this->assertEquals(7, ReportAwareness::where('type', '=', 'finish')->count(), 'There should only be 7 entries for the report awareness being tracked - Two of which already existed.');

        $this->assertEquals(2, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['1']->id ], [ 'type', '=', 'finish' ] ])->whereNotIn('report_id', [ 15, 16 ])->count(), 'There should be only 2 entry for Organisation #1.');
        $this->assertEquals(1, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['2']->id ], [ 'type', '=', 'finish' ] ])->count(), 'There should be only 1 entry for Organisation #2.');
        $this->assertEquals(0, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['3']->id ], [ 'type', '=', 'finish' ] ])->count(), 'There should be no entries for Organisation #3.');
        $this->assertEquals(2, ReportAwareness::where([ [ 'organisation_id', '=', $this->organisations['4']->id ], [ 'type', '=', 'finish' ] ])->count(), 'There should be only 2 entry for Organisation #4.');

    }

    /**
     *  User Tests:
     *      - Retrieve Destination Tables
     *      - Retrieve Destination Reports
     *      - Determine Valid Destination Report Access
     */

    public function testRetrieveDestinationTables(): void
    {
        /**
         * Scenarios - You will need to try this pre-cache and post-cache:
         *      - User 1 accesses Organisation 1 table - Should see table
         *      - User 2 accesses Organisation 2 table - Should see table
         *      - User 1 attempts to inject valid filters
         *      - User 2 attempts to inject invalid filters
         *      - Administrator accesses Admin tables - Should see table
         */

        foreach([ 'PRE-CACHE', 'POST-CACHE' ] as $type) {

            if($type === 'POST-CACHE') {
                $this->testDestinationTableCache();
            }

            // Organisation 1 accesses it's own table
            $scenario_request = $this->actingAs($this->users['1'])->withoutMiddleware()->get('api/goliath/report/single?start=0&length=100');

            $this->assertEquals(200, $scenario_request->status(), "Organisation 1 accessing their own table should result in a HTTP 200 ($type)");
            $this->assertEquals(
                md5(json_encode(json_decode(file_get_contents(app_path("Development/Reporting/Tables/OrganisationReports-1.json")), true)['data'])),
                md5(json_encode($scenario_request->json()['data'], true)),
                "Organisation 1 accessing their own table needs to produce a hash consistent with the stored JSON file. ($type)"
            );

            // Organisation 2 accesses it's own table
            $scenario_request = $this->actingAs($this->users['2'])->withoutMiddleware()->get('api/goliath/report/single?start=0&length=100');

            $this->assertEquals(200, $scenario_request->status(), "Organisation 2 accessing their own table should result in a HTTP 200 ($type)");
            $this->assertEquals(
                md5(json_encode(json_decode(file_get_contents(app_path("Development/Reporting/Tables/OrganisationReports-2.json")), true)['data'])),
                md5(json_encode($scenario_request->json()['data'], true)),
                "Organisation 2 accessing their own table needs to produce a hash consistent with the stored JSON file. ($type)"
            );

            // Organisation 1 injects valid filters
            $scenario_request = $this->actingAs($this->users['1'])->withoutMiddleware()->get('api/goliath/report/single?start=0&length=100&order_column=destination&order_direction=desc&filter[global]=testing');

            $this->assertEquals(200, $scenario_request->status(), "Organisation 1 when injecting valid filters should result in a HTTP 200 ($type)");
            $this->assertEquals(
                md5(json_encode(json_decode(file_get_contents(app_path("Development/Reporting/Tables/OrganisationReports-1.json")), true)['data'])),
                md5(json_encode($scenario_request->json()['data'], true)),
                "Organisation 1 injecting valid filters needs to produce a hash consistent with the stored JSON file. ($type)"
            );

            // Organisation 2 injects valid filters
            $scenario_request = $this->actingAs($this->users['2'])->withoutMiddleware()->get('api/goliath/report/single?start=0&length=100&order_column=destination&order_direction=desc&filter[global]=testing&filter[column][destination]=dldldldldldld');

            $this->assertEquals(302, $scenario_request->status(), 'Organisation 2 when injecting valid filters should result in a HTTP 302 - We are redirecting ($type)');

            // Administrator accesses admin table - Should see it
            $scenario_request = $this->actingAs($this->users['1'])->withoutMiddleware()->get('api/admin/goliath/report/single?start=0&length=100');

            $this->assertEquals(200, $scenario_request->status(), "Admin table should result in a HTTP 200 ($type)");
            $this->assertEquals(
                md5(json_encode(json_decode(file_get_contents(app_path("Development/Reporting/Tables/OrganisationReports-Admin.json")), true)['data'])),
                md5(json_encode($scenario_request->json()['data'], true)),
                "Admin table needs to produce a hash consistent with the stored JSON file. ($type)"
            );
        }

    }

    public function testRetrieveDestinationReports(): void
    {
        /**
         * Scenarios - You will need to try this pre-cache and post-cache:
         *      - User 1 should be able to access report 10 through 13
         *      - User 2 should be able to access report 20 through 23
         *      - User 4 should be able to access report 10 through 13 and 40, 41 (Subtenancy + Own)
         *      - User 1 cannot access reports 20 through 43
         *      - User 4 cannot access reports 20 through 33
         *      - User admin should be able to access any report
         */

        $this->artisan('goliath:monitor-attacks', ['--thread-pool' => 1, '--minutes' => 1]);

        foreach([ 'PRE-CACHE', 'POST-CACHE' ] as $type) {

            if($type === 'POST-CACHE') {
                $this->testDestinationReportCache();
            }

            // User 1 attempts to access reports 10 through 13

            for($i = 10; $i <= 13; ++$i) {

                $scenario_request = $this->actingAs($this->users['1'])->withoutMiddleware()->get("api/goliath/report/single/$i");

                $this->assertEquals(200, $scenario_request->getStatusCode(), "Organisation 1 accessing their own report result in a HTTP 200 ($type)");

                ob_start();
                $scenario_request->baseResponse->sendContent();
                $content = ob_get_clean();

                $this->assertEquals(
                    md5(file_get_contents(app_path("Development/Reporting/Reports/OrganisationReport-1.pdf"))),
                    md5($content),
                    "User 1 accessing their own reports needs to produce a hash consistent with the stored PDF. ($type)"
                );

            }

            // User 2 attempts to access reports 20 through 23

            for($i = 20; $i <= 23; ++$i) {

                $scenario_request = $this->actingAs($this->users['2'])->withoutMiddleware()->get("api/goliath/report/single/$i");

                $this->assertEquals(200, $scenario_request->getStatusCode(), "Organisation 2 accessing their own report result in a HTTP 200 ($type)");

                ob_start();
                $scenario_request->baseResponse->sendContent();
                $content = ob_get_clean();

                $this->assertEquals(
                    md5(file_get_contents(app_path("Development/Reporting/Reports/OrganisationReport-2.pdf"))),
                    md5($content),
                    "User 2 accessing their own reports needs to produce a hash consistent with the stored PDF. ($type)"
                );

            }

            // User 4 attempts to access reports 10-13, 40, 41

            foreach([10, 11, 12, 13, 40, 41] as $i) {

                $scenario_request = $this->actingAs($this->users['4'])->withoutMiddleware()->get("api/goliath/report/single/$i");

                if($i === 40 || $i === 41) {
                    $this->assertEquals(500, $scenario_request->getStatusCode(), "Organisation 4 accessing their own report result in a HTTP 500 as no report can be retrieved. ($type)");
                }
                else {
                    $this->assertEquals(200, $scenario_request->getStatusCode(), "Organisation 4 accessing their subtenanted report result in a HTTP 200 ($type)");

                    ob_start();
                    $scenario_request->baseResponse->sendContent();
                    $content = ob_get_clean();

                    $this->assertEquals(
                        md5(file_get_contents(app_path("Development/Reporting/Reports/OrganisationReport-1.pdf"))),
                        md5($content),
                        "User 4 accessing their own reports or subtenancy needs to produce a hash consistent with the stored PDF. ($type)"
                    );
                }

            }

            // User 1 cannot access reports 20 through 43

            foreach([20, 21, 22, 23, 30, 31, 32, 33, 40, 41] as $i) {

                $scenario_request = $this->actingAs($this->users['1'])->withoutMiddleware()->get("api/goliath/report/single/$i");

                $this->assertEquals(400, $scenario_request->getStatusCode(), "Organisation 1 is able to access inappropriate reports - It should be a HTTP 400 ($type)");

            }

            // User 4 cannot access reports 20 through 33

            foreach([20, 21, 22, 23, 30, 31, 32, 33] as $i) {

                $scenario_request = $this->actingAs($this->users['4'])->withoutMiddleware()->get("api/goliath/report/single/$i");

                $this->assertEquals(400, $scenario_request->getStatusCode(), "Organisation 4 is able to access inappropriate reports - It should be a HTTP 400 ($type)");

            }

            // Admin can access all reports.
            foreach([10, 11, 12, 13, 20, 21, 22, 23, 30, 31, 32, 33, 40, 41] as $i) {

                $scenario_request = $this->actingAs($this->users['4'])->withoutMiddleware()->get("api/admin/goliath/report/single/$i");

                if($i === 40 || $i === 41) {
                    $this->assertEquals(500, $scenario_request->getStatusCode(), "Admin an Organisation 4 report should result in a HTTP 500 as no report can be retrieved. ($type)");
                }
                else {
                    $this->assertEquals(200, $scenario_request->getStatusCode(), "An admin accessing any report should result in a HTTP 200 ($type)");

                    ob_start();
                    $scenario_request->baseResponse->sendContent();
                    $content = ob_get_clean();

                    $report_id = (int)($i / 10);

                    $this->assertEquals(
                        md5(file_get_contents(app_path("Development/Reporting/Reports/OrganisationReport-$report_id.pdf"))),
                        md5($content),
                        "An admin accessing a report subtenancy needs to produce a hash consistent with the stored PDF. ($type)"
                    );
                }

            }


        }

    }

}
