// 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.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;
using Xunit.Abstractions;

#nullable disable

namespace Microsoft.Build.Tasks.UnitTests
{
    public sealed class WriteLinesToFile_Tests
    {
        private readonly ITestOutputHelper _output;

        public WriteLinesToFile_Tests(ITestOutputHelper output)
        {
            _output = output;
        }

        /// <summary>
        /// Invalid encoding
        /// </summary>
        [Fact]
        public void InvalidEncoding()
        {
            var a = new WriteLinesToFile
            {
                BuildEngine = new MockEngine(_output),
                Encoding = "||invalid||",
                File = new TaskItem("c:\\" + Guid.NewGuid().ToString()),
                Lines = new TaskItem[] { new TaskItem("x") }
            };

            Assert.False(a.Execute());
            ((MockEngine)a.BuildEngine).AssertLogContains("MSB3098");
            Assert.False(File.Exists(a.File.ItemSpec));
        }

        /// <summary>
        /// Reading blank lines from a file should be ignored.
        /// </summary>
        [Fact]
        public void Encoding()
        {
            var file = FileUtilities.GetTemporaryFile();
            try
            {
                // Write default encoding: UTF8
                var a = new WriteLinesToFile
                {
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(file),
                    Lines = new ITaskItem[] { new TaskItem("\uBDEA") }
                };
                Assert.True(a.Execute());

                var r = new ReadLinesFromFile
                {
                    File = new TaskItem(file)
                };
                Assert.True(r.Execute());

                Assert.Equal("\uBDEA", r.Lines[0].ItemSpec);

                File.Delete(file);

                // Write ANSI .. that won't work! 
                a = new WriteLinesToFile
                {
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(file),
                    Lines = new ITaskItem[] { new TaskItem("\uBDEA") },
                    Encoding = "ASCII"
                };
                Assert.True(a.Execute());

                // Read the line from the file.
                r = new ReadLinesFromFile
                {
                    File = new TaskItem(file)
                };
                Assert.True(r.Execute());

                Assert.NotEqual("\uBDEA", r.Lines[0].ItemSpec);
            }
            finally
            {
                File.Delete(file);
            }
        }

        [Fact]
        public void WriteLinesWriteOnlyWhenDifferentTest()
        {
            var file = FileUtilities.GetTemporaryFile();
            try
            {
                // Write an initial file.
                var a = new WriteLinesToFile
                {
                    Overwrite = true,
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(file),
                    WriteOnlyWhenDifferent = true,
                    Lines = new ITaskItem[] { new TaskItem("File contents1") }
                };

                a.Execute().ShouldBeTrue();

                // Verify contents
                var r = new ReadLinesFromFile { File = new TaskItem(file) };
                r.Execute().ShouldBeTrue();
                r.Lines[0].ItemSpec.ShouldBe("File contents1");

                var writeTime = DateTime.Now.AddHours(-1);

                File.SetLastWriteTime(file, writeTime);

                // Write the same contents to the file, timestamps should match.
                var a2 = new WriteLinesToFile
                {
                    Overwrite = true,
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(file),
                    WriteOnlyWhenDifferent = true,
                    Lines = new ITaskItem[] { new TaskItem("File contents1") }
                };
                a2.Execute().ShouldBeTrue();
                File.GetLastWriteTime(file).ShouldBe(writeTime, tolerance: TimeSpan.FromSeconds(1));

                // Write different contents to the file, last write time should differ.
                var a3 = new WriteLinesToFile
                {
                    Overwrite = true,
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(file),
                    WriteOnlyWhenDifferent = true,
                    Lines = new ITaskItem[] { new TaskItem("File contents2") }
                };

                a3.Execute().ShouldBeTrue();
                File.GetLastWriteTime(file).ShouldBeGreaterThan(writeTime.AddSeconds(1));
            }
            finally
            {
                File.Delete(file);
            }
        }

        [Fact]
        public void RedundantParametersAreLogged()
        {
            using TestEnvironment testEnv = TestEnvironment.Create(_output);

            MockEngine engine = new(_output);

            string file = testEnv.ExpectFile().Path;

            WriteLinesToFile task = new()
            {
                BuildEngine = engine,
                File = new TaskItem(file),
                Lines = new ITaskItem[] { new TaskItem($"{nameof(RedundantParametersAreLogged)} Test") },
                WriteOnlyWhenDifferent = true,
                Overwrite = false,
            };

            task.Execute().ShouldBeTrue();
            engine.AssertLogContainsMessageFromResource(AssemblyResources.GetString, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", file);
        }

        /// <summary>
        /// Question WriteLines to return false when a write will be required.
        /// </summary>
        [Fact]
        public void QuestionWriteLinesWriteOnlyWhenDifferentTest()
        {
            var file = FileUtilities.GetTemporaryFile();
            try
            {
                // Write an initial file.
                var a = new WriteLinesToFile
                {
                    Overwrite = true,
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(file),
                    WriteOnlyWhenDifferent = true,
                    Lines = new ITaskItem[] { new TaskItem("File contents1") }
                };

                a.Execute().ShouldBeTrue();

                // Verify contents
                var r = new ReadLinesFromFile { File = new TaskItem(file) };
                r.Execute().ShouldBeTrue();
                r.Lines[0].ItemSpec.ShouldBe("File contents1");

                var writeTime = DateTime.Now.AddHours(-1);

                File.SetLastWriteTime(file, writeTime);

                // Write the same contents to the file, timestamps should match.
                var a2 = new WriteLinesToFile
                {
                    Overwrite = true,
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(file),
                    WriteOnlyWhenDifferent = true,
                    Lines = new ITaskItem[] { new TaskItem("File contents1") },
                    FailIfNotIncremental = true,
                };
                a2.Execute().ShouldBeTrue();
                File.GetLastWriteTime(file).ShouldBe(writeTime, tolerance: TimeSpan.FromSeconds(1));

                // Write different contents to the file, last write time should differ.
                var a3 = new WriteLinesToFile
                {
                    Overwrite = true,
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(file),
                    WriteOnlyWhenDifferent = true,
                    Lines = new ITaskItem[] { new TaskItem("File contents2") },
                    FailIfNotIncremental = true,
                };
                a3.Execute().ShouldBeFalse();
                File.GetLastWriteTime(file).ShouldBe(writeTime, tolerance: TimeSpan.FromSeconds(1));
            }
            finally
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Question WriteLines to return true when Lines are empty.
        /// </summary>
        [Fact]
        public void QuestionWriteLinesWhenLinesAreEmpty()
        {
            // Test the combination of:
            // 1) File exists
            // 2) Overwrite
            // 3) WriteOnlyWhenDifferent

            var fileExists = FileUtilities.GetTemporaryFile();
            var fileNotExists = FileUtilities.GetTemporaryFileName();
            try
            {
                TestWriteLines(fileExists, fileNotExists, Overwrite: true, WriteOnlyWhenDifferent: true);
                TestWriteLines(fileExists, fileNotExists, Overwrite: false, WriteOnlyWhenDifferent: true);
                TestWriteLines(fileExists, fileNotExists, Overwrite: true, WriteOnlyWhenDifferent: false);
                TestWriteLines(fileExists, fileNotExists, Overwrite: false, WriteOnlyWhenDifferent: false);
            }
            finally
            {
                File.Delete(fileExists);
            }

            void TestWriteLines(string fileExists, string fileNotExists, bool Overwrite, bool WriteOnlyWhenDifferent)
            {
                var test1 = new WriteLinesToFile
                {
                    Overwrite = Overwrite,
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(fileExists),
                    WriteOnlyWhenDifferent = WriteOnlyWhenDifferent,
                    FailIfNotIncremental = true,
                    // Tests Lines = null.
                };
                test1.Execute().ShouldBeTrue();

                var test2 = new WriteLinesToFile
                {
                    Overwrite = Overwrite,
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(fileNotExists),
                    WriteOnlyWhenDifferent = WriteOnlyWhenDifferent,
                    FailIfNotIncremental = true,
                    Lines = Array.Empty<ITaskItem>(),  // Test empty.
                };
                test2.Execute().ShouldBeTrue();
            }
        }

        /// <summary>
        /// Should create directory structure when target <see cref="WriteLinesToFile.File"/> does not exist.
        /// </summary>
        [Fact]
        public void WriteLinesToFileDoesCreateDirectory()
        {
            using (var testEnv = TestEnvironment.Create())
            {
                var directory = testEnv.CreateFolder(folderPath: null, createFolder: false);
                var file = Path.Combine(directory.Path, $"{Guid.NewGuid().ToString("N")}.tmp");

                var WriteLinesToFile = new WriteLinesToFile
                {
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(file),
                    Lines = new ITaskItem[] { new TaskItem("WriteLinesToFileDoesCreateDirectory Test") }
                };

                // Verify that the diretory doesn't exist. Otherwise the test would pass - even it should not.
                Directory.Exists(directory.Path).ShouldBeFalse();

                WriteLinesToFile.Execute().ShouldBeTrue();

                Directory.Exists(directory.Path).ShouldBeTrue();
            }
        }

        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        private void WritingNothingErasesExistingFile(bool useNullLines)
        {
            ITaskItem[] lines = useNullLines ? null : Array.Empty<ITaskItem>();

            using (var testEnv = TestEnvironment.Create())
            {
                var file = testEnv.CreateFile("FileToBeEmptied.txt", "Contents that should be erased");

                File.Exists(file.Path).ShouldBeTrue();
                File.ReadAllText(file.Path).ShouldNotBeEmpty();

                new WriteLinesToFile
                {
                    Overwrite = true,
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(file.Path),
                    Lines = lines
                }.Execute().ShouldBeTrue();

                File.Exists(file.Path).ShouldBeTrue();
                File.ReadAllText(file.Path).ShouldBeEmpty();
            }
        }

        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        private void WritingNothingCreatesNewFile(bool useNullLines)
        {
            ITaskItem[] lines = useNullLines ? null : Array.Empty<ITaskItem>();

            using (var testEnv = TestEnvironment.Create())
            {
                var file = testEnv.GetTempFile();

                File.Exists(file.Path).ShouldBeFalse();

                new WriteLinesToFile
                {
                    Overwrite = true,
                    BuildEngine = new MockEngine(_output),
                    File = new TaskItem(file.Path),
                    Lines = lines
                }.Execute().ShouldBeTrue();

                File.Exists(file.Path).ShouldBeTrue();
                File.ReadAllText(file.Path).ShouldBeEmpty();
            }
        }
    }
}
