
namespace QuanTAlib.Tests;

public class GBMTests
{
    [Fact]
    public void Next_DefaultParameter_GeneratesNewBar()
    {
        var gbm = new GBM(startPrice: 100.0);
        
        var bar1 = gbm.Next();
        var bar2 = gbm.Next();
        
        Assert.NotEqual(bar1.Time, bar2.Time);
        Assert.True(bar2.Time > bar1.Time);
    }

    [Fact]
    public void Next_IsNewTrue_AdvancesToNewBar()
    {
        var gbm = new GBM(startPrice: 100.0);
        
        var bar1 = gbm.Next(isNew: true);
        var bar2 = gbm.Next(isNew: true);
        
        Assert.NotEqual(bar1.Time, bar2.Time);
        Assert.True(bar2.Time > bar1.Time);
    }

    [Fact]
    public void Next_IsNewFalse_UpdatesCurrentBar()
    {
        var gbm = new GBM(startPrice: 100.0);
        
        var bar1 = gbm.Next(isNew: true);
        long initialTime = bar1.Time;
        
        var bar2 = gbm.Next(isNew: false);
        
        Assert.Equal(initialTime, bar2.Time);
        // Price likely changed (GBM random walk)
        Assert.NotEqual(bar1.Close, bar2.Close);
    }

    [Fact]
    public void Next_RefBool_HonorsRequest()
    {
        var gbm = new GBM(startPrice: 100.0);
        
        // GBM always honors isNew - parameter should remain unchanged
        bool isNew1 = true;
        var bar1 = gbm.Next(ref isNew1);
        Assert.True(isNew1, "GBM should honor isNew=true request");
        
        bool isNew2 = false;
        long time1 = bar1.Time;
        var bar2 = gbm.Next(ref isNew2);
        Assert.False(isNew2, "GBM should honor isNew=false request");
        Assert.Equal(time1, bar2.Time);
        
        bool isNew3 = true;
        var bar3 = gbm.Next(ref isNew3);
        Assert.True(isNew3, "GBM should honor isNew=true request");
        Assert.NotEqual(time1, bar3.Time);
    }

    [Fact]
    public void Fetch_GeneratesCorrectCount()
    {
        var gbm = new GBM(startPrice: 100.0);
        int count = 10;
        long startTime = DateTime.UtcNow.Ticks;
        var interval = TimeSpan.FromMinutes(1);
        
        var series = gbm.Fetch(count, startTime, interval);
        
        Assert.Equal(count, series.Count);
    }

    [Fact]
    public void Fetch_GeneratesSequentialBars()
    {
        var gbm = new GBM(startPrice: 100.0);
        long startTime = DateTime.UtcNow.Ticks;
        var interval = TimeSpan.FromMinutes(1);
        
        var series = gbm.Fetch(5, startTime, interval);
        
        // Verify time sequence
        for (int i = 1; i < series.Count; i++)
        {
            Assert.True(series[i].Time > series[i - 1].Time);
        }
    }

    [Fact]
    public void Fetch_RespectsInterval()
    {
        var gbm = new GBM(startPrice: 100.0);
        var interval = TimeSpan.FromHours(1);
        long startTime = DateTime.UtcNow.Ticks;
        
        var series = gbm.Fetch(5, startTime, interval);
        
        // Verify interval spacing
        for (int i = 1; i < series.Count; i++)
        {
            long expectedDiff = interval.Ticks;
            long actualDiff = series[i].Time - series[i - 1].Time;
            Assert.Equal(expectedDiff, actualDiff);
        }
    }

    [Fact]
    public void Fetch_StartsAtSpecifiedTime()
    {
        var gbm = new GBM(startPrice: 100.0);
        var startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks;
        var interval = TimeSpan.FromMinutes(5);
        
        var series = gbm.Fetch(3, startTime, interval);
        
        Assert.Equal(startTime, series[0].Time);
        Assert.Equal(startTime + interval.Ticks, series[1].Time);
        Assert.Equal(startTime + 2 * interval.Ticks, series[2].Time);
    }

    [Fact]
    public void Fetch_WithDifferentIntervals_WorksCorrectly()
    {
        var gbm = new GBM(startPrice: 100.0);
        long startTime = DateTime.UtcNow.Ticks;
        
        // Test different intervals
        var intervals = new[] { 
            TimeSpan.FromMinutes(1), 
            TimeSpan.FromMinutes(5), 
            TimeSpan.FromHours(1) 
        };
        
        foreach (var interval in intervals)
        {
            var series = gbm.Fetch(3, startTime, interval);
            
            // Verify spacing
            for (int i = 1; i < series.Count; i++)
            {
                long expectedDiff = interval.Ticks;
                long actualDiff = series[i].Time - series[i - 1].Time;
                Assert.Equal(expectedDiff, actualDiff);
            }
        }
    }

    [Fact]
    public void GeneratesRealisticOHLCV()
    {
        var gbm = new GBM(startPrice: 100.0);
        long startTime = DateTime.UtcNow.Ticks;
        var interval = TimeSpan.FromMinutes(1);
        var series = gbm.Fetch(10, startTime, interval);
        
        for (int i = 0; i < series.Count; i++)
        {
            var bar = series[i];
            
            // High should be >= max(Open, Close)
            Assert.True(bar.High >= Math.Max(bar.Open, bar.Close));
            
            // Low should be <= min(Open, Close)
            Assert.True(bar.Low <= Math.Min(bar.Open, bar.Close));
            
            // Volume should be positive
            Assert.True(bar.Volume > 0);
            
            // All prices should be positive
            Assert.True(bar.Open > 0);
            Assert.True(bar.High > 0);
            Assert.True(bar.Low > 0);
            Assert.True(bar.Close > 0);
        }
    }

    [Fact]
    public void IntraBarUpdates_ModifyCurrentBar()
    {
        var gbm = new GBM(startPrice: 100.0);
        
        var bar1 = gbm.Next(isNew: true);
        long initialTime = bar1.Time;
        double initialClose = bar1.Close;
        
        // Loop until price changes (random walk might stay same but unlikely)
        bool changed = false;
        for (int i = 0; i < 10; i++)
        {
            var bar = gbm.Next(isNew: false);
            Assert.Equal(initialTime, bar.Time);
            if (Math.Abs(bar.Close - initialClose) > double.Epsilon)
            {
                changed = true;
                break;
            }
        }
        
        Assert.True(changed, "Price should change during intra-bar updates");
    }

    [Fact]
    public void MixedStreamingAndBatch_WorksCorrectly()
    {
        var gbm = new GBM(startPrice: 100.0);
        
        // Start with streaming
        _ = gbm.Next();
        var bar2 = gbm.Next();
        
        // Batch generation with explicit time
        long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks;
        var interval = TimeSpan.FromMinutes(1);
        var series = gbm.Fetch(3, startTime, interval);
        
        Assert.True(series[0].Time > bar2.Time);
        Assert.Equal(3, series.Count);
        
        // Continue streaming after batch (uses internal state)
        var bar3 = gbm.Next();
        Assert.True(bar3.Time > series[2].Time);
    }

    [Fact]
    public void DriftAndVolatility_AffectPriceMovement()
    {
        // High volatility should produce more price variation
        var gbmLowVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.01);
        var gbmHighVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5);
        
        long startTime = DateTime.UtcNow.Ticks;
        var interval = TimeSpan.FromMinutes(1);
        var seriesLow = gbmLowVol.Fetch(100, startTime, interval);
        var seriesHigh = gbmHighVol.Fetch(100, startTime, interval);
        
        // Calculate price ranges
        double rangeLow = seriesLow[99].Close - seriesLow[0].Open;
        double rangeHigh = seriesHigh[99].Close - seriesHigh[0].Open;
        
        // High volatility should generally produce larger absolute movements
        Assert.True(Math.Abs(rangeHigh) > Math.Abs(rangeLow) * 0.5);
    }

    [Fact]
    public void ConsecutiveCalls_MaintainContinuity()
    {
        var gbm = new GBM(startPrice: 100.0);
        
        var previousBar = gbm.Next();
        var currentBar = gbm.Next();
        
        // currentBar.Open should equal previousBar.Close (continuity)
        Assert.Equal(previousBar.Close, currentBar.Open);
    }

    [Fact]
    public void Stateless_NoHistoryStorage()
    {
        var gbm = new GBM(startPrice: 100.0);
        
        // Generate multiple bars
        for (int i = 0; i < 100; i++)
        {
            _ = gbm.Next();
        }
        
        // GBM should not expose any history storage
        var type = gbm.GetType();
        var barsProperty = type.GetProperty("Bars");
        
        Assert.Null(barsProperty);
    }
}
