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

namespace Microsoft.CodeAnalysis.UnitTests
{
    public sealed class LargeTextTests : TestBase
    {
        private static SourceText CreateSourceText(string s, Encoding encoding = null)
        {
            using (MemoryStream stream = new MemoryStream())
            {
                using (StreamWriter sw = new StreamWriter(stream, encoding ?? Encoding.UTF8, bufferSize: 1024, leaveOpen: true))
                {
                    sw.Write(s);
                }

                return CreateSourceText(stream, encoding);
            }
        }

        private static SourceText CreateSourceText(Stream stream, Encoding encoding = null)
        {
            return LargeText.Decode(stream, encoding ?? Encoding.UTF8, SourceHashAlgorithm.Sha1, throwIfBinaryDetected: true, canBeEmbedded: false);
        }

        private static SourceText CreateSourceText(TextReader reader, int length, Encoding encoding = null)
        {
            return LargeText.Decode(reader, length, encoding ?? Encoding.UTF8, SourceHashAlgorithm.Sha1);
        }

        private const string HelloWorld = "Hello, world!";

        [Fact]
        public void BasicTest()
        {
            var text = CreateSourceText(HelloWorld);
            Assert.Equal(HelloWorld, text.ToString());
            Assert.Equal(HelloWorld.Length, text.Length);
        }

        [Fact]
        public void EmptyTest()
        {
            using (MemoryStream stream = new MemoryStream())
            {
                var text = CreateSourceText(stream);
                Assert.Equal(0, text.Length);
            }
        }

        [Fact]
        public void IndexerTest()
        {
            var text = CreateSourceText(HelloWorld);
            Assert.Throws<IndexOutOfRangeException>(() => text[-1]);
            Assert.Throws<IndexOutOfRangeException>(() => text[HelloWorld.Length]);
            for (int i = HelloWorld.Length - 1; i >= 0; i--)
            {
                Assert.Equal(HelloWorld[i], text[i]);
            }
        }

        [Fact]
        public void CopyToTest()
        {
            var text = CreateSourceText(HelloWorld);

            const int destOffset = 10;
            char[] buffer = new char[HelloWorld.Length + destOffset];

            // Copy the entire text to a non-zero offset in the destination
            text.CopyTo(0, buffer, destOffset, text.Length);

            for (int i = 0; i < destOffset; i++)
            {
                Assert.Equal('\0', buffer[i]);
            }

            for (int i = destOffset; i < buffer.Length; i++)
            {
                Assert.Equal(HelloWorld[i - destOffset], buffer[i]);
            }

            Array.Clear(buffer, 0, buffer.Length);

            // Copy a sub-string
            text.CopyTo(3, buffer, 0, 3);
            Assert.Equal(HelloWorld[3], buffer[0]);
            Assert.Equal(HelloWorld[4], buffer[1]);
            Assert.Equal(HelloWorld[5], buffer[2]);
            for (int i = 3; i < buffer.Length; i++)
            {
                Assert.Equal('\0', buffer[i]);
            }
        }

        [Fact]
        public void CopyToLargeTest()
        {
            // Tests CopyTo at the chunk boundaries
            int targetLength = LargeText.ChunkSize * 2;

            using (MemoryStream stream = new MemoryStream())
            {
                using (StreamWriter sw = new StreamWriter(stream, Encoding.UTF8, bufferSize: 1024, leaveOpen: true))
                {
                    while (stream.Length < targetLength)
                    {
                        sw.Write(HelloWorld);
                    }
                }

                var text = SourceText.From(stream);
                Assert.IsType<LargeText>(text);

                char[] buffer = new char[HelloWorld.Length];
                for (int start = 0; start < text.Length; start += HelloWorld.Length)
                {
                    text.CopyTo(start, buffer, 0, HelloWorld.Length);
                    Assert.Equal(HelloWorld, new string(buffer));
                }
            }
        }

        private static 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 static 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 static 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()
        {
            var newline = Environment.NewLine;
            var data = CreateSourceText("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 = CreateSourceText(text);
            Assert.Equal(3, data.Lines.Count);
            var newlineLength = Environment.NewLine.Length;
            CheckLine(data, lineNumber: 0, start: 0, length: 3, newlineLength, lineText: "goo");
            CheckLine(data, lineNumber: 1, start: 3 + newlineLength, length: 3, newlineLength, lineText: "bar");
            CheckLine(data, lineNumber: 2, start: 2 * (3 + newlineLength), length: 3, newlineLength: 0, lineText: "baz");
        }

        [Fact]
        public void NewLines3()
        {
            var data = CreateSourceText("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 = CreateSourceText("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 NewLines5()
        {
            // Trailing CR
            var data = CreateSourceText("goo\r");
            Assert.Equal(2, data.Lines.Count);
            CheckLine(data, lineNumber: 0, start: 0, length: 3, newlineLength: 1, lineText: "goo");
            CheckLine(data, lineNumber: 1, start: 4, length: 0, newlineLength: 0, lineText: "");
        }

        [Fact]
        public void NewLines6()
        {
            // Trailing CR+LF
            var data = CreateSourceText("goo\r\n");
            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: 0, newlineLength: 0, lineText: "");
        }

        [Fact]
        public void NewLines7()
        {
            // Consecutive CR
            var data = CreateSourceText("goo\r\rbar");
            Assert.Equal(3, 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: 0, lineText: "bar");
        }

        [Fact]
        public void NewLines8()
        {
            // Mix CR with CR+LF
            const string cr = "\r";
            const string crLf = "\r\n";
            var data = CreateSourceText("goo" + cr + crLf + cr + "bar");
            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: 2, lineText: "");
            CheckLine(data, lineNumber: 2, start: 6, length: 0, newlineLength: 1, lineText: "");
            CheckLine(data, lineNumber: 3, start: 7, length: 3, newlineLength: 0, lineText: "bar");
        }

        [Fact]
        public void NewLines9()
        {
            // Mix CR with CR+LF
            const string cr = "\r";
            const string crLf = "\r\n";
            const string lf = "\n";
            var data = CreateSourceText("goo" + cr + crLf + lf + "bar");
            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: 2, lineText: "");
            CheckLine(data, lineNumber: 2, start: 6, length: 0, newlineLength: 1, lineText: "");
            CheckLine(data, lineNumber: 3, start: 7, length: 3, newlineLength: 0, lineText: "bar");
        }

        [Fact]
        public void Empty()
        {
            var data = CreateSourceText("");
            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 = CreateSourceText(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 = CreateSourceText(text);
            Assert.Equal("goo", data.Lines[0].ToString());
        }

        [Fact]
        public void FromTextReader()
        {
            var expected = "goo";
            var expectedSourceText = CreateSourceText(expected);

            var actual = new StringReader(expected);
            var actualSourceText = CreateSourceText(actual, expected.Length);

            Assert.Equal("goo", actualSourceText.Lines[0].ToString());
            Assert.Equal<byte>(expectedSourceText.GetChecksum(), actualSourceText.GetChecksum());
        }
    }
}
