// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Construction;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Unittest;
using Shouldly;
using Xunit;

#nullable disable

namespace Microsoft.Build.UnitTests.BackEnd
{
    public class ResultsCache_Tests
    {
        [Fact]
        public void TestConstructor()
        {
            ResultsCache cache = new ResultsCache();
        }

        [Fact]
        public void TestAddAndRetrieveResults()
        {
            ResultsCache cache = new ResultsCache();
            BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget" }, null, BuildEventContext.Invalid, null); BuildResult result = new BuildResult(request);
            result.AddResultsForTarget("testTarget", BuildResultUtilities.GetEmptyFailingTargetResult());
            cache.AddResult(result);

            BuildResult retrievedResult = cache.GetResultForRequest(request);

            Assert.True(AreResultsIdentical(result, retrievedResult));
        }

        [Fact]
        public void TestAddAndRetrieveResultsByConfiguration()
        {
            ResultsCache cache = new ResultsCache();
            BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget" }, null, BuildEventContext.Invalid, null);
            BuildResult result = new BuildResult(request);
            result.AddResultsForTarget("testTarget", BuildResultUtilities.GetEmptyFailingTargetResult());
            cache.AddResult(result);

            request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "otherTarget" }, null, BuildEventContext.Invalid, null);
            result = new BuildResult(request);
            result.AddResultsForTarget("otherTarget", BuildResultUtilities.GetEmptySucceedingTargetResult());
            cache.AddResult(result);

            BuildResult retrievedResult = cache.GetResultsForConfiguration(1);

            Assert.True(retrievedResult.HasResultsForTarget("testTarget"));
            Assert.True(retrievedResult.HasResultsForTarget("otherTarget"));
        }

        [Fact]
        public void CacheCanBeEnumerated()
        {
            ResultsCache cache = new ResultsCache();
            BuildRequest request = new BuildRequest(submissionId: 1, nodeRequestId: 0, configurationId: 1, new string[1] { "testTarget" }, null, BuildEventContext.Invalid, null);
            BuildResult result = new BuildResult(request);
            result.AddResultsForTarget("result1target1", BuildResultUtilities.GetEmptyFailingTargetResult());
            cache.AddResult(result);

            request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "otherTarget" }, null, BuildEventContext.Invalid, null);
            result = new BuildResult(request);
            result.AddResultsForTarget("result1target2", BuildResultUtilities.GetEmptySucceedingTargetResult());
            cache.AddResult(result);

            BuildResult result2 = new BuildResult(new BuildRequest(submissionId: 1, nodeRequestId: 0, configurationId: 2, new string[1] { "testTarget" }, null, BuildEventContext.Invalid, null));
            result2.AddResultsForTarget("result2target1", BuildResultUtilities.GetEmptyFailingTargetResult());
            cache.AddResult(result2);

            var results = cache.ToArray();

            results.Length.ShouldBe(2);

            Assert.True(results[0].HasResultsForTarget("result1target1"));
            Assert.True(results[0].HasResultsForTarget("result1target2"));
            Assert.True(results[1].HasResultsForTarget("result2target1"));
        }

        [Fact]
        public void TestMissingResults()
        {
            ResultsCache cache = new ResultsCache();

            BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget" }, null, BuildEventContext.Invalid, null);
            BuildResult retrievedResult = cache.GetResultForRequest(request);
            Assert.Null(retrievedResult);
        }

        [Fact]
        public void TestRetrieveMergedResults()
        {
            ResultsCache cache = new ResultsCache();
            BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[2] { "testTarget", "testTarget2" }, null, BuildEventContext.Invalid, null);
            BuildResult result = new BuildResult(request);
            result.AddResultsForTarget("testTarget", BuildResultUtilities.GetEmptyFailingTargetResult());
            cache.AddResult(result);

            BuildResult result2 = new BuildResult(request);
            result2.AddResultsForTarget("testTarget2", BuildResultUtilities.GetEmptySucceedingTargetResult());
            cache.AddResult(result2);

            BuildResult retrievedResult = cache.GetResultForRequest(request);

            Assert.True(AreResultsIdenticalForTarget(result, retrievedResult, "testTarget"));
            Assert.True(AreResultsIdenticalForTarget(result2, retrievedResult, "testTarget2"));
        }

        [Fact]
        public void TestMergeResultsWithException()
        {
            ResultsCache cache = new ResultsCache();
            BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[] { "testTarget" }, null, BuildEventContext.Invalid, null);
            BuildResult result = new BuildResult(request);
            result.AddResultsForTarget("testTarget", BuildResultUtilities.GetEmptyFailingTargetResult());
            cache.AddResult(result);

            BuildResult result2 = new BuildResult(request, new Exception("Test exception"));
            cache.AddResult(result2);

            BuildResult retrievedResult = cache.GetResultForRequest(request);

            Assert.NotNull(retrievedResult.Exception);
        }

        [Fact]
        public void TestRetrieveIncompleteResults()
        {
            Assert.Throws<InternalErrorException>(() =>
            {
                ResultsCache cache = new ResultsCache();
                BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[2] { "testTarget", "testTarget2" }, null, BuildEventContext.Invalid, null);
                BuildResult result = new BuildResult(request);
                result.AddResultsForTarget("testTarget", BuildResultUtilities.GetEmptyFailingTargetResult());
                cache.AddResult(result);

                cache.GetResultForRequest(request);
            });
        }
        [Fact]
        public void TestRetrieveSubsetResults()
        {
            ResultsCache cache = new ResultsCache();
            BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget2" }, null, BuildEventContext.Invalid, null);
            BuildResult result = new BuildResult(request);
            result.AddResultsForTarget("testTarget", BuildResultUtilities.GetEmptyFailingTargetResult());
            cache.AddResult(result);

            BuildResult result2 = new BuildResult(request);
            result2.AddResultsForTarget("testTarget2", BuildResultUtilities.GetEmptySucceedingTargetResult());
            cache.AddResult(result2);

            BuildResult retrievedResult = cache.GetResultForRequest(request);

            Assert.True(AreResultsIdenticalForTarget(result2, retrievedResult, "testTarget2"));
        }

        /// <summary>
        /// If a result had multiple targets associated with it and we only requested some of their
        /// results, the returned result should only contain the targets we asked for, and the overall
        /// status of the result should reflect the targets we asked for as well.
        /// </summary>
        [Fact]
        public void TestRetrieveSubsetTargetsFromResult()
        {
            ResultsCache cache = new ResultsCache();
            BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget2" }, null, BuildEventContext.Invalid, null);

            BuildResult result = new BuildResult(request);
            result.AddResultsForTarget("testTarget", BuildResultUtilities.GetEmptyFailingTargetResult());
            result.AddResultsForTarget("testTarget2", BuildResultUtilities.GetEmptySucceedingTargetResult());
            cache.AddResult(result);

            ResultsCacheResponse response = cache.SatisfyRequest(request, new List<string>(), new List<string>(new string[] { "testTarget2" }), skippedResultsDoNotCauseCacheMiss: false);

            Assert.Equal(ResultsCacheResponseType.Satisfied, response.Type);

            Assert.True(AreResultsIdenticalForTarget(result, response.Results, "testTarget2"));
            Assert.False(response.Results.HasResultsForTarget("testTarget"));
            Assert.Equal(BuildResultCode.Success, response.Results.OverallResult);
        }

        [Fact]
        public void TestCacheOnDifferentBuildFlagsPerRequest_ProvideProjectStateAfterBuild()
        {
            string targetName = "testTarget1";
            int submissionId = 1;
            int nodeRequestId = 0;
            int configurationId = 1;

            BuildRequest requestWithNoBuildDataFlags = new BuildRequest(
               submissionId,
               nodeRequestId,
               configurationId,
               new string[1] { targetName } /* escapedTargets */,
               null /* hostServices */,
               BuildEventContext.Invalid /* parentBuildEventContext */,
               null /* parentRequest */,
               BuildRequestDataFlags.None);

            BuildRequest requestWithProjectStateFlag = new BuildRequest(
               submissionId,
               nodeRequestId,
               configurationId,
               new string[1] { targetName } /* escapedTargets */,
               null /* hostServices */,
               BuildEventContext.Invalid /* parentBuildEventContext */,
               null /* parentRequest */,
               BuildRequestDataFlags.ProvideProjectStateAfterBuild);

            BuildRequest requestWithNoBuildDataFlags2 = new BuildRequest(
               submissionId,
               nodeRequestId,
               configurationId,
               new string[1] { targetName } /* escapedTargets */,
               null /* hostServices */,
               BuildEventContext.Invalid /* parentBuildEventContext */,
               null /* parentRequest */,
               BuildRequestDataFlags.None);

            BuildResult resultForRequestWithNoBuildDataFlags = new(requestWithNoBuildDataFlags);
            resultForRequestWithNoBuildDataFlags.AddResultsForTarget(targetName, BuildResultUtilities.GetEmptySucceedingTargetResult());
            ResultsCache cache = new();
            cache.AddResult(resultForRequestWithNoBuildDataFlags);

            ResultsCacheResponse cacheResponseForRequestWithNoBuildDataFlags = cache.SatisfyRequest(
               requestWithNoBuildDataFlags,
               new List<string>(),
               new List<string>(new string[] { targetName }),
               skippedResultsDoNotCauseCacheMiss: false);

            ResultsCacheResponse cachedResponseForProjectState = cache.SatisfyRequest(
               requestWithProjectStateFlag,
               new List<string>(),
               new List<string>(new string[] { targetName }),
               skippedResultsDoNotCauseCacheMiss: false);

            ResultsCacheResponse cacheResponseForNoBuildDataFlags2 = cache.SatisfyRequest(
               requestWithNoBuildDataFlags2,
               new List<string>(),
               new List<string>(new string[] { targetName }),
               skippedResultsDoNotCauseCacheMiss: false);

            Assert.Equal(ResultsCacheResponseType.Satisfied, cacheResponseForRequestWithNoBuildDataFlags.Type);

            // Because ProvideProjectStateAfterBuildFlag was provided as a part of BuildRequest
            Assert.Equal(ResultsCacheResponseType.NotSatisfied, cachedResponseForProjectState.Type);

            Assert.Equal(ResultsCacheResponseType.Satisfied, cacheResponseForNoBuildDataFlags2.Type);
        }

        [Fact]
        public void TestCacheOnDifferentBuildFlagsPerRequest_ProvideSubsetOfStateAfterBuild()
        {
            string targetName = "testTarget1";
            int submissionId = 1;
            int nodeRequestId = 0;
            int configurationId = 1;

            RequestedProjectState requestedProjectState1 = new()
            {
                PropertyFilters = ["property1", "property2"],
            };
            BuildRequest requestWithSubsetFlag1 = new BuildRequest(
                submissionId,
                nodeRequestId,
                configurationId,
                new string[1] { targetName } /* escapedTargets */,
                null /* hostServices */,
                BuildEventContext.Invalid /* parentBuildEventContext */,
                null /* parentRequest */,
                BuildRequestDataFlags.ProvideSubsetOfStateAfterBuild,
                requestedProjectState1);

            RequestedProjectState requestedProjectState2 = new()
            {
                PropertyFilters = ["property1"],
            };
            BuildRequest requestWithSubsetFlag2 = new BuildRequest(
                submissionId,
                nodeRequestId,
                configurationId,
                new string[1] { targetName } /* escapedTargets */,
                null /* hostServices */,
                BuildEventContext.Invalid /* parentBuildEventContext */,
                null /* parentRequest */,
                BuildRequestDataFlags.ProvideSubsetOfStateAfterBuild,
                requestedProjectState2);

            BuildResult resultForRequestWithSubsetFlag1 = new(requestWithSubsetFlag1);
            resultForRequestWithSubsetFlag1.AddResultsForTarget(targetName, BuildResultUtilities.GetEmptySucceedingTargetResult());

            using TextReader textReader = new StringReader(@"
              <Project>
                <PropertyGroup>
                  <property1>Value1</property1>
                  <property2>Value2</property2>
                </PropertyGroup>
              </Project>
            ");
            using XmlReader xmlReader = XmlReader.Create(textReader);
            resultForRequestWithSubsetFlag1.ProjectStateAfterBuild = new ProjectInstance(ProjectRootElement.Create(xmlReader)).FilteredCopy(requestedProjectState1);

            ResultsCache cache = new();
            cache.AddResult(resultForRequestWithSubsetFlag1);

            ResultsCacheResponse cachedResponseWithSubsetFlag1 = cache.SatisfyRequest(
                requestWithSubsetFlag1,
                new List<string>(),
                new List<string>(new string[] { targetName }),
                skippedResultsDoNotCauseCacheMiss: false);

            ResultsCacheResponse cachedResponseWithSubsetFlag2 = cache.SatisfyRequest(
                requestWithSubsetFlag2,
                new List<string>(),
                new List<string>(new string[] { targetName }),
                skippedResultsDoNotCauseCacheMiss: false);

            // We used the same filter that was used for the ProjectInstance in the cache -> cache hit.
            Assert.Equal(ResultsCacheResponseType.Satisfied, cachedResponseWithSubsetFlag1.Type);
            Assert.Equal("Value1", cachedResponseWithSubsetFlag1.Results.ProjectStateAfterBuild.GetPropertyValue("property1"));
            Assert.Equal("Value2", cachedResponseWithSubsetFlag1.Results.ProjectStateAfterBuild.GetPropertyValue("property2"));

            // We used a filter that's a subset of the one used for the ProjectInstance in the cache -> cache hit.
            Assert.Equal(ResultsCacheResponseType.Satisfied, cachedResponseWithSubsetFlag2.Type);
            Assert.Equal("Value1", cachedResponseWithSubsetFlag2.Results.ProjectStateAfterBuild.GetPropertyValue("property1"));
            Assert.Equal("", cachedResponseWithSubsetFlag2.Results.ProjectStateAfterBuild.GetPropertyValue("property2"));
        }

        [Fact]
        public void TestClearResultsCache()
        {
            ResultsCache cache = new ResultsCache();
            cache.ClearResults();

            BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget2" }, null, BuildEventContext.Invalid, null);
            BuildResult result = new BuildResult(request);
            result.AddResultsForTarget("testTarget", BuildResultUtilities.GetEmptyFailingTargetResult());
            cache.AddResult(result);

            cache.ClearResults();

            Assert.Null(cache.GetResultForRequest(request));
        }

        public static IEnumerable<object[]> CacheSerializationTestData
        {
            get
            {
                yield return new[] { new ResultsCache() };

                var request1 = new BuildRequest(1, 2, 3, new[] { "target1" }, null, BuildEventContext.Invalid, null);
                var request2 = new BuildRequest(4, 5, 6, new[] { "target2" }, null, BuildEventContext.Invalid, null);

                var br1 = new BuildResult(request1);
                br1.AddResultsForTarget("target1", BuildResultUtilities.GetEmptySucceedingTargetResult());

                var resultsCache = new ResultsCache();
                resultsCache.AddResult(br1.Clone());

                yield return new[] { resultsCache };

                var br2 = new BuildResult(request2);
                br2.AddResultsForTarget("target2", BuildResultUtilities.GetEmptyFailingTargetResult());

                var resultsCache2 = new ResultsCache();
                resultsCache2.AddResult(br1.Clone());
                resultsCache2.AddResult(br2.Clone());

                yield return new[] { resultsCache2 };
            }
        }

        // Serialize latest version and deserialize latest version of the cache
        [Theory]
        [MemberData(nameof(CacheSerializationTestData))]
        public void TestResultsCacheTranslation(object obj)
        {
            var resultsCache = (ResultsCache)obj;

            resultsCache.Translate(TranslationHelpers.GetWriteTranslator());

            var copy = new ResultsCache(TranslationHelpers.GetReadTranslator());

            CompareResultsCache(resultsCache, copy);
        }

        [Theory]
        [InlineData(1, 1)] // Serialize version 0 and deserialize version 0 
        [InlineData(1, 0)] // Serialize version 0 and deserialize latest version
        public void TestResultsCacheTranslationAcrossVersions(int envValue1, int envValue2)
        {
            using (var env = TestEnvironment.Create())
            {
                env.SetEnvironmentVariable("MSBUILDDONOTVERSIONBUILDRESULT", $"{envValue1}");

                // Create a ResultsCache
                var request1 = new BuildRequest(1, 2, 3, new[] { "target1" }, null, BuildEventContext.Invalid, null);
                var request2 = new BuildRequest(4, 5, 6, new[] { "target2" }, null, BuildEventContext.Invalid, null);

                var br1 = new BuildResult(request1);
                var br2 = new BuildResult(request2);
                br2.AddResultsForTarget("target2", BuildResultUtilities.GetEmptyFailingTargetResult());

                var resultsCache = new ResultsCache();
                resultsCache.AddResult(br1.Clone());
                resultsCache.AddResult(br2.Clone());

                resultsCache.Translate(TranslationHelpers.GetWriteTranslator());

                env.SetEnvironmentVariable("MSBUILDDONOTVERSIONBUILDRESULT", $"{envValue2}");
                Traits.UpdateFromEnvironment();

                var copy = new ResultsCache(TranslationHelpers.GetReadTranslator());

                CompareResultsCache(resultsCache, copy);
            }
        }

        private void CompareResultsCache(ResultsCache resultsCache1, ResultsCache resultsCache2)
        {
            resultsCache2.ResultsDictionary.Keys.ToHashSet().SetEquals(resultsCache1.ResultsDictionary.Keys.ToHashSet()).ShouldBeTrue();

            foreach (var configId in resultsCache2.ResultsDictionary.Keys)
            {
                var copiedBuildResult = resultsCache2.ResultsDictionary[configId];
                var initialBuildResult = resultsCache1.ResultsDictionary[configId];

                copiedBuildResult.SubmissionId.ShouldBe(initialBuildResult.SubmissionId);
                copiedBuildResult.ConfigurationId.ShouldBe(initialBuildResult.ConfigurationId);

                copiedBuildResult.ResultsByTarget.Keys.ToHashSet().SetEquals(initialBuildResult.ResultsByTarget.Keys.ToHashSet()).ShouldBeTrue();

                foreach (var targetKey in copiedBuildResult.ResultsByTarget.Keys)
                {
                    var copiedTargetResult = copiedBuildResult.ResultsByTarget[targetKey];
                    var initialTargetResult = initialBuildResult.ResultsByTarget[targetKey];

                    copiedTargetResult.WorkUnitResult.ResultCode.ShouldBe(initialTargetResult.WorkUnitResult.ResultCode);
                    copiedTargetResult.WorkUnitResult.ActionCode.ShouldBe(initialTargetResult.WorkUnitResult.ActionCode);
                }
            }
        }

        #region Helper Methods

        internal static bool AreResultsIdentical(BuildResult a, BuildResult b)
        {
            if (a.ConfigurationId != b.ConfigurationId)
            {
                return false;
            }

            if ((a.Exception == null) ^ (b.Exception == null))
            {
                return false;
            }

            if (a.Exception != null)
            {
                if (a.Exception.GetType() != b.Exception.GetType())
                {
                    return false;
                }
            }

            if (a.OverallResult != b.OverallResult)
            {
                return false;
            }

            foreach (KeyValuePair<string, TargetResult> targetResult in a.ResultsByTarget)
            {
                if (!AreResultsIdenticalForTarget(a, b, targetResult.Key))
                {
                    return false;
                }
            }

            foreach (KeyValuePair<string, TargetResult> targetResult in b.ResultsByTarget)
            {
                if (!AreResultsIdenticalForTarget(a, b, targetResult.Key))
                {
                    return false;
                }
            }

            return true;
        }

        internal static bool AreResultsIdenticalForTargets(BuildResult a, BuildResult b, string[] targets)
        {
            foreach (string target in targets)
            {
                if (!AreResultsIdenticalForTarget(a, b, target))
                {
                    return false;
                }
            }

            return true;
        }

        private static bool AreResultsIdenticalForTarget(BuildResult a, BuildResult b, string target)
        {
            if (!a.HasResultsForTarget(target) || !b.HasResultsForTarget(target))
            {
                return false;
            }

            if (a[target].ResultCode != b[target].ResultCode)
            {
                return false;
            }

            if (!AreItemsIdentical(a[target].Items, b[target].Items))
            {
                return false;
            }

            return true;
        }

        private static bool AreItemsIdentical(IList<ITaskItem> a, IList<ITaskItem> b)
        {
            // Exhaustive comparison of items should not be necessary since we don't merge on the item level.
            return a.Count == b.Count;
        }

        #endregion
    }
}
