namespace QuanTAlib.Tests;

/// <summary>
/// Validation tests for YZVAMA (Yang-Zhang Volatility Adjusted Moving Average).
/// Validates mathematical properties rather than comparing against an external library.
/// </summary>
public class YzvamaValidationTests
{
    private const double Tolerance = 1e-10;

    [Fact]
    public void Yzvama_ZeroVolatility_EqualsMaxLength_SMA()
    {
        const int maxLength = 30;
        var yzvama = new Yzvama(yzvShortPeriod: 3, yzvLongPeriod: 50, percentileLookback: 50, minLength: 5, maxLength: maxLength);
        var sma = new Sma(maxLength);

        // Using close-only input creates synthetic bars with O=H=L=C which yields yzv_short=0,
        // thus percentile ~ 0 and adjusted length ~= maxLength.
        var values = Enumerable.Range(1, 300).Select(i => (double)i).ToArray();
        foreach (var val in values)
        {
            var tv = new TValue(DateTime.UtcNow, val);
            yzvama.Update(tv, isNew: true);
            sma.Update(tv, isNew: true);
        }

        Assert.Equal(sma.Last.Value, yzvama.Last.Value, 1.0);
    }

    [Fact]
    public void Yzvama_ConstantInput_OutputEqualsInput()
    {
        var yzvama = new Yzvama();
        const double constantValue = 42.5;

        for (int i = 0; i < 300; i++)
        {
            yzvama.Update(new TValue(DateTime.UtcNow, constantValue), isNew: true);
        }

        Assert.Equal(constantValue, yzvama.Last.Value, Tolerance);
    }

    [Fact]
    public void Yzvama_OutputWithinInputRange()
    {
        var yzvama = new Yzvama();
        var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 123);
        var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));

        double minInput = double.MaxValue;
        double maxInput = double.MinValue;
        var outputs = new List<double>();

        foreach (var bar in bars)
        {
            minInput = Math.Min(minInput, bar.Close);
            maxInput = Math.Max(maxInput, bar.Close);
            outputs.Add(yzvama.Update(bar, isNew: true).Value);
        }

        var hotOutputs = outputs.Skip(150).ToList();
        foreach (var output in hotOutputs)
        {
            Assert.True(output >= minInput - 1 && output <= maxInput + 1,
                $"Output {output} should be within input range [{minInput}, {maxInput}]");
        }
    }

    [Fact]
    public void Yzvama_VolatilitySpike_DrivesTowardMinLength()
    {
        // Prime with a stable low-volatility regime (yzv_short ~ 0 => percentile low => maxLength)
        const int percentileLookback = 20;
        const int minLength = 5;
        const int maxLength = 50;

        var yzvama = new Yzvama(yzvShortPeriod: 3, yzvLongPeriod: 50, percentileLookback: percentileLookback, minLength: minLength, maxLength: maxLength);
        long t = DateTime.UtcNow.Ticks;

        for (int i = 0; i < percentileLookback; i++)
        {
            var bar = new TBar(t + i, 100, 100, 100, 100, 0);
            yzvama.Update(bar, isNew: true);
        }

        // One large-range bar should rank at the top of the volatility buffer (percentile ~= 100),
        // which maps adjusted length to minLength. The SMA then uses the last minLength closes.
        var spike = new TBar(t + percentileLookback, 100, 200, 50, 200, 0);
        var result = yzvama.Update(spike, isNew: true);

        // Last 5 closes: 100,100,100,100,200 => average = 120
        Assert.Equal(120.0, result.Value, 1e-6);
    }
}

