#region Copyright notice and license

// Copyright 2018 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using System.IO;
using Microsoft.Build.Framework;
using Moq;
using NUnit.Framework;

namespace Grpc.Tools.Tests
{
    public class ProtoCompileCommandLineGeneratorTest : ProtoCompileBasicTest
    {
        [SetUp]
        public new void SetUp()
        {
            _task.Generator = "csharp";
            _task.OutputDir = "outdir";
            _task.Protobuf = Utils.MakeSimpleItems("a.proto");
        }

        void ExecuteExpectSuccess()
        {
            _mockEngine
              .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
              .Callback((BuildErrorEventArgs e) =>
                  Assert.Fail($"Error logged by build engine:\n{e.Message}"));
            bool result = _task.Execute();
            Assert.IsTrue(result);
        }

        [Test]
        public void MinimalCompile()
        {
            ExecuteExpectSuccess();
            Assert.That(_task.LastPathToTool, Does.Match(@"protoc(.exe)?$"));
            Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
                "--csharp_out=outdir", "--error_format=msvs", "a.proto" }));
        }

        [Test]
        public void CompileTwoFiles()
        {
            _task.Protobuf = Utils.MakeSimpleItems("a.proto", "foo/b.proto");
            ExecuteExpectSuccess();
            Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
                "--csharp_out=outdir", "--error_format=msvs", "a.proto", "foo/b.proto" }));
        }

        [Test]
        public void CompileWithProtoPaths()
        {
            _task.ProtoPath = new[] { "/path1", "/path2" };
            ExecuteExpectSuccess();
            Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
                "--csharp_out=outdir", "--proto_path=/path1",
                "--proto_path=/path2", "--error_format=msvs", "a.proto" }));
        }

        [TestCase("Cpp")]
        [TestCase("CSharp")]
        [TestCase("Java")]
        [TestCase("Javanano")]
        [TestCase("Js")]
        [TestCase("Objc")]
        [TestCase("Php")]
        [TestCase("Python")]
        [TestCase("Ruby")]
        public void CompileWithOptions(string gen)
        {
            _task.Generator = gen;
            _task.OutputOptions = new[] { "foo", "bar" };
            ExecuteExpectSuccess();
            gen = gen.ToLowerInvariant();
            Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
                $"--{gen}_out=outdir", $"--{gen}_opt=foo,bar", "--error_format=msvs", "a.proto" }));
        }

        [Test]
        public void OutputDependencyFile()
        {
            _task.DependencyOut = "foo/my.protodep";
            // Task fails trying to read the non-generated file; we ignore that.
            _task.Execute();
            Assert.That(_task.LastResponseFile,
                Does.Contain("--dependency_out=foo/my.protodep"));
        }

        [Test]
        public void OutputDependencyWithProtoDepDir()
        {
            _task.ProtoDepDir = "foo";
            // Task fails trying to read the non-generated file; we ignore that.
            _task.Execute();
            Assert.That(_task.LastResponseFile,
                Has.One.Match(@"^--dependency_out=foo[/\\].+_a.protodep$"));
        }

        [Test]
        public void GenerateGrpc()
        {
            _task.GrpcPluginExe = "/foo/grpcgen";
            ExecuteExpectSuccess();
            Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] {
                "--csharp_out=outdir", "--grpc_out=outdir",
                "--plugin=protoc-gen-grpc=/foo/grpcgen" }));
        }

        [Test]
        public void GenerateGrpcWithOutDir()
        {
            _task.GrpcPluginExe = "/foo/grpcgen";
            _task.GrpcOutputDir = "gen-out";
            ExecuteExpectSuccess();
            Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] {
                "--csharp_out=outdir", "--grpc_out=gen-out" }));
        }

        [Test]
        public void GenerateGrpcWithOptions()
        {
            _task.GrpcPluginExe = "/foo/grpcgen";
            _task.GrpcOutputOptions = new[] { "baz", "quux" };
            ExecuteExpectSuccess();
            Assert.That(_task.LastResponseFile,
                        Does.Contain("--grpc_opt=baz,quux"));
        }

        [Test]
        public void AdditionalProtocArguments()
        {
            _task.AdditionalProtocArguments = new[] { "--experimental_allow_proto3_optional" };
            ExecuteExpectSuccess();
            Assert.That(_task.LastResponseFile,
                Does.Contain("--experimental_allow_proto3_optional"));
        }

        [Test]
        public void DirectoryArgumentsSlashTrimmed()
        {
            _task.GrpcPluginExe = "/foo/grpcgen";
            _task.GrpcOutputDir = "gen-out/";
            _task.OutputDir = "outdir/";
            _task.ProtoPath = new[] { "/path1/", "/path2/" };
            ExecuteExpectSuccess();
            Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] {
        "--proto_path=/path1", "--proto_path=/path2",
        "--csharp_out=outdir", "--grpc_out=gen-out" }));
        }

        [TestCase(".", ".")]
        [TestCase("/", "/")]
        [TestCase("//", "/")]
        [TestCase("/foo/", "/foo")]
        [TestCase("/foo", "/foo")]
        [TestCase("foo/", "foo")]
        [TestCase("foo//", "foo")]
        [TestCase("foo/\\", "foo")]
        [TestCase("foo\\/", "foo")]
        [TestCase("C:\\foo", "C:\\foo")]
        [TestCase("C:", "C:")]
        [TestCase("C:\\", "C:\\")]
        [TestCase("C:\\\\", "C:\\")]
        public void DirectorySlashTrimmingCases(string given, string expect)
        {
            if (Path.DirectorySeparatorChar == '/')
                expect = expect.Replace('\\', '/');
            _task.OutputDir = given;
            ExecuteExpectSuccess();
            Assert.That(_task.LastResponseFile,
                        Does.Contain("--csharp_out=" + expect));
        }

        [TestCase(
            "../Protos/greet.proto(19) : warning in column=5 : warning : When enum name is stripped and label is PascalCased (Zero) this value label conflicts with Zero.",
            "../Protos/greet.proto",
            19,
            5,
            "warning : When enum name is stripped and label is PascalCased (Zero) this value label conflicts with Zero.")]
        [TestCase(
            "../Protos/greet.proto: warning: Import google/protobuf/empty.proto but not used.",
            "../Protos/greet.proto",
            0,
            0,
            "Import google/protobuf/empty.proto but not used.")]
        [TestCase("../Protos/greet.proto(14) : error in column=10: \"name\" is already defined in \"Greet.HelloRequest\".", null, 0, 0, null)]
        [TestCase("../Protos/greet.proto: Import \"google / protobuf / empty.proto\" was listed twice.", null, 0, 0, null)]
        [TestCase("[libprotobuf WARNING T:\\altsrc\\github\\...\\csharp\\csharp_enum.cc:74] Duplicate enum value Work (originally Work) in PhoneType; adding underscore to distinguish",
            "T:\\altsrc\\github\\...\\csharp\\csharp_enum.cc",
            74, 0,
            "Duplicate enum value Work (originally Work) in PhoneType; adding underscore to distinguish")]
        [TestCase("[libprotobuf ERROR T:\\path\\...\\filename:23] Some message", null, 0, 0, null)]
        [TestCase("[libprotobuf FATAL T:\\path\\...\\filename:23] Some message", null, 0, 0, null)]
        public void WarningsParsed(string stderr, string file, int line, int col, string message)
        {
            _task.StdErrMessages.Add(stderr);

            bool matched = false;
            _mockEngine
                .Setup(me => me.LogWarningEvent(It.IsAny<BuildWarningEventArgs>()))
                .Callback((BuildWarningEventArgs e) => {
                    matched = true;
                    if (file != null)
                    {
                        Assert.AreEqual(file, e.File);
                        Assert.AreEqual(line, e.LineNumber);
                        Assert.AreEqual(col, e.ColumnNumber);
                        Assert.AreEqual(message, e.Message);
                    }
                    else
                    {
                        Assert.Fail($"Error logged by build engine:\n{e.Message}");
                    }
                });

            bool result = _task.Execute();
            Assert.IsFalse(result);

            // To get here in the test then either the event fired and the values matched
            // or the event did not fire (input did not parse as a warning).
            // If it did not parse as a warning then check that the expected message is null
            if (!matched && message != null)
            {
                Assert.Fail($"Expected match: {message}");
            }
        }

        [TestCase(
            "../Protos/greet.proto(14) : error in column=10: \"name\" is already defined in \"Greet.HelloRequest\".",
            "../Protos/greet.proto",
            14,
            10,
            "\"name\" is already defined in \"Greet.HelloRequest\".")]
        [TestCase(
            "../Protos/greet.proto: Import \"google / protobuf / empty.proto\" was listed twice.",
            "../Protos/greet.proto",
            0,
            0,
            "Import \"google / protobuf / empty.proto\" was listed twice.")]
        [TestCase("../Protos/greet.proto(19) : warning in column=5 : warning : When enum name is stripped and label is PascalCased (Zero) this value label conflicts with Zero.", null, 0, 0, null)]
        [TestCase("../Protos/greet.proto: warning: Import google/protobuf/empty.proto but not used.", null, 0, 0, null)]
        [TestCase("[libprotobuf WARNING T:\\altsrc\\github\\...\\csharp\\csharp_enum.cc:74] Duplicate enum value Work (originally Work) in PhoneType; adding underscore to distinguish",
            null, 0, 0, null)]
        [TestCase("[libprotobuf ERROR T:\\path\\...\\filename:23] Some message",
            "T:\\path\\...\\filename", 23, 0, "ERROR Some message")]
        [TestCase("[libprotobuf FATAL T:\\path\\...\\filename:23] Some message",
            "T:\\path\\...\\filename", 23, 0, "FATAL Some message")]
        public void ErrorsParsed(string stderr, string file, int line, int col, string message)
        {
            _task.StdErrMessages.Add(stderr);

            bool matched = false;
            _mockEngine
                .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
                .Callback((BuildErrorEventArgs e) => {
                    matched = true;
                    if (file != null)
                    {
                        Assert.AreEqual(file, e.File);
                        Assert.AreEqual(line, e.LineNumber);
                        Assert.AreEqual(col, e.ColumnNumber);
                        Assert.AreEqual(message, e.Message);
                    }
                    else
                    {
                        // Ignore expected error
                        // "protoc/protoc.exe" existed with code -1.
                        if (!e.Message.EndsWith("exited with code -1."))
                        {
                            Assert.Fail($"Error logged by build engine:\n{e.Message}");
                        }
                    }
                });


            bool result = _task.Execute();
            Assert.IsFalse(result);

            // To get here in the test then either the event fired and the values matched
            // or the event did not fire (input did not parse as an error).
            // If it did not parse as an error then check that the expected message is null
            if (!matched && message != null)
            {
                Assert.Fail($"Expected match: {message}");
            }
        }
    };
}
