﻿using Xunit;
using System;
using System.Linq;
using LanguageExt;
using static LanguageExt.Prelude;
using static LanguageExt.List;

namespace LanguageExt.Tests
{
    public class ListTests
    {
        [Fact]
        public void ConsTest1()
        {
            var test = Prelude.Cons(1, Prelude.Cons(2, Prelude.Cons(3, Prelude.Cons(4, Prelude.Cons(5, LanguageExt.List.empty<int>())))));

            var array = test.ToArray();

            Assert.True(array[0] == 1);
            Assert.True(array[1] == 2);
            Assert.True(array[2] == 3);
            Assert.True(array[3] == 4);
            Assert.True(array[4] == 5);
        }

        [Fact]
        public void ListConstruct()
        {
            var test = List(1, 2, 3, 4, 5);

            var array = test.ToArray();

            Assert.True(array[0] == 1);
            Assert.True(array[1] == 2);
            Assert.True(array[2] == 3);
            Assert.True(array[3] == 4);
            Assert.True(array[4] == 5);
        }

        [Fact]
        public void MapTest()
        {
            // Generates 10,20,30,40,50
            var input = List(1, 2, 3, 4, 5);
            var output1 = map(input, x => x * 10);

            // Generates 30,40,50
            var output2 = filter(output1, x => x > 20);

            // Generates 120
            var output3 = fold(output2, 0, (x, s) => s + x);

            Assert.True(output3 == 120);
        }

        [Fact]
        public void ReduceTest()
        {
            // Generates 10,20,30,40,50
            var input = List(1, 2, 3, 4, 5);
            var output1 = map(input, x => x * 10);

            // Generates 30,40,50
            var output2 = filter(output1, x => x > 20);

            // Generates 120
            var output3 = reduce(output2, (x, s) => s + x);

            Assert.True(output3 == 120);
        }

        [Fact]
        public void MapTestFluent()
        {
            var res = List(1, 2, 3, 4, 5)
                        .Map(x => x * 10)
                        .Filter(x => x > 20)
                        .Fold(0, (x, s) => s + x);

            Assert.True(res == 120);
        }

        [Fact]
        public void ReduceTestFluent()
        {
            var res = List(1, 2, 3, 4, 5)
                        .Map(x => x * 10)
                        .Filter(x => x > 20)
                        .Reduce((x, s) => s + x);

            Assert.True(res == 120);
        }

        [Fact]
        public void RangeTest1()
        {
            var r = Range(0, 10).AsEnumerable();
            for (int i = 0; i < 10; i++)
            {
                Assert.True(r.First() == i);
                r = r.Skip(1);
            }
        }

        [Fact]
        public void RangeTest2()
        {
            var r = Range(0, 100, 10).AsEnumerable();
            for (int i = 0; i < 10; i+=10)
            {
                Assert.True(r.First() == i);
                r = r.Skip(1);
            }
        }

        [Fact]
        public void RangeTest3()
        {
            var r = Range(Range(0, 5), Range(10, 20));

            for (int i = 0; i < 5; i ++)
            {
                Assert.True(r.First() == i);
                r = r.Skip(1);
            }

            for (int i = 0; i < 10; i++)
            {
                Assert.True(r.First() == i + 10);
                r = r.Skip(1);
            }
        }

        [Fact]
        public void RangeTest4()
        {
            var r = Range('a', 'f');
            Assert.True(String.Join("", r) == "abcdef");
        }

        [Fact]
        public void RangeTest5()
        {
            var r = Range('f', 'a');
            Assert.True(String.Join("", r) == "fedcba");
        }

        [Fact]
        public void RepeatTest()
        {
            var r = repeat("Hello", 10);

            foreach (var item in r)
            {
                Assert.True(item == "Hello");
            }
        }


        [Fact]
        public void GenerateTest()
        {
            var r = generate(10, i => "Hello " + i );

            for (int i = 0; i < 10; i++)
            {
                Assert.True(r.First() == "Hello "+i);
                r = r.Skip(1);
            }
        }

        [Fact]
        public void UnfoldTest()
        {
            var test = List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181);

            var fibs = take(unfold((0, 1), tup => map(tup, (a, b) => Some((a, (b, a + b))))), 20);

            Assert.True( test.SequenceEqual(fibs) );
        }

        [Fact]
        public void UnfoldTupleTest()
        {
            var test = List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181);

            var fibs = take( unfold( (0, 1), (a, b) => Some((a, b, a + b)) ), 20);

            Assert.True(test.SequenceEqual(fibs));
        }

        [Fact]
        public void UnfoldSingleTest()
        {
            var e = new Exception("Outer", new Exception("Inner"));

            var list = unfold(e, (state) =>
                           state == null
                               ? None
                               : Optional(state.InnerException)
                               );

            var res = list.ToList();

            Assert.True(res[0].Message == "Outer" && res[1].Message == "Inner");
        }

        [Fact]
        public void ReverseListTest1()
        {
            var list = List(1, 2, 3, 4, 5);
            var rev = list.Rev();

            Assert.True(rev[0] == 5);
            Assert.True(rev[4] == 1);
        }

        [Fact]
        public void ReverseListTest2()
        {
            var list = List(1, 2, 3, 4, 5);
            var rev = list.Rev();

            Assert.True(rev.IndexOf(1) == 4, "Should have been 4, actually is: "+ rev.IndexOf(1));
            Assert.True(rev.IndexOf(5) == 0, "Should have been 0, actually is: " + rev.IndexOf(5));
        }

        [Fact]
        public void ReverseListTest3()
        {
            var list = List(1, 1, 2, 2, 2);
            var rev = list.Rev();

            Assert.True(rev.LastIndexOf(1) == 4, "Should have been 4, actually is: " + rev.LastIndexOf(1));
            Assert.True(rev.LastIndexOf(2) == 2, "Should have been 2, actually is: " + rev.LastIndexOf(5));
        }

        [Fact]
        public void OpEqualTest()
        {
            var goodOnes = List(
                Tuple(List(1, 2, 3), List(1, 2, 3)),
                Tuple(Lst<int>.Empty, Lst<int>.Empty)
            );
            var badOnes = List(
                Tuple(List(1, 2, 3), List(1, 2, 4)),
                Tuple(List(1, 2, 3), Lst<int>.Empty)
            );

            goodOnes.Iter(t => t.Iter((fst, snd) =>
            {
                Assert.True(fst == snd, $"'{fst}' == '{snd}'");
                Assert.False(fst != snd, $"'{fst}' != '{snd}'");
            }));

            badOnes.Iter(t => t.Iter((fst, snd) =>
            {
                Assert.True(fst != snd, $"'{fst}' != '{snd}'");
                Assert.False(fst == snd, $"'{fst}' == '{snd}'");
            }));
        }

        [Fact]
        public void IterSimpleTest()
        {
            var embeddedSideEffectResult = 0;
            var expression = from dummy in Some(unit)
                from i in List(2, 3, 5)
                let _ = fun(() => embeddedSideEffectResult += i)()
                select i;

            Assert.Equal(0, embeddedSideEffectResult);

            var sideEffectByAction = 0;

            expression.Iter(i => sideEffectByAction += i * i);
            Assert.Equal(2 + 3 + 5, embeddedSideEffectResult);
            Assert.Equal(2 * 2 + 3 * 3 + 5 * 5, sideEffectByAction);
        }

        [Fact]
        public void IterPositionalTest()
        {
            var embeddedSideEffectResult = 0;
            var expression = from dummy in Some(unit)
                from i in List(2, 3, 5)
                let _ = fun(() => embeddedSideEffectResult += i)()
                select i;

            Assert.Equal(0, embeddedSideEffectResult);

            var sideEffectByAction = 0;

            expression.Iter((pos, i) => sideEffectByAction += i * pos);
            Assert.Equal(2 + 3 + 5, embeddedSideEffectResult);
            Assert.Equal(2 * 0 + 3 * 1 + 5 * 2, sideEffectByAction);
        }

        [Fact]
        public void ConsumeTest()
        {
            var embeddedSideEffectResult = 0;
            System.Collections.Generic.IEnumerable<int> expression = from dummy in Some(unit)
                from i in List(2, 3, 5)
                let _ = fun(() => embeddedSideEffectResult += i)()
                select i;

            Assert.Equal(0, embeddedSideEffectResult);
            expression.Consume();
            Assert.Equal(2 + 3+ 5, embeddedSideEffectResult);
        }
        
        [Fact]
        public void SkipLastTest1()
        {
            var list = List(1, 2, 3, 4, 5);

            var skipped = list.SkipLast().Freeze();

            Assert.True(skipped == List(1, 2, 3, 4));
        }

        [Fact]
        public void SkipLastTest2()
        {
            var list = List<int>();

            var skipped = list.SkipLast().Freeze();

            Assert.True(skipped == list);
        }

        [Fact]
        public void SkipLastTest3()
        {
            var list = List(1, 2, 3, 4, 5);

            var skipped = list.SkipLast(2).Freeze();

            Assert.True(skipped == List(1, 2, 3));
        }

        [Fact]
        public void SkipLastTest4()
        {
            var list = List<int>();

            var skipped = list.SkipLast(2).Freeze();

            Assert.True(skipped == list);
        }

        [Fact]
        public void SetItemTest()
        {
            Lst<int> lint = new Lst<int>();
            lint = lint.Insert(0, 0).Insert(1, 1).Insert(2, 2).Insert(3, 3);

            Assert.True(lint[0] == 0);
            Assert.True(lint[1] == 1);
            Assert.True(lint[2] == 2);
            Assert.True(lint[3] == 3);

            lint = lint.SetItem(2, 500);

            Assert.True(lint[0] == 0);
            Assert.True(lint[1] == 1);
            Assert.True(lint[2] == 500);
            Assert.True(lint[3] == 3);
        }

        [Fact]
        public void RemoveAllTest()
        {
            var test = List(1, 2, 3, 4, 5);
            Assert.True(test.RemoveAll(x => x % 2 == 0) == List(1, 3, 5));
        }

        [Fact]
        public void RemoveAtInsertTest()
        {
            Lst<int> lint = new Lst<int>();
            lint = lint.Insert(0, 0).Insert(1, 1).Insert(2, 2).Insert(3, 3);

            Assert.True(lint[0] == 0);
            Assert.True(lint[1] == 1);
            Assert.True(lint[2] == 2);
            Assert.True(lint[3] == 3);

            lint = lint.RemoveAt(2);

            Assert.True(lint[0] == 0);
            Assert.True(lint[1] == 1);
            Assert.True(lint[2] == 3);

            lint = lint.Insert(2, 500);

            Assert.True(lint[0] == 0);
            Assert.True(lint[1] == 1);
            Assert.True(lint[2] == 500);
            Assert.True(lint[3] == 3);
        }

        [Fact]
        public void SetItemManyTest()
        {
            var range = Range(0, 100).Freeze();
            for (int i = 0; i < 100; i++)
            {
                range = range.SetItem(i, i * 2);
                Assert.True(range[i] == i * 2);
                for(var b = 0; b < i; b++)
                {
                    Assert.True(range[b] == b * 2);
                }
                for (var a = i + 1; a < 100; a++)
                {
                    Assert.True(range[a] == a);
                }
            }
        }

        [Fact]
        public void RemoveAtInsertManyTest()
        {
            var range = Range(0, 100).Freeze();
            for (int i = 0; i < 100; i++)
            {
                range = range.RemoveAt(i);
                Assert.True(range.Count == 99);
                range = range.Insert(i, i * 2);
                Assert.True(range[i] == i * 2);
                for (var b = 0; b < i; b++)
                {
                    Assert.True(range[b] == b * 2);
                }
                for (var a = i + 1; a < 100; a++)
                {
                    Assert.True(range[a] == a);
                }
            }
        }

        [Fact]
        public void EqualsTest()
        {
            Assert.False(List(1, 2, 3).Equals(List<int>()));
            Assert.False(List<int>().Equals(List<int>(1, 2, 3)));
            Assert.True(List<int>().Equals(List<int>()));
            Assert.True(List<int>(1).Equals(List<int>(1)));
            Assert.True(List<int>(1, 2).Equals(List<int>(1, 2)));
            Assert.False(List<int>(1, 2).Equals(List<int>(1, 2, 3)));
            Assert.False(List<int>(1, 2, 3).Equals(List<int>(1, 2)));
        }

        [Fact]
        public void ListShouldRemoveByReference()
        {
            var o0 = new Object();
            var o1 = new Object();
            var o2 = new Object();
            var l = List(o0, o1);
            l = l.Remove(o2);
            Assert.Equal(2, l.Count);
            l = l.Remove(o0);
            Assert.Equal(1, l.Count);
            l = l.Remove(o1);
            Assert.Equal(0, l.Count);
        }

        [Fact]
        public void ListShouldRemoveByReferenceForReverseLists()
        {
            var o0 = new Object();
            var o1 = new Object();
            var o2 = new Object();
            var l = List(o0, o1).Reverse();
            l = l.Remove(o2);
            Assert.Equal(2, l.Count);
            l = l.Remove(o0);
            Assert.Equal(1, l.Count);
            l = l.Remove(o1);
            Assert.Equal(0, l.Count);
        }

        [Fact]
        public void FoldTest()
        {
            var input = List(1, 2, 3, 4, 5);
            var output1 = fold(input, "", (s, x) => s + x.ToString());
            Assert.Equal("12345", output1);
        }

        [Fact]
        public void FoldBackTest()
        {
            var input = List(1, 2, 3, 4, 5);
            var output1 = foldBack(input, "", (s, x) => s + x.ToString());
            Assert.Equal("54321", output1);
        }

        [Fact]
        public void FoldWhileTest()
        {
            var input = List(10, 20, 30, 40, 50);

            var output1 = foldWhile(input, "", (s, x) => s + x.ToString(), x => x < 40);
            Assert.Equal("102030", output1);

            var output2 = foldWhile(input, "", (s, x) => s + x.ToString(), (string s) => s.Length < 6);
            Assert.Equal("102030", output2);

            var output3 = foldWhile(input, 0, (s, x) => s + x, preditem: x => x < 40);
            Assert.Equal(60, output3);

            var output4 = foldWhile(input, 0, (s, x) => s + x, predstate: s => s < 60);
            Assert.Equal(60, output4);
        }

        [Fact]
        public void FoldBackWhileTest()
        {
            var input = List(10, 20, 30, 40, 50);

            var output1 = foldBackWhile(input, "", (s, x) => s + x.ToString(), x => x >= 40);
            Assert.Equal("5040", output1);

            var output2 = foldBackWhile(input, "", (s, x) => s + x.ToString(), (string s) => s.Length < 4);
            Assert.Equal("5040", output2);

            var output3 = foldBackWhile(input, 0, (s, x) => s + x, preditem: x => x >= 40);
            Assert.Equal(90, output3);

            var output4 = foldBackWhile(input, 0, (s, x) => s + x, predstate: s => s < 90);
            Assert.Equal(90, output4);
        }

        [Fact]
        public void FoldUntilTest()
        {
            var input = List(10, 20, 30, 40, 50);

            var output1 = foldUntil(input, "", (s, x) => s + x.ToString(), x => x >= 40);
            Assert.Equal("102030", output1);

            var output2 = foldUntil(input, "", (s, x) => s + x.ToString(), (string s) => s.Length >= 6);
            Assert.Equal("102030", output2);

            var output3 = foldUntil(input, 0, (s, x) => s + x, preditem: x => x >= 40);
            Assert.Equal(60, output3);

            var output4 = foldUntil(input, 0, (s, x) => s + x, predstate: s => s >= 60);
            Assert.Equal(60, output4);
        }

        [Fact]
        public void FoldBackUntilTest()
        {
            var input = List(10, 20, 30, 40, 50);

            var output1 = foldBackUntil(input, "", (s, x) => s + x.ToString(), x => x < 40);
            Assert.Equal("5040", output1);

            var output2 = foldBackUntil(input, "", (s, x) => s + x.ToString(), (string s) => s.Length >= 4);
            Assert.Equal("5040", output2);

            var output3 = foldBackUntil(input, 0, (s, x) => s + x, preditem: x => x < 40);
            Assert.Equal(90, output3);

            var output4 = foldBackUntil(input, 0, (s, x) => s + x, predstate: s => s >= 90);
            Assert.Equal(90, output4);
        }
    }
}
