using Xunit;

namespace QuanTAlib.Tests;

public class BbandsTests
{
    [Fact]
    public void Bbands_Constructor_ValidParameters()
    {
        // Arrange & Act
        Bbands bbands = new(period: 20, multiplier: 2.0);

        // Assert
        Assert.NotNull(bbands);
        Assert.Equal("Bbands(20,2.0)", bbands.Name);
        Assert.Equal(20, bbands.WarmupPeriod);
        Assert.False(bbands.IsHot);
    }

    [Fact]
    public void Bbands_Constructor_InvalidPeriod_ThrowsArgumentOutOfRangeException()
    {
        // Arrange, Act & Assert
        ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>(
            () => new Bbands(period: 1));
        Assert.Equal("period", exception.ParamName);
    }

    [Fact]
    public void Bbands_Constructor_InvalidMultiplier_ThrowsArgumentOutOfRangeException()
    {
        // Arrange, Act & Assert
        ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>(
            () => new Bbands(period: 20, multiplier: 0.05));
        Assert.Equal("multiplier", exception.ParamName);
    }

    [Fact]
    public void Bbands_Update_ReturnsCorrectMiddleBand()
    {
        // Arrange
        Bbands bbands = new(period: 5, multiplier: 2.0);
        double[] prices = [10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
        DateTime time = DateTime.UtcNow;

        // Act
        TValue result = default;
        for (int i = 0; i < prices.Length; i++)
        {
            result = bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
        }

        // Assert - Middle should be SMA(5) = (11+12+13+14+15)/5 = 13.0
        Assert.Equal(13.0, result.Value, precision: 10);
        Assert.True(bbands.IsHot);
    }

    [Fact]
    public void Bbands_BandCalculations_CorrectValues()
    {
        // Arrange
        Bbands bbands = new(period: 3, multiplier: 2.0);
        DateTime time = DateTime.UtcNow;
        double[] prices = [10.0, 12.0, 14.0];

        // Act
        for (int i = 0; i < prices.Length; i++)
        {
            bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
        }

        // Assert
        // SMA = (10+12+14)/3 = 12.0
        Assert.Equal(12.0, bbands.Middle.Value, precision: 10);

        // StdDev = sqrt(((10-12)^2 + (12-12)^2 + (14-12)^2) / 3) = sqrt(8/3) ≈ 1.6329931618554521
        double expectedStdDev = Math.Sqrt(8.0 / 3.0);
        double expectedUpper = 12.0 + (2.0 * expectedStdDev);
        double expectedLower = 12.0 - (2.0 * expectedStdDev);

        Assert.Equal(expectedUpper, bbands.Upper.Value, precision: 10);
        Assert.Equal(expectedLower, bbands.Lower.Value, precision: 10);
        Assert.Equal(expectedUpper - expectedLower, bbands.Width.Value, precision: 10);
    }

    [Fact]
    public void Bbands_PercentB_CorrectCalculation()
    {
        // Arrange
        Bbands bbands = new(period: 3, multiplier: 2.0);
        DateTime time = DateTime.UtcNow;
        double[] prices = [10.0, 12.0, 14.0, 13.0];

        // Act
        for (int i = 0; i < prices.Length; i++)
        {
            bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
        }

        // Assert
        // For the last value (13.0) with window [12.0, 14.0, 13.0]
        // SMA = 13.0, StdDev = sqrt(2/3), Lower ≈ 11.367, Upper ≈ 14.633
        // %B = (13.0 - Lower) / (Upper - Lower)
        double percentB = bbands.PercentB.Value;
        Assert.True(percentB >= 0.0 && percentB <= 1.0);
    }

    [Fact]
    public void Bbands_IsNew_False_RollsBackCorrectly()
    {
        // Arrange
        Bbands bbands = new(period: 3, multiplier: 2.0);
        DateTime time = DateTime.UtcNow;

        // Act
        bbands.Update(new TValue(time, 10.0), isNew: true);
        bbands.Update(new TValue(time.AddSeconds(1), 12.0), isNew: true);
        bbands.Update(new TValue(time.AddSeconds(2), 14.0), isNew: true);
        double middleBefore = bbands.Middle.Value;

        bbands.Update(new TValue(time.AddSeconds(2), 15.0), isNew: false);
        double middleAfter = bbands.Middle.Value;

        // Assert - Value should change due to replacement
        Assert.NotEqual(middleBefore, middleAfter);
    }

    [Fact]
    public void Bbands_NaN_HandledGracefully()
    {
        // Arrange
        Bbands bbands = new(period: 3, multiplier: 2.0);
        DateTime time = DateTime.UtcNow;

        // Act
        bbands.Update(new TValue(time, 10.0), isNew: true);
        bbands.Update(new TValue(time.AddSeconds(1), 12.0), isNew: true);

        bbands.Update(new TValue(time.AddSeconds(2), double.NaN), isNew: true);
        double afterNaN = bbands.Middle.Value;

        // Assert - Should substitute last valid value
        Assert.True(double.IsFinite(afterNaN));
    }

    [Fact]
    public void Bbands_Reset_ClearsState()
    {
        // Arrange
        Bbands bbands = new(period: 3, multiplier: 2.0);
        DateTime time = DateTime.UtcNow;

        bbands.Update(new TValue(time, 10.0));
        bbands.Update(new TValue(time.AddSeconds(1), 12.0));
        bbands.Update(new TValue(time.AddSeconds(2), 14.0));

        // Act
        bbands.Reset();

        // Assert
        Assert.False(bbands.IsHot);
    }

    [Fact]
    public void Bbands_WarmupPeriod_IsHotTransition()
    {
        // Arrange
        Bbands bbands = new(period: 5, multiplier: 2.0);
        DateTime time = DateTime.UtcNow;

        // Act & Assert
        for (int i = 0; i < 4; i++)
        {
            bbands.Update(new TValue(time.AddSeconds(i), 10.0 + i));
            Assert.False(bbands.IsHot);
        }

        bbands.Update(new TValue(time.AddSeconds(4), 14.0));
        Assert.True(bbands.IsHot);
    }

    [Fact]
    public void Bbands_UpdateTSeries_ReturnsValidSeries()
    {
        // Arrange
        int period = 5;
        Bbands bbands = new(period, multiplier: 2.0);
        var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
        var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
        TSeries source = bars.Close;

        // Act
        TSeries result = bbands.Update(source);

        // Assert
        Assert.Equal(source.Count, result.Count);
        Assert.True(bbands.IsHot);

        // Verify last values match streaming
        Bbands streamingBbands = new(period, multiplier: 2.0);
        for (int i = Math.Max(0, source.Count - period); i < source.Count; i++)
        {
            streamingBbands.Update(source[i], isNew: true);
        }
        Assert.Equal(streamingBbands.Middle.Value, result[^1].Value, precision: 10);
    }

    [Fact]
    public void Bbands_StaticCalculate_ReturnsValidSeries()
    {
        // Arrange
        var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
        var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
        TSeries source = bars.Close;

        // Act
        TSeries result = Bbands.Calculate(source, period: 5, multiplier: 2.0);

        // Assert
        Assert.Equal(source.Count, result.Count);
        // Note: Static Calculate doesn't set IsHot on any instance, just returns the series
    }

    [Fact]
    public void Bbands_SpanCalculate_MatchesTSeries()
    {
        // Arrange
        var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
        var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
        TSeries source = bars.Close;
        int period = 20;
        double multiplier = 2.0;

        // Act - Copy to arrays before Span usage to avoid ref local issues
        double[] sourceArray = source.Values.ToArray();
        double[] middleArray = new double[source.Count];
        double[] upperArray = new double[source.Count];
        double[] lowerArray = new double[source.Count];
        Bbands.Calculate(sourceArray.AsSpan(), middleArray, upperArray, lowerArray, period, multiplier);

        TSeries seriesResult = Bbands.Calculate(source, period, multiplier);

        // Assert - Compare last 10 values
        for (int i = source.Count - 10; i < source.Count; i++)
        {
            Assert.Equal(seriesResult[i].Value, middleArray[i], precision: 10);
        }
    }

    [Fact]
    public void Bbands_SpanCalculate_InvalidLength_ThrowsArgumentException()
    {
        // Arrange
        double[] sourceArr = new double[10];
        double[] middleArr = new double[10];
        double[] upperArr = new double[10];
        double[] lowerArr = new double[9]; // Wrong length

        // Act & Assert
        ArgumentException exception = Assert.Throws<ArgumentException>(
            () => Bbands.Calculate(sourceArr.AsSpan(), middleArr.AsSpan(), upperArr.AsSpan(), lowerArr.AsSpan()));
        Assert.Equal("source", exception.ParamName);
    }

    [Fact]
    public void Bbands_Chainability_WorksCorrectly()
    {
        // Arrange
        var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
        var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
        TSeries source = bars.Close;
        var pubSource = new TSeries();
        Bbands bbands1 = new(pubSource, period: 5, multiplier: 2.0);
        Bbands bbands2 = new(bbands1, period: 3, multiplier: 1.5);

        // Act
        bool eventFired = false;
        bbands2.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;

        foreach (var item in source)
        {
            pubSource.Add(item);
        }

        // Assert
        Assert.True(eventFired);
        Assert.True(bbands2.IsHot);
    }

    [Fact]
    public void Bbands_Consistency_StreamingVsBatchVsSpan()
    {
        // Arrange
        var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
        var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
        TSeries source = bars.Close;
        int period = 20;
        double multiplier = 2.0;

        // Streaming
        Bbands streamingBbands = new(period, multiplier);
        foreach (var item in source)
        {
            streamingBbands.Update(item);
        }

        // Batch
        TSeries batchResult = Bbands.Calculate(source, period, multiplier);

        // Span - Copy arrays before using to avoid ref local lambda issue
        double[] sourceArray = source.Values.ToArray();
        double[] middleArray = new double[source.Count];
        double[] upperArray = new double[source.Count];
        double[] lowerArray = new double[source.Count];
        Bbands.Calculate(sourceArray.AsSpan(), middleArray, upperArray, lowerArray, period, multiplier);

        // Assert - Compare last 50 values (streaming only has last value)
        Assert.Equal(batchResult[^1].Value, streamingBbands.Middle.Value, precision: 8);
        Assert.Equal(middleArray[^1], streamingBbands.Middle.Value, precision: 8);
    }
}
