<?php

declare(strict_types=1);

namespace App\Tests\Unit\Reputation;

use App\Domain\Reputation\Decay;
use App\Domain\Reputation\DecayFunction;
use PHPUnit\Framework\TestCase;

/**
 * Hand-computed reference values for both decay shapes. The exponential
 * cases hit half-life multiples so the answers are clean fractions.
 */
final class DecayTest extends TestCase
{
    public function testLinearAtZeroReturnsFullWeight(): void
    {
        self::assertSame(1.0, Decay::value(DecayFunction::Linear, 0.0, 30.0));
    }

    public function testLinearMidwayReturnsHalf(): void
    {
        self::assertEqualsWithDelta(0.5, Decay::value(DecayFunction::Linear, 15.0, 30.0), 1e-9);
    }

    public function testLinearAtOrPastDecayParamClampsToZero(): void
    {
        self::assertSame(0.0, Decay::value(DecayFunction::Linear, 30.0, 30.0));
        self::assertSame(0.0, Decay::value(DecayFunction::Linear, 100.0, 30.0));
    }

    public function testExponentialAtZeroReturnsFullWeight(): void
    {
        self::assertSame(1.0, Decay::value(DecayFunction::Exponential, 0.0, 14.0));
    }

    public function testExponentialAtOneHalfLifeReturnsHalf(): void
    {
        self::assertEqualsWithDelta(0.5, Decay::value(DecayFunction::Exponential, 14.0, 14.0), 1e-9);
    }

    public function testExponentialAtTwoHalfLivesReturnsQuarter(): void
    {
        self::assertEqualsWithDelta(0.25, Decay::value(DecayFunction::Exponential, 28.0, 14.0), 1e-9);
    }

    public function testNegativeAgeClampsToFullWeight(): void
    {
        // Future-dated reports shouldn't happen, but if they do they count
        // at full weight rather than blowing up.
        self::assertSame(1.0, Decay::value(DecayFunction::Linear, -5.0, 30.0));
        self::assertSame(1.0, Decay::value(DecayFunction::Exponential, -5.0, 14.0));
    }

    public function testZeroDecayParamReturnsZero(): void
    {
        self::assertSame(0.0, Decay::value(DecayFunction::Linear, 5.0, 0.0));
        self::assertSame(0.0, Decay::value(DecayFunction::Exponential, 5.0, 0.0));
    }

    public function testDaysUntilThresholdLinearHalfwayCase(): void
    {
        // Linear decay, T=30 days. Score=2.0, threshold=1.0 → score reaches
        // threshold when 2.0 * (1 − Δ/30) = 1.0, i.e. Δ = 15.
        self::assertEqualsWithDelta(
            15.0,
            Decay::daysUntilThreshold(DecayFunction::Linear, 30.0, 2.0, 1.0) ?? -1.0,
            1e-9,
        );
    }

    public function testDaysUntilThresholdExponentialHalfLife(): void
    {
        // Exponential half-life=14 days. Score=2.0, threshold=1.0 → Δ = 14.
        self::assertEqualsWithDelta(
            14.0,
            Decay::daysUntilThreshold(DecayFunction::Exponential, 14.0, 2.0, 1.0) ?? -1.0,
            1e-9,
        );
        // Two half-lives: 4.0 → 1.0 takes 28 days.
        self::assertEqualsWithDelta(
            28.0,
            Decay::daysUntilThreshold(DecayFunction::Exponential, 14.0, 4.0, 1.0) ?? -1.0,
            1e-9,
        );
    }

    public function testDaysUntilThresholdReturnsZeroWhenAlreadyBelow(): void
    {
        self::assertSame(0.0, Decay::daysUntilThreshold(DecayFunction::Linear, 30.0, 0.5, 1.0));
        self::assertSame(0.0, Decay::daysUntilThreshold(DecayFunction::Exponential, 14.0, 1.0, 1.0));
    }

    public function testDaysUntilThresholdReturnsNullForNonPositiveThreshold(): void
    {
        self::assertNull(Decay::daysUntilThreshold(DecayFunction::Linear, 30.0, 5.0, 0.0));
        self::assertNull(Decay::daysUntilThreshold(DecayFunction::Exponential, 14.0, 5.0, -0.5));
    }
}
