using NUnit.Framework;
using e_suite.Database.Core.Helpers;
using Microsoft.EntityFrameworkCore;
using Moq;

namespace e_suite.Database.Core.UnitTests;

[TestFixture]
public class DeltaUnitTests
{
    private static DbSet<T> GetQueryableMockDbSet<T>(List<T> sourceList) where T : class
    {
        var queryable = sourceList.AsQueryable();

        var dbSet = new Mock<DbSet<T>>();
        dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
        dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
        dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
        dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
        dbSet.Setup(d => d.Add(It.IsAny<T>())).Callback<T>((s) => sourceList.Add(s));
        dbSet.Setup(d => d.RemoveRange(It.IsAny<IEnumerable<T>>())).Callback<IEnumerable<T>>(
            items =>
            {
                foreach (var item in items)
                {
                    sourceList.Remove(item);
                }
            });

        dbSet.Setup(d => d.AddRangeAsync(It.IsAny<IEnumerable<T>>(), It.IsAny<CancellationToken>())).Callback<IEnumerable<T>, CancellationToken>(
            (items, cancellationToken) =>
            {
                foreach (var item in items)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    sourceList.Add(item);
                }
            });

        return dbSet.Object;
    }

    private static void AssertListContents(IReadOnlyList<string> listToTest, List<string> list)
    {
        foreach (var item in list)
            Assert.That(listToTest.Contains(item), Is.True);
    }
    private static void AssertDeltaMatches(IReadOnlyList<DeltaMatch<string>> actualResultMatches, List<string> list)
    {
        foreach (var item in actualResultMatches)
        {
            var originalIndex = list.IndexOf(item.Original);
            var updatedIndex = list.IndexOf(item.Updated);

            Assert.That(originalIndex, Is.GreaterThan(-1));
            Assert.That(updatedIndex, Is.GreaterThan(-1));
            Assert.That(originalIndex, Is.EqualTo(updatedIndex));
        }
    }

    [Test]
    public void CalculateDelta_CalculatesAdditions()
    {
        //Arrange
        var originalList = new List<string>();
        var updatedList = new List<string> { "a", "b", "c" };

        //Act
        var actualResult = Delta<string>.CalculateDelta(updatedList, originalList, StringComparer.InvariantCultureIgnoreCase );

        //Assert
        Assert.That(actualResult.Additions.Count, Is.EqualTo(3));
        AssertListContents(actualResult.Additions, ["a", "b", "c"]);
        Assert.That(actualResult.Deletions.Count, Is.EqualTo(0));
        Assert.That(actualResult.Matches.Count, Is.EqualTo(0));
    }
    
    [Test]
    public void CalculateDelta_CalculatesDeletions()
    {
        //Arrange
        var originalList = new List<string> { "d", "e", "f" };
        var updatedList = new List<string>();

        //Act
        var actualResult = Delta<string>.CalculateDelta(updatedList, originalList, StringComparer.InvariantCultureIgnoreCase);

        //Assert
        Assert.That(actualResult.Additions.Count, Is.EqualTo(0));
        Assert.That(actualResult.Deletions.Count, Is.EqualTo(3));
        AssertListContents(actualResult.Deletions, ["d", "e", "f"]);
        Assert.That(actualResult.Matches.Count, Is.EqualTo(0));
    }

    [Test]
    public void CalculateDelta_CalculatesMatches()
    {
        //Arrange
        var originalList = new List<string> { "g", "h", "i" };
        var updatedList = new List<string> { "g", "h", "i" };

        //Act
        var actualResult = Delta<string>.CalculateDelta(updatedList, originalList, StringComparer.InvariantCultureIgnoreCase);

        //Assert
        Assert.That(actualResult.Additions.Count, Is.EqualTo(0));
        Assert.That(actualResult.Deletions.Count, Is.EqualTo(0));
        Assert.That(actualResult.Matches.Count, Is.EqualTo(3));
        AssertDeltaMatches(actualResult.Matches, ["g", "h", "i"]);
    }

    [Test]
    public void CalculateDelta_CanCalulateACombinationOfAddDeleteAndMatch()
    {
        //Arrange
        var originalList = new List<string> { "j", "k", "l", "p", "q", "r" };
        var updatedList = new List<string> { "m", "n", "o", "p", "q", "r" };

        //Act
        var actualResult = Delta<string>.CalculateDelta(updatedList, originalList, StringComparer.InvariantCultureIgnoreCase);

        //Assert
        Assert.That(actualResult.Additions.Count, Is.EqualTo(3));
        AssertListContents(actualResult.Additions, ["m", "n", "o"]);
        Assert.That(actualResult.Deletions.Count, Is.EqualTo(3));
        AssertListContents(actualResult.Deletions, ["j", "k", "l"]);
        Assert.That(actualResult.Matches.Count, Is.EqualTo(3));
        AssertDeltaMatches(actualResult.Matches, ["p", "q", "r"]);
    }

    [Test]
    public void CalculateDelta_CancellationTokenThrowsExceptionWhenCancelled()
    {
        //Arrange
        var originalList = new List<string> { "j", "k", "l", "p", "q", "r" };
        var updatedList = new List<string> { "m", "n", "o", "p", "q", "r" };

        CancellationTokenSource cts = new();
        CancellationToken cancellationToken = cts.Token;
        cts.Cancel();
        
        //Assert
        Assert.Throws<OperationCanceledException>(() =>
        {
            //Act
            var actualResult = Delta<string>.CalculateDelta(updatedList, originalList,
                StringComparer.InvariantCultureIgnoreCase, cancellationToken);
        });
    }

    [Test]
    public void UpdateDbSetAsync_TokenCancelled_ThrowsException()
    {
        //Arrange
        var originalList = new List<string> { "j", "k", "l", "p", "q", "r" };
        var updatedList = new List<string> { "m", "n", "o", "p", "q", "r" };
        CancellationTokenSource cts = new();
        CancellationToken cancellationToken = cts.Token;

        var delta = Delta<string>.CalculateDelta(updatedList, originalList,
            StringComparer.InvariantCultureIgnoreCase, cancellationToken);


        cts.Cancel();

        var mockDBSet = GetQueryableMockDbSet(originalList);

        //Assert
        Assert.ThrowsAsync<OperationCanceledException>(async () =>
        {
            //Act
            await delta.UpdateDbSetAsync(mockDBSet, cancellationToken);
        });
    }

    [Test]
    public async Task UpdateDbSetAsync_UpdateCompletes_UpdatedDBSetContainsExpectedValues()
    {
        //Arrange
        var sourceList = new List<string> { "j", "k", "l", "p", "q", "r" };
        var mockDbSet = GetQueryableMockDbSet(sourceList);

        var originalList = new List<string>(sourceList);
        var updatedList = new List<string> { "m", "n", "o", "p", "q", "r" };
        CancellationTokenSource cts = new();
        CancellationToken cancellationToken = cts.Token;

        var delta = Delta<string>.CalculateDelta(updatedList, originalList,
            StringComparer.InvariantCultureIgnoreCase, cancellationToken);
        
        //Act
        await delta.UpdateDbSetAsync(mockDbSet, cancellationToken);

        //Assert
        Assert.That(mockDbSet.Count(), Is.EqualTo(6) );

        foreach (var entry in mockDbSet)
            Assert.That(updatedList.Contains(entry), Is.True);
    }
}