// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using Microsoft.Build.Shared.LanguageParser;
using Xunit;

#nullable disable

namespace Microsoft.Build.UnitTests
{
    public sealed class StreamMappedString_Tests
    {
        /// <summary>
        /// Test for a string that has ANSI but non-ascii characters.
        /// </summary>
        [Fact]
        public void Regress_Mutation_ForceANSIWorks_RelatedTo172107()
        {
            // Can't embed the 'Ã' directly because the string is Unicode already and the Unicode<-->ANSI transform
            // isn't bidirectional.
            MemoryStream sourcesStream = (MemoryStream)StreamHelpers.StringToStream("namespace d?a { class Class {} }");

            // Instead, directly write the ANSI character into the memory buffer.
            sourcesStream.Seek(11, SeekOrigin.Begin);
            sourcesStream.WriteByte(0xc3);    // Plug the 'Ã' in 
            sourcesStream.Seek(0, SeekOrigin.Begin);

            // Should not throw an exception because we force ANSI.
            StreamMappedString s = new StreamMappedString(sourcesStream, /* forceANSI */ true);
            s.GetAt(11);
        }

        [Fact]
        public void Regress_Mutation_BackingUpMoreThanOnePageWorks()
        {
            Stream stream = StreamHelpers.StringToStream("A" + new String('b', StreamMappedString.DefaultPageSize * 4));
            StreamMappedString s = new StreamMappedString(stream, false);

            // Get the last character...
            s.GetAt(StreamMappedString.DefaultPageSize * 4);

            // ...now get the first character.
            Assert.Equal('A', s.GetAt(0));
        }

        [Fact]
        public void Regress_Mutation_RetrievingFromLastPageWorks()
        {
            Stream stream = StreamHelpers.StringToStream("A" + new String('b', StreamMappedString.DefaultPageSize));
            StreamMappedString s = new StreamMappedString(stream, false);

            // Get the last character...
            s.GetAt(StreamMappedString.DefaultPageSize);

            // ...now get the first character (which should be saved on lastPage).
            Assert.Equal('A', s.GetAt(0));
        }

        [Fact]
        public void Regress_Mutation_LastCharacterShouldBeNewLine()
        {
            Stream stream = StreamHelpers.StringToStream("A");
            StreamMappedString s = new StreamMappedString(stream, false);

            // Get the last character (which should be the appended newLine).
            Assert.Equal('\xd', s.GetAt(1));
        }

        [Fact]
        public void Regress_Mutation_1AShouldBeStripped()
        {
            Stream stream = StreamHelpers.StringToStream("x\x1Ay");
            StreamMappedString s = new StreamMappedString(stream, false);

            // Get the last character (which should be 'y' and not 0x1A).
            Assert.Equal('y', s.GetAt(1));
        }

        [Fact]
        public void Regress_Mutation_MultiplePagesOf1AShouldBeStripped()
        {
            Stream stream = StreamHelpers.StringToStream(new String('\x1a', StreamMappedString.DefaultPageSize * 2) + "x");
            StreamMappedString s = new StreamMappedString(stream, false);

            // Get the last character (which should be 'x' and not 0x1A).
            Assert.Equal('x', s.GetAt(0));
        }

        [Fact]
        public void Regress_Mutation_NewLineGetsAppendedAcrossPageBoundaries()
        {
            Stream stream = StreamHelpers.StringToStream(new String('x', StreamMappedString.DefaultPageSize));
            StreamMappedString s = new StreamMappedString(stream, false);

            // Get the last character (which should be '\xd').
            Assert.Equal('\xd', s.GetAt(StreamMappedString.DefaultPageSize));
        }

        [Fact]
        public void Regress_Mutation_SubstringWorks()
        {
            Stream stream = StreamHelpers.StringToStream("abcdefg");
            StreamMappedString s = new StreamMappedString(stream, false);

            Assert.Equal("bcd", s.Substring(1, 3));
        }

        [Fact]
        public void Regress_Mutation_SubstringWorksWithPageSizeOne()
        {
            Stream stream = StreamHelpers.StringToStream("abcdefg");
            StreamMappedString s = new StreamMappedString(stream, false, /* pageSize */ 1);

            Assert.Equal("bcd", s.Substring(1, 3));
        }

        [Fact]
        public void Regress_Mutation_SubstringWorksFromPriorPage()
        {
            Stream stream = StreamHelpers.StringToStream("abcxdef");
            StreamMappedString s = new StreamMappedString(stream, false, 7);

            // Move to the last page
            s.GetAt(7);

            // And then extract a string from the beginning page.
            Assert.Equal("abcxdef", s.Substring(0, 7));
        }

        [Fact]
        public void Regress_Mutation_SubstringReadPastEndThrowsException()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() =>
            {
                Stream stream = StreamHelpers.StringToStream("abcdefg");
                StreamMappedString s = new StreamMappedString(stream, false);

                Assert.Equal(String.Empty, s.Substring(1, 30));
            });
        }
        [Fact]
        public void Regress_Mutation_SubstringOnLastPageWorks()
        {
            Stream stream = StreamHelpers.StringToStream("abcdefg" + new String('x', StreamMappedString.DefaultPageSize));
            StreamMappedString s = new StreamMappedString(stream, false);

            // Move to the second page
            s.GetAt(StreamMappedString.DefaultPageSize);

            // Get a string from the firstPage
            Assert.Equal("abc", s.Substring(0, 3));
        }

        [Fact]
        public void Regress_Mutation_UnicodeIsDetected()
        {
            Stream stream = StreamHelpers.StringToStream("\u00C3ngelo's Steak House", System.Text.Encoding.UTF32);
            StreamMappedString s = new StreamMappedString(stream, false);

            // This won't read correctly with ANSI encoding.
            Assert.Equal('\u00C3', s.GetAt(0));
        }

        [Fact]
        public void Regress_Mutation_ReadingCharactersForwardOnlyShouldCauseNoAdditionalResets()
        {
            RestartCountingStream stream = new RestartCountingStream(StreamHelpers.StringToStream("abcdefg"));
            StreamMappedString s = new StreamMappedString(stream, false);

            // Get a few characters.
            s.GetAt(0);
            s.GetAt(1);
            s.GetAt(2);
            s.GetAt(3);
            s.GetAt(4);

            // There should be exactly one reset for this.
            Assert.Equal(0, stream.ResetCount);
        }

        [Fact]
        public void Regress_Mutation_IsPastEndWorks()
        {
            RestartCountingStream stream = new RestartCountingStream(StreamHelpers.StringToStream("a"));
            StreamMappedString s = new StreamMappedString(stream, false);

            // There's only one character, so IsPastEnd(2) should be true.
            Assert.True(s.IsPastEnd(2)); // <-- 2 required because of extra \xd added.
        }

        [Fact]
        public void Regress_Mutation_MinimizePagesAllocated()
        {
            Stream stream = StreamHelpers.StringToStream("a" + new String('x', StreamMappedString.DefaultPageSize * 2));
            StreamMappedString s = new StreamMappedString(stream, false);

            // Get a few characters.
            s.GetAt(0);
            s.GetAt(StreamMappedString.DefaultPageSize);
            s.GetAt(StreamMappedString.DefaultPageSize * 2);

            // Even though three pages were read, only two allocations should have occurred.
            Assert.Equal(2, s.PagesAllocated);
        }

        [Fact]
        public void Regress_Mutation_1DNotAppendedIfAlreadyThere()
        {
            RestartCountingStream stream = new RestartCountingStream(StreamHelpers.StringToStream("\xd"));
            StreamMappedString s = new StreamMappedString(stream, false);

            // There's only one \x1d so IsPastEnd(1) should be true.
            Assert.True(s.IsPastEnd(1));
        }

        [Fact]
        public void Regress_Codereview_RequestPageWellPastEnd()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() =>
            {
                Stream stream = StreamHelpers.StringToStream("x");
                StreamMappedString s = new StreamMappedString(stream, false);

                // Read something way past the end. This should result in a range exception.
                s.GetAt(1000000);
            });
        }

        [Fact]
        public void Regress_Mutation_FirstCharacterOnPagePastEndDoesntExist()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() =>
            {
                Stream stream = StreamHelpers.StringToStream("abc");
                StreamMappedString s = new StreamMappedString(stream, false, 256);

                s.GetAt(256);
            });
        }

        [Fact]
        public void Regress_Mutation_RequestPageWellPastEnd()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() =>
            {
                Stream stream = StreamHelpers.StringToStream(new String('x', StreamMappedString.DefaultPageSize * 2));
                StreamMappedString s = new StreamMappedString(stream, false);

                // Read something way past the end. This should result in a range exception.
                s.GetAt(1000000);
            });
        }


        /// <summary>
        /// A stream class that counts the number of times it was reset.
        /// </summary>
        private sealed class RestartCountingStream : Stream
        {
            private int _resetCount;
            private Stream _stream;

            public RestartCountingStream(Stream stream)
            {
                _stream = stream;
            }

            /// <summary>
            /// Returns the number of times this stream was reset.
            /// </summary>
            public int ResetCount
            {
                get { return _resetCount; }
            }

            public override bool CanRead
            {
                get { return _stream.CanRead; }
            }

            public override bool CanSeek
            {
                get { throw new Exception("The method or operation is not implemented."); }
            }

            public override bool CanWrite
            {
                get { throw new Exception("The method or operation is not implemented."); }
            }

            public override void Flush()
            {
                throw new Exception("The method or operation is not implemented.");
            }

            public override long Length
            {
                get { throw new Exception("The method or operation is not implemented."); }
            }

            public override long Position
            {
                get
                {
                    return _stream.Position;
                }
                set
                {
                    throw new Exception("The method or operation is not implemented.");
                }
            }

            public override int Read(byte[] buffer, int offset, int count)
            {
                return _stream.Read(buffer, offset, count);
            }

            public override long Seek(long offset, SeekOrigin origin)
            {
                ++_resetCount;
                return this.Seek(offset, origin);
            }

            public override void SetLength(long value)
            {
                throw new Exception("The method or operation is not implemented.");
            }

            public override void Write(byte[] buffer, int offset, int count)
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }
    }
}
