using FluentAssertions; using Xunit; namespace OECS.Tests; // ── Phantom types for relationship differentiation ─────────────────── public struct ChildOf { } public struct ParentOf { } public struct OwnedBy { } public struct MemberOf { } public struct Thing { } // ── Tests ──────────────────────────────────────────────────────────── public class RelationshipTests { [Fact] public void AddRelationship_UpdatesReverseIndex() { var world = new World(); var child = world.CreateEntity(); var parent = world.CreateEntity(); world.AddComponent(child, new Relationship { Target = parent }); var sources = world.GetSources>(parent); sources.Should().BeEquivalentTo([child]); } [Fact] public void RemoveRelationship_UpdatesReverseIndex() { var world = new World(); var child = world.CreateEntity(); var parent = world.CreateEntity(); world.AddComponent(child, new Relationship { Target = parent }); world.RemoveComponent>(child); var sources = world.GetSources>(parent); sources.Should().BeEmpty(); } [Fact] public void ReplaceRelationship_UpdatesReverseIndex() { var world = new World(); var child = world.CreateEntity(); var parent1 = world.CreateEntity(); var parent2 = world.CreateEntity(); world.AddComponent(child, new Relationship { Target = parent1 }); // Replace with a new target. world.AddComponent(child, new Relationship { Target = parent2 }); world.GetSources>(parent1).Should().BeEmpty(); world.GetSources>(parent2).Should().BeEquivalentTo([child]); } [Fact] public void DestroySourceEntity_CleansUpRelationships() { var world = new World(); var child = world.CreateEntity(); var parent = world.CreateEntity(); world.AddComponent(child, new Relationship { Target = parent }); world.DestroyEntity(child); // The relationship should be gone from the reverse index. world.GetSources>(parent).Should().BeEmpty(); // The destroyed entity should not be alive. world.IsAlive(child).Should().BeFalse(); } [Fact] public void DestroyTargetEntity_CleansUpIncomingRelationships() { var world = new World(); var child = world.CreateEntity(); var parent = world.CreateEntity(); world.AddComponent(child, new Relationship { Target = parent }); world.DestroyEntity(parent); // The relationship component should be removed from the source entity. world.HasComponent>(child).Should().BeFalse(); // The reverse index should be empty for the destroyed target. world.GetSources>(parent).Should().BeEmpty(); } [Fact] public void MultipleSources_ForSameTarget() { var world = new World(); var child1 = world.CreateEntity(); var child2 = world.CreateEntity(); var child3 = world.CreateEntity(); var parent = world.CreateEntity(); world.AddComponent(child1, new Relationship { Target = parent }); world.AddComponent(child2, new Relationship { Target = parent }); world.AddComponent(child3, new Relationship { Target = parent }); var sources = world.GetSources>(parent); sources.Should().BeEquivalentTo([child1, child2, child3]); } [Fact] public void DifferentRelationshipTypes_AreIndependent() { var world = new World(); var entity = world.CreateEntity(); var owner = world.CreateEntity(); var group = world.CreateEntity(); world.AddComponent(entity, new Relationship { Target = owner }); world.AddComponent(entity, new Relationship { Target = group }); world.GetSources>(owner).Should().BeEquivalentTo([entity]); world.GetSources>(group).Should().BeEquivalentTo([entity]); // Cross-check: OwnedBy sources should not appear in MemberOf lookups. world.GetSources>(owner).Should().BeEmpty(); } [Fact] public void ReverseLookup_ReturnsEmpty_WhenNoRelationships() { var world = new World(); var entity = world.CreateEntity(); var sources = world.GetSources>(entity); sources.Should().BeEmpty(); } [Fact] public void Relationship_CanBeQueried_LikeNormalComponent() { var world = new World(); var child = world.CreateEntity(); var parent = world.CreateEntity(); world.AddComponent(child, new Relationship { Target = parent }); var results = new List(); foreach (var it in world.Select>()) { results.Add(it.Entity); it.Val1.Target.Should().Be(parent); } results.Should().BeEquivalentTo([child]); } [Fact] public void GetSources_PreservesInsertionOrder() { var world = new World(); var parent = world.CreateEntity(); // Add sources in a specific order. var sources = new Entity[5]; for (int i = 0; i < 5; i++) { sources[i] = world.CreateEntity(); world.AddComponent(sources[i], new Relationship { Target = parent }); } var result = world.GetSources>(parent); result.Should().Equal(sources); } [Fact] public void GetSources_OrderIsStableAfterMiddleRemove() { var world = new World(); var parent = world.CreateEntity(); var a = world.CreateEntity(); var b = world.CreateEntity(); var c = world.CreateEntity(); world.AddComponent(a, new Relationship { Target = parent }); world.AddComponent(b, new Relationship { Target = parent }); world.AddComponent(c, new Relationship { Target = parent }); // Remove the middle source. world.RemoveComponent>(b); // Remaining sources should preserve insertion order. var result = world.GetSources>(parent); result.Should().Equal([a, c]); } [Fact] public void ReorderSources_ChangesIterationOrder() { var world = new World(); var parent = world.CreateEntity(); var a = world.CreateEntity(); var b = world.CreateEntity(); var c = world.CreateEntity(); world.AddComponent(a, new Relationship { Target = parent }); world.AddComponent(b, new Relationship { Target = parent }); world.AddComponent(c, new Relationship { Target = parent }); // Reverse the order. world.ReorderSources>(parent, [c, b, a]); var result = world.GetSources>(parent); result.Should().Equal([c, b, a]); } [Fact] public void ReorderSources_NoopsWhenTargetHasNoRelationships() { var world = new World(); var entity = world.CreateEntity(); // Should not throw. world.ReorderSources>(entity, []); } [Fact] public void DestroyEntity_WithBothIncomingAndOutgoingRelationships() { var world = new World(); var a = world.CreateEntity(); var b = world.CreateEntity(); var c = world.CreateEntity(); // a → b (a is child of b) world.AddComponent(a, new Relationship { Target = b }); // c → a (c is child of a) world.AddComponent(c, new Relationship { Target = a }); // Destroy a. This should: // 1. Remove the a→b relationship (a is source). // 2. Remove the c→a relationship (a is target, so c's component is removed). world.DestroyEntity(a); world.IsAlive(a).Should().BeFalse(); world.HasComponent>(c).Should().BeFalse(); world.GetSources>(b).Should().BeEmpty(); } }