<?php

declare(strict_types=1);

namespace Tests\Framework\Waf\MachineLearning;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\DateTime;
use App\Framework\Http\IpAddress;
use App\Framework\MachineLearning\Core\AnomalyDetectorInterface;
use App\Framework\MachineLearning\Core\FeatureExtractorMetadata;
use App\Framework\MachineLearning\ValueObjects\AnomalyDetection;
use App\Framework\MachineLearning\ValueObjects\AnomalyType;
use App\Framework\MachineLearning\ValueObjects\Feature;
use App\Framework\MachineLearning\ValueObjects\FeatureType;
use App\Framework\Waf\Analysis\ValueObjects\RequestAnalysisData;
use App\Framework\Waf\MachineLearning\MachineLearningEngine;
use App\Framework\Waf\MachineLearning\MachineLearningResult;
use App\Framework\Waf\MachineLearning\WafFeatureExtractor;
use Mockery;
use Mockery\MockInterface;

// Hilfsfunktion zum Erstellen eines Mock-Extraktors
function createMockExtractorMLE(bool $enabled = true, ?FeatureType $featureType = null, array $features = []): MockInterface
{
    $featureType = $featureType ?? FeatureType::STRUCTURAL_PATTERN;

    $extractor = Mockery::mock(WafFeatureExtractor::class, FeatureExtractorMetadata::class);
    $extractor->shouldReceive('isEnabled')->andReturn($enabled);
    $extractor->shouldReceive('getFeatureType')->andReturn($featureType);
    $extractor->shouldReceive('getPriority')->andReturn(10);
    $extractor->shouldReceive('canExtract')->with(Mockery::type(RequestAnalysisData::class))->andReturn(true);
    $extractor->shouldReceive('extractFeatures')->with(Mockery::type(RequestAnalysisData::class), Mockery::type('array'))->andReturn($features);
    $extractor->shouldReceive('getExpectedProcessingTime')->andReturn(10);
    $extractor->shouldReceive('supportsParallelExecution')->andReturn(true);
    $extractor->shouldReceive('getDependencies')->andReturn([]);

    return $extractor;
}

// Hilfsfunktion zum Erstellen eines Mock-Detektors
function createMockDetectorMLE(bool $enabled = true, array $supportedTypes = [], array $anomalies = []): MockInterface
{
    $supportedTypes = $supportedTypes ?: [FeatureType::STRUCTURAL_PATTERN];

    $detector = Mockery::mock(AnomalyDetectorInterface::class);
    $detector->shouldReceive('isEnabled')->andReturn($enabled);
    $detector->shouldReceive('getName')->andReturn('MockDetector');
    $detector->shouldReceive('getSupportedFeatureTypes')->andReturn($supportedTypes);
    $detector->shouldReceive('canAnalyze')->andReturn(true);
    $detector->shouldReceive('detectAnomalies')->andReturn($anomalies);
    $detector->shouldReceive('updateModel')->andReturn(null);

    return $detector;
}

// Hilfsfunktion zum Erstellen einer Beispiel-RequestAnalysisData
function createSampleRequestData(): RequestAnalysisData
{
    return new RequestAnalysisData(
        method: 'GET',
        url: '/test',
        path: '/test',
        queryString: '',
        headers: ['User-Agent' => 'TestAgent'],
        queryParameters: [],
        postParameters: [],
        cookies: [],
        body: '',
        files: [],
        clientIp: IpAddress::localhost(),
        timestamp: Timestamp::now()
    );
}

// Hilfsfunktion zum Erstellen einer Beispiel-Clock
function createMockClockMLE(): MockInterface
{
    $clock = Mockery::mock(Clock::class);
    $dateTime = DateTime::fromString('2025-07-31 13:42:00');
    $timestamp = Timestamp::fromDateTime($dateTime);
    $clock->shouldReceive('time')->andReturn($timestamp);

    return $clock;
}

test('gibt leeres Ergebnis zurück wenn deaktiviert', function () {
    // Arrange
    $engine = new MachineLearningEngine(
        enabled: false,
        extractors: [],
        detectors: [],
        clock: createMockClockMLE(),
        analysisTimeout: Duration::fromSeconds(10),
        confidenceThreshold: Percentage::from(50.0)
    );

    // Act
    $result = $engine->analyzeRequest(createSampleRequestData());

    // Assert
    expect($result)->toBeInstanceOf(MachineLearningResult::class);
    expect($result->enabled)->toBeFalse();
    expect($result->features)->toBeEmpty();
    expect($result->anomalies)->toBeEmpty();
    expect($result->confidence->getValue())->toBe(0.0);
});

test('extrahiert Features aus Request-Daten', function () {
    // Arrange
    $feature = new Feature(
        type: FeatureType::STRUCTURAL_PATTERN,
        name: 'test_feature',
        value: 42.0,
        unit: 'count'
    );

    $extractor = createMockExtractorMLE(true, FeatureType::STRUCTURAL_PATTERN, [$feature]);

    $engine = new MachineLearningEngine(
        enabled: true,
        extractors: [$extractor],
        detectors: [],
        clock: createMockClockMLE(),
        analysisTimeout: Duration::fromSeconds(10),
        confidenceThreshold: Percentage::from(50.0)
    );

    // Act
    $result = $engine->analyzeRequest(createSampleRequestData());

    // Assert
    expect($result->error)->toBeNull();
    expect($result->features)->toHaveCount(1);
    expect($result->features[0])->toBeInstanceOf(Feature::class);
    expect($result->features[0]->name)->toBe('test_feature');
    expect($result->features[0]->value)->toBe(42.0);
});

test('erkennt Anomalien in Features', function () {
    // Arrange
    $feature = new Feature(
        type: FeatureType::STRUCTURAL_PATTERN,
        name: 'test_feature',
        value: 42.0,
        unit: 'count'
    );

    $anomaly = new AnomalyDetection(
        type: AnomalyType::STATISTICAL_ANOMALY,
        featureType: FeatureType::STRUCTURAL_PATTERN,
        confidence: Percentage::from(75.0),
        anomalyScore: 0.8,
        description: 'Test anomaly',
        features: [$feature],
        evidence: [
            'metric' => 'test_feature',
            'value' => 42.0,
            'expected_value' => 10.0,
            'z_score' => 2.5,
        ]
    );

    $extractor = createMockExtractorMLE(true, FeatureType::STRUCTURAL_PATTERN, [$feature]);
    $detector = createMockDetectorMLE(true, [FeatureType::STRUCTURAL_PATTERN], [$anomaly]);

    $engine = new MachineLearningEngine(
        enabled: true,
        extractors: [$extractor],
        detectors: [$detector],
        clock: createMockClockMLE(),
        analysisTimeout: Duration::fromSeconds(10),
        confidenceThreshold: Percentage::from(50.0)
    );

    // Act
    $result = $engine->analyzeRequest(createSampleRequestData());

    // Assert
    expect($result->anomalies)->toHaveCount(1);
    expect($result->anomalies[0])->toBeInstanceOf(AnomalyDetection::class);
    expect($result->anomalies[0]->type)->toBe(AnomalyType::STATISTICAL_ANOMALY);
    expect($result->anomalies[0]->confidence->getValue())->toBe(75.0);
});

test('filtert Anomalien basierend auf Konfidenz-Schwellenwert', function () {
    // Arrange
    $feature = new Feature(
        type: FeatureType::STRUCTURAL_PATTERN,
        name: 'test_feature',
        value: 42.0,
        unit: 'count'
    );

    $highConfidenceAnomaly = new AnomalyDetection(
        type: AnomalyType::STATISTICAL_ANOMALY,
        featureType: FeatureType::STRUCTURAL_PATTERN,
        confidence: Percentage::from(80.0),
        anomalyScore: 0.8,
        description: 'High confidence anomaly',
        features: [$feature],
        evidence: ['value' => 42.0, 'expected_value' => 10.0]
    );

    $lowConfidenceAnomaly = new AnomalyDetection(
        type: AnomalyType::STATISTICAL_ANOMALY,
        featureType: FeatureType::STRUCTURAL_PATTERN,
        confidence: Percentage::from(40.0),
        anomalyScore: 0.3,
        description: 'Low confidence anomaly',
        features: [$feature],
        evidence: ['value' => 42.0, 'expected_value' => 30.0]
    );

    $extractor = createMockExtractorMLE(true, FeatureType::STRUCTURAL_PATTERN, [$feature]);
    $detector = createMockDetectorMLE(true, [FeatureType::STRUCTURAL_PATTERN], [$highConfidenceAnomaly, $lowConfidenceAnomaly]);

    $engine = new MachineLearningEngine(
        enabled: true,
        extractors: [$extractor],
        detectors: [$detector],
        clock: createMockClockMLE(),
        analysisTimeout: Duration::fromSeconds(10),
        confidenceThreshold: Percentage::from(50.0)
    );

    // Act
    $result = $engine->analyzeRequest(createSampleRequestData());

    // Assert
    expect($result->anomalies)->toHaveCount(1);
    expect($result->anomalies[0]->confidence->getValue())->toBe(80.0);
});

test('berechnet Gesamt-Konfidenz korrekt', function () {
    // Arrange
    $feature = new Feature(
        type: FeatureType::STRUCTURAL_PATTERN,
        name: 'test_feature',
        value: 42.0,
        unit: 'count'
    );

    $anomaly1 = new AnomalyDetection(
        type: AnomalyType::STATISTICAL_ANOMALY,
        featureType: FeatureType::STRUCTURAL_PATTERN,
        confidence: Percentage::from(60.0),
        anomalyScore: 0.6,
        description: 'Anomaly 1',
        features: [$feature],
        evidence: ['value' => 42.0, 'expected_value' => 10.0]
    );

    $anomaly2 = new AnomalyDetection(
        type: AnomalyType::CLUSTERING_DEVIATION,
        featureType: FeatureType::STRUCTURAL_PATTERN,
        confidence: Percentage::from(80.0),
        anomalyScore: 0.4,
        description: 'Anomaly 2',
        features: [$feature],
        evidence: ['value' => 42.0, 'expected_value' => 10.0]
    );

    $extractor = createMockExtractorMLE(true, FeatureType::STRUCTURAL_PATTERN, [$feature]);
    $detector = createMockDetectorMLE(true, [FeatureType::STRUCTURAL_PATTERN], [$anomaly1, $anomaly2]);

    $engine = new MachineLearningEngine(
        enabled: true,
        extractors: [$extractor],
        detectors: [$detector],
        clock: createMockClockMLE(),
        analysisTimeout: Duration::fromSeconds(10),
        confidenceThreshold: Percentage::from(50.0)
    );

    // Act
    $result = $engine->analyzeRequest(createSampleRequestData());

    // Assert
    // Erwartete Konfidenz: (60.0 * 0.6 + 80.0 * 0.4) / (0.6 + 0.4) = (36 + 32) / 1 = 68
    expect($result->confidence->getValue())->toBeGreaterThan(67.9);
    expect($result->confidence->getValue())->toBeLessThan(68.1);
});

test('dedupliziert und sortiert Anomalien', function () {
    // Arrange
    $feature = new Feature(
        type: FeatureType::STRUCTURAL_PATTERN,
        name: 'test_feature',
        value: 42.0,
        unit: 'count'
    );

    // Zwei Anomalien mit gleichem Typ und FeatureType, aber unterschiedlicher Konfidenz
    $anomaly1 = new AnomalyDetection(
        type: AnomalyType::STATISTICAL_ANOMALY,
        featureType: FeatureType::STRUCTURAL_PATTERN,
        confidence: Percentage::from(60.0),
        anomalyScore: 0.6,
        description: 'Anomaly 1',
        features: [$feature],
        evidence: ['value' => 42.0, 'expected_value' => 10.0]
    );

    $anomaly2 = new AnomalyDetection(
        type: AnomalyType::STATISTICAL_ANOMALY,
        featureType: FeatureType::STRUCTURAL_PATTERN,
        confidence: Percentage::from(80.0),
        anomalyScore: 0.8,
        description: 'Anomaly 2',
        features: [$feature],
        evidence: ['value' => 42.0, 'expected_value' => 10.0]
    );

    // Eine Anomalie mit anderem Typ
    $anomaly3 = new AnomalyDetection(
        type: AnomalyType::CLUSTERING_DEVIATION,
        featureType: FeatureType::STRUCTURAL_PATTERN,
        confidence: Percentage::from(70.0),
        anomalyScore: 0.4,
        description: 'Anomaly 3',
        features: [$feature],
        evidence: ['value' => 42.0, 'expected_value' => 10.0]
    );

    $extractor = createMockExtractorMLE(true, FeatureType::STRUCTURAL_PATTERN, [$feature]);
    $detector = createMockDetectorMLE(true, [FeatureType::STRUCTURAL_PATTERN], [$anomaly1, $anomaly2, $anomaly3]);

    $engine = new MachineLearningEngine(
        enabled: true,
        extractors: [$extractor],
        detectors: [$detector],
        clock: createMockClockMLE(),
        analysisTimeout: Duration::fromSeconds(10),
        confidenceThreshold: Percentage::from(50.0)
    );

    // Act
    $result = $engine->analyzeRequest(createSampleRequestData());

    // Assert
    // Erwartet: 2 Anomalien (anomaly2 und anomaly3), da anomaly1 und anomaly2 dedupliziert werden
    // und anomaly2 mit höherer Konfidenz behalten wird
    expect($result->anomalies)->toHaveCount(2);

    // Sortierung nach anomalyScore (absteigend), also anomaly2 vor anomaly3
    expect($result->anomalies[0]->type)->toBe(AnomalyType::STATISTICAL_ANOMALY);
    expect($result->anomalies[0]->confidence->getValue())->toBe(80.0);

    expect($result->anomalies[1]->type)->toBe(AnomalyType::CLUSTERING_DEVIATION);
    expect($result->anomalies[1]->confidence->getValue())->toBe(70.0);
});

test('gibt Konfiguration korrekt zurück', function () {
    // Arrange
    $engine = new MachineLearningEngine(
        enabled: true,
        extractors: [createMockExtractorMLE()],
        detectors: [createMockDetectorMLE()],
        clock: createMockClockMLE(),
        analysisTimeout: Duration::fromSeconds(5),
        confidenceThreshold: Percentage::from(75.0),
        enableParallelProcessing: true,
        enableFeatureCaching: false,
        maxFeaturesPerRequest: 50
    );

    // Act
    $config = $engine->getConfiguration();

    // Assert
    expect($config)->toBeArray();
    expect($config['enabled'])->toBeTrue();
    expect($config['analysis_timeout_ms'])->toBe(5000.0);
    expect($config['confidence_threshold'])->toBe(75.0);
    expect($config['enable_parallel_processing'])->toBeTrue();
    expect($config['enable_feature_caching'])->toBeFalse();
    expect($config['max_features_per_request'])->toBe(50);
    expect($config['extractor_count'])->toBe(1);
    expect($config['detector_count'])->toBe(1);
});

test('fängt Ausnahmen ab und gibt Fehlermeldung zurück', function () {
    // Arrange
    $extractor = Mockery::mock(WafFeatureExtractor::class, FeatureExtractorMetadata::class);
    $extractor->shouldReceive('isEnabled')->andReturn(true);
    $extractor->shouldReceive('getFeatureType')->andReturn(FeatureType::STRUCTURAL_PATTERN);
    $extractor->shouldReceive('getPriority')->andReturn(10);
    $extractor->shouldReceive('canExtract')->andReturn(true);
    $extractor->shouldReceive('extractFeatures')->andThrow(new \RuntimeException('Test exception'));

    $engine = new MachineLearningEngine(
        enabled: true,
        extractors: [$extractor],
        detectors: [],
        clock: createMockClockMLE(),
        analysisTimeout: Duration::fromSeconds(10),
        confidenceThreshold: Percentage::from(50.0)
    );

    // Act
    $result = $engine->analyzeRequest(createSampleRequestData());

    // Assert
    // Individual extractor exceptions don't propagate to top-level error
    // They're caught and logged to extractorResults
    expect($result->error)->toBeNull();
    expect($result->features)->toBeEmpty();
    expect($result->anomalies)->toBeEmpty();
});

// Bereinigung nach jedem Test
afterEach(function () {
    Mockery::close();
});
