﻿// 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 Microsoft.CodeAnalysis.Text;
using Xunit;
using System.Text;
using System.IO;
using Roslyn.Test.Utilities;
using System.Security.Cryptography;
using System.Collections.Immutable;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.UnitTests
{
    /// <summary>
    /// This is a test class for StringTextTest and is intended
    /// to contain all StringTextTest Unit Tests
    /// </summary>
    public class StringTextTest
    {
        internal static string ChecksumToHexQuads(ImmutableArray<byte> checksum)
        {
            var builder = new StringBuilder();

            for (int i = 0; i < checksum.Length; i++)
            {
                if (i > 0 && ((i % 4) == 0))
                {
                    builder.Append(' ');
                }

                byte b = checksum[i];
                builder.Append(b.ToString("x2"));
            }

            return builder.ToString();
        }

        [Fact]
        public void FromString()
        {
            var data = SourceText.From("goo", Encoding.UTF8);
            Assert.Equal(1, data.Lines.Count);
            Assert.Equal(3, data.Lines[0].Span.Length);
        }

        [Fact]
        public void FromString_DefaultEncoding()
        {
            var data = SourceText.From("goo");
            Assert.Null(data.Encoding);
        }

        [Fact]
        public void FromStringEmpty()
        {
            var data = SourceText.From(string.Empty);
            Assert.Equal(1, data.Lines.Count);
            Assert.Equal(0, data.Lines[0].Span.Length);
        }

        [Fact]
        public void FromString_Errors()
        {
            Assert.Throws<ArgumentNullException>(() => SourceText.From((string)null, Encoding.UTF8));
        }

        [Fact]
        public void FromStream_Errors()
        {
            Assert.Throws<ArgumentNullException>(() => SourceText.From((Stream)null, Encoding.UTF8));
            Assert.Throws<ArgumentException>(() => SourceText.From(new TestStream(canRead: false, canSeek: true), Encoding.UTF8));
            Assert.Throws<NotImplementedException>(() => SourceText.From(new TestStream(canRead: true, canSeek: false), Encoding.UTF8));
        }

        [Fact]
        public void Indexer1()
        {
            var data = SourceText.From(string.Empty, Encoding.UTF8);
            Assert.Throws<IndexOutOfRangeException>(
                () => { var value = data[-1]; });
        }

        private void CheckEqualLine(TextLine first, TextLine second)
        {
            Assert.Equal(first, second);
#if false
            // We do not guarantee either identity or Equals!
            Assert.Equal(first.Extent, second.Extent);
            Assert.Equal(first.ExtentIncludingLineBreak, second.ExtentIncludingLineBreak);
#endif
        }

        private void CheckNotEqualLine(TextLine first, TextLine second)
        {
            Assert.NotEqual(first, second);
#if false
            Assert.NotEqual(first, second);
            Assert.NotEqual(first.Extent, second.Extent);
            Assert.NotEqual(first.ExtentIncludingLineBreak, second.ExtentIncludingLineBreak);
#endif
        }

        private void CheckLine(SourceText text, int lineNumber, int start, int length, int newlineLength, string lineText)
        {
            var textLine = text.Lines[lineNumber];

            Assert.Equal(start, textLine.Start);
            Assert.Equal(start + length, textLine.End);
            Assert.Equal(start + length + newlineLength, textLine.EndIncludingLineBreak);
            Assert.Equal(start, textLine.Span.Start);
            Assert.Equal(length, textLine.Span.Length);
            Assert.Equal(start, textLine.SpanIncludingLineBreak.Start);
            Assert.Equal(length + newlineLength, textLine.SpanIncludingLineBreak.Length);
            Assert.Equal(lineNumber, textLine.LineNumber);
            Assert.Equal(lineText, textLine.ToString());
            Assert.Equal(text.ToString().Substring(start, length), textLine.ToString());

            CheckEqualLine(textLine, text.Lines[lineNumber]);
            for (int p = textLine.Start; p < textLine.EndIncludingLineBreak; ++p)
            {
                CheckEqualLine(textLine, text.Lines.GetLineFromPosition(p));
                Assert.Equal(lineNumber, text.Lines.IndexOf(p));
                Assert.Equal(lineNumber, text.Lines.GetLinePosition(p).Line);
                Assert.Equal(p - start, text.Lines.GetLinePosition(p).Character);
            }

            if (start != 0)
            {
                CheckNotEqualLine(textLine, text.Lines.GetLineFromPosition(start - 1));
                Assert.Equal(lineNumber - 1, text.Lines.IndexOf(start - 1));
                Assert.Equal(lineNumber - 1, text.Lines.GetLinePosition(start - 1).Line);
            }

            int nextPosition = start + length + newlineLength;
            if (nextPosition < text.Length)
            {
                CheckNotEqualLine(textLine, text.Lines.GetLineFromPosition(nextPosition));
                Assert.Equal(lineNumber + 1, text.Lines.IndexOf(nextPosition));
                Assert.Equal(lineNumber + 1, text.Lines.GetLinePosition(nextPosition).Line);
            }
        }

        [Fact]
        public void NewLines1()
        {
            string newLine = Environment.NewLine;
            var data = SourceText.From("goo" + newLine + " bar");
            Assert.Equal(2, data.Lines.Count);
            CheckLine(data, lineNumber: 0, start: 0, length: 3, newlineLength: newLine.Length, lineText: "goo");
            CheckLine(data, lineNumber: 1, start: 3 + newLine.Length, length: 4, newlineLength: 0, lineText: " bar");
        }

        [Fact]
        public void NewLines2()
        {
            var text =
@"goo
bar
baz";
            var data = SourceText.From(text);
            Assert.Equal(3, data.Lines.Count);
            var newlineLength = Environment.NewLine.Length;
            CheckLine(data, lineNumber: 0, start: 0, length: 3, newlineLength: newlineLength, lineText: "goo");
            CheckLine(data, lineNumber: 1, start: 3 + newlineLength, length: 3, newlineLength: newlineLength, lineText: "bar");
            CheckLine(data, lineNumber: 2, start: 2 * (3 + newlineLength), length: 3, newlineLength: 0, lineText: "baz");
        }

        [Fact]
        public void NewLines3()
        {
            var data = SourceText.From("goo\r\nbar");
            Assert.Equal(2, data.Lines.Count);
            CheckLine(data, lineNumber: 0, start: 0, length: 3, newlineLength: 2, lineText: "goo");
            CheckLine(data, lineNumber: 1, start: 5, length: 3, newlineLength: 0, lineText: "bar");
        }

        [Fact]
        public void NewLines4()
        {
            var data = SourceText.From("goo\n\rbar\u2028");
            Assert.Equal(4, data.Lines.Count);
            CheckLine(data, lineNumber: 0, start: 0, length: 3, newlineLength: 1, lineText: "goo");
            CheckLine(data, lineNumber: 1, start: 4, length: 0, newlineLength: 1, lineText: "");
            CheckLine(data, lineNumber: 2, start: 5, length: 3, newlineLength: 1, lineText: "bar");
            CheckLine(data, lineNumber: 3, start: 9, length: 0, newlineLength: 0, lineText: "");
        }

        [Fact]
        public void Empty()
        {
            var data = SourceText.From("");
            Assert.Equal(1, data.Lines.Count);
            CheckLine(data, lineNumber: 0, start: 0, length: 0, newlineLength: 0, lineText: "");
        }

        [Fact]
        public void LinesGetText1()
        {
            var text =
@"goo
bar baz";
            var data = SourceText.From(text);
            Assert.Equal(2, data.Lines.Count);
            Assert.Equal("goo", data.Lines[0].ToString());
            Assert.Equal("bar baz", data.Lines[1].ToString());
        }

        [Fact]
        public void LinesGetText2()
        {
            var text = "goo";
            var data = SourceText.From(text);
            Assert.Equal("goo", data.Lines[0].ToString());
        }

        [Fact]
        public void CheckSum_Utf8_BOM()
        {
            var data = SourceText.From("The quick brown fox jumps over the lazy dog", Encoding.UTF8);
            var checksum = data.GetChecksum();
            Assert.Equal("88d3ed78 9b0bae8b ced8e348 91133516 b79ba9fb", ChecksumToHexQuads(checksum));
        }

        [Fact]
        public void FromStream_CheckSum_BOM()
        {
            var bytes = new byte[] { 0xef, 0xbb, 0xbf, 0x61, 0x62, 0x63 };

            var source = SourceText.From(new MemoryStream(bytes), Encoding.ASCII);
            Assert.Equal("abc", source.ToString());

            var checksum = source.GetChecksum();
            AssertEx.Equal(CryptographicHashProvider.ComputeSha1(bytes), checksum);
        }

        [Fact]
        public void FromStream_CheckSum_NoBOM()
        {
            // Note: The 0x95 is outside the ASCII range, so a question mark will
            // be substituted in decoded text. Note, however, that the checksum
            // should be derived from the original input.
            var bytes = new byte[] { 0x61, 0x62, 0x95 };

            var source = SourceText.From(new MemoryStream(bytes), Encoding.ASCII);
            Assert.Equal("ab?", source.ToString());

            var checksum = source.GetChecksum();
            AssertEx.Equal(CryptographicHashProvider.ComputeSha1(bytes), checksum);
        }

        [Fact]
        public void FromStream_CheckSum_DefaultEncoding()
        {
            var bytes = Encoding.UTF8.GetBytes("\u1234");

            var source = SourceText.From(new MemoryStream(bytes));
            Assert.Equal("\u1234", source.ToString());

            var checksum = source.GetChecksum();
            AssertEx.Equal(CryptographicHashProvider.ComputeSha1(bytes), checksum);
        }

        [Fact]
        public void FromStream_CheckSum_SeekToBeginning()
        {
            var bytes = new byte[] { 0xef, 0xbb, 0xbf, 0x61, 0x62, 0x63 };

            var stream = new MemoryStream(bytes);
            stream.Seek(3, SeekOrigin.Begin);

            var source = SourceText.From(stream, Encoding.ASCII);
            Assert.Equal("abc", source.ToString());

            var checksum = source.GetChecksum();
            AssertEx.Equal(CryptographicHashProvider.ComputeSha1(bytes), checksum);
        }
    }
}
