using BlueLaminate.Core.Tradeups; using Xunit; namespace BlueLaminate.Tests.Tradeups; public class TradeupSelectorTests { private const decimal Bucket = 0.005m; private static SelectableInput Item(decimal fraction, decimal price) => new(fraction, new InputListing( SkinId: 1, MarketHashName: "Test Skin", Marketplace: "test", InspectLink: null, ExternalId: "0", FloatValue: fraction, Price: price)); [Fact] public void Cheapest_full_selection_is_the_ten_cheapest_copies() { // 12 copies priced 1..12 at assorted fractions. With no binding float target the // cheapest ten (cost 55) must be attainable at some summed-fraction bucket. var pool = new List(); for (var i = 1; i <= 12; i++) { pool.Add(Item(fraction: 0.01m * i, price: i)); } var selection = TradeupSelector.Solve(pool, contractSize: 10, Bucket); var selections = selection.Selections().ToList(); Assert.NotEmpty(selections); var cheapest = selections.MinBy(s => s.Cost); Assert.Equal(55m, cheapest.Cost); var picks = cheapest.Picks.ToList(); Assert.Equal(10, picks.Count); Assert.Equal(Enumerable.Range(1, 10).Select(i => (decimal)i), picks.Select(p => p.Price).OrderBy(p => p)); } [Fact] public void Reported_average_fraction_is_a_conservative_upper_bound() { var pool = new List(); for (var i = 0; i < 10; i++) { pool.Add(Item(fraction: 0.123m, price: 1m)); } var only = Assert.Single(TradeupSelector.Solve(pool, 10, Bucket).Selections()); var trueAverage = only.Picks.ToList().Average(p => p.FloatValue); Assert.True(only.AverageFraction >= trueAverage, $"bucketed average {only.AverageFraction} should round up from true {trueAverage}"); // 0.123 rounds up to the 0.125 bucket (0.005 grid). Assert.Equal(0.125m, only.AverageFraction); } [Fact] public void Selecting_cheaper_low_float_copies_lowers_the_attainable_average() { // Cheap high-float copies vs. pricier low-float copies. A lower average is only // reachable by paying for the low-float set, so a low-average selection must cost // more than the unconstrained cheapest. var pool = new List(); for (var i = 0; i < 10; i++) { pool.Add(Item(fraction: 0.60m, price: 1m)); // cheap, bad float } for (var i = 0; i < 10; i++) { pool.Add(Item(fraction: 0.10m, price: 5m)); // pricey, good float } var selections = TradeupSelector.Solve(pool, 10, Bucket).Selections().ToList(); var cheapest = selections.MinBy(s => s.Cost); Assert.Equal(10m, cheapest.Cost); // ten cheap copies Assert.Equal(0.60m, cheapest.AverageFraction); var lowestFloat = selections.MinBy(s => s.AverageFraction); Assert.Equal(0.10m, lowestFloat.AverageFraction); Assert.Equal(50m, lowestFloat.Cost); // forced onto the pricey low-float set } [Fact] public void No_full_selection_when_pool_is_too_small() { var pool = Enumerable.Range(0, 5).Select(i => Item(0.2m, i + 1)).ToList(); Assert.Empty(TradeupSelector.Solve(pool, 10, Bucket).Selections()); } private static TradeupSelector.RewardItem Reward(int bucket, double reward, decimal price = 0m) => new(bucket, reward, new InputListing(1, "x", "m", null, "0", 0.1m, price)); [Fact] public void MaxReward_picks_the_highest_reward_set_within_the_cap() { // Six items; pick 3 maximising reward with bucket sum ≤ 6. The three best rewards // (9, 8, 7) sit at buckets 2,2,2 = 6 ≤ cap, so they win. var items = new[] { Reward(2, 9), Reward(2, 8), Reward(2, 7), Reward(1, 1), Reward(1, 2), Reward(1, 3), }; var picks = TradeupSelector.SolveMaxReward(items, contractSize: 3, capBucket: 6); Assert.NotNull(picks); Assert.Equal(3, picks!.ToList().Count); } [Fact] public void MaxReward_respects_the_float_cap_even_at_the_cost_of_reward() { // The fat-reward items sit at high buckets; the cap forces the low-bucket set. var items = new[] { Reward(5, 100, price: 50m), // too high-float to fit under a tight cap Reward(1, 5, price: 1m), Reward(1, 4, price: 1m), Reward(1, 3, price: 1m), }; var picks = TradeupSelector.SolveMaxReward(items, contractSize: 3, capBucket: 3); Assert.NotNull(picks); var chosen = picks!.ToList(); Assert.Equal(3, chosen.Count); Assert.All(chosen, p => Assert.Equal(1m, p.Price)); // the three low-float copies } [Fact] public void MaxReward_returns_null_when_the_contract_cannot_be_filled_under_the_cap() { // Only two items fit under the cap, but three are required. var items = new[] { Reward(1, 5), Reward(1, 4), Reward(9, 100) }; Assert.Null(TradeupSelector.SolveMaxReward(items, contractSize: 3, capBucket: 3)); } } public class OutputPriceBookTests { private static TradeupListingRow Row(WearBand band, bool st, decimal price) { // A float squarely inside the band, so Build files it where we expect. var f = band switch { WearBand.FactoryNew => 0.03m, WearBand.MinimalWear => 0.10m, WearBand.FieldTested => 0.25m, WearBand.WellWorn => 0.40m, _ => 0.60m, }; return new TradeupListingRow(1, "M4A4 | X-Ray", "csmoney", null, "0", st, false, f, price); } [Fact] public void Liquid_band_uses_its_own_price() { // Ten MW listings -> the MW band is trusted at its own lowest ask. var rows = Enumerable.Range(0, 10).Select(i => Row(WearBand.MinimalWear, true, 90m + i)); var book = TradeupListingData.Build(rows).OutputPrices; var r = book.Resolve(1, statTrak: true, outputFloat: 0.10m, thinThreshold: 10); Assert.Equal(OutputPriceBasis.Band, r.Basis); Assert.Equal(90m, r.LowestAsk); } [Fact] public void Thin_band_falls_back_to_the_skin_overall_floor() { // The X-Ray case: two outlier FN listings ($1287) but a liquid MW market (~$90). // A produced FN output must price off the $90 floor, not the $1287 band outlier. var rows = new List { Row(WearBand.FactoryNew, true, 1287m), Row(WearBand.FactoryNew, true, 1290m), }; rows.AddRange(Enumerable.Range(0, 10).Select(i => Row(WearBand.MinimalWear, true, 90m + i))); var book = TradeupListingData.Build(rows).OutputPrices; var r = book.Resolve(1, statTrak: true, outputFloat: 0.0695m, thinThreshold: 10); Assert.Equal(OutputPriceBasis.Floor, r.Basis); Assert.Equal(90m, r.LowestAsk); // skin-wide floor, not the $1287 FN outlier Assert.Equal(2, r.BandLiquidity); // still reports the thin band count → triggers CSFloat } [Fact] public void Unlisted_skin_resolves_to_none() { var book = TradeupListingData.Build(Array.Empty()).OutputPrices; var r = book.Resolve(1, statTrak: false, outputFloat: 0.1m, thinThreshold: 10); Assert.Equal(OutputPriceBasis.None, r.Basis); Assert.Null(r.LowestAsk); } }