#!/usr/local/cpanel/3rdparty/bin/perl -w
#                                      Copyright 2025 WebPros International, LLC
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

# BEGIN Augment generated code block
# Randomness quality validation tests for Random
# Tests entropy distribution, runs test, and secure random bytes quality
# END Augment generated code block

use cPstrict;

use Test2::V0;
use Test2::Tools::Explain;
use Test2::Plugin::NoWarnings;
use Test2::Tools::Subtest;
use Test2::Tools::Exception;

use Test::MockModule qw/strict/;

use Test::Cpanel::Policy testing => 'Cpanel::Plugins::Components::Rules::Random';

# Mock the True rule for testing
{

    package Cpanel::Plugins::Components::Rules::True;
    use Moo;
    extends 'Cpanel::Plugins::Components::Rules::Base';
    sub is_allowed { return !!1; }
}

$INC{'Cpanel/Plugins/Components/Rules/True.pm'} = 'inline';

use Cpanel::Plugins::Components::Rules::Random;

subtest 'Runs test for randomness' => sub {
    my $config = {
        rule        => { true => 1 },
        sample_size => 50000            # 50% probability for easier testing
    };

    my @results;
    my $total_tests = 1000;

    # Collect results from many independent instances
    for my $i ( 1 .. $total_tests ) {
        my $filter = Cpanel::Plugins::Components::Rules::Random->new($config);
        push @results, $filter->is_allowed();
    }

    # Test for runs (consecutive identical values) - should be roughly random
    my $runs = 1;
    for my $i ( 1 .. $#results ) {
        $runs++ if $results[$i] != $results[ $i - 1 ];
    }

    # Expected runs for random sequence ≈ (2*n*p*(1-p) + 1) where p=0.5
    my $p             = 0.5;
    my $expected_runs = 2 * $total_tests * $p * ( 1 - $p ) + 1;
    my $run_variance  = 2 * $total_tests * $p * ( 1 - $p ) * ( 1 - 2 * $p * ( 1 - $p ) );
    my $run_std_dev   = sqrt($run_variance);

    ok(
        abs( $runs - $expected_runs ) < 3 * $run_std_dev,
        sprintf(
            "Runs test passes: %d runs within 3σ of expected %.1f",
            $runs, $expected_runs
        )
    );

    note("Observed runs: $runs");
    note( "Expected runs: " . sprintf( "%.1f", $expected_runs ) );
    note( "Standard deviation: " . sprintf( "%.1f", $run_std_dev ) );

    # Additional check: count 0s and 1s should be roughly equal
    my $ones          = grep { $_ } @results;
    my $zeros         = $total_tests - $ones;
    my $balance_ratio = $ones / $total_tests;

    ok(
        abs( $balance_ratio - 0.5 ) < 0.1,
        sprintf( "Balanced distribution: %.3f (should be ~0.5)", $balance_ratio )
    );

    note("Ones: $ones, Zeros: $zeros, Ratio: $balance_ratio");
};

subtest 'Autocorrelation test' => sub {
    my $config = {
        rule        => { true => 1 },
        sample_size => 50000
    };

    my @results;
    my $total_tests = 500;

    for my $i ( 1 .. $total_tests ) {
        my $filter = Cpanel::Plugins::Components::Rules::Random->new($config);
        push @results, $filter->is_allowed();
    }

    # Test lag-1 autocorrelation (should be near 0 for random sequence)
    my $mean        = ( grep { $_ } @results ) / @results;
    my $numerator   = 0;
    my $denominator = 0;

    for my $i ( 0 .. $#results - 1 ) {
        $numerator += ( $results[$i] - $mean ) * ( $results[ $i + 1 ] - $mean );
    }

    for my $i ( 0 .. $#results ) {
        $denominator += ( $results[$i] - $mean )**2;
    }

    my $autocorr = $denominator > 0 ? $numerator / $denominator : 0;

    # Autocorrelation should be close to 0 for random data
    ok(
        abs($autocorr) < 0.2,
        sprintf( "Low autocorrelation indicates randomness: %.3f", $autocorr )
    );

    note("Lag-1 autocorrelation: $autocorr");
    note("Sample mean: $mean");
};

subtest 'Secure random bytes quality' => sub {
    my $filter = Cpanel::Plugins::Components::Rules::Random->new(
        {
            rule        => { true => 1 },
            sample_size => 1000
        }
    );

    # Test that secure random bytes are actually random
    my @byte_samples;
    for my $i ( 1 .. 100 ) {

        # Note: This assumes get_secure_random_bytes method exists
        # If not available, we'll test the underlying randomness differently
        eval {
            my $bytes = $filter->get_secure_random_bytes(32);
            push @byte_samples, $bytes;
        };

        if ($@) {

            # Fallback: test multiple filter instances for uniqueness
            my $filter_instance = Cpanel::Plugins::Components::Rules::Random->new(
                {
                    rule        => { true => 1 },
                    sample_size => 1000
                }
            );
            push @byte_samples, "$i-" . $filter_instance->is_allowed();
        }
    }

    # Ensure all samples are different (extremely high probability with good RNG)
    my %seen;
    my $duplicates = 0;
    for my $sample (@byte_samples) {
        $duplicates++ if $seen{$sample}++;
    }

    is( $duplicates, 0, 'All random samples are unique' );

    # Test sample count
    is( scalar(@byte_samples), 100, 'Generated expected number of samples' );

    note( "Generated " . scalar(@byte_samples) . " unique samples" );
};

subtest 'Frequency distribution within blocks' => sub {
    my $config = {
        rule        => { true => 1 },
        sample_size => 10000,           # 10% probability instead of 50%
        fleet_size  => 100000
    };

    my $block_size = 50;
    my $num_blocks = 20;
    my @block_frequencies;

    for my $block ( 1 .. $num_blocks ) {
        my $true_count = 0;

        for my $i ( 1 .. $block_size ) {
            my $filter = Cpanel::Plugins::Components::Rules::Random->new($config);
            $true_count++ if $filter->is_allowed();
        }

        push @block_frequencies, $true_count / $block_size;
    }

    # Calculate variance of block frequencies
    my $sum_freq = 0;
    $sum_freq += $_ for @block_frequencies;
    my $mean_freq = $sum_freq / @block_frequencies;
    my $variance  = 0;

    for my $freq (@block_frequencies) {
        $variance += ( $freq - $mean_freq )**2;
    }
    $variance /= @block_frequencies;

    my $std_dev = sqrt($variance);

    # For Random with independent instances, we expect higher variance
    # than a simple binomial distribution because each instance uses independent
    # entropy sources (hostname, timestamp, process ID, secure random bytes).
    #
    # The variance will be closer to the variance of independent Bernoulli trials
    # rather than samples from a single random stream. For independent instances,
    # we expect variance around p(1-p) for the frequency across blocks.
    #
    # We'll use a more lenient threshold that accounts for the independent
    # random generation in each Random instance.
    my $expected_probability        = 0.1;                                                      # 10% probability (10000/100000)
    my $expected_variance_per_trial = $expected_probability * ( 1 - $expected_probability );    # p(1-p)
    my $expected_variance_per_block = $expected_variance_per_trial / $block_size;
    my $expected_std_dev            = sqrt($expected_variance_per_block);

    # Use a more lenient multiplier (10x instead of 3x) to account for
    # the independent random generation and entropy mixing in Random
    my $tolerance_multiplier = 10;

    ok(
        $std_dev < $tolerance_multiplier * $expected_std_dev,
        sprintf(
            "Block frequency variance reasonable: %.4f < %.4f",
            $std_dev, $tolerance_multiplier * $expected_std_dev
        )
    );

    # Also check that the mean frequency is reasonably close to expected probability
    ok(
        abs( $mean_freq - $expected_probability ) < 0.15,
        sprintf( "Mean frequency close to expected %.1f: %.4f", $expected_probability, $mean_freq )
    );

    # Check that we have some variation (not all blocks identical)
    ok(
        $std_dev > 0.01,
        sprintf( "Standard deviation shows variation: %.4f > 0.01", $std_dev )
    );

    note("Observed std dev: $std_dev");
    note("Expected std dev (binomial): $expected_std_dev");
    note( "Tolerance threshold: " . ( $tolerance_multiplier * $expected_std_dev ) );
    note("Mean frequency: $mean_freq");
};

done_testing;
