// Copyright (c) 2026 Koni Marti. All rights reserved.
// Use of this source code is governed by the MIT license.
module std::math::distributions_test;
import std::io, std::math, std::math::distributions @public;

const double TOLERANCE = 1e-10;
const double RELAXED_TOLERANCE = 1e-6;
const double VERY_RELAXED_TOLERANCE = 1e-3;

macro approx(double left, double right, double tol, String $msg = "")
{
	assert(math::is_approx(left, right, tol), "%s != %s (eps: %g): %s", left, right, tol, $msg);
}

fn void test_uniform_mean() @test
{
	UniformDist dist = distributions::uniform(0.0, 10.0);
	double mean = dist.mean();
	approx(mean, 5.0, TOLERANCE, "Uniform mean should be (a+b)/2");
}

fn void test_uniform_variance() @test
{
	UniformDist dist = distributions::uniform(0.0, 10.0);
	double variance = dist.variance();
	double expected = 100.0 / 12.0;
	approx(variance, expected, TOLERANCE, "Uniform variance should be (b-a)²/12");
}

fn void test_uniform_pdf() @test
{
	UniformDist dist = distributions::uniform(0.0, 10.0);

	// PDF should be constant in range
	approx(dist.pdf(5.0), 0.1, TOLERANCE, "PDF at midpoint");
	approx(dist.pdf(0.0), 0.1, TOLERANCE, "PDF at lower bound");
	approx(dist.pdf(10.0), 0.1, TOLERANCE, "PDF at upper bound");

	// PDF should be 0 outside range
	approx(dist.pdf(-1.0), 0.0, TOLERANCE, "PDF below range");
	approx(dist.pdf(11.0), 0.0, TOLERANCE, "PDF above range");
}

fn void test_uniform_cdf() @test
{
	UniformDist dist = distributions::uniform(0.0, 10.0);

	approx(dist.cdf(-1.0), 0.0, TOLERANCE, "CDF below range");
	approx(dist.cdf(0.0), 0.0, TOLERANCE, "CDF at lower bound");
	approx(dist.cdf(5.0), 0.5, TOLERANCE, "CDF at midpoint");
	approx(dist.cdf(7.5), 0.75, TOLERANCE, "CDF at 75%");
	approx(dist.cdf(10.0), 1.0, TOLERANCE, "CDF at upper bound");
	approx(dist.cdf(11.0), 1.0, TOLERANCE, "CDF above range");
}

fn void test_uniform_quantile() @test
{
	UniformDist dist = distributions::uniform(0.0, 10.0);

	approx(dist.quantile(0.0), 0.0, TOLERANCE);
	approx(dist.quantile(0.25), 2.5, TOLERANCE);
	approx(dist.quantile(0.5), 5.0, TOLERANCE);
	approx(dist.quantile(0.75), 7.5, TOLERANCE);
	approx(dist.quantile(1.0), 10.0, TOLERANCE);
}

fn void test_uniform_round_trip() @test
{
	UniformDist dist = distributions::uniform(0.0, 10.0);

	for (double x = 1.0; x <= 9.0; x += 2.0)
	{
		double p = dist.cdf(x);
		double x_recovered = dist.quantile(p);
		approx(x_recovered, x, TOLERANCE, "CDF/inverse_CDF round-trip");
	}
}

fn void test_uniform_random_samples() @test
{
	UniformDist dist = distributions::uniform(0.0, 10.0);

	DefaultRandom r;
	random::seed_entropy(&r);

	double sum;
	int n_samples = 10_000;

	for (int i = 0; i < n_samples; i++)
	{
		double sample = dist.random(&r);
		sum += sample;
		assert(sample >= 0.0 && sample <= 10.0, "Random sample in range");
	}
	approx(sum/n_samples, 5.000, 0.1);
}

fn void test_normal_mean_variance() @test
{
	NormalDist dist = distributions::normal(10.0, 2.0);
	approx(dist.mean(), 10.0, TOLERANCE);
	approx(dist.variance(), 4.0, TOLERANCE);
}

fn void test_normal_pdf_at_mean() @test
{
	NormalDist std_normal = distributions::normal(0.0, 1.0);
	double pdf_0 = std_normal.pdf(0.0);
	double expected = 1.0 / math::sqrt(2.0 * math::PI);
	approx(pdf_0, expected, TOLERANCE, "PDF at mean");
}

fn void test_normal_pdf_symmetry() @test
{
	NormalDist std_normal = distributions::normal(0.0, 1.0);
	approx(std_normal.pdf(-1.0), std_normal.pdf(1.0), TOLERANCE, "PDF symmetry");
	approx(std_normal.pdf(-2.0), std_normal.pdf(2.0), TOLERANCE, "PDF symmetry at 2");
}

fn void test_normal_cdf_known_values() @test
{
	NormalDist std_normal = distributions::normal(0.0, 1.0);

	approx(std_normal.cdf(0.0),   0.500, TOLERANCE, "CDF at mean");
	approx(std_normal.cdf(-1.96), 0.025, VERY_RELAXED_TOLERANCE, "CDF at -1.96");
	approx(std_normal.cdf(1.96),  0.975, VERY_RELAXED_TOLERANCE, "CDF at 1.96");
	approx(std_normal.cdf(-2.58), 0.005, VERY_RELAXED_TOLERANCE, "CDF at -2.58");
	approx(std_normal.cdf(2.58),  0.995, VERY_RELAXED_TOLERANCE, "CDF at 2.58");
}

fn void test_normal_quantile() @test
{
	NormalDist std_normal = distributions::normal(0.0, 1.0);

	approx(std_normal.quantile(0.5), 0.0, TOLERANCE, "Median");

	double z_975 = std_normal.quantile(0.975);
	approx(z_975, 1.96, 0.02, "97.5th percentile");

	double z_025 = std_normal.quantile(0.025);
	approx(z_025, -1.96, 0.02, "2.5th percentile");
}

fn void test_normal_round_trip() @test
{
	NormalDist std_normal = distributions::normal(0.0, 1.0);

	for (double x = -3.0; x <= 3.0; x += 1.0)
	{
		double p = std_normal.cdf(x);
		double x_recovered = std_normal.quantile(p);
		approx(x_recovered, x, TOLERANCE, "Normal round-trip");
	}
}

fn void test_normal_random_samples() @test
{
	NormalDist std_normal = distributions::normal(0.0, 1.0);

	double sum = 0.0;
	double sum_sq = 0.0;
	int n_samples = 10_000;

	DefaultRandom r;
	random::seed_entropy(&r);

	for (int i = 0; i < n_samples; i++)
	{
		double sample = std_normal.random(&r);
		sum += sample;
		sum_sq += sample * sample;
	}

	double sample_mean = sum / (double)n_samples;
	double sample_var = sum_sq / (double)n_samples - sample_mean * sample_mean;

	approx(sample_mean, 0.0, 0.1, "Sample mean ~0");
	approx(sample_var, 1.0, 0.1, "Sample variance ~1");
}

fn void test_normal_custom_parameters() @test
{
	NormalDist custom = distributions::normal(100.0, 15.0);

	approx(custom.mean(), 100.0, TOLERANCE);
	approx(custom.variance(), 225.0, TOLERANCE);
	approx(custom.cdf(100.0), 0.5, TOLERANCE, "CDF at mean");
}

fn void test_exponential_mean_variance() @test
{
	ExponentialDist dist = distributions::exponential(2.0);
	approx(dist.mean(), 0.5, TOLERANCE);
	approx(dist.variance(), 0.25, TOLERANCE);
}

fn void test_exponential_pdf() @test
{
	ExponentialDist dist = distributions::exponential(2.0);

	approx(dist.pdf(0.0), 2.0, TOLERANCE, "PDF at 0");
	approx(dist.pdf(-1.0), 0.0, TOLERANCE, "PDF for negative x");

	double pdf_1 = dist.pdf(1.0);
	double expected = 2.0 * math::exp(-2.0);
	approx(pdf_1, expected, TOLERANCE, "PDF at 1");
}

fn void test_exponential_cdf() @test
{
	ExponentialDist dist = distributions::exponential(2.0);

	approx(dist.cdf(0.0), 0.0, TOLERANCE, "CDF at 0");
	approx(dist.cdf(-1.0), 0.0, TOLERANCE, "CDF for negative x");

	double cdf_mean = dist.cdf(dist.mean());
	approx(cdf_mean, 1.0 - math::exp(-1.0), TOLERANCE, "CDF at mean");
}

fn void test_exponential_quantile() @test
{
	ExponentialDist dist = distributions::exponential(2.0);

	approx(dist.quantile(0.0), 0.0, TOLERANCE);

	double median = dist.quantile(0.5);
	double expected_median = math::ln(2.0) / 2.0;
	approx(median, expected_median, TOLERANCE, "Median");
}

fn void test_exponential_round_trip() @test
{
	ExponentialDist dist = distributions::exponential(2.0);

	for (double p = 0.1; p <= 0.9; p += 0.1)
	{
		double x = dist.quantile(p);
		double p_recovered = dist.cdf(x);
		approx(p_recovered, p, TOLERANCE, "Exponential round-trip");
	}
}

fn void test_exponential_random_samples() @test
{
	ExponentialDist dist = distributions::exponential(2.0);

	DefaultRandom r;
	random::seed_entropy(&r);

	for (int i = 0; i < 100; i++)
	{
		double sample = dist.random(&r);
		assert(sample >= 0.0, "Random sample non-negative");
	}
}

fn void test_t_mean_variance() @test
{
	TDist dist = distributions::t_distribution(10.0);
	approx(dist.mean(), 0.0, TOLERANCE);
	approx(dist.variance(), 10.0/8.0, TOLERANCE);
}

fn void test_t_pdf_symmetry() @test
{
	TDist dist = distributions::t_distribution(10.0);
	approx(dist.pdf(-1.0), dist.pdf(1.0), TOLERANCE, "PDF symmetry");
	approx(dist.pdf(-2.0), dist.pdf(2.0), TOLERANCE, "PDF symmetry at 2");
}

fn void test_t_cdf_symmetry() @test
{
	TDist dist = distributions::t_distribution(10.0);

	approx(dist.cdf(0.0), 0.5, TOLERANCE, "CDF at 0");

	double cdf_1 = dist.cdf(1.0);
	double cdf_neg_1 = dist.cdf(-1.0);
	approx(cdf_1 + cdf_neg_1, 1.0, TOLERANCE, "CDF symmetry");
}

fn void test_t_quantile() @test
{
	TDist dist = distributions::t_distribution(10.0);

	approx(dist.quantile(0.5), 0.0, RELAXED_TOLERANCE, "Median is 0");

	double upper = dist.quantile(0.975);
	double lower = dist.quantile(0.025);
	approx(upper, -lower, RELAXED_TOLERANCE, "Inverse CDF symmetry");
}


fn void test_t_random_samples() @test
{
	TDist dist = distributions::t_distribution(10.0);

	double sum = 0.0;
	int n_samples = 10_000;

	DefaultRandom r;
	random::seed_entropy(&r);

	for (int i = 0; i < n_samples; i++)
	{
		sum += dist.random(&r);
	}

	double sample_mean = sum / (double)n_samples;
	approx(sample_mean, 0.0, 0.1, "Sample mean ~0");
}

fn void test_f_mean() @test
{
	FDist dist =distributions::f_distribution(5.0, 10.0);
	double expected_mean = 10.0 / 8.0;
	approx(dist.mean(), expected_mean, TOLERANCE);
}

fn void test_f_pdf() @test
{
	// FIXME: add tests for pdf
	FDist dist =distributions::f_distribution(5.0, 10.0);
	approx(dist.pdf(0.5), 0.687607, RELAXED_TOLERANCE, "PDF at x=0.5 for F(5,10) is wrong");
	approx(dist.pdf(10.0), 0.000478163, RELAXED_TOLERANCE, "PDF at x=10.0 for F(5,10) is wrong");
}

fn void test_f_cdf() @test
{
	FDist dist =distributions::f_distribution(5.0, 10.0);
	approx(dist.cdf(0.0), 0.0, TOLERANCE, "CDF at 0");
	approx(dist.cdf(-1.0), 0.0, TOLERANCE, "CDF for negative x");
}

fn void test_f_quantile() @test
{
	FDist dist =distributions::f_distribution(5.0, 10.0);
	double x_median = dist.quantile(0.5);
	assert(x_median > 0.0, "Median positive");

	double x_25 = dist.quantile(0.25);
	double x_75 = dist.quantile(0.75);
	assert(x_75 > x_25, "Inverse CDF monotonic");
}

fn void test_f_random_samples() @test
{
	FDist dist =distributions::f_distribution(5.0, 10.0);

 	DefaultRandom r;
	random::seed_entropy(&r);

	for (int i = 0; i < 100; i++)
	{
		double sample = dist.random(&r);
		assert(sample > 0.0, "Random sample positive");
	}
}

fn void test_chi_squared_mean_variance() @test
{
	ChiSquaredDist dist = distributions::chi_squared(5.0);
	approx(dist.mean(), 5.0, TOLERANCE);
	approx(dist.variance(), 10.0, TOLERANCE);
}

fn void test_chi_squared_pdf() @test
{
	ChiSquaredDist dist = distributions::chi_squared(5.0);
	approx(dist.pdf(-1.0), 0.0, TOLERANCE, "PDF for negative x");
	approx(dist.pdf(1.145), 0.091910, RELAXED_TOLERANCE);
	approx(dist.pdf(5.000), 0.122042, RELAXED_TOLERANCE);
}

fn void test_chi_squared_cdf() @test
{
	ChiSquaredDist dist = distributions::chi_squared(1.0);
	approx(dist.cdf(1.0), 0.682689, RELAXED_TOLERANCE, "");

	dist.k = 5;
	approx(dist.cdf(5.0), 0.5841, VERY_RELAXED_TOLERANCE);
}

fn void test_chi_squared_quantile() @test
{
	ChiSquaredDist dist = distributions::chi_squared(5.0);
	approx(dist.quantile(0.95), 11.0705, 0.1, "95th percentile");
}

fn void test_chi_squared_round_trip() @test
{
	ChiSquaredDist dist = distributions::chi_squared(5.0);
	for (double p = 0.1; p <= 0.9; p += 0.2)
	{
		double x = dist.quantile(p);
		assert(x > 0.0, "Inverse CDF positive");
		double p_recovered = dist.cdf(x);
		approx(p_recovered, p, RELAXED_TOLERANCE, "Chi-squared round-trip");
	}
}

fn void test_chi_squared_random_samples() @test
{
	ChiSquaredDist dist = distributions::chi_squared(5.0);

 	DefaultRandom r;
	random::seed_entropy(&r);

	for (int i = 0; i < 100; i++)
	{
		double sample = dist.random(&r);
		assert(sample >= 0.0, "Random sample non-negative");
	}
}

fn void test_binomial_mean_variance() @test
{
	BinomialDist dist = distributions::binomial(10, 0.5);
	approx(dist.mean(), 5.0, TOLERANCE);
	approx(dist.variance(), 2.5, TOLERANCE);
}

fn void test_binomial_pmf_symmetry() @test
{
	BinomialDist dist = distributions::binomial(10, 0.5);
	for (int k = 0; k <= 5; k++)
	{
		double pmf_k = dist.pmf(k);
		double pmf_10_minus_k = dist.pmf(10 - k);
		approx(pmf_k, pmf_10_minus_k, TOLERANCE, "PMF symmetry");
	}
}

fn void test_binomial_pmf_sums_to_one() @test
{
	BinomialDist dist = distributions::binomial(10, 0.5);
	double sum = 0.0;
	for (int k = 0; k <= 10; k++)
	{
		sum += dist.pmf(k);
	}
	approx(sum, 1.0, TOLERANCE, "PMF sums to 1");
}

fn void test_binomial_cdf() @test
{
	BinomialDist dist = distributions::binomial(10, 0.5);
	approx(dist.cdf(-1), 0.0, TOLERANCE, "CDF below 0");
	approx(dist.cdf(10), 1.0, TOLERANCE, "CDF at n");
	approx(dist.cdf(5), 0.623, VERY_RELAXED_TOLERANCE, "CDF at mean");
}

fn void test_binomial_random_samples() @test
{
	BinomialDist dist = distributions::binomial(10, 0.5);

	DefaultRandom r;
	random::seed_entropy(&r);

	for (int i = 0; i < 100; i++)
	{
		double sample = dist.random(&r);
		assert(sample >= 0.0 && sample <= 10.0, "Random in range");
	}
}

fn void test_poisson_mean_variance() @test
{
	PoissonDist dist = distributions::poisson(3.0);
	approx(dist.mean(), 3.0, TOLERANCE);
	approx(dist.variance(), 3.0, TOLERANCE);
}

fn void test_poisson_pmf_sums_to_one() @test
{
	PoissonDist dist = distributions::poisson(3.0);
	double sum = 0.0;
	for (int k = 0; k <= 20; k++)
	{
		sum += dist.pmf(k);
	}
	assert(sum > 0.99, "PMF sums to ~1");
}

fn void test_poisson_pmf_at_zero() @test
{
	PoissonDist dist = distributions::poisson(3.0);
	double pmf_0 = dist.pmf(0);
	double expected = math::exp(-3.0);
	approx(pmf_0, expected, TOLERANCE, "PMF at 0");
}

fn void test_poisson_cdf() @test
{
	PoissonDist dist = distributions::poisson(3.0);
	approx(dist.cdf(-1), 0.0, TOLERANCE, "CDF for negative k");
}

fn void test_poisson_random_samples() @test
{
	PoissonDist dist = distributions::poisson(3.0);

	DefaultRandom r;
	random::seed_entropy(&r);

	for (int i = 0; i < 100; i++)
	{
		double sample = dist.random(&r);
		assert(sample >= 0.0, "Random non-negative");
	}
}

fn void test_poisson_large_lambda() @test
{
	PoissonDist large = distributions::poisson(50.0);
	approx(large.mean(), 50.0, TOLERANCE, "Large lambda mean");
}

fn void test_beta_function() @test
{
	assert(distributions::beta_function(2.5, 1.5) == distributions::beta_function(1.5, 2.5), "Beta function not symmetrical.");
	assert(math::is_approx(distributions::beta_function(3.0, 2.0), 0.08333333333, 1e-6), "Beta(3,2) is wrong");
	assert(math::is_approx(distributions::beta_function(4.0, 1.0), 1.0/4.0, 1e-6), "Beta(4,1) is wrong");
}

fn void test_lower_incomplete_gamma() @test
{
	approx(distributions::lower_incomplete_gamma(0.5,0.5), 0.682689, 1e-4, "");
}

