<?php

declare(strict_types=1);

namespace Tests\Framework\Waf\Feedback;

use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Logging\Logger;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\DetectionSeverity;
use App\Framework\Waf\Feedback\DetectionFeedback;
use App\Framework\Waf\Feedback\FeedbackLearningService;
use App\Framework\Waf\Feedback\FeedbackRepositoryInterface;
use App\Framework\Waf\Feedback\FeedbackType;
use App\Framework\Waf\MachineLearning\ValueObjects\ModelAdjustment;
use PHPUnit\Framework\TestCase;

/**
 * Tests for the FeedbackLearningService
 */
class FeedbackLearningServiceTest extends TestCase
{
    private FeedbackRepositoryInterface $repository;

    private TestMachineLearningEngine $mlEngine;

    private TestClock $clock;

    private Logger $logger;

    private FeedbackLearningService $service;

    protected function setUp(): void
    {
        $this->repository = $this->createMock(FeedbackRepositoryInterface::class);
        $this->mlEngine = new TestMachineLearningEngine();
        $this->clock = new TestClock('2025-08-04 18:39:00');
        $this->logger = $this->createMock(Logger::class);

        $this->service = new FeedbackLearningService(
            $this->repository,
            $this->mlEngine,
            $this->clock,
            $this->logger,
            5, // minimumFeedbackThreshold
            0.3 // learningRate
        );
    }

    public function testLearnFromFeedbackWithInsufficientData(): void
    {
        // Set up repository to return stats with insufficient feedback
        $this->repository->method('getFeedbackStats')->willReturn([
            'total_count' => 3, // Less than minimum threshold of 5
        ]);

        // Execute the learning process
        $result = $this->service->learnFromFeedback();

        // Verify the result
        $this->assertFalse($result['success']);
        $this->assertEquals('Not enough feedback for learning', $result['message']);
        $this->assertEquals(3, $result['feedback_count']);
        $this->assertEquals(5, $result['minimum_threshold']);
    }

    public function testLearnFromFeedbackWithSufficientData(): void
    {
        // Set up repository to return stats with sufficient feedback
        $this->repository->method('getFeedbackStats')->willReturn([
            'total_count' => 10,
        ]);

        // Set up repository to return feedback data
        $falsePositives = [
            $this->createFalsePositiveFeedback('detection1', DetectionCategory::SQL_INJECTION),
            $this->createFalsePositiveFeedback('detection2', DetectionCategory::SQL_INJECTION),
            $this->createFalsePositiveFeedback('detection3', DetectionCategory::SQL_INJECTION),
            $this->createFalsePositiveFeedback('detection4', DetectionCategory::XSS),
        ];

        $falseNegatives = [
            $this->createFalseNegativeFeedback('detection5', DetectionCategory::COMMAND_INJECTION),
            $this->createFalseNegativeFeedback('detection6', DetectionCategory::COMMAND_INJECTION),
        ];

        $severityAdjustments = [
            $this->createSeverityAdjustmentFeedback(
                'detection7',
                DetectionCategory::PATH_TRAVERSAL,
                DetectionSeverity::MEDIUM,
                DetectionSeverity::HIGH
            ),
            $this->createSeverityAdjustmentFeedback(
                'detection8',
                DetectionCategory::PATH_TRAVERSAL,
                DetectionSeverity::MEDIUM,
                DetectionSeverity::HIGH
            ),
            $this->createSeverityAdjustmentFeedback(
                'detection9',
                DetectionCategory::PATH_TRAVERSAL,
                DetectionSeverity::MEDIUM,
                DetectionSeverity::HIGH
            ),
        ];

        $this->repository->method('getFeedbackByFeedbackType')
            ->willReturnMap([
                [FeedbackType::FALSE_POSITIVE, null, $falsePositives],
                [FeedbackType::FALSE_NEGATIVE, null, $falseNegatives],
                [FeedbackType::SEVERITY_ADJUSTMENT, null, $severityAdjustments],
                [FeedbackType::CORRECT_DETECTION, null, []],
            ]);

        // Set up ML engine to return success for adjustments
        $this->mlEngine->withApplyFeedbackAdjustmentsResult([
            'success' => true,
            'applied_count' => 3,
            'failed_count' => 0,
            'results' => [],
        ]);

        // Execute the learning process
        $result = $this->service->learnFromFeedback();

        // Verify the result
        $this->assertTrue($result['success']);
        $this->assertEquals(4, $result['false_positives_processed']);
        $this->assertEquals(2, $result['false_negatives_processed']);
        $this->assertEquals(3, $result['severity_adjustments_processed']);
        $this->assertGreaterThan(0, $result['total_adjustments_applied']);
    }

    public function testProcessFalsePositives(): void
    {
        // Create a service with reflection to access private methods
        $service = new FeedbackLearningService(
            $this->repository,
            $this->mlEngine,
            $this->clock,
            $this->logger,
            5,
            0.3
        );

        $reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'processfalsePositives');
        $reflectionMethod->setAccessible(true);

        // Create test data
        $falsePositives = [
            $this->createFalsePositiveFeedback('detection1', DetectionCategory::SQL_INJECTION),
            $this->createFalsePositiveFeedback('detection2', DetectionCategory::SQL_INJECTION),
            $this->createFalsePositiveFeedback('detection3', DetectionCategory::SQL_INJECTION),
            $this->createFalsePositiveFeedback('detection4', DetectionCategory::XSS),
        ];

        // Execute the method
        $adjustments = $reflectionMethod->invoke($service, $falsePositives);

        // Verify the adjustments
        $this->assertCount(2, $adjustments); // One for SQL_INJECTION, one for XSS

        // Check SQL_INJECTION adjustment
        $sqlInjectionAdjustment = null;
        $xssAdjustment = null;

        foreach ($adjustments as $adjustment) {
            if ($adjustment->category === DetectionCategory::SQL_INJECTION) {
                $sqlInjectionAdjustment = $adjustment;
            } elseif ($adjustment->category === DetectionCategory::XSS) {
                $xssAdjustment = $adjustment;
            }
        }

        $this->assertNotNull($sqlInjectionAdjustment);
        $this->assertNotNull($xssAdjustment);

        // SQL_INJECTION had 3 false positives, so should have stronger adjustment
        $this->assertGreaterThan(
            $xssAdjustment->thresholdAdjustment->getValue(),
            $sqlInjectionAdjustment->thresholdAdjustment->getValue()
        );

        // Threshold adjustments should be positive for false positives (make detection harder)
        $this->assertGreaterThan(0, $sqlInjectionAdjustment->thresholdAdjustment->getValue());
        $this->assertGreaterThan(0, $xssAdjustment->thresholdAdjustment->getValue());

        // Confidence adjustments should be negative for false positives (less certain)
        $this->assertLessThan(0, $sqlInjectionAdjustment->confidenceAdjustment->getValue());
        $this->assertLessThan(0, $xssAdjustment->confidenceAdjustment->getValue());
    }

    public function testProcessFalseNegatives(): void
    {
        // Create a service with reflection to access private methods
        $service = new FeedbackLearningService(
            $this->repository,
            $this->mlEngine,
            $this->clock,
            $this->logger,
            5,
            0.3
        );

        $reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'processFalseNegatives');
        $reflectionMethod->setAccessible(true);

        // Create test data
        $falseNegatives = [
            $this->createFalseNegativeFeedback('detection1', DetectionCategory::COMMAND_INJECTION),
            $this->createFalseNegativeFeedback('detection2', DetectionCategory::COMMAND_INJECTION),
        ];

        // Execute the method
        $adjustments = $reflectionMethod->invoke($service, $falseNegatives);

        // Verify the adjustments
        $this->assertCount(1, $adjustments); // One for COMMAND_INJECTION

        $adjustment = reset($adjustments);

        // Threshold adjustments should be negative for false negatives (make detection easier)
        $this->assertLessThan(0, $adjustment->thresholdAdjustment->getValue());

        // Confidence adjustments should be positive for false negatives (more certain)
        $this->assertGreaterThan(0, $adjustment->confidenceAdjustment->getValue());
    }

    public function testProcessSeverityAdjustments(): void
    {
        // Create a service with reflection to access private methods
        $service = new FeedbackLearningService(
            $this->repository,
            $this->mlEngine,
            $this->clock,
            $this->logger,
            5,
            0.3
        );

        $reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'processSeverityAdjustments');
        $reflectionMethod->setAccessible(true);

        // Create test data
        $severityAdjustments = [
            $this->createSeverityAdjustmentFeedback(
                'detection1',
                DetectionCategory::PATH_TRAVERSAL,
                DetectionSeverity::MEDIUM,
                DetectionSeverity::HIGH
            ),
            $this->createSeverityAdjustmentFeedback(
                'detection2',
                DetectionCategory::PATH_TRAVERSAL,
                DetectionSeverity::MEDIUM,
                DetectionSeverity::HIGH
            ),
            $this->createSeverityAdjustmentFeedback(
                'detection3',
                DetectionCategory::PATH_TRAVERSAL,
                DetectionSeverity::MEDIUM,
                DetectionSeverity::HIGH
            ),
        ];

        // Execute the method
        $result = $reflectionMethod->invoke($service, $severityAdjustments);

        // Verify the result
        $this->assertArrayHasKey('adjustments', $result);
        $this->assertArrayHasKey('severity_changes', $result);

        $adjustments = $result['adjustments'];
        $severityChanges = $result['severity_changes'];

        $this->assertCount(1, $adjustments); // One for PATH_TRAVERSAL
        $this->assertCount(1, $severityChanges); // One change from MEDIUM to HIGH

        $adjustment = reset($adjustments);

        // For severity increase, confidence adjustment should be positive
        $this->assertGreaterThan(0, $adjustment->confidenceAdjustment->getValue());

        // Threshold should not be adjusted for severity changes
        $this->assertEquals(0, $adjustment->thresholdAdjustment->getValue());
    }

    public function testGenerateFeatureWeightAdjustments(): void
    {
        // Create a service with reflection to access private methods
        $service = new FeedbackLearningService(
            $this->repository,
            $this->mlEngine,
            $this->clock,
            $this->logger,
            5,
            0.3
        );

        $reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'generateFeatureWeightAdjustments');
        $reflectionMethod->setAccessible(true);

        // Test for SQL_INJECTION false positive
        $sqlInjectionFalsePositiveAdjustments = $reflectionMethod->invoke(
            $service,
            DetectionCategory::SQL_INJECTION,
            true // isfalsePositive
        );

        // Test for SQL_INJECTION false negative
        $sqlInjectionFalseNegativeAdjustments = $reflectionMethod->invoke(
            $service,
            DetectionCategory::SQL_INJECTION,
            false // isfalsePositive
        );

        // Verify the adjustments
        $this->assertNotEmpty($sqlInjectionFalsePositiveAdjustments);
        $this->assertNotEmpty($sqlInjectionFalseNegativeAdjustments);

        // For false positives, adjustments should be negative (reduce feature importance)
        foreach ($sqlInjectionFalsePositiveAdjustments as $feature => $adjustment) {
            $this->assertLessThan(0, $adjustment);
        }

        // For false negatives, adjustments should be positive (increase feature importance)
        foreach ($sqlInjectionFalseNegativeAdjustments as $feature => $adjustment) {
            $this->assertGreaterThan(0, $adjustment);
        }
    }

    public function testApplyModelAdjustments(): void
    {
        // Create test data
        $adjustments = [
            'adjustment1' => new ModelAdjustment(
                'adjustment1',
                DetectionCategory::SQL_INJECTION,
                Percentage::from(10.0),
                Percentage::from(-10.0),
                ['sql_keywords_count' => -0.2],
                'Test adjustment 1',
                Timestamp::fromClock($this->clock)
            ),
            'adjustment2' => new ModelAdjustment(
                'adjustment2',
                DetectionCategory::XSS,
                Percentage::from(5.0),
                Percentage::from(-5.0),
                ['script_tag_count' => -0.3],
                'Test adjustment 2',
                Timestamp::fromClock($this->clock)
            ),
        ];

        // Set up ML engine to return success
        $this->mlEngine->withApplyFeedbackAdjustmentsResult([
            'success' => true,
            'applied_count' => 2,
            'failed_count' => 0,
            'results' => [],
        ]);

        // Create a service with reflection to access private methods
        $service = new FeedbackLearningService(
            $this->repository,
            $this->mlEngine,
            $this->clock,
            $this->logger,
            5,
            0.3
        );

        $reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'applyModelAdjustments');
        $reflectionMethod->setAccessible(true);

        // Execute the method
        $result = $reflectionMethod->invoke($service, $adjustments);

        // Verify the result
        $this->assertTrue($result['success']);
        $this->assertEquals(2, $result['applied_count']);
        $this->assertEquals(0, $result['failed_count']);

        // Verify that the correct adjustments were passed to the ML engine
        $this->assertSame($adjustments, $this->mlEngine->getReceivedAdjustments());
    }

    public function testWithMinimumFeedbackThreshold(): void
    {
        $newService = $this->service->withMinimumFeedbackThreshold(10);

        $this->assertNotSame($this->service, $newService);
        $this->assertEquals(10, $newService->getMinimumFeedbackThreshold());
        $this->assertEquals(5, $this->service->getMinimumFeedbackThreshold());
    }

    public function testWithLearningRate(): void
    {
        $newService = $this->service->withLearningRate(0.5);

        $this->assertNotSame($this->service, $newService);
        $this->assertEquals(0.5, $newService->getLearningRate());
        $this->assertEquals(0.3, $this->service->getLearningRate());
    }

    /**
     * Helper method to create a false positive feedback
     */
    private function createFalsePositiveFeedback(
        string $detectionId,
        DetectionCategory $category,
        DetectionSeverity $severity = DetectionSeverity::MEDIUM
    ): DetectionFeedback {
        return DetectionFeedback::falsePositive(
            $detectionId,
            'test-user',
            'This is a false positive',
            $category,
            $severity,
            ['test_context' => true]
        );
    }

    /**
     * Helper method to create a false negative feedback
     */
    private function createFalseNegativeFeedback(
        string $detectionId,
        DetectionCategory $category,
        DetectionSeverity $severity = DetectionSeverity::MEDIUM
    ): DetectionFeedback {
        return DetectionFeedback::falseNegative(
            $detectionId,
            'test-user',
            'This is a false negative',
            $category,
            $severity,
            ['test_context' => true]
        );
    }

    /**
     * Helper method to create a severity adjustment feedback
     */
    private function createSeverityAdjustmentFeedback(
        string $detectionId,
        DetectionCategory $category,
        DetectionSeverity $currentSeverity,
        DetectionSeverity $suggestedSeverity
    ): DetectionFeedback {
        return DetectionFeedback::severityAdjustment(
            $detectionId,
            'test-user',
            'This severity should be adjusted',
            $category,
            $currentSeverity,
            $suggestedSeverity,
            ['test_context' => true]
        );
    }
}
