<?php

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

use NoahVet\Reef\Application\Security\Service\PolicyOrchestratorInterface;
use NoahVet\Reef\Application\Security\UseCase\SyncPoliciesUseCase;
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\PolicyResultPublisherInterface;
use NoahVet\Reef\Domain\Security\Port\PolicyResultStoreInterface;
use PHPUnit\Framework\TestCase;

class SyncPoliciesUseCaseTest extends TestCase
{
    private PolicyResultPublisherInterface $publisher;

    private PolicyResultStoreInterface $store;

    private PolicyOrchestratorInterface $orchestrator;

    private PolicyInterface $policy;

    private object $subject;

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

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

        $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;
            }
        };
    }

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

        $this->publisher->expects(self::once())->method('publish');

        $useCase = new SyncPoliciesUseCase($this->orchestrator, $this->store, $this->publisher);
        $useCase->sync($this->subject);
    }

    public function testMatchingEtagSkipsPublish(): void
    {
        $this->orchestrator->method('getPoliciesFor')->willReturn([$this->policy]);
        $this->store->method('findEtag')->willReturn('etag-subject-1');

        $this->publisher->expects(self::never())->method('publish');

        $useCase = new SyncPoliciesUseCase($this->orchestrator, $this->store, $this->publisher);
        $useCase->sync($this->subject);
    }

    public function testDifferentEtagTriggersPublish(): void
    {
        $this->orchestrator->method('getPoliciesFor')->willReturn([$this->policy]);
        $this->store->method('findEtag')->willReturn('old-etag');

        $this->publisher->expects(self::once())->method('publish');

        $useCase = new SyncPoliciesUseCase($this->orchestrator, $this->store, $this->publisher);
        $useCase->sync($this->subject);
    }

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

        $this->store->expects(self::never())->method('findEtag');
        $this->publisher->expects(self::never())->method('publish');

        $useCase = new SyncPoliciesUseCase($this->orchestrator, $this->store, $this->publisher);
        $useCase->sync($this->subject);
    }

    public function testTwoPoliciesEachCheckedIndependently(): void
    {
        $resourceType = ResourceType::fromString('reef:test');

        $policy2 = new class($resourceType) implements PolicyInterface {
            public function __construct(private readonly ResourceType $resourceType)
            {
            }

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

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

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

            public function apply(object $subject): PolicyResult
            {
                return new PolicyResult($this->getName(), (string) $subject, $this->resourceType);
            }

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

        $this->orchestrator->method('getPoliciesFor')->willReturn([$this->policy, $policy2]);

        $this->store
            ->method('findEtag')
            ->willReturnMap([
                ['test-policy', 'subject-1', 'etag-subject-1'],
                ['other-policy', 'subject-1', 'stale-etag'],
            ])
        ;

        // test-policy matches → no publish; other-policy mismatches → publish
        $this->publisher->expects(self::once())->method('publish');

        $useCase = new SyncPoliciesUseCase($this->orchestrator, $this->store, $this->publisher);
        $useCase->sync($this->subject);
    }
}
