// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Text;
using Xunit;
using System.Collections.Generic;

namespace Microsoft.CodeAnalysis.Differencing.UnitTests
{
    public class LongestCommonSubsequenceTests
    {
        private readonly LongestCommonSubsequenceString lcs = new LongestCommonSubsequenceString();

        private class LongestCommonSubsequenceString : LongestCommonSubsequence<string>
        {
            protected override bool ItemsEqual(string oldSequence, int oldIndex, string newSequence, int newIndex)
                => oldSequence[oldIndex] == newSequence[newIndex];

            public IEnumerable<KeyValuePair<int, int>> GetMatchingPairs(string oldSequence, string newSequence)
                => GetMatchingPairs(oldSequence, oldSequence.Length, newSequence, newSequence.Length);

            public IEnumerable<SequenceEdit> GetEdits(string oldSequence, string newSequence)
                => GetEdits(oldSequence, oldSequence.Length, newSequence, newSequence.Length);

            public double ComputeDistance(string oldSequence, string newSequence)
                => ComputeDistance(oldSequence, oldSequence.Length, newSequence, newSequence.Length);
        }

        private static void VerifyMatchingPairs(IEnumerable<KeyValuePair<int, int>> actualPairs, string expectedPairsStr)
        {
            var sb = new StringBuilder(expectedPairsStr.Length);
            foreach (var actPair in actualPairs)
            {
                sb.AppendFormat("[{0},{1}]", actPair.Key, actPair.Value);
            }

            var actualPairsStr = sb.ToString();
            Assert.Equal(expectedPairsStr, actualPairsStr);
        }

        private static void VerifyEdits(string oldStr, string newStr, IEnumerable<SequenceEdit> edits)
        {
            var oldChars = oldStr.ToCharArray();
            var newChars = new char[newStr.Length];

            foreach (var edit in edits)
            {
                Assert.True(edit.Kind is EditKind.Delete or EditKind.Insert or EditKind.Update);
                switch (edit.Kind)
                {
                    case EditKind.Delete:
                        Assert.True(edit.OldIndex < oldStr.Length);
                        oldChars[edit.OldIndex] = '\0';
                        break;

                    case EditKind.Insert:
                        Assert.True(edit.NewIndex < newStr.Length);
                        newChars[edit.NewIndex] = newStr[edit.NewIndex];
                        break;

                    case EditKind.Update:
                        Assert.True(edit.OldIndex < oldStr.Length);
                        Assert.True(edit.NewIndex < newStr.Length);
                        newChars[edit.NewIndex] = oldStr[edit.OldIndex];
                        oldChars[edit.OldIndex] = '\0';
                        break;
                }
            }

            var editedStr = new String(newChars);
            Assert.Equal(editedStr, newStr);

            Array.ForEach(oldChars, (c) => { Assert.Equal('\0', c); });
        }

        [Fact]
        public void EmptyStrings()
        {
            var str1 = "";
            var str2 = "";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.0, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void InsertToEmpty()
        {
            var str1 = "";
            var str2 = "ABC";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(1.0, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void InsertAtBeginning()
        {
            var str1 = "ABC";
            var str2 = "XYZABC";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[2,5][1,4][0,3]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.5, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void InsertAtEnd()
        {
            var str1 = "ABC";
            var str2 = "ABCXYZ";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[2,2][1,1][0,0]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.5, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void InsertInMidlle()
        {
            var str1 = "ABC";
            var str2 = "ABXYC";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[2,4][1,1][0,0]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.4, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void DeleteToEmpty()
        {
            var str1 = "ABC";
            var str2 = "";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(1.0, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void DeleteAtBeginning()
        {
            var str1 = "ABCD";
            var str2 = "C";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[2,0]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.75, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void DeleteAtEnd()
        {
            var str1 = "ABCD";
            var str2 = "AB";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[1,1][0,0]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.5, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void DeleteInMiddle()
        {
            var str1 = "ABCDE";
            var str2 = "ADE";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[4,2][3,1][0,0]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.4, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void ReplaceAll()
        {
            var str1 = "ABC";
            var str2 = "XYZ";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(1.0, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void ReplaceAtBeginning()
        {
            var str1 = "ABCD";
            var str2 = "XYD";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[3,2]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.75, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void ReplaceAtEnd()
        {
            var str1 = "ABCD";
            var str2 = "ABXYZ";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[1,1][0,0]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.6, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void ReplaceInMiddle()
        {
            var str1 = "ABCDE";
            var str2 = "AXDE";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[4,3][3,2][0,0]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.4, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void Combination1()
        {
            var str1 = "ABBCDEFIJ";
            var str2 = "AABDEEGH";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[5,4][4,3][1,2][0,0]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.556, lcs.ComputeDistance(str1, str2), precision: 3);
        }

        [Fact]
        public void Combination2()
        {
            var str1 = "AAABBCCDDD";
            var str2 = "ABXCD";

            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[7,4][5,3][3,1][0,0]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.6, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void Combination3()
        {
            var str1 = "ABCABBA";
            var str2 = "CBABAC";

            // 2 possible matches:
            // "[6,4][4,3][3,2][1,1]" <- this one is backwards compatible
            // "[6,4][4,3][3,2][2,0]"
            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[6,4][4,3][3,2][1,1]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.429, lcs.ComputeDistance(str1, str2), precision: 3);
        }

        [Fact]
        public void Reorder1()
        {
            var str1 = "AB";
            var str2 = "BA";

            // 2 possible matches:
            // "[0,1]" <- this one is backwards compatible
            // "[1,0]"
            VerifyMatchingPairs(lcs.GetMatchingPairs(str1, str2), "[0,1]");

            VerifyEdits(str1, str2, lcs.GetEdits(str1, str2));

            Assert.Equal(0.5, lcs.ComputeDistance(str1, str2));
        }

        [Fact]
        public void LongString()
        {
            var s = "A";

            var x9 = new string('x', 9);
            var x10 = new string('x', 10);
            var x99 = new string('x', 99);
            var x1000 = new string('x', 1000);

            var y1000 = new string('y', 1000);

            var sx9 = s + x9;
            var sx99 = s + x99;
            var sx1000 = s + new string('x', 1000);
            var sx100000000 = s + new string('x', 100000000);

            Assert.Equal(0.900, lcs.ComputeDistance(s, sx9), precision: 3);
            Assert.Equal(0.990, lcs.ComputeDistance(s, sx99), precision: 3);
            Assert.Equal(1.000, lcs.ComputeDistance(s, sx1000), precision: 3);
            Assert.Equal(1.000, lcs.ComputeDistance(s, sx100000000), precision: 3);

            Assert.Equal(0.900, lcs.ComputeDistance(sx9, s), precision: 3);
            Assert.Equal(0.990, lcs.ComputeDistance(sx99, s), precision: 3);
            Assert.Equal(1.000, lcs.ComputeDistance(sx1000, s), precision: 3);
            Assert.Equal(1.000, lcs.ComputeDistance(sx100000000, s), precision: 3);

            Assert.Equal(1.000, lcs.ComputeDistance(x10 + y1000, x10), precision: 3);
            Assert.Equal(0.5, lcs.ComputeDistance(x1000 + y1000, x1000), precision: 3);
        }
    }
}
