using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Orleans;
using Orleans.Runtime;
using Orleans.Runtime.Configuration;
using Orleans.Storage;
using Xunit;
using Xunit.Abstractions;

namespace UnitTests.StorageTests
{
    [Serializable]
    public enum ProviderType
    {
        AzureTable,
        Memory,
        Mock,
        File,
        Sql
    }

    public class LocalStoreTestsFixture : IDisposable
    {
        public LocalStoreTestsFixture()
        {
            BufferPool.InitGlobalBufferPool(new MessagingConfiguration(false));

            ClientConfiguration cfg = ClientConfiguration.LoadFromFile("ClientConfigurationForTesting.xml");
            LogManager.Initialize(cfg);
        }

        public void Dispose()
        {
            LocalDataStoreInstance.LocalDataStore = null;
        }
    }

    public class LocalStoreTests : IClassFixture<LocalStoreTestsFixture>
    {
        private readonly ITestOutputHelper output;

        public LocalStoreTests(ITestOutputHelper output)
        {
            this.output = output;
            LocalDataStoreInstance.LocalDataStore = null;
        }

        [Fact, TestCategory("Functional"), TestCategory("Persistence"), TestCategory("MemoryStore")]
        public void Store_Read()
        {
            string name = Guid.NewGuid().ToString();//TestContext.TestName;

            ILocalDataStore store = new HierarchicalKeyStore(2);

            GrainReference reference = GrainReference.FromGrainId(GrainId.NewId());
            TestStoreGrainState state = new TestStoreGrainState();
            var stateProperties = AsDictionary(state);
            var keys = GetKeys(name, reference);
            store.WriteRow(keys, stateProperties, null);
            Stopwatch sw = new Stopwatch();
            sw.Start();
            var data = store.ReadRow(keys);
            TimeSpan readTime = sw.Elapsed;
            output.WriteLine("{0} - Read time = {1}", store.GetType().FullName, readTime);
            Assert.Equal(state.A,  data["A"]);  // "A"
            Assert.Equal(state.B,  data["B"]);  // "B"
            Assert.Equal(state.C,  data["C"]);  // "C"
        }

        [Fact, TestCategory("Functional"), TestCategory("Persistence"), TestCategory("MemoryStore")]
        public void Store_WriteRead()
        {
            string name = Guid.NewGuid().ToString();//TestContext.TestName;

            ILocalDataStore store = new HierarchicalKeyStore(2);

            GrainReference reference = GrainReference.FromGrainId(GrainId.NewId());
            var state = TestStoreGrainState.NewRandomState();
            Stopwatch sw = new Stopwatch();
            sw.Start();
            var keys = GetKeys(name, reference);
            var stateProperties = AsDictionary(state.State);
            store.WriteRow(keys, stateProperties, state.ETag);
            TimeSpan writeTime = sw.Elapsed;
            sw.Restart();
            var data = store.ReadRow(keys);
            TimeSpan readTime = sw.Elapsed;
            output.WriteLine("{0} - Write time = {1} Read time = {2}", store.GetType().FullName, writeTime, readTime);
            Assert.Equal(state.State.A,  data["A"]);  // "A"
            Assert.Equal(state.State.B,  data["B"]);  // "B"
            Assert.Equal(state.State.C,  data["C"]);  // "C"
        }

        [Fact, TestCategory("Functional"), TestCategory("Persistence"), TestCategory("MemoryStore")]
        public void Store_Delete()
        {
            string name = Guid.NewGuid().ToString();//TestContext.TestName;

            ILocalDataStore store = new HierarchicalKeyStore(2);

            GrainReference reference = GrainReference.FromGrainId(GrainId.NewId());
            var data = TestStoreGrainState.NewRandomState();

            output.WriteLine("Using store = {0}", store.GetType().FullName);
            Stopwatch sw = new Stopwatch();

            var keys = GetKeys(name, reference);

            sw.Restart();
            string eTag = store.WriteRow(keys, AsDictionary(data.State), null);
            output.WriteLine("Write returned Etag={0} after {1} {2}", eTag, sw.Elapsed, StorageProviderUtils.PrintOneWrite(keys, data, eTag));

            sw.Restart();
            var storedData = store.ReadRow(keys);
            output.WriteLine("Read returned {0} after {1}", StorageProviderUtils.PrintOneWrite(keys, storedData, eTag), sw.Elapsed);
            Assert.NotNull(data); // Should get some data from Read

            sw.Restart();
            bool ok = store.DeleteRow(keys, eTag);
            Assert.True(ok, $"Row deleted OK after {sw.Elapsed}. Etag={eTag} Keys={StorageProviderUtils.PrintKeys(keys)}");

            sw.Restart();
            storedData = store.ReadRow(keys); // Try to re-read after delete
            output.WriteLine("Re-Read took {0} and returned {1}", sw.Elapsed, StorageProviderUtils.PrintData(storedData));
            Assert.NotNull(data); // Should not get null data from Re-Read
            Assert.True(storedData.Count == 0, $"Should get no data from Re-Read but got: {StorageProviderUtils.PrintData(storedData)}");

            sw.Restart();
            const string oldEtag = null;
            eTag = store.WriteRow(keys, storedData, oldEtag);
            output.WriteLine("Write for Keys={0} Etag={1} Data={2} returned New Etag={3} after {4}",
                StorageProviderUtils.PrintKeys(keys), oldEtag, StorageProviderUtils.PrintData(storedData),
                eTag, sw.Elapsed);

            sw.Restart();
            ok = store.DeleteRow(keys, eTag);
            Assert.True(ok, $"Row deleted OK after {sw.Elapsed}. Etag={eTag} Keys={StorageProviderUtils.PrintKeys(keys)}");
        }

        [Fact, TestCategory("Functional"), TestCategory("Persistence"), TestCategory("MemoryStore")]
        public void Store_ReadMulti()
        {
            string name = Guid.NewGuid().ToString();//TestContext.TestName;

            ILocalDataStore store = new HierarchicalKeyStore(2);

            // Write #1
            IList<Tuple<string, string>> keys = new[]
            {
                Tuple.Create("GrainType", name),
                Tuple.Create("GrainId", "1")
            }.ToList();
            var grainState = TestStoreGrainState.NewRandomState();
            var state = grainState.State;
            state.A = name;
            store.WriteRow(keys, AsDictionary(state), grainState.ETag);

            // Write #2
            keys = new[]
            {
                Tuple.Create("GrainType", name),
                Tuple.Create("GrainId", "2")
            }.ToList();
            grainState = TestStoreGrainState.NewRandomState();
            state = grainState.State;
            state.A = name;
            store.WriteRow(keys, AsDictionary(state), grainState.ETag);

            // Multi Read
            keys = new[]
            {
                Tuple.Create("GrainType", name)
            }.ToList();

            var results = store.ReadMultiRow(keys);

            Assert.Equal(2,  results.Count);  // "Count"
        }

        [Fact, TestCategory("Functional"), TestCategory("Persistence"), TestCategory("MemoryStore")]
        public void GrainState_Store_WriteRead()
        {
            string name = Guid.NewGuid().ToString();//TestContext.TestName;

            ILocalDataStore store = new HierarchicalKeyStore(2);

            GrainReference reference = GrainReference.FromGrainId(GrainId.NewId());
            var grainState = TestStoreGrainState.NewRandomState();
            var state = grainState.State;
            Stopwatch sw = new Stopwatch();
            sw.Start();
            IList<Tuple<string, string>> keys = new[]
            {
                Tuple.Create("GrainType", name),
                Tuple.Create("GrainId", reference.GrainId.GetPrimaryKey().ToString("N"))
            }.ToList();
            store.WriteRow(keys, AsDictionary(state), grainState.ETag);
            TimeSpan writeTime = sw.Elapsed;
            sw.Restart();
            var data = store.ReadRow(keys);
            TimeSpan readTime = sw.Elapsed;
            output.WriteLine("{0} - Write time = {1} Read time = {2}", store.GetType().FullName, writeTime, readTime);
            Assert.Equal(state.A,  data["A"]);  // "A"
            Assert.Equal(state.B,  data["B"]);  // "B"
            Assert.Equal(state.C,  data["C"]);  // "C"
        }

        // ---------- Utility methods ----------

        private static IList<Tuple<string, string>> GetKeys(string grainTypeName, GrainReference grain)
        {
            var keys = new[]
            {
                Tuple.Create("GrainType", grainTypeName),
                Tuple.Create("GrainId", grain.GrainId.GetPrimaryKey().ToString("N"))
            };
            return keys.ToList();
        }

        private static Dictionary<string, object> AsDictionary(object state)
        {
            return state.GetType().GetProperties()
                .Select(v => new KeyValuePair<string, object>(v.Name, v.GetValue(state)))
                .ToDictionary(pair => pair.Key, pair => pair.Value);
        }
    }

    #region Grain State class for these tests

    [Serializable]
    public class TestStoreGrainState
    {
        public string A { get; set; }
        public int B { get; set; }
        public long C { get; set; }

        internal static GrainState<TestStoreGrainState> NewRandomState(int? aPropertyLength = null)
        {
            return new GrainState<TestStoreGrainState>
            {
                State = new TestStoreGrainState
                {
                    A = aPropertyLength == null
                        ? TestConstants.random.Next().ToString(CultureInfo.InvariantCulture)
                        : GenerateRandomDigitString(aPropertyLength.Value),
                    B = TestConstants.random.Next(),
                    C = TestConstants.random.Next()
                }
            };
        }

        private static string GenerateRandomDigitString(int stringLength)
        {
            var characters = new char[stringLength];
            for (var i = 0; i < stringLength; ++i)
            {
                characters[i] = (char)TestConstants.random.Next('0', '9' + 1);
            }
            return new string(characters);
        }
    }

    #endregion Grain State class for these tests
}