<?php

namespace NoahVet\Reef\Test\A_Unit\Application\Security\UseCase;

use NoahVet\Reef\Application\Security\Service\PolicyOrchestratorInterface;
use NoahVet\Reef\Application\Security\UseCase\BatchSyncPoliciesUseCase;
use NoahVet\Reef\Domain\Security\Model\ResourceType;
use NoahVet\Reef\Domain\Security\Policy\PolicyInterface;
use NoahVet\Reef\Domain\Security\Policy\PolicyResult;
use NoahVet\Reef\Domain\Security\Port\AsyncPolicyPublisherInterface;
use NoahVet\Reef\Domain\Security\Port\PolicyResultPublisherInterface;
use NoahVet\Reef\Domain\Security\Port\PolicyResultStoreInterface;
use PHPUnit\Framework\TestCase;

class BatchSyncPoliciesUseCaseTest extends TestCase
{
    private PolicyOrchestratorInterface $orchestrator;

    private PolicyResultStoreInterface $store;

    private PolicyInterface $policy;

    /** @var object&\PHPUnit\Framework\MockObject\MockObject */
    private object $subjectA;

    /** @var object&\PHPUnit\Framework\MockObject\MockObject */
    private object $subjectB;

    protected function setUp(): void
    {
        $this->orchestrator = $this->createMock(PolicyOrchestratorInterface::class);
        $this->store = $this->createMock(PolicyResultStoreInterface::class);

        $resourceType = ResourceType::fromString('reef:test');
        $this->policy = new class($resourceType) implements PolicyInterface {
            public function __construct(private readonly ResourceType $resourceType)
            {
            }

            public function getName(): string
            {
                return 'test-policy';
            }

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

            public function computeEtag(object $subject): string
            {
                return 'etag-'.(string) $subject;
            }

            public function apply(object $subject): PolicyResult
            {
                $result = new PolicyResult($this->getName(), (string) $subject, $this->resourceType);
                $result->etag = $this->computeEtag($subject);

                return $result;
            }

            public function canHandle(object $subject): bool
            {
                return true;
            }
        };

        $this->subjectA = $this->createMock(\Stringable::class);
        $this->subjectA->method('__toString')->willReturn('subject-a');

        $this->subjectB = $this->createMock(\Stringable::class);
        $this->subjectB->method('__toString')->willReturn('subject-b');
    }

    public function testFindEtagsCalledOnceForAllSubjects(): void
    {
        $this->orchestrator
            ->method('getPoliciesFor')
            ->willReturn([$this->policy])
        ;

        $this->store
            ->expects(self::once())
            ->method('findEtags')
            ->with('test-policy', self::countOf(2))
            ->willReturn([])
        ;

        $publisher = $this->createMock(PolicyResultPublisherInterface::class);

        $useCase = new BatchSyncPoliciesUseCase($this->orchestrator, $this->store, $publisher);
        $useCase->batchSync([$this->subjectA, $this->subjectB]);
    }

    public function testSubjectsWithUnchangedEtagAreNotPublished(): void
    {
        $this->orchestrator->method('getPoliciesFor')->willReturn([$this->policy]);
        $this->store->method('findEtags')->willReturn([
            'subject-a' => 'etag-subject-a',
            'subject-b' => 'etag-subject-b',
        ]);

        $publisher = $this->createMock(PolicyResultPublisherInterface::class);
        $publisher->expects(self::never())->method('publish');

        $useCase = new BatchSyncPoliciesUseCase($this->orchestrator, $this->store, $publisher);
        $useCase->batchSync([$this->subjectA, $this->subjectB]);
    }

    public function testSubjectsWithChangedEtagArePublished(): void
    {
        $this->orchestrator->method('getPoliciesFor')->willReturn([$this->policy]);
        $this->store->method('findEtags')->willReturn([
            'subject-a' => 'stale-etag',
            'subject-b' => 'etag-subject-b',
        ]);

        $publisher = $this->createMock(PolicyResultPublisherInterface::class);
        $publisher->expects(self::once())->method('publish');

        $useCase = new BatchSyncPoliciesUseCase($this->orchestrator, $this->store, $publisher);
        $useCase->batchSync([$this->subjectA, $this->subjectB]);
    }

    public function testAsyncPublisherUsesEnqueueAndFlush(): void
    {
        $this->orchestrator->method('getPoliciesFor')->willReturn([$this->policy]);
        $this->store->method('findEtags')->willReturn([]);

        $publisher = $this->createMock(AsyncPolicyPublisherInterface::class);
        $publisher->expects(self::exactly(2))->method('enqueue');
        $publisher->expects(self::once())->method('flush');

        $useCase = new BatchSyncPoliciesUseCase($this->orchestrator, $this->store, $publisher);
        $useCase->batchSync([$this->subjectA, $this->subjectB]);
    }

    public function testAsyncPublisherNeverCallsPublish(): void
    {
        $this->orchestrator->method('getPoliciesFor')->willReturn([$this->policy]);
        $this->store->method('findEtags')->willReturn([]);

        $publisher = $this->createMock(AsyncAndSyncPublisherInterface::class);
        $publisher->expects(self::never())->method('publish');
        $publisher->method('enqueue');
        $publisher->method('flush');

        $useCase = new BatchSyncPoliciesUseCase($this->orchestrator, $this->store, $publisher);
        $useCase->batchSync([$this->subjectA]);
    }

    public function testEmptySubjectsListNeverCallsStoreOrPublisher(): void
    {
        $this->store->expects(self::never())->method('findEtags');

        $publisher = $this->createMock(PolicyResultPublisherInterface::class);
        $publisher->expects(self::never())->method('publish');

        $useCase = new BatchSyncPoliciesUseCase($this->orchestrator, $this->store, $publisher);
        $useCase->batchSync([]);
    }
}

/**
 * Helper interface used to verify AsyncPolicyPublisherInterface does NOT call publish().
 */
interface AsyncAndSyncPublisherInterface extends PolicyResultPublisherInterface, AsyncPolicyPublisherInterface
{
}
