using System.Reflection; using FluentAssertions; using Xunit; namespace BucketChain.Test; public sealed class PersonTests { [Fact] public void Construction() { var person = new Person(); person.LeftNeighbor.Should().BeNull("not set yet"); person.RightNeighbor.Should().BeNull("not set yet"); person.HasBucket.Should().BeFalse("no bucket received yet"); person.LastStepPerformed.Should().Be(0, "performed in no step yet"); } [Fact] public void JoinChain_OneOther_Right() { var firstPerson = new Person(); var newPerson = new Person(); newPerson.JoinChain(firstPerson, true); firstPerson.LeftNeighbor.Should().BeNull(); firstPerson.RightNeighbor.Should().BeSameAs(newPerson, "joined on right side"); newPerson.LeftNeighbor.Should().BeSameAs(firstPerson); newPerson.RightNeighbor.Should().BeNull(); } [Fact] public void JoinChain_OneOther_Left() { var firstPerson = new Person(); var newPerson = new Person(); newPerson.JoinChain(firstPerson, false); firstPerson.LeftNeighbor.Should().BeSameAs(newPerson, "joined on left side"); firstPerson.RightNeighbor.Should().BeNull(); newPerson.LeftNeighbor.Should().BeNull(); newPerson.RightNeighbor.Should().BeSameAs(firstPerson); } [Fact] public void JoinChain_TwoOthers_Left() { var (firstPerson, secondPerson) = CreateMinimalChain(); var newPerson = new Person(); newPerson.JoinChain(firstPerson, false); firstPerson.LeftNeighbor.Should().BeSameAs(newPerson, "joined on left side of first person"); firstPerson.RightNeighbor.Should().BeSameAs(secondPerson, "unchanged"); secondPerson.LeftNeighbor.Should().BeSameAs(firstPerson, "unchanged"); secondPerson.RightNeighbor.Should().BeNull(); newPerson.LeftNeighbor.Should().BeNull(); newPerson.RightNeighbor.Should().BeSameAs(firstPerson); } [Fact] public void JoinChain_TwoOthers_Right() { var (firstPerson, secondPerson) = CreateMinimalChain(); var newPerson = new Person(); newPerson.JoinChain(secondPerson, true); firstPerson.LeftNeighbor.Should().BeNull(); firstPerson.RightNeighbor.Should().BeSameAs(secondPerson, "unchanged"); secondPerson.LeftNeighbor.Should().BeSameAs(firstPerson, "unchanged"); secondPerson.RightNeighbor.Should().BeSameAs(newPerson, "joined on right side of second person"); newPerson.LeftNeighbor.Should().BeSameAs(secondPerson); newPerson.RightNeighbor.Should().BeNull(); } [Fact] public void JoinChain_TwoOthers_Center_Option1() { var (firstPerson, secondPerson) = CreateMinimalChain(); var newPerson = new Person(); newPerson.JoinChain(firstPerson, true); firstPerson.LeftNeighbor.Should().BeNull(); firstPerson.RightNeighbor.Should().BeSameAs(newPerson, "joined on right side of first person"); secondPerson.LeftNeighbor.Should().BeSameAs(newPerson, "joined on left side of second person"); secondPerson.RightNeighbor.Should().BeNull(); newPerson.LeftNeighbor.Should().BeSameAs(firstPerson); newPerson.RightNeighbor.Should().BeSameAs(secondPerson); } [Fact] public void JoinChain_TwoOthers_Center_Option2() { var (firstPerson, secondPerson) = CreateMinimalChain(); var newPerson = new Person(); newPerson.JoinChain(secondPerson, false); firstPerson.LeftNeighbor.Should().BeNull(); firstPerson.RightNeighbor.Should().BeSameAs(newPerson, "joined on right side of first person"); secondPerson.LeftNeighbor.Should().BeSameAs(newPerson, "joined on left side of second person"); secondPerson.RightNeighbor.Should().BeNull(); newPerson.LeftNeighbor.Should().BeSameAs(firstPerson); newPerson.RightNeighbor.Should().BeSameAs(secondPerson); } [Fact] public void FightFire_Simple() { const double FireInitialSize = 45.67D; const double BucketSize = 2.4D; var fire = new Fire(FireInitialSize, default(double)); var bucket = CreateFilledBucket(BucketSize); var person = new Person(); SetBucket(person, bucket, true); person.FightFire(fire); fire.FireSize.Should().BeApproximately(FireInitialSize - BucketSize, double.Epsilon, "fire shrank by amount of water in the bucket"); bucket.IsEmpty.Should().BeTrue("bucket content has been used"); } [Fact] public void FightFire_NoBucket() { const double FireInitialSize = 45.67D; var fire = new Fire(FireInitialSize, default(double)); var person = new Person(); person.FightFire(fire); fire.FireSize.Should().BeApproximately(FireInitialSize, double.Epsilon, "fire size did not change"); } [Fact] public void UseWell_ExistingBucket_Forward() { const double WellInitialFillLevel = 20D; const double BucketSize = 1.86D; var well = new Well(WellInitialFillLevel, default(double)); var bucket = new Bucket(BucketSize); var person = new Person(); SetBucket(person, bucket, true); person.UseWell(well, null); bucket.IsEmpty.Should().BeFalse("has been filled"); well.LitersRemaining.Should().BeApproximately(WellInitialFillLevel - BucketSize, double.Epsilon, "well level has been reduced by bucket amount"); } [Fact] public void UseWell_ExistingBucket_Backward() { const double WellInitialFillLevel = 20D; const double BucketSize = 1.86D; var well = new Well(WellInitialFillLevel, default(double)); var bucket = new Bucket(BucketSize); var person = new Person(); SetBucket(person, bucket, false); person.UseWell(well, null); bucket.IsEmpty.Should().BeFalse("has been filled"); well.LitersRemaining.Should().BeApproximately(WellInitialFillLevel - BucketSize, double.Epsilon, "well level has been reduced by bucket amount"); } [Fact] public void UseWell_NoBucket() { const double WellInitialFillLevel = 20D; var well = new Well(WellInitialFillLevel, default(double)); var person = new Person(); person.UseWell(well, null); well.LitersRemaining.Should().Be(WellInitialFillLevel, "unchanged"); } [Fact] public void UseWell_AdditionalBucket() { const double WellInitialFillLevel = 20D; const double BucketSize = 1.86D; var well = new Well(WellInitialFillLevel, default(double)); var bucket = new Bucket(BucketSize); var person = new Person(); person.UseWell(well, bucket); bucket.IsEmpty.Should().BeFalse("has been filled"); well.LitersRemaining.Should().BeApproximately(WellInitialFillLevel - BucketSize, double.Epsilon, "well level has been reduced by bucket amount"); } [Fact] public void MoveBucket_Forward_Simple() { var (first, second) = CreateMinimalChain(); var bucket = CreateFilledBucket(1.2D); SetBucket(first, bucket, true); HasBucket(first, true).Should().BeTrue(); HasBucket(second, true).Should().BeFalse(); first.MoveBucket(1) .Should().BeTrue("successfully moved forward to next person"); HasBucket(first, true).Should().BeFalse("sent forward"); HasBucket(second, true).Should().BeTrue("received"); HasBucket(first, false).Should().BeFalse(); HasBucket(second, false).Should().BeFalse(); } [Fact] public void MoveBucket_Backward_Simple() { var (first, second) = CreateMinimalChain(); var bucket = new Bucket(1.2D); SetBucket(second, bucket, false); HasBucket(first, true).Should().BeFalse(); HasBucket(second, true).Should().BeFalse(); HasBucket(second, false).Should().BeTrue(); first.MoveBucket(1) .Should().BeTrue("successfully moved backward from next person"); HasBucket(first, true).Should().BeFalse(); HasBucket(second, true).Should().BeFalse(); HasBucket(first, false).Should().BeTrue("received"); HasBucket(second, false).Should().BeFalse("sent backward"); } [Fact] public void MoveBucket_Forward_And_Backward() { var (first, second) = CreateMinimalChain(); var bucket = CreateFilledBucket(1.2D); var backwardBucket = new Bucket(1.2D); SetBucket(first, bucket, true); SetBucket(second, backwardBucket, false); HasBucket(first, true).Should().BeTrue(); HasBucket(second, true).Should().BeFalse(); HasBucket(first, false).Should().BeFalse(); HasBucket(second, false).Should().BeTrue(); first.MoveBucket(1) .Should().BeTrue("successfully moved forward to and backward from next person"); HasBucket(first, true).Should().BeFalse("sent forward"); HasBucket(second, true).Should().BeTrue("received"); HasBucket(first, false).Should().BeTrue("received"); HasBucket(second, false).Should().BeFalse("sent backward"); } [Fact] public void MoveBucket_NoBuckets() { var (first, second) = CreateMinimalChain(); HasBucket(first, true).Should().BeFalse(); HasBucket(second, true).Should().BeFalse(); HasBucket(first, false).Should().BeFalse(); HasBucket(second, false).Should().BeFalse(); first.MoveBucket(1) .Should().BeTrue("nothing was moved, but there was no error either"); HasBucket(first, true).Should().BeFalse(); HasBucket(second, true).Should().BeFalse(); HasBucket(first, false).Should().BeFalse(); HasBucket(second, false).Should().BeFalse(); } [Fact] public void MoveBucket_ForwardBlocked() { var (first, second) = CreateMinimalChain(); var bucket1 = CreateFilledBucket(1.2D); var bucket2 = CreateFilledBucket(1.2D); SetBucket(first, bucket1, true); SetBucket(second, bucket2, true); HasBucket(first, true).Should().BeTrue(); HasBucket(second, true).Should().BeTrue(); first.MoveBucket(1) .Should().BeTrue("nothing was moved, but there was no error either"); HasBucket(first, true).Should().BeTrue(); HasBucket(second, true).Should().BeTrue(); } [Fact] public void MoveBucket_BackwardBlocked() { var (first, second) = CreateMinimalChain(); var bucket1 = CreateFilledBucket(1.2D); var bucket2 = CreateFilledBucket(1.2D); SetBucket(first, bucket1, false); SetBucket(second, bucket2, false); HasBucket(first, false).Should().BeTrue(); HasBucket(second, false).Should().BeTrue(); first.MoveBucket(1) .Should().BeTrue("nothing was moved, but there was no error either"); HasBucket(first, false).Should().BeTrue(); HasBucket(second, false).Should().BeTrue(); } [Fact] public void MoveBucket_AlreadyPerformedInStep_Simple() { const int Step = 12; var person = new Person(); SetLastStepPerformed(person, Step); person.MoveBucket(Step) .Should().BeFalse("already performed in this step"); } [Fact] public void MoveBucket_AlreadyPerformedInStep_Complex() { const int Step = 12; var (first, second) = CreateMinimalChain(); var third = new Person(); third.JoinChain(second, true); SetBucket(first, CreateFilledBucket(1.2D), true); first.MoveBucket(Step) .Should().BeTrue("can move"); second.MoveBucket(Step) .Should().BeFalse("already performed in this step"); } [Fact] public void MoveBucket_NoNeighbor() { var person = new Person(); SetBucket(person, CreateFilledBucket(1.2D), true); person.MoveBucket(1) .Should().BeFalse("no right neighbor to move to/from"); } [Theory] [MemberData(nameof(StringRepresentationData))] public void StringRepresentation(Person person, string expected, string reason) { person.ToString() .Should().Be(expected, reason); } public static TheoryData StringRepresentationData() { return new() { { CreatePerson(null), "🫲", "has no bucket" }, { CreatePerson(false), "🔘", "has empty forward bucket only" }, { CreatePerson(false, false), "🔘", "has empty backward bucket only" }, { CreatePerson(true), "🔵", "has full forward bucket only" }, { CreatePerson(true, false), "🔵", "has full backward bucket only" }, { CreatePerson(false, then: p => SetBucket(p, CreateFilledBucket(1), false)), "🔘", "has empty forward bucket and full backward bucket" }, { CreatePerson(true, then: p => SetBucket(p, new Bucket(1D), false)), "🔵", "has full forward bucket and empty backward bucket" } }; static Person CreatePerson(bool? bucketFull, bool forwardBucket = true, Action? then = null) { var person = new Person(); if (bucketFull == null) { return person; } var bucket = bucketFull.Value ? CreateFilledBucket(1D) : new Bucket(1D); SetBucket(person, bucket, forwardBucket); then?.Invoke(person); return person; } } private static (Person FirstPerson, Person SecondPerson) CreateMinimalChain() { var firstPerson = new Person(); var secondPerson = new Person(); secondPerson.JoinChain(firstPerson, true); return (firstPerson, secondPerson); } private static Bucket CreateFilledBucket(double size) { var bucket = new Bucket(size); bucket.Fill(size); return bucket; } #region Test Helper - ignore private static void SetBucket(Person person, Bucket? bucket, bool forwardBucket) { var field = GetBucketField(forwardBucket); field.SetValue(person, bucket); } private static bool HasBucket(Person person, bool forwardBucket) { var field = GetBucketField(forwardBucket); return field.GetValue(person) != null; } private static FieldInfo GetBucketField(bool forwardBucket) => typeof(Person).GetField(forwardBucket ? "_forwardBucket" : "_backwardBucket", BindingFlags.Instance | BindingFlags.NonPublic)!; private static void SetLastStepPerformed(Person person, int step) { var prop = typeof(Person).GetProperty("LastStepPerformed", BindingFlags.Instance | BindingFlags.Public); prop!.SetValue(person, step); } #endregion }