using NUnit.Framework;
using FluentAssertions;

using Encompass;

using System;
using System.Linq;
using System.Collections.Generic;
using Encompass.Exceptions;

namespace Tests
{
    struct MockComponent : IComponent
    {
        public int myInt;
        public string myString;
    }

    public class EngineTest
    {
        static List<ValueTuple<Guid, MockComponent>> resultComponents;
        static MockComponent resultComponent;

        static List<MockMessage> resultMessages;

        [Reads(typeof(MockComponent))]
        public class ReadComponentsTestEngine : Engine
        {
            public override void Update(double dt)
            {
                resultComponents = ReadComponents<MockComponent>().ToList();
            }
        }


        [Reads(typeof(MockComponent))]
        public class ReadComponentTestEngine : Engine
        {
            public override void Update(double dt)
            {
                resultComponent = ReadComponent<MockComponent>().Item2;
            }
        }

        [Test]
        public void ReadComponents()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new ReadComponentsTestEngine());

            var entity = worldBuilder.CreateEntity();

            MockComponent mockComponent;
            mockComponent.myInt = 0;
            mockComponent.myString = "hello";

            MockComponent mockComponentB;
            mockComponentB.myInt = 1;
            mockComponentB.myString = "howdy";

            var componentAID = worldBuilder.AddComponent(entity, mockComponent);
            var componentBID = worldBuilder.AddComponent(entity, mockComponentB);

            var world = worldBuilder.Build();

            world.Update(0.01f);

            var resultComponentValues = resultComponents.Select((kv) => kv.Item2);
            resultComponentValues.Should().Contain(mockComponent);
            resultComponentValues.Should().Contain(mockComponentB);
        }

        [Test]
        public void ReadComponent()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new ReadComponentTestEngine());

            var entity = worldBuilder.CreateEntity();

            MockComponent mockComponent;
            mockComponent.myInt = 0;
            mockComponent.myString = "hello";

            worldBuilder.AddComponent(entity, mockComponent);

            var world = worldBuilder.Build();

            world.Update(0.01f);

            Assert.AreEqual(mockComponent, resultComponent);
        }

        [Test]
        public void ReadComponentWhenMultipleComponents()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new ReadComponentTestEngine());

            var entity = worldBuilder.CreateEntity();

            MockComponent mockComponent;
            mockComponent.myInt = 0;
            mockComponent.myString = "hello";

            MockComponent mockComponentB;
            mockComponentB.myInt = 1;
            mockComponentB.myString = "howdy";

            worldBuilder.AddComponent(entity, mockComponent);
            worldBuilder.AddComponent(entity, mockComponentB);

            var world = worldBuilder.Build();

            world.Update(0.01);

            Assert.That(resultComponent, Is.EqualTo(mockComponent).Or.EqualTo(mockComponentB));
        }

        [Reads(typeof(MockComponent))]
        [Updates(typeof(MockComponent))]
        public class UpdateComponentTestEngine : Engine
        {
            public override void Update(double dt)
            {
                (var componentID, var component) = ReadComponent<MockComponent>();

                component.myInt = 420;
                component.myString = "blaze it";
                UpdateComponent(componentID, component);
            }
        }

        // this test needs to be improved...

        [Test]
        public void UpdateComponent()
        {
            var worldBuilder = new WorldBuilder();

            worldBuilder.AddEngine(new UpdateComponentTestEngine());
            worldBuilder.AddEngine(new ReadComponentTestEngine());

            var entity = worldBuilder.CreateEntity();

            MockComponent mockComponent;
            mockComponent.myInt = 0;
            mockComponent.myString = "hello";

            worldBuilder.AddComponent(entity, mockComponent);

            var world = worldBuilder.Build();

            world.Update(0.01);
            world.Update(0.01);

            Assert.AreEqual(420, resultComponent.myInt);
            Assert.AreEqual("blaze it", resultComponent.myString);
        }

        [Reads(typeof(MockComponent))]
        public class UndeclaredUpdateComponentTestEngine : Engine
        {
            public override void Update(double dt)
            {
                (var componentID, var component) = ReadComponent<MockComponent>();

                component.myInt = 420;
                component.myString = "blaze it";
                UpdateComponent(componentID, component);

                component = ReadComponent<MockComponent>().Item2;
            }
        }

        [Test]
        public void UpdateUndeclaredComponent()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new UndeclaredUpdateComponentTestEngine());

            var entity = worldBuilder.CreateEntity();

            MockComponent mockComponent;
            mockComponent.myInt = 0;
            mockComponent.myString = "hello";

            worldBuilder.AddComponent(entity, mockComponent);

            var world = worldBuilder.Build();

            var ex = Assert.Throws<IllegalUpdateException>(() => world.Update(0.01f));
            Assert.That(ex.Message, Is.EqualTo("Engine UndeclaredUpdateComponentTestEngine tried to update undeclared Component MockComponent"));
        }

        struct MockMessage : IMessage
        {
            public string myString;
        }

        [Sends(typeof(MockMessage))]
        public class MessageEmitEngine : Engine
        {
            public override void Update(double dt)
            {
                MockMessage message;
                message.myString = "howdy";

                this.SendMessage(message);
            }
        }

        [Receives(typeof(MockMessage))]
        public class MessageReadEngine : Engine
        {
            public override void Update(double dt)
            {
                resultMessages = this.ReadMessages<MockMessage>().ToList();
            }
        }

        [Test]
        public void EmitAndReadMessage()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new MessageEmitEngine());
            worldBuilder.AddEngine(new MessageReadEngine());

            var world = worldBuilder.Build();

            world.Update(0.01f);

            Assert.AreEqual(resultMessages.First().myString, "howdy");
        }

        public class UndeclaredMessageEmitEngine : Engine
        {
            public override void Update(double dt)
            {
                MockMessage message;
                message.myString = "howdy";

                this.SendMessage(message);
            }
        }

        static IEnumerable<MockMessage> emptyReadMessagesResult;

        [Receives(typeof(MockMessage))]
        class ReadMessagesWhenNoneExistEngine : Engine
        {
            public override void Update(double dt)
            {
                emptyReadMessagesResult = ReadMessages<MockMessage>();
            }
        }

        [Test]
        public void ReadMessagesWhenNoneHaveBeenEmitted()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new ReadMessagesWhenNoneExistEngine());

            var world = worldBuilder.Build();

            world.Update(0.01f);

            emptyReadMessagesResult.Should().BeEmpty();
        }

        [Test]
        public void EmitUndeclaredMessage()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new UndeclaredMessageEmitEngine());

            var world = worldBuilder.Build();

            var ex = Assert.Throws<IllegalSendException>(() => world.Update(0.01f));
            Assert.That(ex.Message, Is.EqualTo("Engine UndeclaredMessageEmitEngine tried to send undeclared Message MockMessage"));
        }

        static bool someTest;

        [Sends(typeof(MockMessage))]
        class EmitMockMessageEngine : Engine
        {
            public override void Update(double dt)
            {
                MockMessage message;
                message.myString = "howdy";

                this.SendMessage(message);
            }
        }

        [Receives(typeof(MockMessage))]
        class SomeMessageTestEngine : Engine
        {
            public override void Update(double dt)
            {
                someTest = this.SomeMessage<MockMessage>();
            }
        }

        [Test]
        public void SomeMessage()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new EmitMockMessageEngine());
            worldBuilder.AddEngine(new SomeMessageTestEngine());

            var world = worldBuilder.Build();

            world.Update(0.01f);

            Assert.That(someTest, Is.True);
        }

        class UndeclaredSomeMessageEngine : Engine
        {
            public override void Update(double dt)
            {
                someTest = this.SomeMessage<MockMessage>();
            }
        }

        [Test]
        public void UndeclaredSomeMessage()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new EmitMockMessageEngine());
            worldBuilder.AddEngine(new UndeclaredSomeMessageEngine());

            var world = worldBuilder.Build();

            Assert.Throws<IllegalReadException>(() => world.Update(0.01f));
        }

        class SomeComponentTestEngine : Engine
        {
            public override void Update(double dt)
            {
                Assert.IsTrue(SomeComponent<MockComponent>());
            }
        }

        [Test]
        public void SomeComponent()
        {
            var worldBuilder = new WorldBuilder();

            var entity = worldBuilder.CreateEntity();
            worldBuilder.AddComponent(entity, new MockComponent());

            var world = worldBuilder.Build();

            world.Update(0.01);
        }

        static ValueTuple<Guid, MockComponent> pairA;
        static ValueTuple<Guid, MockComponent> pairB;

        [Reads(typeof(MockComponent))]
        class SameValueComponentReadEngine : Engine
        {
            public override void Update(double dt)
            {
                var components = ReadComponents<MockComponent>();

                pairA = components.First();
                pairB = components.Last();
            }
        }

        // Tests that components with identical values should be distinguishable by ID
        [Test]
        public void SameValueComponents()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new SameValueComponentReadEngine());

            MockComponent componentA;
            componentA.myInt = 20;
            componentA.myString = "hello";

            MockComponent componentB;
            componentB.myInt = 20;
            componentB.myString = "hello";

            var entity = worldBuilder.CreateEntity();
            worldBuilder.AddComponent(entity, componentA);
            worldBuilder.AddComponent(entity, componentB);

            var world = worldBuilder.Build();
            world.Update(0.01f);

            Assert.That(pairA, Is.Not.EqualTo(pairB));
            Assert.That(pairA.Item2, Is.EqualTo(pairB.Item2));
        }

        static IEnumerable<ValueTuple<Guid, MockComponent>> emptyComponentReadResult;

        [Reads(typeof(MockComponent))]
        class ReadEmptyMockComponentsEngine : Engine
        {
            public override void Update(double dt)
            {
                emptyComponentReadResult = ReadComponents<MockComponent>();
            }
        }

        [Test]
        public void ReadComponentsOfTypeWhereNoneExist()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new ReadEmptyMockComponentsEngine());

            var world = worldBuilder.Build();
            world.Update(0.01f);

            Assert.That(emptyComponentReadResult, Is.Empty);
        }

        struct DestroyerComponent : IComponent { }

        [Reads(typeof(DestroyerComponent))]
        class DestroyerEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (var componentPair in ReadComponents<DestroyerComponent>())
                {
                    var componentID = componentPair.Item1;
                    var entityID = GetEntityIDByComponentID(componentID);
                    Destroy(entityID);
                }
            }
        }

        static List<(Guid, MockComponent)> results;

        [Reads(typeof(MockComponent))]
        class ReaderEngine : Engine
        {
            public override void Update(double dt)
            {
                results = ReadComponents<MockComponent>().ToList();
            }
        }

        [Test]
        public void DestroyEntity()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new DestroyerEngine());
            worldBuilder.AddEngine(new ReaderEngine());

            var entity = worldBuilder.CreateEntity();
            var entityB = worldBuilder.CreateEntity();
            var entityC = worldBuilder.CreateEntity();

            DestroyerComponent destroyerComponent;
            MockComponent mockComponent;
            mockComponent.myInt = 2;
            mockComponent.myString = "blah";

            worldBuilder.AddComponent(entity, destroyerComponent);
            var componentID = worldBuilder.AddComponent(entity, mockComponent);

            worldBuilder.AddComponent(entityB, destroyerComponent);
            var componentBID = worldBuilder.AddComponent(entityB, mockComponent);

            var componentCID = worldBuilder.AddComponent(entityC, mockComponent);

            var world = worldBuilder.Build();

            world.Update(0.01);
            world.Update(0.01);

            Assert.That(results, Does.Not.Contain((componentID, mockComponent)));
            Assert.That(results, Does.Not.Contain((componentBID, mockComponent)));
            Assert.That(results, Does.Contain((componentCID, mockComponent)));
        }

        [Reads(typeof(DestroyerComponent), typeof(MockComponent))]
        class DestroyAndAddComponentEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (var componentPair in ReadComponents<DestroyerComponent>())
                {
                    var componentID = componentPair.Item1;
                    var entity = GetEntityByComponentID(componentID);
                    var (id, _) = GetComponent<MockComponent>(entity);
                    RemoveComponent(id);
                    Destroy(entity.ID);
                }
            }
        }

        [Test]
        public void DestroyEntityWhileRemovingComponent()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new DestroyAndAddComponentEngine());
            worldBuilder.AddEngine(new ReaderEngine());

            var entity = worldBuilder.CreateEntity();

            worldBuilder.AddComponent(entity, new DestroyerComponent());
            worldBuilder.AddComponent(entity, new MockComponent());

            var world = worldBuilder.Build();

            Assert.DoesNotThrow(() => world.Update(0.01));
        }

        static Entity entityFromComponentIDResult;

        [Reads(typeof(MockComponent))]
        class GetEntityFromComponentIDEngine : Engine
        {
            public override void Update(double dt)
            {
                var componentID = ReadComponent<MockComponent>().Item1;
                entityFromComponentIDResult = GetEntityByComponentID(componentID);
            }
        }

        [Test]
        public void GetEntityFromComponentID()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new GetEntityFromComponentIDEngine());

            MockComponent component;
            component.myInt = 2;
            component.myString = "howdy";

            var entity = worldBuilder.CreateEntity();
            worldBuilder.AddComponent(entity, component);

            var world = worldBuilder.Build();
            world.Update(0.01f);

            Assert.That(entity, Is.EqualTo(entityFromComponentIDResult));
        }

        static MockComponent mockComponentByIDResult;

        [Reads(typeof(MockComponent))]
        class GetComponentByIDEngine : Engine
        {
            public override void Update(double dt)
            {
                var componentID = ReadComponent<MockComponent>().Item1;
                mockComponentByIDResult = GetComponentByID<MockComponent>(componentID);
            }
        }
        [Test]
        public void GetComponentByID()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new GetComponentByIDEngine());

            MockComponent component;
            component.myInt = 2;
            component.myString = "howdy";

            var entity = worldBuilder.CreateEntity();
            worldBuilder.AddComponent(entity, component);

            var world = worldBuilder.Build();
            world.Update(0.01f);

            Assert.That(component, Is.EqualTo(mockComponentByIDResult));
        }

        struct OtherComponent : IComponent { }

        [Reads(typeof(MockComponent))]
        class GetComponentByIDWithTypeMismatchEngine : Engine
        {
            public override void Update(double dt)
            {
                var componentID = ReadComponent<MockComponent>().Item1;
                GetComponentByID<OtherComponent>(componentID);
            }
        }

        [Test]
        public void GetComponentByIDWithTypeMismatch()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new GetComponentByIDWithTypeMismatchEngine());

            MockComponent component;
            component.myInt = 2;
            component.myString = "howdy";

            var entity = worldBuilder.CreateEntity();
            worldBuilder.AddComponent(entity, component);

            var world = worldBuilder.Build();

            Assert.Throws<ComponentTypeMismatchException>(() => world.Update(0.01f));
        }

        struct EntityIDComponent : IComponent { public Guid entityID;  }
        static bool hasEntity;

        [Reads(typeof(EntityIDComponent))]
        class HasEntityTestEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (var (mockComponentID, mockComponent) in ReadComponents<EntityIDComponent>())
                {
                    hasEntity = EntityExists(mockComponent.entityID);
                    if (hasEntity) { Destroy(mockComponent.entityID); }
                }
            }
        }

        [Test]
        public void EntityExists()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new HasEntityTestEngine());

            var entity = worldBuilder.CreateEntity();
            var entityTwo = worldBuilder.CreateEntity();

            EntityIDComponent entityIDComponent;
            entityIDComponent.entityID = entityTwo.ID;

            worldBuilder.AddComponent(entity, entityIDComponent);

            var world = worldBuilder.Build();

            world.Update(0.01);

            Assert.IsTrue(hasEntity);

            world.Update(0.01);

            Assert.IsFalse(hasEntity);
        }

        struct MockComponentUpdateMessage : IMessage
        {
            public Guid componentID;
            public MockComponent mockComponent;
        }

        [Receives(typeof(MockComponentUpdateMessage))]
        [Updates(typeof(MockComponent))]
        class RepeatUpdateEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (var mockComponentUpdateMessage in ReadMessages<MockComponentUpdateMessage>())
                {
                    UpdateComponent(mockComponentUpdateMessage.componentID, mockComponentUpdateMessage.mockComponent);
                    UpdateComponent(mockComponentUpdateMessage.componentID, mockComponentUpdateMessage.mockComponent);
                }
            }
        }

        [Test]
        public void EngineUpdatesComponentMultipleTimes()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new RepeatUpdateEngine());

            var entity = worldBuilder.CreateEntity();

            MockComponent mockComponent;
            mockComponent.myInt = 1;
            mockComponent.myString = "5";

            var mockComponentID = worldBuilder.AddComponent(entity, mockComponent);

            MockComponentUpdateMessage mockComponentUpdateMessage;
            mockComponentUpdateMessage.componentID = mockComponentID;
            mockComponentUpdateMessage.mockComponent = mockComponent;
            worldBuilder.SendMessage(mockComponentUpdateMessage);

            var world = worldBuilder.Build();
            Assert.Throws<RepeatUpdateComponentException>(() => world.Update(0.01));
        }
    }
}