#if !NOFILEIO
using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using NBitcoin.BitcoinCore;
using Xunit;

namespace NBitcoin.Tests
{
    public class RepositoryTests
    {
        public RepositoryTests()
        {
            // These flags may get set due to static network initializers
            // which include the initializers for Stratis.
            Transaction.TimeStamp = false;
            Block.BlockSignature = false;
        }

        public class RawData : IBitcoinSerializable
        {
            public RawData()
            {

            }
            public RawData(byte[] data)
            {
                _Data = data;
            }
            #region IBitcoinSerializable Members

            public void ReadWrite(BitcoinStream stream)
            {
                stream.ReadWriteAsVarString(ref _Data);
            }

            private byte[] _Data = new byte[0];
            public byte[] Data
            {
                get
                {
                    return _Data;
                }
            }

            #endregion
        }
        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanReadStoredBlockFile()
        {
            int count = 0;

            foreach(var stored in StoredBlock.EnumerateFile(@"data\blocks\blk00000.dat"))
            {
                Assert.True(stored.Item.Header.CheckProofOfWork(Network.Main.Consensus));
                Assert.True(stored.Item.CheckMerkleRoot());
                count++;
            }
            Assert.Equal(300, count);
            count = 0;
            var twoLast = StoredBlock.EnumerateFile(@"data\blocks\blk00000.dat").Skip(298).ToList();
            foreach(var stored in StoredBlock.EnumerateFile(@"data\blocks\blk00000.dat", range: new DiskBlockPosRange(twoLast[0].BlockPosition)))
            {
                count++;
            }
            Assert.Equal(2, count);
        }

        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanEnumerateBlockCountRange()
        {
            var store = new BlockStore(@"data\blocks", Network.Main);
            var expectedBlock = store.Enumerate(false).Skip(4).First();
            var actualBlocks = store.Enumerate(false, 4, 2).ToArray();
            Assert.Equal(2, actualBlocks.Length);
            Assert.Equal(expectedBlock.Item.Header.GetHash(), actualBlocks[0].Item.Header.GetHash());
            Assert.True(actualBlocks[0].Item.CheckMerkleRoot());
        }

        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanEnumerateBlockInAFileRange()
        {
            var store = new BlockStore(@"data\blocks", Network.Main);
            var result = store.Enumerate(new DiskBlockPosRange(new DiskBlockPos(0, 0), new DiskBlockPos(1, 0))).ToList();
            Assert.Equal(300, result.Count);
        }

        [Fact]
        [Trait("UnitTest", "UnitTest")]
        //The last block is off by 1 byte + lots of padding zero at the end
        public void CanEnumerateIncompleteBlk()
        {
            Assert.Equal(301, StoredBlock.EnumerateFile(@"data\blocks\incompleteblk.dat").Count());
        }

        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanBuildChainFromBlocks()
        {
            var store = new BlockStore(@"data\blocks", Network.Main);
            var chain = store.GetChain();
            Assert.True(chain.Height == 599);

        }

        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanIndexBlock()
        {
            var index = CreateIndexedStore();
            foreach(var block in StoredBlock.EnumerateFile(@"data\blocks\blk00000.dat").Take(50))
            {
                index.Put(block.Item);
            }
            var genesis = index.Get(uint256.Parse("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"));
            Assert.NotNull(genesis);
            var invalidBlock = index.Get(uint256.Parse("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26e"));
            Assert.Null(invalidBlock);
        }


        public static IndexedBlockStore CreateIndexedStore([CallerMemberName]string folderName = null)
        {
            TestUtils.EnsureNew(folderName);
            return new IndexedBlockStore(new InMemoryNoSqlRepository(), new BlockStore(folderName, Network.Main));
        }

        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanStoreBlocks()
        {
            var store = CreateBlockStore();
            var allBlocks = StoredBlock.EnumerateFile(@"data\blocks\blk00000.dat").Take(50).ToList();

            foreach(var s in allBlocks)
            {
                store.Append(s.Item);
            }
            var storedBlocks = store.Enumerate(true).ToList();
            Assert.Equal(allBlocks.Count, storedBlocks.Count);

            foreach(var s in allBlocks)
            {
                var retrieved = store.Enumerate(true).First(b => b.BlockPosition == s.BlockPosition);
                Assert.True(retrieved.Item.HeaderOnly);
            }
        }
        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanStoreBlocksInMultipleFiles()
        {
            var store = CreateBlockStore();
            store.MaxFileSize = 10; //Verify break all block in one respective file with extreme settings
            var allBlocks = StoredBlock.EnumerateFile(@"data\blocks\blk00000.dat").Take(10).ToList();
            foreach(var s in allBlocks)
            {
                store.Append(s.Item);
            }
            var storedBlocks = store.Enumerate(true).ToList();
            Assert.Equal(allBlocks.Count, storedBlocks.Count);
            Assert.Equal(11, store.Folder.GetFiles().Length); //10 files + lock file
        }


        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanReIndex()
        {
            var source = new BlockStore(@"data\blocks", Network.Main);
            var store = CreateBlockStore("CanReIndexFolder");
            store.AppendAll(source.Enumerate(false).Take(100).Select(b => b.Item));


            var test = new IndexedBlockStore(new InMemoryNoSqlRepository(), store);
            var reIndexed = test.ReIndex();
            Assert.Equal(100, reIndexed);
            int i = 0;
            foreach(var b in store.Enumerate(true))
            {
                var result = test.Get(b.Item.GetHash());
                Assert.Equal(result.GetHash(), b.Item.GetHash());
                i++;
            }
            Assert.Equal(100, i);

            var last = source.Enumerate(false).Skip(100).FirstOrDefault();
            store.Append(last.Item);

            reIndexed = test.ReIndex();
            Assert.Equal(1, reIndexed);

            reIndexed = test.ReIndex();
            Assert.Equal(0, reIndexed);
        }


        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public static void CanParseRev()
        {
            BlockUndoStore src = new BlockUndoStore(@"data\blocks", Network.Main);
            BlockUndoStore dest = CreateBlockUndoStore();
            int count = 0;
            foreach(var un in src.EnumerateFolder())
            {
                var expectedSize = un.Header.ItemSize;
                var actualSize = (uint)un.Item.GetSerializedSize();
                Assert.Equal(expectedSize, actualSize);
                dest.Append(un.Item);
                count++;
            }
            Assert.Equal(40, count);

            count = 0;
            foreach(var un in dest.EnumerateFolder())
            {
                var expectedSize = un.Header.ItemSize;
                var actualSize = (uint)un.Item.GetSerializedSize();
                Assert.Equal(expectedSize, actualSize);
                count++;
            }
            Assert.Equal(40, count);
        }

        /* TODO: Fix this test. The test is getting HTML pages instead of JSON from the external service.
         * 
        [Fact]
        //[Trait("UnitTest", "UnitTest")]
        public static void CanRequestBlockr()
        {
            var repo = new BlockrTransactionRepository(Network.Main);
            var result = repo.Get("c3462373f1a722c66cbb1b93712df94aa7b3731f4142cd8413f10c9e872927de");
            Assert.NotNull(result);
            Assert.Equal("c3462373f1a722c66cbb1b93712df94aa7b3731f4142cd8413f10c9e872927de", result.GetHash().ToString());

            result = repo.Get("c3462373f1a722c66cbb1b93712df94aa7b3731f4142cd8413f10c9e872927df");
            Assert.Null(result);

            var unspent = repo.GetUnspentAsync("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa").Result;
            Assert.True(unspent.Count != 0);

            repo = new BlockrTransactionRepository(Network.TestNet);
            result = repo.Get("7d4c5d69a85c70ff70daff789114b9b76fb6d2613ac18764bd96f0a2b9358782");
            Assert.NotNull(result);

            unspent = repo.GetUnspentAsync("2N66DDrmjDCMM3yMSYtAQyAqRtasSkFhbmX").Result;
            Assert.True(unspent.Count != 0);
        }
        */

        /* TODO: Fix this test. The test is getting HTML pages instead of JSON from the external service.
         * 
        [Fact]
        //[Trait("UnitTest", "UnitTest")]
        public static void CanPushTxBlockr()
        {
            var repo = new BlockrTransactionRepository(Network.Main);
            var result = repo.Get("c3462373f1a722c66cbb1b93712df94aa7b3731f4142cd8413f10c9e872927de");
            Assert.NotNull(result);
            Assert.Equal("c3462373f1a722c66cbb1b93712df94aa7b3731f4142cd8413f10c9e872927de", result.GetHash().ToString());

            var pushPath = BlockrTransactionRepository.BroadcastPath;

            try
            {
                BlockrTransactionRepository.BroadcastPath = "tx/decode";
                repo.BroadcastAsync(result).GetAwaiter().GetResult();
            }
            finally
            {
                BlockrTransactionRepository.BroadcastPath = pushPath;
            }
        }
        */

        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public static void CanRequestTransactionOnQBit()
        {
            var repo = new QBitNinjaTransactionRepository(Network.Main);
            var result = repo.Get("c3462373f1a722c66cbb1b93712df94aa7b3731f4142cd8413f10c9e872927de");
            Assert.NotNull(result);
            Assert.Equal("c3462373f1a722c66cbb1b93712df94aa7b3731f4142cd8413f10c9e872927de", result.GetHash().ToString());

            result = repo.Get("c3462373f1a722c66cbb1b93712df94aa7b3731f4142cd8413f10c9e872927df");
            Assert.Null(result);

            repo = new QBitNinjaTransactionRepository(Network.TestNet);
            result = repo.Get("7d4c5d69a85c70ff70daff789114b9b76fb6d2613ac18764bd96f0a2b9358782");
            Assert.NotNull(result);
        }

        private static Coin RandomCoin(Key bob, Money amount, bool p2pkh = false)
        {
            return new Coin(new uint256(Enumerable.Range(0, 32).Select(i => (byte)0xaa).ToArray()), 0, amount, p2pkh ? bob.PubKey.Hash.ScriptPubKey : bob.PubKey.WitHash.ScriptPubKey);
        }

        [Fact]
        public void Play()
        {
        }

        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanBuildTransactionWithSubstractFeeAndSendEstimatedFees()
        {
            var signer = new Key();
            var builder = new TransactionBuilder();
            builder.AddKeys(signer);
            builder.AddCoins(RandomCoin(signer, Money.Coins(1)));
            builder.Send(new Key().ScriptPubKey, Money.Coins(1));
            builder.SubtractFees();
            builder.SendEstimatedFees(new FeeRate(Money.Satoshis(100), 1));
            Transaction result = builder.BuildTransaction(true);
            Assert.Equal(Money.Coins(0.00011200m), result.GetFee(builder.FindSpentCoins(result)));
        }

        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanStoreInBlockRepository()
        {
            var blockRepository = CreateBlockRepository();
            var firstblk1 = StoredBlock.EnumerateFile(@"data\blocks\blk00000.dat").First();
            blockRepository.WriteBlockHeader(firstblk1.Item.Header);
            var result = blockRepository.GetBlock(firstblk1.Item.GetHash());
            Assert.True(result.HeaderOnly);

            blockRepository.WriteBlock(firstblk1.Item);
            result = blockRepository.GetBlock(firstblk1.Item.GetHash());
            Assert.False(result.HeaderOnly);
        }


        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanReadStoredBlockFolder()
        {
            var blk0 = StoredBlock.EnumerateFile(@"data\blocks\blk00000.dat", (uint)0).ToList();
            var blk1 = StoredBlock.EnumerateFile(@"data\blocks\blk00001.dat", (uint)1).ToList();

            int count = 0;
            foreach(var stored in StoredBlock.EnumerateFolder(@"data\blocks"))
            {
                if(count == 0)
                    Assert.Equal(blk0[0].Item.GetHash(), stored.Item.GetHash());
                if(count == 300)
                    Assert.Equal(blk1[0].Item.GetHash(), stored.Item.GetHash());
                Assert.True(stored.Item.Header.CheckProofOfWork(Network.Main.Consensus));
                Assert.True(stored.Item.CheckMerkleRoot());
                count++;
            }
            Assert.Equal(600, count);

            count = 0;
            foreach(var stored in StoredBlock.EnumerateFolder(@"data\blocks", new DiskBlockPosRange(blk1[298].BlockPosition)))
            {
                count++;
            }
            Assert.Equal(2, count);

            count = 0;
            foreach(var stored in StoredBlock.EnumerateFolder(@"data\blocks", new DiskBlockPosRange(blk0[298].BlockPosition)))
            {
                count++;
            }
            Assert.Equal(302, count);

            count = 0;
            foreach(var stored in StoredBlock.EnumerateFolder(@"data\blocks",
                                                        new DiskBlockPosRange(blk0[298].BlockPosition, blk1[2].BlockPosition)))
            {
                count++;
            }
            Assert.Equal(4, count);

            count = 0;
            foreach(var stored in StoredBlock.EnumerateFolder(@"data\blocks", new DiskBlockPosRange(blk0[30].BlockPosition, blk0[34].BlockPosition)))
            {
                count++;
            }
            Assert.Equal(4, count);
        }

        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanCacheNoSqlRepository()
        {
            var cached = new CachedNoSqlRepository(new InMemoryNoSqlRepository());
            byte[] data1 = new byte[] { 1, 2, 3, 4, 5, 6 };
            byte[] data2 = new byte[] { 11, 22, 33, 4, 5, 66 };
            cached.InnerRepository.Put("data1", new RawData(data1));
            Assert.NotNull(cached.Get<RawData>("data1"));
            cached.InnerRepository.Put("data1", new RawData(data2));
            cached.Flush();
            var data1Actual = cached.InnerRepository.Get<RawData>("data1");
            AssertEx.CollectionEquals(data1Actual.Data, data2);
            cached.Put("data1", new RawData(data1));

            data1Actual = cached.InnerRepository.Get<RawData>("data1");
            AssertEx.CollectionEquals(data1Actual.Data, data2);

            cached.Flush();

            data1Actual = cached.InnerRepository.Get<RawData>("data1");
            AssertEx.CollectionEquals(data1Actual.Data, data1);

            cached.Put("data1", null);
            cached.Flush();
            Assert.Null(cached.InnerRepository.Get<RawData>("data1"));

            cached.Put("data1", new RawData(data1));
            cached.Put("data1", null);
            cached.Flush();
            Assert.Null(cached.InnerRepository.Get<RawData>("data1"));

            cached.Put("data1", null);
            cached.Put("data1", new RawData(data1));
            cached.Flush();
            Assert.NotNull(cached.InnerRepository.Get<RawData>("data1"));
        }

        [Fact]
        [Trait("UnitTest", "UnitTest")]
        public void CanStoreInNoSql()
        {
            var repositories = new NoSqlRepository[]
            {
                new InMemoryNoSqlRepository(),
                new CachedNoSqlRepository(new InMemoryNoSqlRepository())
            };

            foreach(var repository in repositories)
            {
                byte[] data1 = new byte[] { 1, 2, 3, 4, 5, 6 };
                byte[] data2 = new byte[] { 11, 22, 33, 4, 5, 66 };
                Assert.Null(repository.Get<RawData>("data1"));

                repository.Put("data1", new RawData(data1));
                var actual = repository.Get<RawData>("data1");
                Assert.NotNull(actual);
                AssertEx.CollectionEquals(actual.Data, data1);

                repository.Put("data1", new RawData(data2));
                actual = repository.Get<RawData>("data1");
                Assert.NotNull(actual);
                AssertEx.CollectionEquals(actual.Data, data2);

                repository.Put("data1", null as RawData);
                actual = repository.Get<RawData>("data1");
                Assert.Null(actual);

                repository.Put("data1", null as RawData);
                actual = repository.Get<RawData>("data1");
                Assert.Null(actual);

                //Test batch
                repository.PutBatch(new[] {new Tuple<string,IBitcoinSerializable>("data1",new RawData(data1)),
                                       new Tuple<string,IBitcoinSerializable>("data2",new RawData(data2))});

                actual = repository.Get<RawData>("data1");
                Assert.NotNull(actual);
                AssertEx.CollectionEquals(actual.Data, data1);

                actual = repository.Get<RawData>("data2");
                Assert.NotNull(actual);
                AssertEx.CollectionEquals(actual.Data, data2);
            }
        }


        private static BlockStore CreateBlockStore([CallerMemberName]string folderName = null)
        {
            if(Directory.Exists(folderName))
                Directory.Delete(folderName, true);
            Thread.Sleep(50);
            Directory.CreateDirectory(folderName);
            Thread.Sleep(50);
            return new BlockStore(folderName, Network.Main);
        }
        private static BlockUndoStore CreateBlockUndoStore([CallerMemberName]string folderName = null)
        {
            TestUtils.EnsureNew(folderName);
            return new BlockUndoStore(folderName, Network.Main);
        }

        private BlockRepository CreateBlockRepository([CallerMemberName]string folderName = null)
        {
            return new BlockRepository(CreateIndexedStore(folderName + "-Blocks"), CreateIndexedStore(folderName + "-Headers"));
        }

    }
}
#endif