<?php

declare(strict_types=1);

namespace App\Tests\Unit\Reputation;

use App\Domain\Ip\Cidr;
use App\Domain\Ip\IpAddress;
use App\Domain\Reputation\CidrEvaluator;
use App\Domain\Reputation\EffectiveStatus;
use App\Domain\Reputation\EffectiveStatusService;
use App\Infrastructure\Reputation\CidrEvaluatorFactory;
use App\Infrastructure\Reputation\IpScoreRepository;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\TestCase;

/**
 * Locks the SPEC §5 precedence: allowlist > manual block > scored > clean.
 * M09 wires `Scored` to "any non-zero score in `ip_scores`" via a stub
 * IpScoreRepository here; the integration tests cover the real path.
 */
final class EffectiveStatusServiceTest extends TestCase
{
    public function testAllowlistWinsOverManualBlock(): void
    {
        $bin = IpAddress::fromString('198.51.100.5')->binary();
        $factory = $this->factoryReturning(new CidrEvaluator(
            manualIpBins: [$bin],
            manualSubnets: [],
            allowlistIpBins: [$bin],
            allowlistSubnets: [],
        ));

        $service = new EffectiveStatusService($factory, $this->scoreRepoWithoutScores());
        self::assertSame(
            EffectiveStatus::Allowlisted,
            $service->forIp(IpAddress::fromString('198.51.100.5'))
        );
    }

    public function testManualBlockReturnedWhenNotAllowlisted(): void
    {
        $bin = IpAddress::fromString('203.0.113.42')->binary();
        $factory = $this->factoryReturning(new CidrEvaluator([$bin], [], [], []));

        $service = new EffectiveStatusService($factory, $this->scoreRepoWithoutScores());
        self::assertSame(
            EffectiveStatus::ManuallyBlocked,
            $service->forIp(IpAddress::fromString('203.0.113.42'))
        );
    }

    public function testIpInsideAllowlistedSubnetEvenWithSeparateManualBlockIsAllowlisted(): void
    {
        $allow = Cidr::fromString('203.0.113.0/24');
        $factory = $this->factoryReturning(new CidrEvaluator(
            manualIpBins: [IpAddress::fromString('203.0.113.42')->binary()],
            manualSubnets: [],
            allowlistIpBins: [],
            allowlistSubnets: [$allow],
        ));

        $service = new EffectiveStatusService($factory, $this->scoreRepoWithoutScores());
        self::assertSame(
            EffectiveStatus::Allowlisted,
            $service->forIp(IpAddress::fromString('203.0.113.42'))
        );
    }

    public function testScoredWhenScoreRepoHasRowsAndNoOverrides(): void
    {
        $factory = $this->factoryReturning(new CidrEvaluator([], [], [], []));
        $service = new EffectiveStatusService($factory, $this->scoreRepoWithScores());

        self::assertSame(
            EffectiveStatus::Scored,
            $service->forIp(IpAddress::fromString('203.0.113.99'))
        );
    }

    public function testCleanWhenNothingMatches(): void
    {
        $factory = $this->factoryReturning(new CidrEvaluator([], [], [], []));
        $service = new EffectiveStatusService($factory, $this->scoreRepoWithoutScores());

        self::assertSame(
            EffectiveStatus::Clean,
            $service->forIp(IpAddress::fromString('203.0.113.99'))
        );
    }

    private function factoryReturning(CidrEvaluator $evaluator): CidrEvaluatorFactory
    {
        return new class ($evaluator) extends CidrEvaluatorFactory {
            public function __construct(private readonly CidrEvaluator $fixed)
            {
                // Deliberately not calling parent::__construct — this stub
                // never queries the DB.
            }

            public function get(): CidrEvaluator
            {
                return $this->fixed;
            }

            public function invalidate(): void
            {
                // no-op
            }
        };
    }

    private function scoreRepoWithoutScores(): IpScoreRepository
    {
        return new class ($this->createMock(Connection::class)) extends IpScoreRepository {
            public function hasAnyScore(string $ipBin): bool
            {
                return false;
            }
        };
    }

    private function scoreRepoWithScores(): IpScoreRepository
    {
        return new class ($this->createMock(Connection::class)) extends IpScoreRepository {
            public function hasAnyScore(string $ipBin): bool
            {
                return true;
            }
        };
    }
}
