//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

#nullable disable

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Microsoft.SqlTools.ServiceLayer.Test.Common;
using NUnit.Framework;

namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests
{
    [TestFixture]
    /// <summary>
    /// Scripting service end-to-end integration tests that use the SqlScriptPublishModel type to generate scripts.
    /// </summary>
    public class SqlScriptPublishModelTests 
    {
        [OneTimeSetUp]
        public void SetupSqlScriptPublishModelTests()
        {
            Fixture = new ScriptingFixture();
        }

        private static ScriptingFixture Fixture { get; set; }

        private SqlTestDb Northwind { get { return Fixture.Database; } }

        [Test]
        public async Task ListSchemaObjects()
        {
            using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
            using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
            {
                ScriptingListObjectsParams requestParams = new ScriptingListObjectsParams
                {
                    ConnectionString = this.Northwind.ConnectionString,
                };

                ScriptingListObjectsResult result = await testService.ListScriptingObjects(requestParams);
                ScriptingListObjectsCompleteParams completeParameters = await testService.Driver.WaitForEvent(ScriptingListObjectsCompleteEvent.Type, TimeSpan.FromSeconds(30));
                Assert.AreEqual(ScriptingFixture.ObjectCountWithoutDatabase, completeParameters.ScriptingObjects.Count);
            }
        }

        [Test]
        public async Task ScriptDatabaseSchema()
        {
            using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
            {
                ScriptingParams requestParams = new ScriptingParams
                {
                    ScriptDestination = "ToEditor",
                    ConnectionString = this.Northwind.ConnectionString,
                    ScriptOptions = new ScriptOptions
                    {
                        TypeOfDataToScript = "SchemaOnly",
                    },
                };

                ScriptingResult result = await testService.Script(requestParams);
                ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(30));
                Assert.True(parameters.Success);
            }
        }

        [Test]
        public async Task ScriptDatabaseSchemaAndData()
        {
            using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
            {
                ScriptingParams requestParams = new ScriptingParams
                {
                    ScriptDestination = "ToEditor",
                    ConnectionString = this.Northwind.ConnectionString,
                    ScriptOptions = new ScriptOptions
                    {
                        TypeOfDataToScript = "SchemaAndData",
                    },
                };

                ScriptingResult result = await testService.Script(requestParams);
                ScriptingCompleteParams completeParameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(30));
                Assert.True(completeParameters.Success);
            }
        }

        [Test]
        public async Task ScriptTable()
        {
            using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
            {
                ScriptingParams requestParams = new ScriptingParams
                {
                    ScriptDestination = "ToEditor",
                    ConnectionString = this.Northwind.ConnectionString,
                    ScriptOptions = new ScriptOptions
                    {
                        TypeOfDataToScript = "SchemaOnly",

                    },
                    ScriptingObjects = new List<ScriptingObject>
                    {
                        new ScriptingObject
                        {
                            Type = "Table",
                            Schema = "dbo",
                            Name = "Customers",
                        },
                    }
                };

                ScriptingResult result = await testService.Script(requestParams);
                ScriptingPlanNotificationParams planEvent = await testService.Driver.WaitForEvent(ScriptingPlanNotificationEvent.Type, TimeSpan.FromSeconds(1));
                ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(30));
                Assert.True(parameters.Success);
                Assert.AreEqual(1, planEvent.Count);
            }
        }

        [Test]
        public async Task ScriptTableUsingIncludeFilter()
        {
            using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
            {
                ScriptingParams requestParams = new ScriptingParams
                {
                    ScriptDestination = "ToEditor",
                    ConnectionString = this.Northwind.ConnectionString,
                    ScriptOptions = new ScriptOptions
                    {
                        TypeOfDataToScript = "SchemaOnly",
                    },
                    IncludeObjectCriteria = new List<ScriptingObject>
                    {
                        new ScriptingObject
                        {
                            Type = "Table",
                            Schema = "dbo",
                            Name = "Customers",
                        },
                    }
                };

                ScriptingResult result = await testService.Script(requestParams);
                ScriptingPlanNotificationParams planEvent = await testService.Driver.WaitForEvent(ScriptingPlanNotificationEvent.Type, TimeSpan.FromSeconds(30));
                ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(30));
                Assert.True(parameters.Success);
                // Work around SMO bug https://github.com/microsoft/sqlmanagementobjects/issues/19 which leads to non-unique URNs in the collection
                Assert.That(planEvent.Count, Is.AtLeast(1), "ScripingPlanNotificationParams.Count");
                Assert.That(planEvent.ScriptingObjects.All(obj => obj.Name == "Customers" && obj.Schema == "dbo" && obj.Type == "Table"), "ScriptingPlanNotificationParams.ScriptingObjects contents");
            }
        }

        [Test]
        public async Task ScriptTableAndData()
        {
            using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
            {
                ScriptingParams requestParams = new ScriptingParams
                {
                    ScriptDestination = "ToEditor",
                    ConnectionString = this.Northwind.ConnectionString,
                    ScriptOptions = new ScriptOptions
                    {
                        TypeOfDataToScript = "SchemaAndData",
                    },
                    ScriptingObjects = new List<ScriptingObject>
                    {
                        new ScriptingObject
                        {
                            Type = "Table",
                            Schema = "dbo",
                            Name = "Customers",
                        },
                    }
                };

                ScriptingResult result = await testService.Script(requestParams);
                ScriptingPlanNotificationParams planEvent = await testService.Driver.WaitForEvent(ScriptingPlanNotificationEvent.Type, TimeSpan.FromSeconds(30));
                ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(30));
                Assert.True(parameters.Success);
                // Work around SMO bug https://github.com/microsoft/sqlmanagementobjects/issues/19 which leads to non-unique URNs in the collection
                Assert.That(planEvent.Count, Is.AtLeast(1), "ScripingPlanNotificationParams.Count");
                Assert.That(planEvent.ScriptingObjects.All(obj => obj.Name == "Customers" && obj.Schema == "dbo" && obj.Type == "Table"), "ScriptingPlanNotificationParams.ScriptingObjects contents");
            }
        }

        [Test]
        public async Task ScriptTableDoesNotExist()
        {
            using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
            {
                ScriptingParams requestParams = new ScriptingParams
                {
                    ScriptDestination = "ToEditor",
                    ConnectionString = this.Northwind.ConnectionString,
                    ScriptOptions = new ScriptOptions
                    {
                        TypeOfDataToScript = "SchemaOnly",
                        ContinueScriptingOnError = false
                    },
                    ScriptingObjects = new List<ScriptingObject>
                    {
                        new ScriptingObject
                        {
                            Type = "Table",
                            Schema = "dbo",
                            Name = "TableDoesNotExist",
                        },
                    }
                };

                ScriptingResult result = await testService.Script(requestParams);
                ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(15));
                Assert.True(parameters.HasError);
                Assert.That(parameters.ErrorMessage, Contains.Substring("An error occurred while scripting the objects."), "parameters.ErrorMessage");
                Assert.That(parameters.ErrorDetails, Contains.Substring("The Table '[dbo].[TableDoesNotExist]' does not exist on the server."), "parameters.ErrorDetails");
            }
        }

        [Test]
        public async Task ScriptSchemaCancel()
        {
            using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
            {
                ScriptingParams requestParams = new ScriptingParams
                {
                    ScriptDestination = "ToEditor",
                    ConnectionString = this.Northwind.ConnectionString,
                    ScriptOptions = new ScriptOptions
                    {
                        TypeOfDataToScript = "SchemaAndData",
                    }
                };

                var result = Task.Run(() => testService.Script(requestParams));
                ScriptingProgressNotificationParams progressParams = await testService.Driver.WaitForEvent(ScriptingProgressNotificationEvent.Type, TimeSpan.FromSeconds(60));
                await Task.Run(() => testService.CancelScript(progressParams.OperationId));
                ScriptingCompleteParams cancelEvent = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(10));
                Assert.True(cancelEvent.Canceled);
            }
        }


        [Test]
        public async Task ScriptSchemaInvalidConnectionString()
        {
            using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
            {
                ScriptingParams requestParams = new ScriptingParams
                {
                    ScriptDestination = "ToEditor",
                    ConnectionString = "I'm an invalid connection string",
                    ScriptOptions = new ScriptOptions
                    {
                        TypeOfDataToScript = "SchemaAndData",
                    },
                };

                ScriptingResult result = await testService.Script(requestParams);
                ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(10));
                Assert.True(parameters.HasError);
                Assert.AreEqual("Error parsing ScriptingParams.ConnectionString property.", parameters.ErrorMessage);
            }
        }

        [Test]
        public async Task ScriptSchemaInvalidFilePath()
        {
            using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
            {
                ScriptingParams requestParams = new ScriptingParams
                {
                    FilePath = "This path doesn't even exist",
                    ConnectionString = "Server=Temp;Database=Temp;User Id=Temp;",
                    ScriptOptions = new ScriptOptions
                    {
                        TypeOfDataToScript = "SchemaAndData",
                    },
                };

                ScriptingResult result = await testService.Script(requestParams);
                ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(10));
                Assert.True(parameters.HasError);
                Assert.AreEqual("Invalid directory specified by the ScriptingParams.FilePath property.", parameters.ErrorMessage);
            }
        }

        [Test]
        public async Task ScriptSelectTable()
        {
            using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
            using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
            {
                await testService.Connect(tempFile.FilePath, testService.TestConnectionService.GetConnectionParameters(serverType: TestServerType.OnPrem));
                ScriptingParams requestParams = new ScriptingParams
                {
                    ScriptDestination = "ToEditor",
                    OwnerUri = tempFile.FilePath,
                    ScriptOptions = new ScriptOptions
                    {
                    },
                    ScriptingObjects = new List<ScriptingObject>
                    {
                        new ScriptingObject
                        {
                            Type = "Table",
                            Schema = "dbo",
                            Name = "Customers",
                        }
                    },
                    Operation = ScriptingOperationType.Select
                };
                ScriptingResult result = await testService.Script(requestParams);
                Assert.True(result.Script.Contains("SELECT"));
            }
        }

        private static void AssertSchemaInFile(string filePath, bool assert = true)
        {
            AssertFileContainsString(
                filePath,
                "CREATE DATABASE",
                assert);

            AssertFileContainsString(
                filePath,
                "create view [dbo].[Invoices] AS",
                assert);

            AssertFileContainsString(
                filePath,
                "CREATE TABLE [dbo].[Products](",
                assert);
        }

        private static void AssertTableDataInFile(string filePath, bool assert = true)
        {
            AssertFileContainsString(
                filePath,
                "INSERT [dbo].[Categories] ([CategoryID], [CategoryName], [Description], [Picture]) VALUES (1, N'Beverages', N'Soft drinks, coffees, teas, beers, and ales', [encoded data, 21494 characters])",
                assert);

            AssertFileContainsString(
                filePath,
                "INSERT [dbo].[Order Details] ([OrderID], [ProductID], [UnitPrice], [Quantity], [Discount]) VALUES (10248, 11, 14.0000, 12, 0)",
                assert);
        }

        private static void AssertFileContainsString(string filePath, string str, bool assertTrue)
        {
            string fileText = File.ReadAllText(filePath);
            bool found = fileText.Contains(str);
            if (assertTrue)
            {
                Assert.True(found, string.Format("The string '{0}' was not found in file.", str));
            }
            else
            {
                Assert.False(found, string.Format("The string '{0}' was found in file.", str));
            }
        }


        public class ScriptingFixture : IDisposable
        {
            public ScriptingFixture()
            {
                this.Database = SqlTestDb.CreateNew(TestServerType.OnPrem);
                this.Database.RunQuery(Scripts.CreateNorthwindSchema, throwOnError: true);
                Console.WriteLine("Northwind setup complete, database name: {0}", this.Database.DatabaseName);
            }

            /// <summary>
            /// The count of object when scripting the entire database including the database object.
            /// </summary>
            public const int ObjectCountWithDatabase = 46;

            /// <summary>
            /// The count of objects when scripting the entire database excluding the database object.
            /// </summary>
            public const int ObjectCountWithoutDatabase = 45;

            public SqlTestDb Database { get; private set; }

            public void Dispose()
            {
                if (this.Database != null)
                {
                    Console.WriteLine("Northwind cleanup, deleting database name: {0}", this.Database.DatabaseName);
                    this.Database.Dispose();
                }
            }
        }
   }
}
