private static void CheckProductClashExposureCount(
            IReadOnlyDictionary <string, Clash> clashesByExternalRef,
            IClashExposureCountService clashExposureCountService,
            SmoothFailureMessagesForSpotsCollection result,
            IReadOnlyDictionary <string, int> spotChildClashCodeCount,
            Spot spot,
            SpotInfo spotInfo,
            IReadOnlyCollection <Spot> childProductClashSpots)
        {
            if (!clashesByExternalRef.TryGetValue(spotInfo.ProductClashCode, out Clash childClash))
            {
                return;
            }

            int exposureCount = clashExposureCountService.Calculate(
                childClash.Differences,
                (childClash.DefaultPeakExposureCount, childClash.DefaultOffPeakExposureCount),
                (spot.StartDateTime, spot.SalesArea)
                );

            if (exposureCount > 0 &&
                childProductClashSpots.Count + spotChildClashCodeCount[spotInfo.ProductClashCode] > exposureCount)
            {
                result.Add(spot.Uid, SmoothFailureMessages.T1_ProductClash);
            }
        }
        private static SmoothFailureMessagesForSpotsCollection RemoveDuplicateFailureMessages(
            IReadOnlyCollection <Spot> spotsForBreak,
            SmoothFailureMessagesForSpotsCollection messages)
        {
            var result = new SmoothFailureMessagesForSpotsCollection();

            foreach (var spotUid in spotsForBreak.Select(s => s.Uid))
            {
                result.InitialiseForSpot(spotUid);

                foreach (var failure in messages[spotUid].Failures
                         .OrderBy(f => Convert.ToInt32(f.FailureMessage)))
                {
                    if (result[spotUid].Failures.Any(f => f.FailureMessage == failure.FailureMessage))
                    {
                        continue;
                    }

                    result.Add(
                        spotUid,
                        failure.FailureMessage,
                        failure.Restriction);
                }
            }

            return(result);
        }
        private static void IndicateIfMultipartTopTailSameBreakSpotsCannotBeAdded(
            SmoothFailureMessagesForSpotsCollection result,
            IReadOnlyCollection <Spot> multipartSpots)
        {
            bool haveFailureMessages = multipartSpots.Any(
                spot => result[spot.Uid].Failures.Count > 0
                );

            if (!haveFailureMessages)
            {
                return;
            }

            bool isTopTailMultipartSpots   = multipartSpots.Any(spot => spot.MultipartSpot == MultipartSpotTypes.TopTail);
            bool isSameBreakMultipartSpots = multipartSpots.Any(spot => spot.MultipartSpot == MultipartSpotTypes.SameBreak);

            if (isTopTailMultipartSpots)
            {
                foreach (var spot in multipartSpots)
                {
                    result.Add(spot.Uid, SmoothFailureMessages.T1_CantAddTopAndTailToSameBreak);
                }
            }
            else if (isSameBreakMultipartSpots)
            {
                foreach (var spot in multipartSpots)
                {
                    result.Add(spot.Uid, SmoothFailureMessages.T1_CantAddSameBreakToSameBreak);
                }
            }
        }
        private static void ValidateWithSpotRestrictions(
            SmoothBreak theSmoothBreak,
            Programme programme,
            SalesArea salesArea,
            IReadOnlyCollection <Spot> spotsForBreak,
            SmoothResources smoothResources,
            IReadOnlyCollection <Break> breaksBeingSmoothed,
            IReadOnlyCollection <Programme> scheduleProgrammes,
            SmoothFailureMessagesForSpotsCollection result)
        {
            var anyRestrictions = CheckRestrictionsForSpots(
                spotsForBreak,
                theSmoothBreak.TheBreak,
                programme,
                salesArea,
                smoothResources,
                breaksBeingSmoothed,
                scheduleProgrammes);

            foreach (var item in anyRestrictions)
            {
                foreach (var failure in item.Value.Failures)
                {
                    result.Add(item.Key, failure.FailureMessage, failure.Restriction);
                }
            }
        }
        private static void ValidateNonMultipartSpots(
            IReadOnlyList <SmoothBreak> progSmoothBreaks,
            SpotPositionRules breakPositionRules,
            SpotPositionRules requestedPositionInBreakRules,
            CanAddSpotService canAddSpotService,
            SmoothFailureMessagesForSpotsCollection result,
            IReadOnlyCollection <Spot> nonMultipartSpots)
        {
            // Determine which break we want to position the spot in if possible
            foreach (var spot in nonMultipartSpots)
            {
                bool canAddSpotAtBreakPosition = canAddSpotService
                                                 .CanAddSpotWithBreakRequestForAtomicSpotWithDefaultPosition(
                    spot.BreakRequest,
                    progSmoothBreaks,
                    breakPositionRules);

                if (!canAddSpotAtBreakPosition)
                {
                    result.Add(spot.Uid, SmoothFailureMessages.T1_BreakPosition);
                }

                // Determine if we can add at requested position in break
                if (!String.IsNullOrEmpty(spot.RequestedPositioninBreak) &&
                    !canAddSpotService.CanAddSpotAtRequestedPosition(spot.RequestedPositioninBreak, requestedPositionInBreakRules))
                {
                    result.Add(spot.Uid, SmoothFailureMessages.T1_RequestedPositionInBreak);
                }
            }
        }
        HasClashExceptionIncludesAndExcludes(
            SmoothBreak theSmoothBreak,
            bool respectClashExceptions,
            SmoothResources smoothResources,
            SmoothFailureMessagesForSpotsCollection result,
            Spot spot,
            Guid spotUid)
        {
            (bool spotHasClashExceptionIncludes, bool spotHasClashExceptionExcludes)clashExceptionResults = (false, false);

            if (!respectClashExceptions)
            {
                return(clashExceptionResults);
            }

            var checkClashExceptionsResults = smoothResources
                                              .ClashExceptionChecker.CheckClashExceptions(theSmoothBreak, spot);

            clashExceptionResults.spotHasClashExceptionIncludes = checkClashExceptionsResults
                                                                  .Any(cer => cer.ClashException.IncludeOrExclude == IncludeOrExclude.I);

            clashExceptionResults.spotHasClashExceptionExcludes = checkClashExceptionsResults
                                                                  .Any(cer => cer.ClashException.IncludeOrExclude == IncludeOrExclude.E);

            if (clashExceptionResults.spotHasClashExceptionIncludes)
            {
                result.Add(spotUid, SmoothFailureMessages.T1_ProductClash);
            }

            return(clashExceptionResults);
        }
 private static void AssertFailureMessage(
     SmoothFailureMessagesForSpotsCollection res,
     SmoothFailureMessages message)
 {
     _ = res.Should().ContainSingle();
     _ = res.First().Value.Failures.Should().ContainSingle();
     _ = res.First().Value.Failures[0].FailureMessage.Should().Be(message);
 }
        public void SingleItemInSingleItemOutNoRestriction()
        {
            // Arrange
            var target  = new SmoothFailureMessagesForSpotsCollection();
            var spotUid = Guid.NewGuid();
            const SmoothFailureMessages specimen = SmoothFailureMessages.T1_BreakPosition;

            // Act
            target.Add(spotUid, specimen);

            // Assert
            _ = target.Should().ContainSingle(becauseArgs: null);
            _ = target[spotUid].Failures[0].FailureMessage.Should().Be(specimen, becauseArgs: null);
        }
        private static void CheckProductClashRulesWhenClashLimitsAreAllowed(
            IReadOnlyDictionary <Guid, SpotInfo> spotInfos,
            IReadOnlyDictionary <string, Clash> clashesByExternalRef,
            SmoothResources smoothResources,
            IClashExposureCountService clashExposureCountService,
            SmoothFailureMessagesForSpotsCollection result,
            IReadOnlyDictionary <string, int> spotChildClashCodeCount,
            IReadOnlyDictionary <string, int> spotParentClashCodeCount,
            IReadOnlyCollection <Spot> spotsAlreadyInTheBreak,
            Spot spot,
            SpotInfo spotInfo,
            IReadOnlyCollection <Spot> childProductClashSpots)
        {
            if (childProductClashSpots.Count > 0 && !String.IsNullOrEmpty(spotInfo.ProductClashCode))
            {
                CheckProductClashExposureCount(
                    clashesByExternalRef,
                    clashExposureCountService,
                    result,
                    spotChildClashCodeCount,
                    spot,
                    spotInfo,
                    childProductClashSpots);
            }

            // Check parent clashes
            var parentProductClashSpots = smoothResources.ProductClashChecker
                                          .GetProductClashesForSingleSpot(
                spot,
                spotsAlreadyInTheBreak,
                spotInfos,
                ClashCodeLevel.Parent)
                                          .ToList();

            if (parentProductClashSpots.Count > 0 && !String.IsNullOrEmpty(spotInfo.ParentProductClashCode))
            {
                CheckProductParentClashExposure(
                    clashesByExternalRef,
                    clashExposureCountService,
                    result,
                    spotParentClashCodeCount,
                    spot,
                    spotInfo,
                    parentProductClashSpots);
            }
        }
        public void SingleItemInSingleItemOutIncludingRestrictionInitialisedCollection()
        {
            // Arrange
            var target      = new SmoothFailureMessagesForSpotsCollection();
            var spotUid     = Guid.NewGuid();
            var restriction = new Mock <Restriction>().Object;

            const SmoothFailureMessages specimen = SmoothFailureMessages.T1_BreakPosition;

            // Act
            target.InitialiseForSpot(spotUid);
            target.Add(spotUid, specimen, restriction);

            // Assert
            _ = target.Should().ContainSingle(becauseArgs: null);
            _ = target[spotUid].Failures[0].FailureMessage.Should().Be(specimen, becauseArgs: null);
        }
        private static SmoothFailureMessagesForSpotsCollection CheckRestrictionsForSpots(
            IReadOnlyCollection <Spot> spotsForBreak,
            Break theBreak,
            Programme programme,
            SalesArea salesArea,
            SmoothResources smoothResources,
            IReadOnlyCollection <Break> breaksBeingSmoothed,
            IReadOnlyCollection <Programme> scheduleProgrammes)
        {
            var result = new SmoothFailureMessagesForSpotsCollection();

            foreach (var spot in spotsForBreak)
            {
                var restrictionCheckerResults = smoothResources
                                                .RestrictionChecker.CheckRestrictions(
                    programme,
                    theBreak,
                    spot,
                    salesArea,
                    breaksBeingSmoothed,
                    scheduleProgrammes
                    );

                foreach (var restrictionCheckerResult in restrictionCheckerResults
                         .Where(r => r.Reason != RestrictionReasons.None)
                         )
                {
                    var failureMessage = Map(restrictionCheckerResult.Reason);
                    if (failureMessage == SmoothFailureMessages.T0_NoFailure)
                    {
                        continue;
                    }

                    result.Add(
                        spot.Uid,
                        failureMessage,
                        restrictionCheckerResult.Restriction
                        );
                }
            }

            return(result);
        }
        private static void ValidateWithSponsorshipRestrictions(
            SmoothBreak theSmoothBreak,
            SponsorshipRestrictionService sponsorshipRestrictionsService,
            SmoothFailureMessagesForSpotsCollection result,
            IReadOnlyCollection <Spot> spotsAlreadyInTheBreak,
            Spot spot)
        {
            Break theBreak = theSmoothBreak.TheBreak;

            IReadOnlyCollection <(Guid spotUid, SmoothFailureMessages failureMessage)>
            serviceResult = sponsorshipRestrictionsService.CheckSponsorshipRestrictions(
                spot,
                theBreak.ExternalBreakRef,
                theBreak.ScheduledDate,
                theBreak.Duration,
                spotsAlreadyInTheBreak
                );

            foreach ((Guid spotUid, SmoothFailureMessages failureMessage) in serviceResult)
            {
                result.Add(spotUid, failureMessage);
            }
        }
        private static void ValidateMultipartSpots(
            IReadOnlyList <SmoothBreak> progSmoothBreaks,
            SpotPositionRules breakPositionRules,
            SpotPositionRules requestedPositionInBreakRules,
            bool canSplitMultipartSpotsOverBreaks,
            CanAddSpotService canAddSpotService,
            SmoothFailureMessagesForSpotsCollection result,
            IReadOnlyCollection <Spot> spotsAlreadyInTheBreak,
            IReadOnlyList <Spot> multipartSpots)
        {
            if (multipartSpots.Count == 0)
            {
                return;
            }

            foreach (var spot in multipartSpots)
            {
                bool canAddSpotAtBreakPosition = canAddSpotService
                                                 .CanAddSpotWithBreakRequestForMultipartSpotWithDefaultPosition(
                    spot.BreakRequest,
                    progSmoothBreaks,
                    breakPositionRules);

                if (!canAddSpotAtBreakPosition)
                {
                    result.Add(spot.Uid, SmoothFailureMessages.T1_BreakPosition);
                }

                // Determine if we can add at requested position in break
                // TODO: Check that multipart Spots are all linked
                if (!canAddSpotService.CanAddMultipartSpotAtRequestedPosition(
                        spot.MultipartSpot,
                        spot.MultipartSpotPosition,
                        requestedPositionInBreakRules))
                {
                    result.Add(spot.Uid, SmoothFailureMessages.T1_RequestedPositionInBreak);
                }
            }

            // For the multipart spot type then check if this is the break
            // that it must be added to
            if (canSplitMultipartSpotsOverBreaks)
            {
                return;
            }

            // Multipart spots can't be split over breaks, ensure that
            // break will contain all linked multipart spots
            if (multipartSpots[0].MultipartSpot == MultipartSpotTypes.TopTail &&
                multipartSpots.Count < 2)
            {
                // We're not placing both spots, ensure that this break
                // has linked spot. Get all linked spots that have been
                // placed in this break.
                var linkedSpotsPlacedInBreak = BreakUtilities.GetLinkedMultipartSpots(
                    multipartSpots[0],
                    spotsAlreadyInTheBreak,
                    includeInputSpotInOutput: false
                    );

                if (linkedSpotsPlacedInBreak.Count == 0)
                {
                    // Break doesn't contain linked spots, can't add to
                    // this break
                    foreach (var spot in multipartSpots)
                    {
                        result.Add(
                            spot.Uid,
                            SmoothFailureMessages.T1_CantAddTopAndTailToSameBreak);
                    }
                }
            }
        }
        /// <summary>
        /// Evaluate whether spots can be added to break. We consider time
        /// remaining, break type, product clashes, campaign clashes, spot end
        /// time (some spots may required to be position in the first N mins of
        /// the programme), sponsor rules.
        /// </summary>
        public static SmoothFailureMessagesForSpotsCollection ValidateAddSpots(
            SmoothBreak theSmoothBreak,
            Programme programme,
            SalesArea salesArea,
            IReadOnlyCollection <Spot> spotsForBreak,
            IReadOnlyDictionary <Guid, SpotInfo> spotInfos,
            IReadOnlyList <SmoothBreak> progSmoothBreaks,
            ProductClashRules productClashRule,
            bool respectCampaignClash,
            bool respectSpotTime,
            bool respectRestrictions,
            bool respectClashExceptions,
            SpotPositionRules breakPositionRules,
            SpotPositionRules requestedPositionInBreakRules,
            IReadOnlyDictionary <string, Clash> clashesByExternalRef,
            bool canSplitMultipartSpotsOverBreaks,
            SmoothResources smoothResources,
            IReadOnlyCollection <Break> breaksBeingSmoothed,
            IReadOnlyCollection <Programme> scheduleProgrammes,
            IClashExposureCountService clashExposureCountService,
            SponsorshipRestrictionService sponsorshipRestrictionsService)
        {
            var result = new SmoothFailureMessagesForSpotsCollection(theSmoothBreak);

            foreach (Guid spotUid in spotsForBreak.Select(s => s.Uid))
            {
                result.InitialiseForSpot(spotUid);
            }

            if (!IsSufficientRemainingDurationToAddSpots(
                    theSmoothBreak.RemainingAvailability,
                    spotsForBreak))
            {
                foreach (Guid spotUid in spotsForBreak.Select(s => s.Uid))
                {
                    result.Add(
                        spotUid,
                        SmoothFailureMessages.T1_InsufficentRemainingDuration);
                }
            }

            var(spotChildClashCodeCount, spotParentClashCodeCount) =
                ClashCodesForSpotsCount(spotsForBreak, spotInfos);

            // Check basics of whether we can add this spot to the break,
            // correct break type, sufficient time remaining, product clashes,
            // campaign clashes
            var spotsAlreadyInTheBreak = theSmoothBreak.SmoothSpots
                                         .ConvertAll(s => s.Spot);

            var canAddSpotService = CanAddSpotService.Factory(theSmoothBreak);

            foreach (Spot spot in spotsForBreak)
            {
                ValidateWithSponsorshipRestrictions(
                    theSmoothBreak,
                    sponsorshipRestrictionsService,
                    result,
                    spotsAlreadyInTheBreak,
                    spot);

                Guid spotUid = spot.Uid;

                if (!canAddSpotService.CanAddSpotWithBreakType(spot.BreakType))
                {
                    result.Add(spotUid, SmoothFailureMessages.T1_InvalidBreakType);
                }

                if (respectSpotTime && !canAddSpotService.CanAddSpotWithTime(spot.StartDateTime, spot.EndDateTime))
                {
                    result.Add(spotUid, SmoothFailureMessages.T1_InvalidSpotTime);
                }

                var(spotHasClashExceptionIncludes, spotHasClashExceptionExcludes) =
                    HasClashExceptionIncludesAndExcludes(
                        theSmoothBreak,
                        respectClashExceptions,
                        smoothResources,
                        result,
                        spot,
                        spotUid);

                bool shouldCheckProductClashRules =
                    (
                        productClashRule == ProductClashRules.NoClashes ||
                        productClashRule == ProductClashRules.LimitOnExposureCount
                    ) &&
                    !spotHasClashExceptionIncludes &&
                    !spotHasClashExceptionExcludes;

                if (shouldCheckProductClashRules)
                {
                    var childProductClashSpots = smoothResources.ProductClashChecker
                                                 .GetProductClashesForSingleSpot(
                        spot,
                        spotsAlreadyInTheBreak,
                        spotInfos,
                        ClashCodeLevel.Child);

                    switch (productClashRule)
                    {
                    case ProductClashRules.NoClashes:
                        var output = CheckProductClashRulesWhenNoClashesAreAllowed(
                            childProductClashSpots);

                        if (output != SmoothFailureMessages.T0_NoFailure)
                        {
                            result.Add(spotUid, output);
                        }

                        break;

                    case ProductClashRules.LimitOnExposureCount:
                        CheckProductClashRulesWhenClashLimitsAreAllowed(
                            spotInfos,
                            clashesByExternalRef,
                            smoothResources,
                            clashExposureCountService,
                            result,
                            spotChildClashCodeCount,
                            spotParentClashCodeCount,
                            spotsAlreadyInTheBreak,
                            spot,
                            spotInfos[spotUid],
                            childProductClashSpots);

                        break;
                    }
                }

                if (respectCampaignClash &&
                    smoothResources.CampaignClashChecker
                    .GetCampaignClashesForNewSpots(
                        new List <Spot> {
                    spot
                },
                        spotsAlreadyInTheBreak)
                    .Count > 0)
                {
                    result.Add(spotUid, SmoothFailureMessages.T1_CampaignClash);
                }
            }

            var multipartSpots = spotsForBreak
                                 .Where(s => s.IsMultipartSpot)
                                 .ToList();

            var nonMultipartSpots = spotsForBreak
                                    .Except(multipartSpots)
                                    .ToList();

            if (nonMultipartSpots.Count > 0)
            {
                ValidateNonMultipartSpots(
                    progSmoothBreaks,
                    breakPositionRules,
                    requestedPositionInBreakRules,
                    canAddSpotService,
                    result,
                    nonMultipartSpots);
            }

            if (multipartSpots.Count > 0)
            {
                ValidateMultipartSpots(
                    progSmoothBreaks,
                    breakPositionRules,
                    requestedPositionInBreakRules,
                    canSplitMultipartSpotsOverBreaks,
                    canAddSpotService,
                    result,
                    spotsAlreadyInTheBreak,
                    multipartSpots);
            }

            if (respectRestrictions)
            {
                ValidateWithSpotRestrictions(
                    theSmoothBreak,
                    programme,
                    salesArea,
                    spotsForBreak,
                    smoothResources,
                    breaksBeingSmoothed,
                    scheduleProgrammes,
                    result);
            }

            IndicateIfMultipartTopTailSameBreakSpotsCannotBeAdded(
                result,
                multipartSpots);

            return(RemoveDuplicateFailureMessages(spotsForBreak, result));
        }