using NUnit.Framework;
using FluentAssertions;

using Encompass;
using System.Runtime.CompilerServices;

namespace Tests
{
    public class ComponentTests
    {
        struct MockComponent : IComponent
        {
            public int myInt;
        }

        struct EntityMessage : IMessage
        {
            public Entity entity;
        }

        static MockComponent gottenMockComponent;

        [Receives(typeof(EntityMessage))]
        [Reads(typeof(MockComponent))]
        class GetMockComponentEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (ref readonly var entityMessage in ReadMessages<EntityMessage>())
                {
                    gottenMockComponent = GetComponent<MockComponent>(entityMessage.entity);
                }
            }
        }

        struct AddComponentTestMessage : IMessage
        {
            public Entity entity;
            public MockComponent mockComponent;
        }

        [Receives(typeof(AddComponentTestMessage))]
        [Reads(typeof(MockComponent))]
        class AddComponentTestEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (ref readonly var addComponentTestMessage in ReadMessages<AddComponentTestMessage>())
                {
                    Assert.IsTrue(HasComponent<MockComponent>(addComponentTestMessage.entity));
                    ref readonly var gottenComponent = ref GetComponent<MockComponent>(addComponentTestMessage.entity);
                    gottenComponent.Should().BeEquivalentTo(addComponentTestMessage.mockComponent);
                }
            }
        }

        [Test]
        public unsafe void AddComponent()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new AddComponentTestEngine());

            var entity = worldBuilder.CreateEntity();

            const string MyString = "hello";

            MockComponent mockComponent;
            mockComponent.myInt = 3;

            worldBuilder.SetComponent(entity, mockComponent);

            AddComponentTestMessage addComponentTestMessage;
            addComponentTestMessage.entity = entity;
            addComponentTestMessage.mockComponent = mockComponent;
            worldBuilder.SendMessage(addComponentTestMessage);

            var world = worldBuilder.Build();

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

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

            var entity = worldBuilder.CreateEntity();
            worldBuilder.SetComponent(entity, new MockComponent { myInt = 20 });
            worldBuilder.SetComponent(entity, new MockComponent { myInt = 50 });
            worldBuilder.SetComponent(entity, new MockComponent { myInt = 40 });

            var world = worldBuilder.Build();

            world.Update(0.01);

            Assert.That(gottenMockComponent.myInt, Is.EqualTo(40));
        }

        [Reads(typeof(MockComponent))]
        [Writes(typeof(MockComponent))]
        class OverwriteEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (ref readonly var entity in ReadEntities<MockComponent>())
                {
                    ref readonly var mockComponent = ref GetComponent<MockComponent>(entity);
                    SetComponent(entity, new MockComponent { myInt = mockComponent.myInt + 1 });
                }
            }
        }

        [Reads(typeof(MockComponent))]
        class ReadMockComponentEngine : Engine
        {
            public override void Update(double dt)
            {
                gottenMockComponent = ReadComponent<MockComponent>();
            }
        }

        [Test]
        public void EngineOverwriteComponent()
        {
            var worldBuilder = new WorldBuilder();
            worldBuilder.AddEngine(new OverwriteEngine());
            worldBuilder.AddEngine(new ReadMockComponentEngine());

            var entity = worldBuilder.CreateEntity();
            worldBuilder.SetComponent(entity, new MockComponent { myInt = 420 });

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

            Assert.That(gottenMockComponent.myInt, Is.EqualTo(420));

            world.Update(0.01);

            Assert.That(gottenMockComponent.myInt, Is.EqualTo(421));

            world.Update(0.01);

            Assert.That(gottenMockComponent.myInt, Is.EqualTo(422));
        }

        [Reads(typeof(MockComponent))]
        [Writes(typeof(MockComponent))]
        class AddAndRemoveComponentEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (ref readonly var entity in ReadEntities<MockComponent>())
                {
                    ref readonly var mockComponent = ref GetComponent<MockComponent>(entity);
                    SetComponent(entity, mockComponent);
                    RemoveComponent<MockComponent>(entity);
                }
            }
        }

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

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

            var world = worldBuilder.Build();

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

        struct AddMockComponentMessage : IMessage
        {
            public Entity entity;
            public MockComponent mockComponent;
        }

        [Sends(typeof(AddMockComponentMessage))]
        class EmitMockComponentMessageEngine : Engine
        {
            private Entity entity;

            public EmitMockComponentMessageEngine(Entity entity)
            {
                this.entity = entity;
            }

            public override void Update(double dt)
            {
                MockComponent mockComponent;
                mockComponent.myInt = 10;

                AddMockComponentMessage addMockComponentMessage;
                addMockComponentMessage.entity = entity;
                addMockComponentMessage.mockComponent = mockComponent;
                SendMessage(addMockComponentMessage);
            }
        }

        [WritesImmediate(typeof(MockComponent))]
        [Receives(typeof(AddMockComponentMessage))]
        [Writes(typeof(MockComponent))]
        class AddMockComponentEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (ref readonly var message in ReadMessages<AddMockComponentMessage>())
                {
                    SetComponent(message.entity, message.mockComponent);
                }
            }
        }

        [ReadsImmediate(typeof(MockComponent))]
        class HasMockComponentEngine : Engine
        {
            private Entity entity;

            public HasMockComponentEngine(Entity entity)
            {
                this.entity = entity;
            }

            public override void Update(double dt)
            {
                Assert.IsTrue(HasComponent<MockComponent>(entity));
            }
        }

        [Test]
        public void AddComponentAndReadSameFrame()
        {
            var worldBuilder = new WorldBuilder();
            var entity = worldBuilder.CreateEntity();

            worldBuilder.AddEngine(new EmitMockComponentMessageEngine(entity));
            worldBuilder.AddEngine(new AddMockComponentEngine());
            worldBuilder.AddEngine(new HasMockComponentEngine(entity));

            var world = worldBuilder.Build();

            world.Update(0.01);
        }

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

            var entity = worldBuilder.CreateEntity();

            MockComponent mockComponent;
            mockComponent.myInt = 3;

            worldBuilder.SetComponent(entity, mockComponent);

            EntityMessage entityMessage;
            entityMessage.entity = entity;
            worldBuilder.SendMessage(entityMessage);

            var world = worldBuilder.Build();

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

            Assert.AreEqual(mockComponent, gottenMockComponent);
        }
        struct HasComponentTestMessage : IMessage
        {
            public Entity entity;
        }

        [Receives(typeof(HasComponentTestMessage))]
        [Reads(typeof(MockComponent))]
        class HasComponentTestEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (ref readonly var hasComponentTestEngine in ReadMessages<HasComponentTestMessage>())
                {
                    Assert.IsTrue(HasComponent<MockComponent>(hasComponentTestEngine.entity));
                }
            }
        }

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

            var entity = worldBuilder.CreateEntity();

            MockComponent mockComponent;
            mockComponent.myInt = 3;

            worldBuilder.SetComponent(entity, mockComponent);

            HasComponentTestMessage hasComponentTestMessage;
            hasComponentTestMessage.entity = entity;
            worldBuilder.SendMessage(hasComponentTestMessage);

            var world = worldBuilder.Build();

            world.Update(0.01);
        }

        static bool hasComponentRuntimeTypeResult;

        [Receives(typeof(HasComponentTestMessage))]
        [Reads(typeof(MockComponent))]
        class HasComponentWithRuntimeTypeEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (ref readonly var hasComponentTestEngine in ReadMessages<HasComponentTestMessage>())
                {
                    hasComponentRuntimeTypeResult = HasComponent(hasComponentTestEngine.entity, typeof(MockComponent));
                }
            }
        }

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

            var entity = worldBuilder.CreateEntity();

            MockComponent mockComponent;
            mockComponent.myInt = 3;

            worldBuilder.SetComponent(entity, mockComponent);

            HasComponentTestMessage hasComponentTestMessage;
            hasComponentTestMessage.entity = entity;
            worldBuilder.SendMessage(hasComponentTestMessage);

            var world = worldBuilder.Build();

            world.Update(0.01);

            Assert.IsTrue(hasComponentRuntimeTypeResult);
        }

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

            var entity = worldBuilder.CreateEntity();

            HasComponentTestMessage hasComponentTestMessage;
            hasComponentTestMessage.entity = entity;
            worldBuilder.SendMessage(hasComponentTestMessage);

            var world = worldBuilder.Build();

            world.Update(0.01);

            Assert.IsFalse(hasComponentRuntimeTypeResult);
        }

        struct RemoveComponentTestMessage : IMessage
        {
            public Entity entity;
        }

        [Reads(typeof(MockComponent))]
        [Receives(typeof(RemoveComponentTestMessage))]
        [Writes(typeof(MockComponent))]
        class RemoveComponentTestEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (ref readonly var removeComponentMessage in ReadMessages<RemoveComponentTestMessage>())
                {
                    RemoveComponent<MockComponent>(removeComponentMessage.entity);
                }
            }
        }

        [Receives(typeof(RemoveComponentTestMessage))]
        [Sends(typeof(CheckHasMockComponentMessage))]
        class DoRemoveCheckEngine : Engine
        {
            private Entity entity;

            public DoRemoveCheckEngine(Entity entity)
            {
                this.entity = entity;
            }

            public override void Update(double dt)
            {
                if (SomeMessage<RemoveComponentTestMessage>())
                {
                    CheckHasMockComponentMessage checkHasMockComponentMessage;
                    checkHasMockComponentMessage.entity = entity;
                    checkHasMockComponentMessage.shouldHaveComponent = true;
                    SendMessage(checkHasMockComponentMessage);
                }
                else
                {
                    CheckHasMockComponentMessage checkHasMockComponentMessage;
                    checkHasMockComponentMessage.entity = entity;
                    checkHasMockComponentMessage.shouldHaveComponent = false;
                    SendMessage(checkHasMockComponentMessage);
                }
            }
        }

        static bool hasComponentResult;

        [Receives(typeof(CheckHasMockComponentMessage))]
        [Reads(typeof(MockComponent))]
        class CheckHasMockComponentEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (ref readonly var checkHasMockComponentMessage in ReadMessages<CheckHasMockComponentMessage>())
                {
                    hasComponentResult = HasComponent<MockComponent>(checkHasMockComponentMessage.entity);
                }
            }
        }

        [Test]
        public void RemovedComponentShouldStillExistOnSameFrame()
        {
            var worldBuilder = new WorldBuilder();
            var entity = worldBuilder.CreateEntity();

            worldBuilder.AddEngine(new RemoveComponentTestEngine());
            worldBuilder.AddEngine(new CheckHasMockComponentEngine());
            worldBuilder.AddEngine(new DoRemoveCheckEngine(entity));

            MockComponent mockComponent;
            mockComponent.myInt = 3;

            worldBuilder.SetComponent(entity, mockComponent);

            RemoveComponentTestMessage removeComponentMessage;
            removeComponentMessage.entity = entity;
            worldBuilder.SendMessage(removeComponentMessage);

            var world = worldBuilder.Build();

            world.Update(0.01f);

            hasComponentResult.Should().BeTrue();

            world.Update(0.01f);

            hasComponentResult.Should().BeFalse();
        }

        struct CheckHasMockComponentMessage : IMessage
        {
            public Entity entity;
            public bool shouldHaveComponent;
        }

        [Receives(typeof(CheckHasMockComponentMessage))]
        [ReadsImmediate(typeof(MockComponent))]
        class CheckHasImmediateMockComponentEngine : Engine
        {
            public override void Update(double dt)
            {
                foreach (ref readonly var checkHasMockComponentMessage in ReadMessages<CheckHasMockComponentMessage>())
                {
                    Assert.IsTrue(HasComponent<MockComponent>(checkHasMockComponentMessage.entity));
                }
            }
        }
    }
}