using Knoks.Framework.DataAccess;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Xunit;

namespace Knoks.Test.Integrations.Framework
{
    public class ProcExecutorTests : IDisposable
    {
        #region Static SQL Commantd

        private const string TableName = "Test_Table_Executor";
        private const string GetProcName = "Test_Proc_Get_Executor";
        private const string SetProcName = "Test_Proc_Set_Executor";

        private static string[] SqlCommands = new[] {
            @"IF EXISTS (SELECT * FROM SYSOBJECTS WHERE NAME = 'Test_Proc_Get_Executor' AND TYPE = 'P')
                DROP PROCEDURE [Test_Proc_Get_Executor];",
            //-------------------------------------------------------------------------------------
            @"IF EXISTS (SELECT * FROM SYSOBJECTS WHERE NAME = 'Test_Proc_Set_Executor' AND TYPE = 'P')
                DROP PROCEDURE [Test_Proc_Set_Executor];",
            //-------------------------------------------------------------------------------------
            @"IF EXISTS (SELECT * FROM SYSOBJECTS WHERE NAME='Test_Table_Executor' AND XTYPE = 'U')
                DROP TABLE [Test_Table_Executor];",
            //-------------------------------------------------------------------------------------
            @"CREATE TABLE [Test_Table_Executor]
            (
	            Id	    BIGINT          NOT NULL IDENTITY(1,1),
	            Col1	BIGINT          NOT NULL,
                Col2	VARCHAR(50)     NOT NULL,
                Col3	NVARCHAR(50)    NOT NULL,
                Col4	DateTime2       NOT NULL
            );",
            //-------------------------------------------------------------------------------------
            @"CREATE PROCEDURE [Test_Proc_Get_Executor]
	            @Id	BIGINT = NULL
            AS
	            SELECT * FROM [Test_Table_Executor] WHERE [Id] = ISNULL(@Id, [Id]);

            RETURN 0",
            //-------------------------------------------------------------------------------------
            @"CREATE PROCEDURE [Test_Proc_Set_Executor]
	            @Col1	BIGINT,
                @Col2	VARCHAR(50),
                @Col3	NVARCHAR(50),
                @Col4	DateTime2,
                @Id     BIGINT OUT
            AS
                INSERT INTO [Test_Table_Executor] VALUES (@Col1, @Col2, @Col3, @Col4);

                SET @Id = SCOPE_IDENTITY()

            RETURN 0"
        };

        static ProcExecutorTests()
        {
            for (var i = 0; i < SqlCommands.Length; i++)
            {
                SqlCommands[i] = Regex.Replace(SqlCommands[i], "Test_Table_Executor", TableName);
                SqlCommands[i] = Regex.Replace(SqlCommands[i], "Test_Proc_Get_Executor", GetProcName);
                SqlCommands[i] = Regex.Replace(SqlCommands[i], "Test_Proc_Set_Executor", SetProcName);
            }
        }

        private static void SqlRunner(IEnumerable<string> sqls)
        {
            using (var con = new SqlConnection(Config.DefaultDbConnection))
            {
                con.Open();
                foreach (var sql in sqls)
                {
                    using (var cmd = new SqlCommand(string.Format(sql), con))
                    {
                        cmd.ExecuteNonQuery();
                    }
                }
            }
        }

        private static void InitializeDatabaseEntities()
        {
            SqlRunner(SqlCommands);
        }

        private static void DestroyDatabaseEntities()
        {
            SqlRunner(SqlCommands.Take(3));
        }

        #endregion

        private ProcExecutor _executor;

        public ProcExecutorTests()
        {
            SqlRunner(SqlCommands);
            _executor = new ProcExecutor(Config.DbConnections, new Logger<ProcExecutor>(new LoggerFactory()));
        }

        public void Dispose()
        {
            SqlRunner(SqlCommands.Take(2));
        }

        private class TestRecord
        {
            public long Col1 { get; set; }
            public string Col2 { get; set; }
            public string Col3 { get; set; }
            public DateTime Col4 { get; set; }

            [ProcParamIgnore]
            public string Col5 { get; set; } = "NotExistColumnInDb";
        }

        class TestRecordComparer : IEqualityComparer<TestRecord>
        {
            public static readonly TestRecordComparer Instance = new TestRecordComparer();

            public bool Equals(TestRecord x, TestRecord y)
            {
                return
                    x.Col1 == y.Col1 &&
                    x.Col2 == y.Col2 &&
                    x.Col3 == y.Col3 &&
                    x.Col4 == y.Col4;
            }

            public int GetHashCode(TestRecord obj)
            {
                return obj.GetHashCode();
            }
        }

        [Fact(DisplayName = "GeneralPassingTest")]
        public async Task GeneralPassingTest()
        {
            var rowsExpected = new List<TestRecord>
            {
                new TestRecord { Col1 = 1, Col2 = "Text 2-1", Col3 = "Text 3-1", Col4 = DateTime.UtcNow },
                new TestRecord { Col1 = 2, Col2 = "Text 2-2", Col3 = "Text 3-2", Col4 = DateTime.UtcNow },
                new TestRecord { Col1 = 3, Col2 = "Text 2-3", Col3 = "Text 3-3", Col4 = DateTime.UtcNow },
                new TestRecord { Col1 = 4, Col2 = "Text 2-4", Col3 = "Text 3-4", Col4 = DateTime.UtcNow },
                new TestRecord { Col1 = 5, Col2 = "Text 2-5", Col3 = "Text 3-5", Col4 = DateTime.UtcNow }
            };

            var rowsActual = new List<TestRecord>();

            foreach (var rowExpected in rowsExpected)
            {
                var dr = await _executor.Go(SetProcName, rowExpected);
                rowsActual.Add((await _executor.Go(GetProcName, dr.OutputParams.First())).Тables[0][0].To<TestRecord>());
            };

            Assert.Equal(rowsExpected, rowsActual, TestRecordComparer.Instance);
        }

        [Fact(DisplayName = "ParamPassingTest")]
        public async Task ParamsPassingTest()
        {
            var testRow = new TestRecord { Col1 = 1, Col2 = "Text 1", Col3 = "Text 1", Col4 = DateTime.UtcNow };
            var rowsExpected = new List<TestRecord> { testRow, testRow, testRow, testRow };

            var d = new Dictionary<string, object> { { "Col1", testRow.Col1 }, { "Col2", testRow.Col2 }, { "Col3", testRow.Col3 }, { "Col4", testRow.Col4 } };
            var objects = new List<object> { testRow, d, d.ToArray(), d.ToList() };

            var rowsActual = new List<TestRecord>();

            foreach (var obj in objects)
            {
                var dr = await _executor.Go(SetProcName, obj);
                rowsActual.Add((await _executor.Go(GetProcName, dr.OutputParams.First())).Тables[0][0].To<TestRecord>());
            }

            Assert.Equal(rowsExpected, rowsActual, TestRecordComparer.Instance);
        }

        [Fact(DisplayName = "MultiParamsPassingTest")]
        public async Task MultiParamsPassingTest()
        {
            var rowsExpected = new TestRecord { Col1 = 1, Col2 = "Text 1", Col3 = "Text 1", Col4 = DateTime.UtcNow };

            var dr = await _executor.Go(SetProcName, new { Col1 = rowsExpected.Col1, Col2 = rowsExpected.Col2 }, new { Col3 = rowsExpected.Col3, Col4 = rowsExpected.Col4 });
            var rowsActual = (await _executor.Go(GetProcName, dr.OutputParams.First())).Тables[0][0].To<TestRecord>();

            Assert.Equal(rowsExpected, rowsActual, TestRecordComparer.Instance);
        }

        [ProcParamIgnore]
        private class IgnoreTestRecord
        {
            public long Col1 { get; set; }
            public string Col2 { get; set; }
            public string Col3 { get; set; }
            public DateTime Col4 { get; set; }
        }

        [Fact(DisplayName = "Ignore record class")]
        public async Task IgnoreRecordClass()
        {
            await Assert.ThrowsAsync<ArgumentException>(async () =>
            {
                await _executor.Go(SetProcName, new IgnoreTestRecord());
            });
        }

        [Fact(DisplayName = "Ignore record KeyValuePair")]
        public async Task IgnoreRecordKeyValuePair()
        {
            await Assert.ThrowsAsync<SqlException>(async () =>
            {
                await _executor.Go(SetProcName, new KeyValuePair<string, object>("key", new IgnoreTestRecord()));
            });
        }

        [Fact(DisplayName = "Ignore records in Dictionary")]
        public async Task IgnoreRecordDictionary()
        {
            await Assert.ThrowsAsync<SqlException>(async () =>
            {
                await _executor.Go(SetProcName, new Dictionary<string, object> { { "k1", new IgnoreTestRecord() }, { "k2", new IgnoreTestRecord() } });
            });
        }
    }
}
