<?php

declare(strict_types=1);

namespace App\Tests\Integration\Reputation;

use App\Domain\Ip\IpAddress;
use App\Domain\Reputation\PairScorer;
use App\Domain\Time\FixedClock;
use App\Infrastructure\Category\CategoryRepository;
use App\Infrastructure\Reputation\ReportRepository;
use App\Tests\Integration\Support\AppTestCase;
use Doctrine\DBAL\ParameterType;

/**
 * The hard cutoff (default 365 days) lives in PairScorer, not in the Decay
 * function. A report older than the cutoff must be entirely excluded from
 * the score even if its decay weight would still be non-zero.
 *
 * For exponential decay, that's the only way the score reaches *exactly*
 * zero (the formula `0.5^(age/half_life)` is strictly positive).
 */
final class PairScorerCutoffTest extends AppTestCase
{
    public function testReportBeyondHardCutoffExcludedEntirely(): void
    {
        $clock = FixedClock::at('2026-04-29T00:00:00Z');
        $reporterId = $this->createReporter('cutoff-test');
        $ip = IpAddress::fromString('203.0.113.99');
        /** @var int $catId */
        $catId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug='brute_force'");

        // 400 days ago — beyond the 365-day default cutoff.
        $this->db->insert('reports', [
            'ip_bin' => $ip->binary(),
            'ip_text' => $ip->text(),
            'category_id' => $catId,
            'reporter_id' => $reporterId,
            'weight_at_report' => '1.00',
            'received_at' => $clock->now()->modify('-400 days')->format('Y-m-d H:i:s'),
        ], ['ip_bin' => ParameterType::LARGE_OBJECT]);

        $scorer = new PairScorer(
            new ReportRepository($this->db),
            new CategoryRepository($this->db),
            $clock,
            365,
        );

        self::assertSame(0.0, $scorer->score($ip->binary(), $catId, $clock->now()));
    }

    public function testReportInsideCutoffIsCounted(): void
    {
        $clock = FixedClock::at('2026-04-29T00:00:00Z');
        $reporterId = $this->createReporter('inside-cutoff-test');
        $ip = IpAddress::fromString('203.0.113.123');
        /** @var int $catId */
        $catId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug='brute_force'");

        // 364 days ago — barely inside the cutoff. With a 14-day half-life,
        // its weight is essentially zero (0.5^26 ≈ 1.5e-8) but still > 0.
        $this->db->insert('reports', [
            'ip_bin' => $ip->binary(),
            'ip_text' => $ip->text(),
            'category_id' => $catId,
            'reporter_id' => $reporterId,
            'weight_at_report' => '1.00',
            'received_at' => $clock->now()->modify('-364 days')->format('Y-m-d H:i:s'),
        ], ['ip_bin' => ParameterType::LARGE_OBJECT]);

        $scorer = new PairScorer(
            new ReportRepository($this->db),
            new CategoryRepository($this->db),
            $clock,
            365,
        );

        $score = $scorer->score($ip->binary(), $catId, $clock->now());
        self::assertGreaterThan(0.0, $score);
        self::assertLessThan(1e-6, $score);
    }
}
