// 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;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Build.Execution;
using Microsoft.Build.Experimental.BuildCheck;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
using static Microsoft.Build.Experimental.BuildCheck.Infrastructure.BuildCheckManagerProvider;

namespace Microsoft.Build.BuildCheck.UnitTests
{
    public class TaskInvocationCheckDataTests : IDisposable
    {
        internal sealed class TestCheck : Check
        {
            #region Check initialization

            public static CheckRule SupportedRule = new CheckRule("BC0000", "TestRule", "TestDescription", "TestMessage",
                new CheckConfiguration() { Severity = CheckResultSeverity.Warning });

            public override string FriendlyName => "MSBuild.TestCheck";

            public override IReadOnlyList<CheckRule> SupportedRules { get; } = [SupportedRule];

            public override void Initialize(ConfigurationContext configurationContext)
            { }

            public override void RegisterActions(IBuildCheckRegistrationContext registrationContext)
            {
                registrationContext.RegisterTaskInvocationAction(TaskInvocationAction);
            }

            #endregion

            /// <summary>
            /// Stores all TaskInvocationAnalysisData reported during the build.
            /// </summary>
            public List<TaskInvocationCheckData> CheckData = new();

            private void TaskInvocationAction(BuildCheckDataContext<TaskInvocationCheckData> context)
            {
                CheckData.Add(context.Data);
            }
        }

        private ITestOutputHelper _output;

        private static TestCheck? s_testCheck;

        public TaskInvocationCheckDataTests(ITestOutputHelper output)
        {
            _output = output;

            BuildCheckManager.s_testFactoriesPerDataSource =
            [
                // BuildCheckDataSource.EventArgs
                [
                    new ([TestCheck.SupportedRule.Id], true, () => s_testCheck = new TestCheck()),
                ],
                // BuildCheckDataSource.Execution
                [],
            ];

            s_testCheck?.CheckData.Clear();
        }

        public void Dispose()
        {
            BuildCheckManager.s_testFactoriesPerDataSource = null;
        }

        private void BuildProject(string taskInvocation)
        {
            using (var env = TestEnvironment.Create(_output))
            {
                var testProject = env.CreateTestProjectWithFiles($"<Project><Target Name=\"Build\">{taskInvocation}</Target></Project>");

                using (var buildManager = new BuildManager())
                {
                    var request = new BuildRequestData(testProject.ProjectFile, new Dictionary<string, string?>(), MSBuildConstants.CurrentToolsVersion, [], null, BuildRequestDataFlags.None);
                    var parameters = new BuildParameters
                    {
                        Loggers = [new MockLogger(_output)],
                        LogTaskInputs = true,
                        IsBuildCheckEnabled = true,
                        ShutdownInProcNodeOnBuildFinish = true,
                    };

                    var result = buildManager.Build(parameters, request);

                    result.OverallResult.ShouldBe(BuildResultCode.Success);
                }

                foreach (var data in s_testCheck.ShouldNotBeNull().CheckData)
                {
                    data.ProjectFilePath.ShouldBe(testProject.ProjectFile);
                    data.TaskInvocationLocation.Line.ShouldBeGreaterThan(0);
                    data.TaskInvocationLocation.Column.ShouldBeGreaterThan(0);
                }
            }
        }

        [Fact]
        public void ReportsSimpleTaskParameters()
        {
            BuildProject("<Message Text='Hello'/>");

            s_testCheck!.CheckData.Count.ShouldBe(1);
            var data = s_testCheck.CheckData[0];
            data.TaskName.ShouldBe("Message");
            data.Parameters.Count.ShouldBe(1);
            data.Parameters["Text"].IsOutput.ShouldBe(false);
            data.Parameters["Text"].Value.ShouldBe("Hello");
        }

        [Theory]
        [InlineData("<Output TaskParameter='CombinedPaths' ItemName='OutputDirectories' />")]
        [InlineData("<Output TaskParameter='CombinedPaths' PropertyName='OutputDirectories' />")]
        public void ReportsComplexTaskParameters(string outputElement)
        {
            BuildProject($"""
                <ItemGroup>
                  <TestItem Include='item1;item2'/>
                </ItemGroup>
                <CombinePath BasePath='base' Paths='@(TestItem)'>
                    {outputElement}
                </CombinePath>
            """);

            s_testCheck!.CheckData.Count.ShouldBe(1);
            var data = s_testCheck.CheckData[0];
            data.TaskName.ShouldBe("CombinePath");
            data.Parameters.Count.ShouldBe(3);

            data.Parameters["Paths"].IsOutput.ShouldBe(false);
            data.Parameters["Paths"].Value.ShouldBeAssignableTo(typeof(IList));
            IList listValue = (IList)data.Parameters["Paths"].Value!;
            listValue.Count.ShouldBe(2);
            listValue[0]!.ShouldBeAssignableTo(typeof(ITaskItem));
            listValue[1]!.ShouldBeAssignableTo(typeof(ITaskItem));
            ((ITaskItem)listValue[0]!).ItemSpec.ShouldBe("item1");
            ((ITaskItem)listValue[1]!).ItemSpec.ShouldBe("item2");
            data.Parameters["CombinedPaths"].IsOutput.ShouldBe(true);
            data.Parameters["CombinedPaths"].Value.ShouldNotBeNull();
        }

        [Fact]
        public void TaskParameterEnumeratesValues()
        {
            var parameter1 = MakeParameter("string");
            parameter1.EnumerateValues().SequenceEqual(["string"]).ShouldBeTrue();
            parameter1.EnumerateStringValues().SequenceEqual(["string"]).ShouldBeTrue();

            var parameter2 = MakeParameter(true);
            parameter2.EnumerateValues().SequenceEqual([true]);
            parameter2.EnumerateStringValues().SequenceEqual(["True"]).ShouldBeTrue();

            var item1 = new TaskItem("item1");
            var parameter3 = MakeParameter(item1);
            parameter3.EnumerateValues().SequenceEqual([item1]).ShouldBeTrue();
            parameter3.EnumerateStringValues().SequenceEqual(["item1"]).ShouldBeTrue();

            var array1 = new object[] { "string1", "string2" };
            var parameter4 = MakeParameter(array1);
            parameter4.EnumerateValues().SequenceEqual(array1).ShouldBeTrue();
            parameter4.EnumerateStringValues().SequenceEqual(array1).ShouldBeTrue();

            var item2 = new TaskItem("item2");
            var array2 = new ITaskItem[] { item1, item2 };
            var parameter5 = MakeParameter(array2);
            parameter5.EnumerateValues().SequenceEqual(array2).ShouldBeTrue();
            parameter5.EnumerateStringValues().SequenceEqual(["item1", "item2"]).ShouldBeTrue();

            static TaskInvocationCheckData.TaskParameter MakeParameter(object value)
                => new TaskInvocationCheckData.TaskParameter(value, IsOutput: false);
        }
    }
}
