require_relative '../rails_helper' RSpec.describe OutfitsHelper, type: :helper do fixtures :zones, :colors, :species, :pet_types # Use the Blue Acara's body_id throughout tests let(:body_id) { pet_types(:blue_acara).body_id } # Helper to create a test outfit with a pet type # Biology assets are just setup noise - we only care about pet_type.body_id def create_test_outfit pet_type = pet_types(:blue_acara) # PetState requires at least one biology asset (validation requirement) bio_asset = SwfAsset.create!( type: "biology", remote_id: (@bio_remote_id = (@bio_remote_id || 1000) + 1), url: "https://images.neopets.example/bio_#{@bio_remote_id}.swf", zone: zones(:body), zones_restrict: "", body_id: 0 ) pet_state = PetState.create!( pet_type: pet_type, pose: "HAPPY_MASC", swf_assets: [bio_asset], swf_asset_ids: [bio_asset.id] ) Outfit.create!(pet_state: pet_state) end # Helper to create SwfAssets for items (matches pattern from item_spec.rb) def build_item_asset(zone, body_id:) @item_remote_id = (@item_remote_id || 0) + 1 SwfAsset.create!( type: "object", remote_id: @item_remote_id, url: "https://images.neopets.example/item_#{@item_remote_id}.swf", zone: zone, zones_restrict: "", body_id: body_id ) end # Helper to create an item with zones def create_item(name, zones_and_bodies) item = Item.create!( name: name, description: "", thumbnail_url: "https://images.neopets.example/#{name.parameterize}.gif", rarity: "Common", price: 100, zones_restrict: "0" * 52 ) zones_and_bodies.each do |zone, body_id| item.swf_assets << build_item_asset(zone, body_id: body_id) end item end describe '#outfit_items_by_zone' do context 'with nil pet_type' do it 'returns empty array' do # Create an outfit without a pet_state (pet_type will be nil) outfit = Outfit.new # Allow the delegation to fail gracefully allow(outfit).to receive(:pet_type).and_return(nil) result = helper.outfit_items_by_zone(outfit) expect(result).to eq([]) end end context 'with empty outfit' do it 'returns empty array' do outfit = create_test_outfit result = helper.outfit_items_by_zone(outfit) expect(result).to eq([]) end end context 'with single-zone items' do let(:outfit) { create_test_outfit } let!(:hat_item) { create_item("Blue Hat", [[zones(:hat), body_id]]) } let!(:jacket_item) { create_item("Red Jacket", [[zones(:jacket), body_id]]) } before do outfit.worn_items << hat_item outfit.worn_items << jacket_item end it 'groups items by zone' do result = helper.outfit_items_by_zone(outfit) expect(result.length).to eq(2) zone_labels = result.map { |g| g[:zone_label] } expect(zone_labels).to contain_exactly("Hat", "Jacket") end it 'sorts zones alphabetically' do result = helper.outfit_items_by_zone(outfit) zone_labels = result.map { |g| g[:zone_label] } expect(zone_labels).to eq(["Hat", "Jacket"]) end it 'includes items in their respective zones' do result = helper.outfit_items_by_zone(outfit) hat_group = result.find { |g| g[:zone_label] == "Hat" } jacket_group = result.find { |g| g[:zone_label] == "Jacket" } expect(hat_group[:items]).to eq([hat_item]) expect(jacket_group[:items]).to eq([jacket_item]) end end context 'with multiple items in same zone' do let(:outfit) { create_test_outfit } let!(:hat1) { create_item("Awesome Hat", [[zones(:hat), body_id]]) } let!(:hat2) { create_item("Cool Hat", [[zones(:hat), body_id]]) } let!(:hat3) { create_item("Blue Hat", [[zones(:hat), body_id]]) } before do outfit.worn_items << hat1 outfit.worn_items << hat2 outfit.worn_items << hat3 end it 'sorts items alphabetically within zone' do result = helper.outfit_items_by_zone(outfit) hat_group = result.find { |g| g[:zone_label] == "Hat" } item_names = hat_group[:items].map(&:name) expect(item_names).to eq(["Awesome Hat", "Blue Hat", "Cool Hat"]) end end context 'with multi-zone item (no conflicts)' do let(:outfit) { create_test_outfit } let!(:bow_tie) do create_item("Bow Tie", [ [zones(:collar), body_id], [zones(:necklace), body_id], [zones(:earrings), body_id] ]) end before do outfit.worn_items << bow_tie end it 'shows item in only one zone (simplification)' do result = helper.outfit_items_by_zone(outfit) # Should show in Collar zone only (first alphabetically) expect(result.length).to eq(1) expect(result[0][:zone_label]).to eq("Collar") expect(result[0][:items]).to eq([bow_tie]) end end context 'with multi-zone simplification (item appears in conflict zone)' do let(:outfit) { create_test_outfit } let!(:multi_zone_item) do create_item("Fancy Outfit", [ [zones(:jacket), body_id], [zones(:collar), body_id] ]) end let!(:collar_item) { create_item("Simple Collar", [[zones(:collar), body_id]]) } before do outfit.worn_items << multi_zone_item outfit.worn_items << collar_item end it 'keeps conflict zone and hides redundant single-item zone' do result = helper.outfit_items_by_zone(outfit) zone_labels = result.map { |g| g[:zone_label] } # Should show Collar (has conflict with 2 items) # Should NOT show Jacket (redundant - item already in Collar) expect(zone_labels).to eq(["Collar"]) collar_group = result.find { |g| g[:zone_label] == "Collar" } item_names = collar_group[:items].map(&:name).sort expect(item_names).to eq(["Fancy Outfit", "Simple Collar"]) end end context 'with zone label disambiguation' do let(:outfit) { create_test_outfit } # Create additional zones with duplicate labels for this test let!(:markings_zone_a) do Zone.create!(label: "Markings", depth: 100, plain_label: "markings_a", type_id: 2) end let!(:markings_zone_b) do Zone.create!(label: "Markings", depth: 101, plain_label: "markings_b", type_id: 2) end let!(:item_zone_a) { create_item("Tattoo A", [[markings_zone_a, body_id]]) } let!(:item_zone_b) { create_item("Tattoo B", [[markings_zone_b, body_id]]) } let!(:item_zone_a_b) { create_item("Tattoo C", [[markings_zone_a, body_id]]) } before do outfit.worn_items << item_zone_a outfit.worn_items << item_zone_b outfit.worn_items << item_zone_a_b end it 'adds zone IDs to duplicate labels' do result = helper.outfit_items_by_zone(outfit) zone_labels = result.map { |g| g[:zone_label] } # Both should have IDs appended since they share the label "Markings" expect(zone_labels).to contain_exactly( "Markings (##{markings_zone_a.id})", "Markings (##{markings_zone_b.id})" ) end it 'groups items correctly by zone despite same label' do result = helper.outfit_items_by_zone(outfit) zone_a_group = result.find { |g| g[:zone_label] == "Markings (##{markings_zone_a.id})" } zone_b_group = result.find { |g| g[:zone_label] == "Markings (##{markings_zone_b.id})" } expect(zone_a_group[:items].map(&:name).sort).to eq(["Tattoo A", "Tattoo C"]) expect(zone_b_group[:items].map(&:name)).to eq(["Tattoo B"]) end end context 'with incompatible items' do let(:outfit) { create_test_outfit } let!(:compatible_item) { create_item("Fits Pet", [[zones(:hat), body_id]]) } let!(:incompatible_item) { create_item("Wrong Body", [[zones(:jacket), 999]]) } before do outfit.worn_items << compatible_item outfit.worn_items << incompatible_item end it 'separates incompatible items into their own section' do result = helper.outfit_items_by_zone(outfit) zone_labels = result.map { |g| g[:zone_label] } expect(zone_labels).to contain_exactly("Hat", "Incompatible") incompatible_group = result.find { |g| g[:zone_label] == "Incompatible" } expect(incompatible_group[:items]).to eq([incompatible_item]) end it 'sorts incompatible items alphabetically' do outfit.worn_items << create_item("Alpha Item", [[zones(:jacket), 999]]) outfit.worn_items << create_item("Zulu Item", [[zones(:jacket), 999]]) result = helper.outfit_items_by_zone(outfit) incompatible_group = result.find { |g| g[:zone_label] == "Incompatible" } item_names = incompatible_group[:items].map(&:name) expect(item_names).to eq(["Alpha Item", "Wrong Body", "Zulu Item"]) end end context 'with complex multi-zone scenario' do let(:outfit) { create_test_outfit } let!(:bg1) { create_item("Forest Background", [[zones(:background), 0]]) } let!(:bg2) { create_item("Beach Background", [[zones(:background), 0]]) } let!(:multi_item) do create_item("Wings and Hat", [ [zones(:wings), 0], [zones(:hat), 0] ]) end let!(:hat_item) { create_item("Simple Hat", [[zones(:hat), 0]]) } before do outfit.worn_items << bg1 outfit.worn_items << bg2 outfit.worn_items << multi_item outfit.worn_items << hat_item end it 'correctly applies all sorting and grouping rules' do result = helper.outfit_items_by_zone(outfit) # Background: has conflict (2 items) # Hat: has conflict (2 items, including multi-zone item) # Wings: should be hidden (multi-zone item already in Hat conflict) zone_labels = result.map { |g| g[:zone_label] } expect(zone_labels).to eq(["Background", "Hat"]) bg_group = result.find { |g| g[:zone_label] == "Background" } expect(bg_group[:items].map(&:name)).to eq(["Beach Background", "Forest Background"]) hat_group = result.find { |g| g[:zone_label] == "Hat" } expect(hat_group[:items].map(&:name)).to eq(["Simple Hat", "Wings and Hat"]) end end end end