<?php

declare(strict_types=1);

namespace NoahVet\Reef\Test\A_Unit\Security\IAM\Policy;

use Http\Client\Exception\NetworkException;
use NoahVet\Reef\Exception\IAMException;
use NoahVet\Reef\Factory\ClientFactory;
use NoahVet\Reef\Jane\Client;
use NoahVet\Reef\Jane\Model\PolicyResultPolicyResultGet;
use NoahVet\Reef\Security\Authentication\BearerClientHMacComputer;
use NoahVet\Reef\Security\IAM\Model\Resource;
use NoahVet\Reef\Security\IAM\Model\ResourceType;
use NoahVet\Reef\Security\IAM\Transformer\PolicyResultTransformer;
use NoahVet\Reef\Security\Policy\PolicyInterface;
use NoahVet\Reef\Security\Policy\PolicyManager;
use NoahVet\Reef\Security\Policy\PolicyResult;
use NoahVet\Reef\Security\Policy\PolicyResultGrant;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Symfony\Component\HttpFoundation\Response;

class PolicyManagerTest extends TestCase
{
    protected PolicyInterface $policy;

    protected function setUp(): void
    {
        $this->policy = new class implements PolicyInterface {
            public function computeEtag(mixed $subject): string
            {
                return (string) $subject;
            }

            public function getName(): string
            {
                return 'phpunitpolicy';
            }

            public function subjectToString(mixed $subject): string
            {
                return (string) $subject;
            }

            public function apply(mixed $subject): PolicyResult
            {
                \assert(\is_string($subject));

                if (!\str_starts_with($subject, 'mappableResource')) {
                    throw new \Exception();
                }

                $policyResultGrant = new PolicyResultGrant(
                    Resource::fromString('reef:phpunit:resource', 'test'),
                    'reef:phpunit:principal:test',
                    [
                        'allow' => ['permission1', 'permission2'],
                        'deny' => ['permission3'],
                    ],
                );
                $policyResult = new PolicyResult(
                    $this->getName(),
                    $this->subjectToString($subject),
                    ResourceType::fromString('reef:phpunit:resource'),
                );
                $policyResult->etag = $this->computeEtag($subject);
                $policyResult->addGrant($policyResultGrant);

                return $policyResult;
            }

            public function canHandle(mixed $subject): bool
            {
                return \is_string($subject) && \str_starts_with($subject, 'mappableResource');
            }
        };
    }

    public function testExecuteAllPolicies(): void
    {
        $client = $this->getMockBuilder(Client::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;

        $clientFactory = $this->getMockBuilder(ClientFactory::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $clientFactory->method('create')
            ->with('sample_token')
            ->willReturn($client)
        ;

        $hmacComputer = $this->getMockBuilder(BearerClientHMacComputer::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $hmacComputer->method('computeClientHMacHeader')
            ->with('sample_token')
            ->willReturn([
                'X-Reef-Client-ID' => 'client_id',
                'X-Reef-Client-HMAC' => 'client_hmac',
            ])
        ;

        $policyManager = new PolicyManager(
            'sample_token',
            $hmacComputer,
            $clientFactory,
            [
                $this->policy,
            ],
            new PolicyResultTransformer(),
        );

        $results = $policyManager->executeAllPolicies('mappableResource');
        self::assertCount(1, $results);

        $results = $policyManager->executeAllPolicies('notMappableResource');
        self::assertCount(0, $results);
    }

    public function testSyncPoliciesCreatedOk(): void
    {
        $client = $this->getMockBuilder(Client::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;

        $client
            ->expects(self::once())
            ->method('getPolicyResultCollection')
            ->with([
                'name' => $this->policy->getName(),
                'subject' => 'mappableResource',
            ])
            ->willReturn([])
        ;

        $responseMock = $this->getMockBuilder(ResponseInterface::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $responseMock
            ->method('getStatusCode')
            ->willReturn(Response::HTTP_OK)
        ;

        $client
            ->expects(self::once())
            ->method('executeRawEndpoint')
            ->withAnyParameters()
            ->willReturn($responseMock)
        ;

        $clientFactory = $this->getMockBuilder(ClientFactory::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $clientFactory->method('create')
            ->with('sample_token')
            ->willReturn($client)
        ;

        $hmacComputer = $this->getMockBuilder(BearerClientHMacComputer::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $hmacComputer->method('computeClientHMacHeader')
            ->with('sample_token')
            ->willReturn([
                'X-Reef-Client-ID' => 'client_id',
                'X-Reef-Client-HMAC' => 'client_hmac',
            ])
        ;

        $policyManager = new PolicyManager(
            'sample_token',
            $hmacComputer,
            $clientFactory,
            [
                $this->policy,
            ],
            new PolicyResultTransformer(),
        );

        $policyManager->syncPolicies('mappableResource');
    }

    public function testSyncPoliciesNotUpdatedOk(): void
    {
        $client = $this->getMockBuilder(Client::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;

        $client
            ->expects(self::once())
            ->method('getPolicyResultCollection')
            ->with([
                'name' => $this->policy->getName(),
                'subject' => 'mappableResource',
            ])
            ->willReturn([
                (new PolicyResultPolicyResultGet())
                    ->setName($this->policy->getName())
                    ->setEtag('mappableResource')
                    ->setSubject('mappableResource')
                    ->setClient('phpunitclient'),
            ])
        ;

        $client
            ->expects(self::never())
            ->method('postPolicyResultCollection')
            ->withAnyParameters()
        ;

        $clientFactory = $this->getMockBuilder(ClientFactory::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $clientFactory->method('create')
            ->with('sample_token')
            ->willReturn($client)
        ;

        $hmacComputer = $this->getMockBuilder(BearerClientHMacComputer::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $hmacComputer->method('computeClientHMacHeader')
            ->with('sample_token')
            ->willReturn([
                'X-Reef-Client-ID' => 'client_id',
                'X-Reef-Client-HMAC' => 'client_hmac',
            ])
        ;

        $policyManager = new PolicyManager(
            'sample_token',
            $hmacComputer,
            $clientFactory,
            [
                $this->policy,
            ],
            new PolicyResultTransformer(),
        );

        $policyManager->syncPolicies('mappableResource');
    }

    public function testSyncPoliciesIamDown(): void
    {
        $client = $this->getMockBuilder(Client::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;

        $networkException = $this
            ->getMockBuilder(NetworkException::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;

        $client
            ->expects(self::once())
            ->method('getPolicyResultCollection')
            ->with([
                'name' => $this->policy->getName(),
                'subject' => 'mappableResource',
            ])
            ->willThrowException($networkException)
        ;

        $client
            ->expects(self::never())
            ->method('postPolicyResultCollection')
            ->withAnyParameters()
        ;

        $clientFactory = $this->getMockBuilder(ClientFactory::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $clientFactory->method('create')
            ->with('sample_token')
            ->willReturn($client)
        ;

        $hmacComputer = $this->getMockBuilder(BearerClientHMacComputer::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $hmacComputer->method('computeClientHMacHeader')
            ->with('sample_token')
            ->willReturn([
                'X-Reef-Client-ID' => 'client_id',
                'X-Reef-Client-HMAC' => 'client_hmac',
            ])
        ;

        $policyManager = new PolicyManager(
            'sample_token',
            $hmacComputer,
            $clientFactory,
            [
                $this->policy,
            ],
            new PolicyResultTransformer(),
        );

        self::expectException(NetworkException::class);

        $policyManager->syncPolicies('mappableResource');
    }

    public function testSyncPoliciesIamDownOnUpdate(): void
    {
        $client = $this->getMockBuilder(Client::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;

        $networkException = $this
            ->getMockBuilder(NetworkException::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;

        $client
            ->expects(self::once())
            ->method('getPolicyResultCollection')
            ->with([
                'name' => $this->policy->getName(),
                'subject' => 'mappableResource',
            ])
            ->willReturn([])
        ;

        $client
            ->expects(self::once())
            ->method('executeRawEndpoint')
            ->withAnyParameters()
            ->willThrowException($networkException)
        ;

        $clientFactory = $this->getMockBuilder(ClientFactory::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $clientFactory->method('create')
            ->with('sample_token')
            ->willReturn($client)
        ;

        $hmacComputer = $this->getMockBuilder(BearerClientHMacComputer::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $hmacComputer->method('computeClientHMacHeader')
            ->with('sample_token')
            ->willReturn([
                'X-Reef-Client-ID' => 'client_id',
                'X-Reef-Client-HMAC' => 'client_hmac',
            ])
        ;

        $policyManager = new PolicyManager(
            'sample_token',
            $hmacComputer,
            $clientFactory,
            [
                $this->policy,
            ],
            new PolicyResultTransformer(),
        );

        self::expectException(NetworkException::class);

        $policyManager->syncPolicies('mappableResource');
    }

    public function testSyncPoliciesIamErrorOnUpdate(): void
    {
        $client = $this->getMockBuilder(Client::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;

        $client
            ->expects(self::once())
            ->method('getPolicyResultCollection')
            ->with([
                'name' => $this->policy->getName(),
                'subject' => 'mappableResource',
            ])
            ->willReturn([])
        ;

        $responseMock = $this->getMockBuilder(ResponseInterface::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $responseMock
            ->method('getStatusCode')
            ->willReturn(Response::HTTP_INTERNAL_SERVER_ERROR)
        ;
        $bodyMock = $this->getMockBuilder(StreamInterface::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $bodyMock
            ->method('getContents')
            ->willReturn('ErrorMessage')
        ;
        $responseMock
            ->method('getBody')
            ->willReturn($bodyMock)
        ;

        $client
            ->expects(self::once())
            ->method('executeRawEndpoint')
            ->withAnyParameters()
            ->willReturn($responseMock)
        ;

        $clientFactory = $this->getMockBuilder(ClientFactory::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $clientFactory->method('create')
            ->with('sample_token')
            ->willReturn($client)
        ;

        $hmacComputer = $this->getMockBuilder(BearerClientHMacComputer::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $hmacComputer->method('computeClientHMacHeader')
            ->with('sample_token')
            ->willReturn([
                'X-Reef-Client-ID' => 'client_id',
                'X-Reef-Client-HMAC' => 'client_hmac',
            ])
        ;

        $policyManager = new PolicyManager(
            'sample_token',
            $hmacComputer,
            $clientFactory,
            [
                $this->policy,
            ],
            new PolicyResultTransformer(),
        );

        self::expectException(IAMException::class);

        $policyManager->syncPolicies('mappableResource');
    }

    public function testBatchSyncPolicies(): void
    {
        $client = $this->getMockBuilder(Client::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;

        $policyResultGet = new PolicyResultPolicyResultGet();
        $policyResultGet
            ->setName($this->policy->getName())
            ->setSubject('mappableResource')
            ->setEtag('wrongetag')
        ;

        $policyResultGetOk = new PolicyResultPolicyResultGet();
        $policyResultGetOk
            ->setName($this->policy->getName())
            ->setSubject('mappableResourceOk')
            ->setEtag('mappableResourceOk')
        ;

        $client
            ->method('getPolicyResultCollection')
            ->withConsecutive(
                [[
                    'name' => $this->policy->getName(),
                    'subject' => ['mappableResource', 'mappableResourceOk'],
                    'page' => 1,
                ]],
                [[
                    'name' => $this->policy->getName(),
                    'subject' => ['mappableResource', 'mappableResourceOk'],
                    'page' => 2,
                ]],
            )
            ->willReturnOnConsecutiveCalls(
                [$policyResultGet, $policyResultGetOk],
                [],
            )
        ;

        $responseMock = $this->getMockBuilder(ResponseInterface::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $responseMock
            ->method('getStatusCode')
            ->willReturn(Response::HTTP_INTERNAL_SERVER_ERROR)
        ;
        $bodyMock = $this->getMockBuilder(StreamInterface::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $bodyMock
            ->method('getContents')
            ->willReturn('ErrorMessage')
        ;
        $responseMock
            ->method('getBody')
            ->willReturn($bodyMock)
        ;

        $client
            ->expects(self::once())
            ->method('executeRawEndpoint')
            ->withAnyParameters()
            ->willReturn($responseMock)
        ;

        $clientFactory = $this->getMockBuilder(ClientFactory::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $clientFactory->method('create')
            ->with('sample_token')
            ->willReturn($client)
        ;

        $hmacComputer = $this->getMockBuilder(BearerClientHMacComputer::class)
            ->disableOriginalConstructor()
            ->getMock()
        ;
        $hmacComputer->method('computeClientHMacHeader')
            ->with('sample_token')
            ->willReturn([
                'X-Reef-Client-ID' => 'client_id',
                'X-Reef-Client-HMAC' => 'client_hmac',
            ])
        ;

        $policyManager = new PolicyManager(
            'sample_token',
            $hmacComputer,
            $clientFactory,
            [
                $this->policy,
            ],
            new PolicyResultTransformer(),
        );

        self::expectException(IAMException::class);

        $policyManager->batchSyncPolicies(['mappableResource', 'mappableResourceOk']);
    }
}
