using Xunit;
namespace Bit.SeederApi.IntegrationTest.DensityModel;
///
/// Validates the multi-collection cipher assignment math from GenerateCiphersStep
/// to ensure no duplicate (CipherId, CollectionId) pairs are produced.
///
public class MultiCollectionAssignmentTests
{
///
/// Simulates the secondary collection assignment loop from GenerateCiphersStep
/// with the extraCount clamp fix applied. Returns the list of (cipherIndex, collectionIndex) pairs.
///
private static List<(int CipherIndex, int CollectionIndex)> SimulateMultiCollectionAssignment(
int cipherCount,
int collectionCount,
double multiCollectionRate,
int maxCollectionsPerCipher)
{
var primaryIndices = new int[cipherCount];
var pairs = new List<(int, int)>();
for (var i = 0; i < cipherCount; i++)
{
primaryIndices[i] = i % collectionCount;
pairs.Add((i, primaryIndices[i]));
}
if (multiCollectionRate > 0 && collectionCount > 1)
{
var multiCount = (int)(cipherCount * multiCollectionRate);
for (var i = 0; i < multiCount; i++)
{
var extraCount = 1 + (i % Math.Max(maxCollectionsPerCipher - 1, 1));
extraCount = Math.Min(extraCount, collectionCount - 1);
for (var j = 0; j < extraCount; j++)
{
var secondaryIndex = (primaryIndices[i] + 1 + j) % collectionCount;
pairs.Add((i, secondaryIndex));
}
}
}
return pairs;
}
[Fact]
public void MultiCollectionAssignment_SmallCollectionCount_NoDuplicates()
{
var pairs = SimulateMultiCollectionAssignment(
cipherCount: 20,
collectionCount: 3,
multiCollectionRate: 1.0,
maxCollectionsPerCipher: 5);
var grouped = pairs.GroupBy(p => p);
Assert.All(grouped, g => Assert.Single(g));
}
[Fact]
public void MultiCollectionAssignment_TwoCollections_NoDuplicates()
{
var pairs = SimulateMultiCollectionAssignment(
cipherCount: 50,
collectionCount: 2,
multiCollectionRate: 1.0,
maxCollectionsPerCipher: 10);
var grouped = pairs.GroupBy(p => p);
Assert.All(grouped, g => Assert.Single(g));
}
[Fact]
public void MultiCollectionAssignment_ExtraCountClamped_ToAvailableCollections()
{
// With 2 collections, extraCount should never exceed 1 (collectionCount - 1)
var collectionCount = 2;
var maxCollectionsPerCipher = 10;
var cipherCount = 20;
for (var i = 0; i < cipherCount; i++)
{
var extraCount = 1 + (i % Math.Max(maxCollectionsPerCipher - 1, 1));
extraCount = Math.Min(extraCount, collectionCount - 1);
Assert.True(extraCount <= collectionCount - 1,
$"extraCount {extraCount} exceeds available secondary slots {collectionCount - 1} at i={i}");
}
}
[Fact]
public void MultiCollectionAssignment_SecondaryNeverEqualsPrimary()
{
var pairs = SimulateMultiCollectionAssignment(
cipherCount: 30,
collectionCount: 3,
multiCollectionRate: 1.0,
maxCollectionsPerCipher: 5);
// Group by cipher index — for each cipher, no secondary should equal primary
var byCipher = pairs.GroupBy(p => p.CipherIndex);
foreach (var group in byCipher)
{
var primary = group.First().CollectionIndex;
var secondaries = group.Skip(1).Select(p => p.CollectionIndex);
Assert.DoesNotContain(primary, secondaries);
}
}
}