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

using System.Globalization;
using System.Text;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.StackTrace.Sources;

namespace Microsoft.Extensions.Internal;

public class ExceptionDetailsProviderTest
{
    public static TheoryData RelativePathsData
    {
        get
        {
            var data = new TheoryData<string>
                {
                    "TestFiles/SourceFile.txt"
                };

            if (!(OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()))
            {
                data.Add(@"TestFiles\SourceFile.txt");
            }

            return data;
        }
    }

    public static TheoryData<string> AbsolutePathsData
    {
        get
        {
            var rootPath = Directory.GetCurrentDirectory();

            var data = new TheoryData<string>()
                {
                    Path.Combine(rootPath, "TestFiles/SourceFile.txt")
                };

            if (!TestPlatformHelper.IsMono)
            {
                Path.Combine(rootPath, @"TestFiles\SourceFile.txt");
            }

            return data;
        }
    }

    [Theory]
    [MemberData(nameof(AbsolutePathsData))]
    public void DisplaysSourceCodeLines_ForAbsolutePaths(string absoluteFilePath)
    {
        // Arrange
        var rootPath = Directory.GetCurrentDirectory();
        // PhysicalFileProvider handles only relative paths but we fall back to work with absolute paths too
        using (var provider = new PhysicalFileProvider(rootPath))
        {
            // Act
            var exceptionDetailProvider = new ExceptionDetailsProvider(provider, logger: null, sourceCodeLineCount: 6);
            var stackFrame = exceptionDetailProvider.GetStackFrameSourceCodeInfo(
                "func1",
                absoluteFilePath,
                lineNumber: 10);

            // Assert
            // Lines 4-16 (inclusive) is the code block
            Assert.Equal(4, stackFrame.PreContextLine);
            Assert.Equal(GetCodeLines(4, 9), stackFrame.PreContextCode);
            Assert.Equal(GetCodeLines(10, 10), stackFrame.ContextCode);
            Assert.Equal(GetCodeLines(11, 16), stackFrame.PostContextCode);
        }
    }

    [Theory]
    [MemberData(nameof(RelativePathsData))]
    public void DisplaysSourceCodeLines_ForRelativePaths(string relativePath)
    {
        // Arrange
        var rootPath = Directory.GetCurrentDirectory();
        using (var provider = new PhysicalFileProvider(rootPath))
        {
            // Act
            var exceptionDetailProvider = new ExceptionDetailsProvider(provider, logger: null, sourceCodeLineCount: 6);
            var stackFrame = exceptionDetailProvider.GetStackFrameSourceCodeInfo(
                "func1",
                relativePath,
                lineNumber: 10);

            // Assert
            // Lines 4-16 (inclusive) is the code block
            Assert.Equal(4, stackFrame.PreContextLine);
            Assert.Equal(GetCodeLines(4, 9), stackFrame.PreContextCode);
            Assert.Equal(GetCodeLines(10, 10), stackFrame.ContextCode);
            Assert.Equal(GetCodeLines(11, 16), stackFrame.PostContextCode);
        }
    }

    [Theory]
    [InlineData("TestFiles/EmbeddedSourceFile.txt")]
    //[InlineData(@"TestFiles\EmbeddedSourceFile.txt")]
    public void DisplaysSourceCodeLines_ForRelativeEmbeddedPaths(string relativePath)
    {
        // Arrange
        var provider = new EmbeddedFileProvider(
            GetType().Assembly,
            baseNamespace: $"{typeof(ExceptionDetailsProviderTest).Assembly.GetName().Name}.Resources");

        // Act
        var exceptionDetailProvider = new ExceptionDetailsProvider(provider, logger: null, sourceCodeLineCount: 6);
        var stackFrame = exceptionDetailProvider.GetStackFrameSourceCodeInfo(
            "func1",
            relativePath,
            lineNumber: 10);

        // Assert
        // Lines 4-16 (inclusive) is the code block
        Assert.Equal(4, stackFrame.PreContextLine);
        Assert.Equal(GetCodeLines(4, 9), stackFrame.PreContextCode);
        Assert.Equal(GetCodeLines(10, 10), stackFrame.ContextCode);
        Assert.Equal(GetCodeLines(11, 16), stackFrame.PostContextCode);
    }

    public static TheoryData<ErrorData> DisplaysSourceCodeLines_PreAndPostErrorLineData
    {
        get
        {
            return new TheoryData<ErrorData>()
                {
                    new ErrorData()
                    {
                        AllLines = GetCodeLines(1, 30),
                        ErrorStartLine = 10,
                        ErrorEndLine = 10,
                        ExpectedPreContextLine = 4,
                        ExpectedPreErrorCode = GetCodeLines(4, 9),
                        ExpectedErrorCode = GetCodeLines(10, 10),
                        ExpectedPostErrorCode = GetCodeLines(11, 16)
                    },
                    new ErrorData()
                    {
                        AllLines = GetCodeLines(1, 30),
                        ErrorStartLine = 10,
                        ErrorEndLine = 13, // multi-line error
                        ExpectedPreContextLine = 4,
                        ExpectedPreErrorCode = GetCodeLines(4, 9),
                        ExpectedErrorCode = GetCodeLines(10, 13),
                        ExpectedPostErrorCode = GetCodeLines(14, 19)
                    },

                    // PreErrorCode less than source code line count
                    new ErrorData()
                    {
                        AllLines = GetCodeLines(1, 10),
                        ErrorStartLine = 1,
                        ErrorEndLine = 1,
                        ExpectedPreContextLine = 1,
                        ExpectedPreErrorCode = Enumerable.Empty<string>(),
                        ExpectedErrorCode = GetCodeLines(1, 1),
                        ExpectedPostErrorCode = GetCodeLines(2, 7)
                    },
                    new ErrorData()
                    {
                        AllLines = GetCodeLines(1, 10),
                        ErrorStartLine = 3,
                        ErrorEndLine = 5,
                        ExpectedPreContextLine = 1,
                        ExpectedPreErrorCode = GetCodeLines(1, 2),
                        ExpectedErrorCode = GetCodeLines(3, 5),
                        ExpectedPostErrorCode = GetCodeLines(6, 10)
                    },

                    // PostErrorCode less than source code line count
                    new ErrorData()
                    {
                        AllLines = GetCodeLines(1, 10),
                        ErrorStartLine = 10,
                        ErrorEndLine = 10,
                        ExpectedPreContextLine = 4,
                        ExpectedPreErrorCode = GetCodeLines(4, 9),
                        ExpectedErrorCode = GetCodeLines(10, 10),
                        ExpectedPostErrorCode = Enumerable.Empty<string>()
                    },
                    new ErrorData()
                    {
                        AllLines = GetCodeLines(1, 10),
                        ErrorStartLine = 7,
                        ErrorEndLine = 10,
                        ExpectedPreContextLine = 1,
                        ExpectedPreErrorCode = GetCodeLines(1, 6),
                        ExpectedErrorCode = GetCodeLines(7, 10),
                        ExpectedPostErrorCode = Enumerable.Empty<string>()
                    },
                    new ErrorData()
                    {
                        AllLines = GetCodeLines(1, 10),
                        ErrorStartLine = 5,
                        ErrorEndLine = 8,
                        ExpectedPreContextLine = 1,
                        ExpectedPreErrorCode = GetCodeLines(1, 4),
                        ExpectedErrorCode = GetCodeLines(5, 8),
                        ExpectedPostErrorCode = GetCodeLines(9, 10)
                    },

                    // Pre and Post error code less than source code line count
                    new ErrorData()
                    {
                        AllLines = GetCodeLines(1, 4),
                        ErrorStartLine = 2,
                        ErrorEndLine = 3,
                        ExpectedPreContextLine = 1,
                        ExpectedPreErrorCode = GetCodeLines(1, 1),
                        ExpectedErrorCode = GetCodeLines(2, 3),
                        ExpectedPostErrorCode = GetCodeLines(4, 4)
                    },
                    new ErrorData()
                    {
                        AllLines = GetCodeLines(1, 4),
                        ErrorStartLine = 1,
                        ErrorEndLine = 4,
                        ExpectedPreContextLine = 1,
                        ExpectedPreErrorCode = Enumerable.Empty<string>(),
                        ExpectedErrorCode = GetCodeLines(1, 4),
                        ExpectedPostErrorCode = Enumerable.Empty<string>()
                    },

                    // change source code line count
                    new ErrorData()
                    {
                        SourceCodeLineCount = 1,
                        AllLines = GetCodeLines(1, 1),
                        ErrorStartLine = 1,
                        ErrorEndLine = 1,
                        ExpectedPreContextLine = 1,
                        ExpectedPreErrorCode = Enumerable.Empty<string>(),
                        ExpectedErrorCode = GetCodeLines(1, 1),
                        ExpectedPostErrorCode = Enumerable.Empty<string>()
                    },
                };
        }
    }

    [Theory]
    [MemberData(nameof(DisplaysSourceCodeLines_PreAndPostErrorLineData))]
    public void DisplaysSourceCodeLines_PreAndPostErrorLine(ErrorData errorData)
    {
        // Arrange
        var stackFrame = new StackFrameSourceCodeInfo();

        // Act
        var exceptionDetailProvider = new ExceptionDetailsProvider(
            new PhysicalFileProvider(Directory.GetCurrentDirectory()),
            logger: null,
            sourceCodeLineCount: 6);

        exceptionDetailProvider.ReadFrameContent(
            stackFrame,
            errorData.AllLines,
            errorData.ErrorStartLine,
            errorData.ErrorEndLine);

        // Assert
        Assert.Equal(errorData.ExpectedPreContextLine, stackFrame.PreContextLine);
        Assert.Equal(errorData.ExpectedPreErrorCode, stackFrame.PreContextCode);
        Assert.Equal(errorData.ExpectedErrorCode, stackFrame.ContextCode);
        Assert.Equal(errorData.ExpectedPostErrorCode, stackFrame.PostContextCode);
    }

    private static IEnumerable<string> GetCodeLines(int fromLine, int toLine)
    {
        var start = fromLine;
        var count = toLine - fromLine + 1;
        return Enumerable.Range(start, count).Select(i => string.Format(CultureInfo.InvariantCulture, "Line{0}", i));
    }

    private class TestFileProvider : IFileProvider
    {
        private readonly IEnumerable<string> _sourceCodeLines;

        public TestFileProvider(IEnumerable<string> sourceCodeLines)
        {
            _sourceCodeLines = sourceCodeLines;
        }

        public IDirectoryContents GetDirectoryContents(string subpath)
        {
            throw new NotImplementedException();
        }

        public IFileInfo GetFileInfo(string subpath)
        {
            return new TestFileInfo(_sourceCodeLines);
        }

        public IChangeToken Watch(string filter)
        {
            throw new NotImplementedException();
        }
    }

    private class TestFileInfo : IFileInfo
    {
        private readonly MemoryStream _stream;

        public TestFileInfo(IEnumerable<string> sourceCodeLines)
        {
            _stream = new MemoryStream();
            using (var writer = new StreamWriter(_stream, Encoding.UTF8, 1024, leaveOpen: true))
            {
                foreach (var line in sourceCodeLines)
                {
                    writer.WriteLine(line);
                }
            }
            _stream.Seek(0, SeekOrigin.Begin);
        }

        public bool Exists
        {
            get
            {
                return true;
            }
        }

        public bool IsDirectory
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public DateTimeOffset LastModified
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public long Length
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public string Name
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public string PhysicalPath
        {
            get
            {
                return null;
            }
        }

        public Stream CreateReadStream()
        {
            return _stream;
        }
    }

    public class ErrorData
    {
        public int SourceCodeLineCount { get; set; } = 6;
        public IEnumerable<string> AllLines { get; set; }
        public int ErrorStartLine { get; set; }
        public int ErrorEndLine { get; set; }
        public int ExpectedPreContextLine { get; set; }
        public IEnumerable<string> ExpectedPreErrorCode { get; set; }
        public IEnumerable<string> ExpectedErrorCode { get; set; }
        public IEnumerable<string> ExpectedPostErrorCode { get; set; }
    }
}
