<?php

namespace Tests\Feature;

use App\Models\ApiScope;
use App\Models\ApiToken;
use App\Models\ApiTokenPermission;
use App\Models\ApiTokenWhitelist;
use App\Models\Group;
use App\Models\Organisation;
use App\Models\Suborganisation;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class TokenModuleTest extends TestCase
{

    use RefreshDatabase;

    private $organisations = [];
    private $users = [];
    private $keys = [];
    private $scopes = [];

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

        // Setup two top level organisations and two suborganisations.
        $this->organisations = [
            'A' => Organisation::create([ 'id' => 1, 'name' => 'A' ]),
            'B' => Organisation::create([ 'id' => 2, 'name' => 'B' ]),
            'C' => Organisation::create([ 'id' => 3, 'name' => 'C' ]),
            'D' => Organisation::create([ 'id' => 4, 'name' => 'D' ]),
            'E' => Organisation::create([ 'id' => 5, 'name' => 'E' ])
        ];

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

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

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

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

        $this->keys = [
            'A' => ApiToken::create([ 'name' => 'A', 'identifier' => 'A', 'token' => 'A', 'active' => 1, 'organisation_id' => $this->organisations['A']->id ]),
            'B' => ApiToken::create([ 'name' => 'B', 'identifier' => 'B', 'token' => 'B', 'active' => 0, 'organisation_id' => $this->organisations['A']->id ]),
            'C' => ApiToken::create([ 'name' => 'C', 'identifier' => 'C', 'token' => 'C', 'active' => 1, 'organisation_id' => $this->organisations['A']->id ]),
            'D' => ApiToken::create([ 'name' => 'D', 'identifier' => 'D', 'token' => 'D', 'active' => 0, 'organisation_id' => $this->organisations['A']->id ]),
            'E' => ApiToken::create([ 'name' => 'E', 'identifier' => 'E', 'token' => 'E', 'active' => 1, 'organisation_id' => $this->organisations['A']->id ]),
            'F' => ApiToken::create([ 'name' => 'F', 'identifier' => 'F', 'token' => 'F', 'active' => 0, 'organisation_id' => $this->organisations['A']->id ])
        ];

        ApiScope::whereRaw('0 = 0')->delete();

        $this->scopes = [
            'A' => ApiScope::create([ 'name' => 'Scope A' ]),
            'B' => ApiScope::create([ 'name' => 'Scope B' ]),
            'C' => ApiScope::create([ 'name' => 'Scope C' ])
        ];

        ApiTokenPermission::create([ 'api_token_id' => $this->keys['A']->id, 'api_scopes_id' => $this->scopes['A']->id ]);
        ApiTokenPermission::create([ 'api_token_id' => $this->keys['A']->id, 'api_scopes_id' => $this->scopes['B']->id ]);
        ApiTokenPermission::create([ 'api_token_id' => $this->keys['A']->id, 'api_scopes_id' => $this->scopes['C']->id ]);

        ApiTokenPermission::create([ 'api_token_id' => $this->keys['C']->id, 'api_scopes_id' => $this->scopes['A']->id ]);
        ApiTokenPermission::create([ 'api_token_id' => $this->keys['C']->id, 'api_scopes_id' => $this->scopes['C']->id ]);

        ApiTokenWhitelist::create([ 'address' => '127.0.0.1', 'token_id' => $this->keys['A']->id, 'organisation_id' => $this->organisations['A']->id ]);
        ApiTokenWhitelist::create([ 'address' => '127.0.0.1', 'token_id' => $this->keys['F']->id, 'organisation_id' => $this->organisations['A']->id ]);

    }

    public function testGetTokens(): void
    {
        $scenario_1 = $this->actingAs($this->users['A'])->withoutMiddleware()->get('/api/token?draw=0&start=0&length=50');

        $this->assertTrue($scenario_1->status() === 200, 'Scenario 1 should have returned a 200 status code.');

        foreach($scenario_1['data'] as $row) {

            $model = ApiToken::find($row['id']);

            foreach([ 'id', 'name', 'identifier', 'active' ] as $col) {
                $this->assertEquals($model->$col, $row[$col], "The $col for the $model->name key is wrong.");
            }

            $row_scopes = $row['scopes'] === null ? [] : explode(',', $row['scopes']);

            $model_scopes = ApiTokenPermission::join('api_scopes', 'api_scopes.id', '=', 'api_token_permissions.api_scopes_id')
                ->where('api_token_id', '=', $row['id'])
                ->whereIn('api_scopes.name', $row_scopes)
                ->get();

            $this->assertTrue( sizeof($row_scopes) === sizeof($model_scopes), "The scopes need to be aligned for $model->name" );

            $row_whitelisted = $row['whitelisted'] === null ? [] : explode(',', $row['whitelisted']);

            $model_whitelisted = ApiTokenWhitelist::where('token_id', '=', $row['id'])
                ->whereIn('address', $row_whitelisted)
                ->get();

            $this->assertTrue( sizeof($row_whitelisted) === sizeof($model_whitelisted), "The whitelist needs to be aligned for $model->name" );

        }

    }

    public function testGetAllScopes(): void
    {
        $scenario_1 = $this->actingAs($this->users['A'])->withoutMiddleware()->get('/api/token/scope?draw=0&start=0&length=50');

        $this->assertTrue($scenario_1->status() === 200, 'Scenario 1 should have returned a 200 status code.');

        $this->assertCount(sizeof($this->scopes), ApiScope::all(), 'Scenario 1 should have returned all the valid scopes.');

    }

    public function testGetTokenDetails(): void
    {

        $scenario_1 = $this->actingAs($this->users['A'])->withoutMiddleware()->get('/api/token/' . $this->keys['C']->id);

        $this->assertTrue($scenario_1->status() === 200, 'Scenario 1 should have returned a 200 status code.');

        // As I've learned assertEquals violates the principle of least surprise - In PHP, if I compare two associative arrays for equality, it'll only be true by luck - But as it turns out, PHPUnit actually checks for equivalence between arrays.
        $this->assertEquals([
            'name' => 'C',
            'all_scopes' => [ ['id' => $this->scopes['A']->id, 'name' => 'Scope A'], ['id' => $this->scopes['B']->id, 'name' => 'Scope B'], ['id' => $this->scopes['C']->id, 'name' => 'Scope C'] ],
            'used_scopes' => [ $this->scopes['A']->id, $this->scopes['C']->id ],
            'whitelist' => []
        ], $scenario_1->json(), 'equals');

        $scenario_2 = $this->actingAs($this->users['A'])->withoutMiddleware()->get('/api/token/' . $this->keys['A']->id);

        $this->assertTrue($scenario_2->status() === 200, 'Scenario 2 should have returned a 200 status code.');

        // As I've learned assertEquals violates the principle of least surprise - In PHP, if I compare two associative arrays for equality, it'll only be true by luck - But as it turns out, PHPUnit actually checks for equivalence between arrays.
        $this->assertEquals([
            'name' => 'A',
            'all_scopes' => [ ['id' => $this->scopes['A']->id, 'name' => 'Scope A'], ['id' => $this->scopes['B']->id, 'name' => 'Scope B'], ['id' => $this->scopes['C']->id, 'name' => 'Scope C'] ],
            'used_scopes' => [ $this->scopes['A']->id, $this->scopes['B']->id, $this->scopes['C']->id ],
            'whitelist' => ['127.0.0.1']
        ], $scenario_2->json());

        $scenario_2 = $this->actingAs($this->users['A'])->withoutMiddleware()->get('/api/token/50000000');

        $this->assertTrue($scenario_2->status() === 500, 'Scenario 3 should have returned a 500 status code.');

    }

    public function testPostToken(): void
    {

        $scenario_1 = $this->actingAs($this->users['A'])->withoutMiddleware()->post('/api/token/', [
            'name' => 'Post Token',
            'active' => true,
            'whitelist' => [ '206.148.22.197' ],
            'scope' => [ $this->scopes['B']->id ]
        ]);

        $this->assertTrue($scenario_1->status() === 200, 'Scenario 1 should have returned a 200 status code.');
        
        $token = $this->actingAs($this->users['A'])->withoutMiddleware()->get('/api/token/' . ApiToken::where('name', '=', 'Post Token')->get()->first()->id)->json();

        $this->assertEquals([
            'name' => 'Post Token',
            'all_scopes' => [ ['id' => $this->scopes['A']->id, 'name' => 'Scope A'], ['id' => $this->scopes['B']->id, 'name' => 'Scope B'], ['id' => $this->scopes['C']->id, 'name' => 'Scope C'] ],
            'used_scopes' => [ $this->scopes['B']->id ],
            'whitelist' => ['206.148.22.197']
        ], $token);

        $scenario_2 = $this->actingAs($this->users['A'])->withoutMiddleware()->post('/api/token/', [
            'name' => 'Post Token',
            'active' => true,
            'whitelist' => [],
            'scope' => [ $this->scopes['B']->id ]
        ]);

        $this->assertTrue($scenario_2->status() === 400, 'Scenario 2 should have returned a 400 status code.');

        $scenario_3 = $this->actingAs($this->users['A'])->withoutMiddleware()->post('/api/token/', [
            'name' => 'Post Token',
            'active' => true,
            'whitelist' => ['206.148.22.197'],
            'scope' => []
        ]);

        $this->assertTrue($scenario_3->status() === 400, 'Scenario 3 should have returned a 400 status code.');

    }

    public function testPatchToken(): void
    {

        $scenario_1 = $this->actingAs($this->users['A'])->withoutMiddleware()->patch('/api/token/' . $this->keys['A']->id, [
            'name' => 'Post Token',
            'whitelist' => [ '206.148.22.197' ],
            'scope' => [ $this->scopes['B']->id ]
        ]);

        $this->assertTrue($scenario_1->status() === 200, 'Scenario 1 should have returned a 200 status code.');
        
        $token = $this->actingAs($this->users['A'])->withoutMiddleware()->get('/api/token/' . $this->keys['A']->id)->json();

        $this->assertEquals([
            'name' => 'Post Token',
            'all_scopes' => [ ['id' => $this->scopes['A']->id, 'name' => 'Scope A'], ['id' => $this->scopes['B']->id, 'name' => 'Scope B'], ['id' => $this->scopes['C']->id, 'name' => 'Scope C'] ],
            'used_scopes' => [ $this->scopes['B']->id ],
            'whitelist' => ['206.148.22.197']
        ], $token);

        $scenario_2 = $this->actingAs($this->users['A'])->withoutMiddleware()->patch('/api/token/' . $this->keys['A']->id, [
            'name' => 'Post Token',
            'whitelist' => [],
            'scope' => [ $this->scopes['B']->id ]
        ]);

        $this->assertTrue($scenario_2->status() === 400, 'Scenario 2 should have returned a 400 status code.');

        $scenario_3 = $this->actingAs($this->users['A'])->withoutMiddleware()->patch('/api/token/' . $this->keys['A']->id, [
            'name' => 'Post Token',
            'whitelist' => ['206.148.22.197'],
            'scope' => []
        ]);

        $this->assertTrue($scenario_3->status() === 400, 'Scenario 3 should have returned a 400 status code.');

    }

    public function testDeleteToken(): void
    {

        $scenario_1 = $this->actingAs($this->users['A'])->withoutMiddleware()->delete('/api/token/' . $this->keys['A']->id);

        $this->assertTrue($scenario_1->status() === 200, 'Scenario 1 should have returned a 200 status code.');

        $token_status = $this->actingAs($this->users['A'])->withoutMiddleware()->get('/api/token/' . $this->keys['A']->id)->status();

        $this->assertTrue($token_status === 500, 'Scenario 1 should have resulted in the token being irretrievable');

    }

    public function testToggleToken(): void
    {

        $scenario_1 = $this->actingAs($this->users['A'])->withoutMiddleware()->patch('/api/token/' . $this->keys['A']->id . '/toggle', [
            'activate' => false
        ]);

        $this->assertTrue($scenario_1->status() === 200, 'Scenario 1 should have returned a 200 status code.');

        $this->assertTrue(ApiToken::find($this->keys['A']->id)->active == false, 'The token should not be active for scenario 1.');

        $scenario_2 = $this->actingAs($this->users['A'])->withoutMiddleware()->patch('/api/token/' . $this->keys['A']->id . '/toggle', [
            'activate' => true
        ]);

        $this->assertTrue($scenario_2->status() === 200, 'Scenario 2 should have returned a 200 status code.');

        $this->assertTrue(ApiToken::find($this->keys['A']->id)->active == true, 'The token should not be active for scenario 2.');

    }


}
