<?php

declare(strict_types=1);

namespace Tests\Framework\Database\NPlusOneDetection\MachineLearning;

use App\Framework\Database\NPlusOneDetection\MachineLearning\NPlusOneDetectionEngine;
use App\Framework\Database\NPlusOneDetection\MachineLearning\Extractors\QueryFeatureExtractor;
use App\Framework\Database\NPlusOneDetection\QueryExecutionContext;
use App\Framework\Waf\MachineLearning\Detectors\StatisticalAnomalyDetector;
use App\Framework\MachineLearning\ValueObjects\Feature;
use App\Framework\MachineLearning\ValueObjects\FeatureType;
use App\Framework\MachineLearning\ValueObjects\AnomalyDetection;
use App\Framework\MachineLearning\ValueObjects\AnomalyType;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\DateTime\SystemClock;
use Mockery;
use Mockery\MockInterface;

// Helper function to create mock extractor
function createMockQueryExtractor(bool $enabled = true, array $features = []): MockInterface
{
    $extractor = Mockery::mock(\App\Framework\MachineLearning\Core\FeatureExtractorInterface::class);
    $extractor->shouldReceive('isEnabled')->andReturn($enabled);
    $extractor->shouldReceive('getFeatureType')->andReturn(FeatureType::FREQUENCY);
    $extractor->shouldReceive('getPriority')->andReturn(10);
    $extractor->shouldReceive('canExtract')->andReturn(true);
    $extractor->shouldReceive('extractFeatures')->andReturn($features);

    return $extractor;
}

// Helper function to create mock detector
function createMockQueryDetector(bool $enabled = true, array $supportedTypes = [], array $anomalies = []): MockInterface
{
    $supportedTypes = $supportedTypes ?: [FeatureType::FREQUENCY];

    $detector = Mockery::mock(\App\Framework\MachineLearning\Core\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);

    return $detector;
}

describe('NPlusOneDetectionEngine', function () {
    beforeEach(function () {
        $this->clock = new SystemClock();
    });

    afterEach(function () {
        Mockery::close();
    });

    it('is disabled when constructed with enabled: false', function () {
        $engine = new NPlusOneDetectionEngine(
            enabled: false,
            extractors: [],
            detectors: [],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        expect($engine->isEnabled())->toBeFalse();
    });

    it('is enabled when constructed with enabled: true', function () {
        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [],
            detectors: [],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        expect($engine->isEnabled())->toBeTrue();
    });

    it('returns configuration', function () {
        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [new QueryFeatureExtractor()],
            detectors: [],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(3),
            confidenceThreshold: Percentage::from(75.0)
        );

        $config = $engine->getConfiguration();

        expect($config)->toBeArray();
        expect($config['enabled'])->toBeTrue();
        expect($config['extractor_count'])->toBe(1);
        expect($config['detector_count'])->toBe(0);
        expect($config['analysis_timeout_ms'])->toBe(3000);
        expect($config['confidence_threshold'])->toBe(75.0);
    });

    it('returns disabled result when engine is disabled', function () {
        $engine = new NPlusOneDetectionEngine(
            enabled: false,
            extractors: [],
            detectors: [],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        $context = QueryExecutionContext::minimal();
        $result = $engine->analyzeQueryContext($context);

        expect($result->enabled)->toBeFalse();
        expect($result->features)->toBeEmpty();
        expect($result->anomalies)->toBeEmpty();
        expect($result->confidence->getValue())->toBe(0.0);
    });

    it('extracts features from query context', function () {
        $feature = new Feature(
            type: FeatureType::FREQUENCY,
            name: 'query_frequency',
            value: 10.0,
            unit: 'queries/second'
        );

        $extractor = createMockQueryExtractor(true, [$feature]);

        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [$extractor],
            detectors: [],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        $context = QueryExecutionContext::minimal();
        $result = $engine->analyzeQueryContext($context);

        expect($result->features)->toHaveCount(1);
        expect($result->features[0]->name)->toBe('query_frequency');
    });

    it('detects anomalies using configured detectors', function () {
        $feature = new Feature(
            type: FeatureType::FREQUENCY,
            name: 'query_frequency',
            value: 100.0, // High frequency
            unit: 'queries/second'
        );

        $anomaly = AnomalyDetection::frequencySpike(
            currentRate: 100.0,
            baseline: 10.0,
            threshold: 3.0
        );

        $extractor = createMockQueryExtractor(true, [$feature]);
        $detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly]);

        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [$extractor],
            detectors: [$detector],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        $context = QueryExecutionContext::minimal();
        $result = $engine->analyzeQueryContext($context);

        expect($result->anomalies)->not->toBeEmpty();
        expect($result->confidence->getValue())->toBeGreaterThan(0.0);
    });

    it('filters anomalies by confidence threshold', function () {
        $feature = new Feature(
            type: FeatureType::FREQUENCY,
            name: 'query_frequency',
            value: 50.0,
            unit: 'queries/second'
        );

        // Low confidence anomaly (should be filtered out)
        $lowConfidenceAnomaly = AnomalyDetection::create(
            type: AnomalyType::FREQUENCY_SPIKE,
            featureType: FeatureType::FREQUENCY,
            anomalyScore: 0.5, // 50% confidence
            description: 'Low confidence anomaly',
            features: [$feature],
            evidence: []
        );

        $extractor = createMockQueryExtractor(true, [$feature]);
        $detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$lowConfidenceAnomaly]);

        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [$extractor],
            detectors: [$detector],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(75.0) // High threshold
        );

        $context = QueryExecutionContext::minimal();
        $result = $engine->analyzeQueryContext($context);

        // Anomaly should be filtered out due to low confidence
        expect($result->anomalies)->toBeEmpty();
    });

    it('skips disabled extractors', function () {
        $enabledExtractor = createMockQueryExtractor(true, [
            new Feature(FeatureType::FREQUENCY, 'feature1', 10.0, 'unit')
        ]);

        $disabledExtractor = createMockQueryExtractor(false, [
            new Feature(FeatureType::FREQUENCY, 'feature2', 20.0, 'unit')
        ]);

        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [$enabledExtractor, $disabledExtractor],
            detectors: [],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        $context = QueryExecutionContext::minimal();
        $result = $engine->analyzeQueryContext($context);

        // Should only have features from enabled extractor
        expect($result->features)->toHaveCount(1);
        expect($result->features[0]->name)->toBe('feature1');
    });

    it('skips disabled detectors', function () {
        $feature = new Feature(
            type: FeatureType::FREQUENCY,
            name: 'query_frequency',
            value: 100.0,
            unit: 'queries/second'
        );

        $anomaly = AnomalyDetection::frequencySpike(100.0, 10.0, 3.0);

        $extractor = createMockQueryExtractor(true, [$feature]);
        $enabledDetector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly]);
        $disabledDetector = createMockQueryDetector(false, [FeatureType::FREQUENCY], [$anomaly]);

        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [$extractor],
            detectors: [$enabledDetector, $disabledDetector],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        $context = QueryExecutionContext::minimal();
        $result = $engine->analyzeQueryContext($context);

        // Should only have anomalies from enabled detector
        expect($result->detectorResults)->toHaveCount(1);
    });

    it('includes extractor results in output', function () {
        $extractor = createMockQueryExtractor(true, [
            new Feature(FeatureType::FREQUENCY, 'feature1', 10.0, 'unit')
        ]);

        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [$extractor],
            detectors: [],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        $context = QueryExecutionContext::minimal();
        $result = $engine->analyzeQueryContext($context);

        expect($result->extractorResults)->not->toBeEmpty();
        expect($result->extractorResults[0]->featuresExtracted)->toBe(1);
        expect($result->extractorResults[0]->success)->toBeTrue();
    });

    it('includes detector results in output', function () {
        $feature = new Feature(
            type: FeatureType::FREQUENCY,
            name: 'query_frequency',
            value: 100.0,
            unit: 'queries/second'
        );

        $anomaly = AnomalyDetection::frequencySpike(100.0, 10.0, 3.0);

        $extractor = createMockQueryExtractor(true, [$feature]);
        $detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly]);

        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [$extractor],
            detectors: [$detector],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        $context = QueryExecutionContext::minimal();
        $result = $engine->analyzeQueryContext($context);

        expect($result->detectorResults)->not->toBeEmpty();
        expect($result->detectorResults[0]->anomaliesDetected)->toBe(1);
        expect($result->detectorResults[0]->success)->toBeTrue();
    });

    it('calculates overall confidence from anomalies', function () {
        $feature = new Feature(
            type: FeatureType::FREQUENCY,
            name: 'query_frequency',
            value: 100.0,
            unit: 'queries/second'
        );

        $anomaly1 = AnomalyDetection::frequencySpike(100.0, 10.0, 3.0); // ~95% confidence
        $anomaly2 = AnomalyDetection::frequencySpike(90.0, 10.0, 3.0);  // ~95% confidence

        $extractor = createMockQueryExtractor(true, [$feature]);
        $detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly1, $anomaly2]);

        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [$extractor],
            detectors: [$detector],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        $context = QueryExecutionContext::minimal();
        $result = $engine->analyzeQueryContext($context);

        // Overall confidence should be average of anomalies
        expect($result->confidence->getValue())->toBeGreaterThan(90.0);
    });

    it('handles exceptions gracefully', function () {
        $extractor = Mockery::mock(\App\Framework\MachineLearning\Core\FeatureExtractorInterface::class);
        $extractor->shouldReceive('isEnabled')->andReturn(true);
        $extractor->shouldReceive('canExtract')->andReturn(true);
        $extractor->shouldReceive('extractFeatures')->andThrow(new \RuntimeException('Extraction failed'));

        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [$extractor],
            detectors: [],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        $context = QueryExecutionContext::minimal();
        $result = $engine->analyzeQueryContext($context);

        // Should not throw, should return error in result
        expect($result->error)->not->toBeNull();
        expect($result->error)->toContain('Extraction failed');
    });

    it('tracks analysis time', function () {
        $extractor = createMockQueryExtractor(true, [
            new Feature(FeatureType::FREQUENCY, 'feature1', 10.0, 'unit')
        ]);

        $engine = new NPlusOneDetectionEngine(
            enabled: true,
            extractors: [$extractor],
            detectors: [],
            clock: $this->clock,
            analysisTimeout: Duration::fromSeconds(5),
            confidenceThreshold: Percentage::from(60.0)
        );

        $context = QueryExecutionContext::minimal();
        $result = $engine->analyzeQueryContext($context);

        // Analysis time should be measured
        expect($result->analysisTime->toMilliseconds())->toBeGreaterThanOrEqual(0);
    });
});
