<?php

declare(strict_types=1);

namespace BotGuard\Tests\Unit;

use BotGuard\Services\Detection\Contracts\DetectionModuleInterface;
use BotGuard\Services\Detection\DetectionEngine;
use BotGuard\Services\Detection\EngineFactory;
use BotGuard\Services\Detection\ModuleResult;
use BotGuard\Services\Detection\Modules\JavaScriptModule;
use BotGuard\Services\Detection\SignalBag;
use PHPUnit\Framework\TestCase;

/**
 * End-to-end tests for the scoring engine using the default config module set
 * (no database) and the deterministic human/bot fixtures.
 */
final class DetectionEngineTest extends TestCase
{
    private function engine(): DetectionEngine
    {
        // Null DB => EngineFactory falls back to config/detection.php.
        return (new EngineFactory(null))->make();
    }

    public function testHumanSessionClassifiedHuman(): void
    {
        $outcome = $this->engine()->evaluate(SignalFixtures::human());

        $this->assertSame('human', $outcome->classification, "Score was {$outcome->score}");
        $this->assertGreaterThanOrEqual(80, $outcome->score);
        $this->assertSame('redirect_primary', $outcome->action);
    }

    public function testBotSessionClassifiedBot(): void
    {
        $outcome = $this->engine()->evaluate(SignalFixtures::bot());

        $this->assertSame('bot', $outcome->classification, "Score was {$outcome->score}");
        $this->assertLessThan(60, $outcome->score);
        $this->assertSame('block', $outcome->action);
    }

    public function testBehavioralModulesAreEnabledByDefault(): void
    {
        $breakdown = $this->engine()->evaluate(SignalFixtures::human())->breakdown();

        foreach (['mouse', 'keyboard', 'scroll', 'link_interaction'] as $key) {
            $this->assertArrayHasKey($key, $breakdown, "Module {$key} should be enabled by default");
        }
    }

    public function testScoreIsWithinBounds(): void
    {
        foreach ([SignalFixtures::human(), SignalFixtures::bot()] as $bag) {
            $outcome = $this->engine()->evaluate($bag);
            $this->assertGreaterThanOrEqual(0, $outcome->score);
            $this->assertLessThanOrEqual(100, $outcome->score);
        }
    }

    public function testWeightNormalizationWithSingleModule(): void
    {
        // One module with weight 10; a fully-human sub-score should map to 100.
        $engine = new DetectionEngine(
            [['key' => 'javascript', 'weight' => 10.0, 'module' => new JavaScriptModule()]],
            80,
            60,
            ['human' => 'redirect_primary', 'suspicious' => 'redirect_alternate', 'bot' => 'block'],
        );

        $human = $engine->evaluate(SignalFixtures::human());
        $this->assertSame(100, $human->score);
        $this->assertSame('human', $human->classification);
    }

    public function testClassifyBoundaries(): void
    {
        $engine = new DetectionEngine([], 80, 60, []);
        $this->assertSame('human', $engine->classify(80));
        $this->assertSame('human', $engine->classify(100));
        $this->assertSame('suspicious', $engine->classify(79));
        $this->assertSame('suspicious', $engine->classify(60));
        $this->assertSame('bot', $engine->classify(59));
        $this->assertSame('bot', $engine->classify(0));
    }

    public function testBrokenModuleIsNeutralisedNotFatal(): void
    {
        $throwing = new class implements DetectionModuleInterface {
            public function key(): string
            {
                return 'honeypot';
            }

            public function analyze(SignalBag $signals): ModuleResult
            {
                throw new \RuntimeException('boom');
            }
        };

        $engine = new DetectionEngine(
            [['key' => 'honeypot', 'weight' => 10.0, 'module' => $throwing]],
            80,
            60,
            ['bot' => 'block', 'suspicious' => 'redirect_alternate', 'human' => 'redirect_primary'],
        );

        $outcome = $engine->evaluate(SignalFixtures::human());
        // Neutral 0.5 => score 50 => suspicious, and no exception escaped.
        $this->assertSame(50, $outcome->score);
    }
}
