/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

namespace Apache.Ignite.Core.Tests.Log
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using Apache.Ignite.Core.Cache.Configuration;
    using Apache.Ignite.Core.Common;
    using Apache.Ignite.Core.Communication.Tcp;
    using Apache.Ignite.Core.Compute;
    using Apache.Ignite.Core.Lifecycle;
    using Apache.Ignite.Core.Log;
    using Apache.Ignite.Core.Resource;
    using NUnit.Framework;

    /// <summary>
    /// Tests that user-defined logger receives Ignite events.
    /// </summary>
    public class CustomLoggerTest
    {
        /** */
        private static readonly LogLevel[] AllLevels = Enum.GetValues(typeof (LogLevel)).OfType<LogLevel>().ToArray();

        /// <summary>
        /// Test setup.
        /// </summary>
        [SetUp]
        public void TestSetUp()
        {
            TestLogger.Clear();
        }

        /// <summary>
        /// Tests the startup output.
        /// </summary>
        [Test]
        public void TestStartupOutput()
        {
            var cfg = GetConfigWithLogger(true);
            using (var ignite = Ignition.Start(cfg))
            {
                // Check injection
                Assert.AreEqual(ignite, ((TestLogger) cfg.Logger).Ignite);

                // Check initial message
                Assert.IsTrue(TestLogger.Entries[0].Message.StartsWith("Starting Ignite.NET"));

                // Check topology message
                Assert.IsTrue(
                    TestUtils.WaitForCondition(() =>
                    {
                        lock (TestLogger.Entries)
                        {
                            return TestLogger.Entries.Any(x => x.Message.Contains("Topology snapshot"));
                        }
                    }, 9000), "No topology snapshot");
            }

            // Test that all levels are present
            foreach (var level in AllLevels.Where(x => x != LogLevel.Error))
            {
                Assert.IsTrue(TestLogger.Entries.Any(x => x.Level == level), "No messages with level " + level);
            }
        }

        /// <summary>
        /// Tests startup error in Java.
        /// </summary>
        [Test]
        public void TestStartupJavaError()
        {
            // Invalid config
            Assert.Throws<IgniteException>(() =>
                Ignition.Start(new IgniteConfiguration(GetConfigWithLogger())
                {
                    CommunicationSpi = new TcpCommunicationSpi
                    {
                        IdleConnectionTimeout = TimeSpan.MinValue
                    }
                }));

            var err = TestLogger.Entries.First(x => x.Level == LogLevel.Error);
            Assert.IsTrue(err.NativeErrorInfo.Contains("SPI parameter failed condition check: idleConnTimeout > 0"));
            Assert.AreEqual("org.apache.ignite.internal.IgniteKernal", err.Category);
            Assert.IsNull(err.Exception);
        }

        /// <summary>
        /// Tests startup error in .NET.
        /// </summary>
        [Test]
        public void TestStartupDotNetError()
        {
            // Invalid bean
            Assert.Throws<IgniteException>(() =>
                Ignition.Start(new IgniteConfiguration(GetConfigWithLogger())
                {
                    LifecycleHandlers = new[] {new FailBean()}
                }));

            var err = TestLogger.Entries.First(x => x.Level == LogLevel.Error);
            Assert.IsInstanceOf<ArithmeticException>(err.Exception);
        }

        /// <summary>
        /// Tests that .NET exception propagates through Java to the log.
        /// </summary>
        [Test]
        public void TestDotNetErrorPropagation()
        {
            // Start 2 nodes: PlatformNativeException does not occur in local scenario
            using (var ignite = Ignition.Start(GetConfigWithLogger()))
            using (Ignition.Start(new IgniteConfiguration(TestUtils.GetTestConfiguration()) {IgniteInstanceName = "1"}))
            {
                var compute = ignite.GetCluster().ForRemotes().GetCompute();

                var ex = Assert.Throws<AggregateException>(() => compute.Call(new FailFunc()));
                Assert.IsNotNull(ex.InnerException);
                Assert.IsInstanceOf<ArithmeticException>(ex.InnerException.InnerException);

                // Log updates may not arrive immediately
                TestUtils.WaitForCondition(() => TestLogger.Entries.Any(x => x.Exception != null), 3000);

                var errFromJava = TestLogger.Entries.Single(x => x.Exception != null);
                Assert.IsNotNull(errFromJava.Exception.InnerException);
                Assert.AreEqual("Error in func.",
                    ((ArithmeticException) errFromJava.Exception.InnerException).Message);
            }
        }

        /// <summary>
        /// Tests the <see cref="QueryEntity"/> validation.
        /// </summary>
        [Test]
        public void TestQueryEntityValidation()
        {
            var cacheCfg = new CacheConfiguration("cache1", new QueryEntity(typeof(uint), typeof(ulong))
            {
                Fields = new[]
                {
                    new QueryField("myField", typeof(ushort))
                }
            });

            var cfg = new IgniteConfiguration(GetConfigWithLogger())
            {
                CacheConfiguration = new[]
                {
                    cacheCfg
                }
            };

            using (var ignite = Ignition.Start(cfg))
            {
                // Check static and dynamic cache start
                cacheCfg.Name = "cache2";
                ignite.CreateCache<int, string>(cacheCfg);

                var warns = TestLogger.Entries.Where(x => x.Level == LogLevel.Warn && x.Args != null)
                    .Select(x => string.Format(x.Message, x.Args)).ToList();

                Assert.AreEqual(6, warns.Count);

                Assert.AreEqual("Validating cache configuration 'cache1', QueryEntity 'java.lang.Integer:java.lang." +
                                "Long': Type 'System.UInt32' maps to Java type 'java.lang.Integer' using unchecked " +
                                "conversion. This may cause issues in SQL queries. You can use 'System.Int32' " +
                                "instead to achieve direct mapping.", warns[0]);

                Assert.AreEqual("Validating cache configuration 'cache1', QueryEntity 'java.lang.Integer:java.lang." +
                                "Long': Type 'System.UInt64' maps to Java type 'java.lang.Long' using unchecked " +
                                "conversion. This may cause issues in SQL queries. You can use 'System.Int64' " +
                                "instead to achieve direct mapping.", warns[1]);

                Assert.AreEqual("Validating cache configuration 'cache1', QueryEntity 'java.lang.Integer:java.lang." +
                                "Long', QueryField 'myField': Type 'System.UInt16' maps to Java type 'java.lang." +
                                "Short' using unchecked conversion. This may cause issues in SQL queries. You " +
                                "can use 'System.Int16' instead to achieve direct mapping.", warns[2]);

                Assert.AreEqual("Validating cache configuration 'cache2', QueryEntity 'java.lang.Integer:java.lang." +
                                "Long': Type 'System.UInt32' maps to Java type 'java.lang.Integer' using unchecked " +
                                "conversion. This may cause issues in SQL queries. You can use 'System.Int32' " +
                                "instead to achieve direct mapping.", warns[3]);

                Assert.AreEqual("Validating cache configuration 'cache2', QueryEntity 'java.lang.Integer:java.lang." +
                                "Long': Type 'System.UInt64' maps to Java type 'java.lang.Long' using unchecked " +
                                "conversion. This may cause issues in SQL queries. You can use 'System.Int64' " +
                                "instead to achieve direct mapping.", warns[4]);

                Assert.AreEqual("Validating cache configuration 'cache2', QueryEntity 'java.lang.Integer:java.lang." +
                                "Long', QueryField 'myField': Type 'System.UInt16' maps to Java type 'java.lang." +
                                "Short' using unchecked conversion. This may cause issues in SQL queries. You " +
                                "can use 'System.Int16' instead to achieve direct mapping.", warns[5]);
            }
        }

        /// <summary>
        /// Tests the nullable <see cref="QueryEntity"/> validation.
        /// </summary>
        [TestCase(typeof(sbyte?), "java.lang.Byte", typeof(byte))]
        [TestCase(typeof(ushort?), "java.lang.Short", typeof(short))]
        [TestCase(typeof(uint?), "java.lang.Integer", typeof(int))]
        [TestCase(typeof(ulong?), "java.lang.Long", typeof(long))]
        public void TestNullableQueryEntityValidation(Type type, string javaType, Type hintType)
        {
            var cfg = new IgniteConfiguration(GetConfigWithLogger())
            {
                CacheConfiguration = new[]
                {
                    new CacheConfiguration("cache1", new QueryEntity(type, type)
                    {
                        Fields = new[]
                        {
                            new QueryField("myField", type)
                        }
                    })
                }
            };

            using (Ignition.Start(cfg))
            {
                var warns = TestLogger.Entries.Where(x => x.Level == LogLevel.Warn && x.Args != null)
                    .Select(x => string.Format(x.Message, x.Args)).ToList();

                Assert.AreEqual(3, warns.Count);

                string pattern = "Validating cache configuration 'cache1', QueryEntity '{0}:{0}': " +
                                 "Type '{1}' maps to Java type '{0}' using unchecked " +
                                 "conversion. This may cause issues in SQL queries. You can use '{2}' " +
                                 "instead to achieve direct mapping.";

                string expected = string.Format(pattern, javaType, type, hintType);

                Assert.AreEqual(expected, warns[0]);
                Assert.AreEqual(expected, warns[1]);

                string fieldsPattern = "Validating cache configuration 'cache1', QueryEntity '{0}:{0}', " +
                                       "QueryField 'myField': Type '{1}' maps to Java type '{0}' using " +
                                       "unchecked conversion. This may cause issues in SQL queries. " +
                                       "You can use '{2}' instead to achieve direct mapping.";

                string fieldsExpected = string.Format(fieldsPattern, javaType, type, hintType);
                Assert.AreEqual(fieldsExpected, warns[2]);
            }
        }

        /// <summary>
        /// Tests cache creation with nullable <see cref="QueryEntity"/> does not log warnings.
        /// </summary>
        [TestCase(typeof(int))]
        [TestCase(typeof(int?))]
        [TestCase(typeof(string))]
        [TestCase(typeof(LogEntry))]
        [TestCase(typeof(CustomLoggerTest))]
        [TestCase(typeof(CustomEnum))]
        [TestCase(typeof(CustomEnum?))]
        public void TestCacheCreationWithDirectQueryEntityMappingsDoesNotLogWarnings(Type type)
        {
            var cfg = new IgniteConfiguration(GetConfigWithLogger())
            {
                CacheConfiguration = new[]
                {
                    new CacheConfiguration("cache1", new QueryEntity(type, type)
                    {
                        Fields = new[]
                        {
                            new QueryField("myField", type)
                        }
                    })
                }
            };

            using (Ignition.Start(cfg))
            {
                int warnsCount = TestLogger.Entries.Count(x => x.Level == LogLevel.Warn && x.Args != null);
                Assert.AreEqual(0, warnsCount);
            }
        }

        /// <summary>
        /// Tests the <see cref="LoggerExtensions"/> methods.
        /// </summary>
        [Test]
        public void TestExtensions()
        {
            var log = new TestLogger(LogLevel.Trace);
            var ex = new FieldAccessException("abc");

            // Log
            log.Log(LogLevel.Trace, "trace");
            CheckLastMessage(LogLevel.Trace, "trace");

            log.Log(LogLevel.Debug, "msg {0} {1}", 1, "2");
            CheckLastMessage(LogLevel.Debug, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);

            log.Log(LogLevel.Info, ex, "msg");
            CheckLastMessage(LogLevel.Info, "msg", e: ex);

            log.Log(LogLevel.Warn, ex, "msg {0}", 1);
            CheckLastMessage(LogLevel.Warn, "msg {0}", new object[] {1}, CultureInfo.InvariantCulture, e: ex);

            // Trace
            log.Trace("trace");
            CheckLastMessage(LogLevel.Trace, "trace");

            log.Trace("msg {0} {1}", 1, "2");
            CheckLastMessage(LogLevel.Trace, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);

            log.Trace(ex, "msg");
            CheckLastMessage(LogLevel.Trace, "msg", e: ex);

            log.Trace(ex, "msg {0}", 1);
            CheckLastMessage(LogLevel.Trace, "msg {0}", new object[] { 1 }, CultureInfo.InvariantCulture, e: ex);

            // Debug
            log.Debug("test");
            CheckLastMessage(LogLevel.Debug, "test");

            log.Debug("msg {0} {1}", 1, "2");
            CheckLastMessage(LogLevel.Debug, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);

            log.Debug(ex, "msg");
            CheckLastMessage(LogLevel.Debug, "msg", e: ex);

            log.Debug(ex, "msg {0}", 1);
            CheckLastMessage(LogLevel.Debug, "msg {0}", new object[] { 1 }, CultureInfo.InvariantCulture, e: ex);

            // Info
            log.Info("test");
            CheckLastMessage(LogLevel.Info, "test");

            log.Info("msg {0} {1}", 1, "2");
            CheckLastMessage(LogLevel.Info, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);

            log.Info(ex, "msg");
            CheckLastMessage(LogLevel.Info, "msg", e: ex);

            log.Info(ex, "msg {0}", 1);
            CheckLastMessage(LogLevel.Info, "msg {0}", new object[] { 1 }, CultureInfo.InvariantCulture, e: ex);

            // Warn
            log.Warn("test");
            CheckLastMessage(LogLevel.Warn, "test");

            log.Warn("msg {0} {1}", 1, "2");
            CheckLastMessage(LogLevel.Warn, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);

            log.Warn(ex, "msg");
            CheckLastMessage(LogLevel.Warn, "msg", e: ex);

            log.Warn(ex, "msg {0}", 1);
            CheckLastMessage(LogLevel.Warn, "msg {0}", new object[] { 1 }, CultureInfo.InvariantCulture, e: ex);

            // Error
            log.Error("test");
            CheckLastMessage(LogLevel.Error, "test");

            log.Error("msg {0} {1}", 1, "2");
            CheckLastMessage(LogLevel.Error, "msg {0} {1}", new object[] { 1, "2" }, CultureInfo.InvariantCulture);

            log.Error(ex, "msg");
            CheckLastMessage(LogLevel.Error, "msg", e: ex);

            log.Error(ex, "msg {0}", 1);
            CheckLastMessage(LogLevel.Error, "msg {0}", new object[] { 1 }, CultureInfo.InvariantCulture, e: ex);

            // GetLogger
            var catLog = log.GetLogger("myCategory");
            catLog.Info("info");
            CheckLastMessage(LogLevel.Info, "info", category: "myCategory");

            catLog.Log(LogLevel.Info, "info", null, null, "explicitCat", null, null);
            CheckLastMessage(LogLevel.Info, "info", category: "explicitCat");

            catLog = catLog.GetLogger("newCat");
            catLog.Info("info");
            CheckLastMessage(LogLevel.Info, "info", category: "newCat");

            catLog.Log(LogLevel.Info, "info", null, null, "explicitCat", null, null);
            CheckLastMessage(LogLevel.Info, "info", category: "explicitCat");
        }

        /// <summary>
        /// Checks the last message.
        /// </summary>
        private static void CheckLastMessage(LogLevel level, string message, object[] args = null,
            IFormatProvider formatProvider = null, string category = null, string nativeErr = null, Exception e = null)
        {
            var msg = TestLogger.Entries.Last();

            Assert.AreEqual(msg.Level, level);
            Assert.AreEqual(msg.Message, message);
            Assert.AreEqual(msg.Args, args);
            Assert.AreEqual(msg.FormatProvider, formatProvider);
            Assert.AreEqual(msg.Category, category);
            Assert.AreEqual(msg.NativeErrorInfo, nativeErr);
            Assert.AreEqual(msg.Exception, e);
        }

        /// <summary>
        /// Gets the configuration with logger.
        /// </summary>
        private static IgniteConfiguration GetConfigWithLogger(bool verbose = false)
        {
            return new IgniteConfiguration(TestUtils.GetTestConfiguration())
            {
                Logger = new TestLogger(verbose ? LogLevel.Trace : LogLevel.Info)
            };
        }

        /// <summary>
        /// Test log entry.
        /// </summary>
        private class LogEntry
        {
            public LogLevel Level;
            public string Message;
            public object[] Args;
            public IFormatProvider FormatProvider;
            public string Category;
            public string NativeErrorInfo;
            public Exception Exception;

            public override string ToString()
            {
                return string.Format("Level: {0}, Message: {1}, Args: {2}, FormatProvider: {3}, Category: {4}, " +
                                     "NativeErrorInfo: {5}, Exception: {6}", Level, Message, Args, FormatProvider,
                                     Category, NativeErrorInfo, Exception);
            }
        }

        /// <summary>
        /// Test logger.
        /// </summary>
        private class TestLogger : ILogger
        {
            private static readonly List<LogEntry> Logs = new List<LogEntry>(5000);

            private readonly LogLevel _minLevel;

            public TestLogger(LogLevel minLevel)
            {
                _minLevel = minLevel;
            }

            public static List<LogEntry> Entries
            {
                get
                {
                    lock (Logs)
                    {
                        return Logs.ToList();
                    }
                }
            }

            public void Log(LogLevel level, string message, object[] args, IFormatProvider formatProvider,
                string category, string nativeErrorInfo, Exception ex)
            {
                if (!IsEnabled(level))
                    return;

                lock (Logs)
                {
                    Logs.Add(new LogEntry
                    {
                        Level = level,
                        Message = message,
                        Args = args,
                        FormatProvider = formatProvider,
                        Category = category,
                        NativeErrorInfo = nativeErrorInfo,
                        Exception = ex
                    });
                }
            }

            public bool IsEnabled(LogLevel level)
            {
                return level >= _minLevel;
            }

            [InstanceResource]
            // ReSharper disable once UnusedAutoPropertyAccessor.Local
            public IIgnite Ignite { get; set; }

            public static void Clear()
            {
                lock (Logs)
                {
                    Logs.Clear();
                }
            }
        }


        /// <summary>
        /// Failing lifecycle bean.
        /// </summary>
        private class FailBean : ILifecycleHandler
        {
            public void OnLifecycleEvent(LifecycleEventType evt)
            {
                throw new ArithmeticException("Failure in bean");
            }
        }

        /// <summary>
        /// Failing computation.
        /// </summary>
        [Serializable]
        private class FailFunc : IComputeFunc<string>
        {
            public string Invoke()
            {
                throw new ArithmeticException("Error in func.");
            }
        }

        /// <summary>
        /// Custom enum for testing.
        /// </summary>
        private struct CustomEnum
        {
            // ReSharper disable once UnusedMember.Local
            public int Field { get; set; }
        }
    }
}
