require 'spec_helper'

module Spree
  describe Product do
    describe "associations" do
      it { is_expected.to belong_to(:supplier) }
      it { is_expected.to belong_to(:primary_taxon) }
    end

    describe "validations and defaults" do
      it "is valid when built from factory" do
        expect(build(:product)).to be_valid
      end

      it "requires a primary taxon" do
        expect(build(:simple_product, taxons: [], primary_taxon: nil)).not_to be_valid
      end

      it "requires a supplier" do
        expect(build(:simple_product, supplier: nil)).not_to be_valid
      end

      it "does not save when master is invalid" do
        product = build(:product)
        product.variant_unit = 'weight'
        product.master.unit_value = nil

        expect(product.save).to eq(false)
      end

      it "defaults available_on to now" do
        Timecop.freeze do
          product = Product.new
          expect(product.available_on).to eq(Time.zone.now)
        end
      end

      describe "permalink" do
        let(:name) { "Banana Permanenta" }

        it "generates a unique permalink" do
          product1 = create(:product, name: "Banana Permanenta", permalink: nil)
          product2 = create(:product, name: "Banana Permanenta", permalink: nil)
          expect(product2.permalink).to_not eq product1.permalink
          # "banana-permanenta" != "banana-permanenta-1" # generated by Spree
        end

        it "generates a unique permalink considering deleted products" do
          product1 = create(:product, name: "Banana Permanenta", permalink: nil)
          product1.destroy
          product2 = create(:product, name: "Banana Permanenta", permalink: nil)
          expect(product2.permalink).to_not eq product1.permalink
          # "banana-permanenta" != "banana-permanenta1" # generated by OFN
        end
      end

      describe "tax category" do
        context "when a tax category is required" do
          it "is invalid when a tax category is not provided" do
            with_products_require_tax_category(true) do
              expect(build(:product, tax_category_id: nil)).not_to be_valid
            end
          end
        end

        context "when a tax category is not required" do
          it "is valid when a tax category is not provided" do
            with_products_require_tax_category(false) do
              expect(build(:product, tax_category_id: nil)).to be_valid
            end
          end
        end
      end

      context "when the product has variants" do
        let(:product) do
          product = create(:simple_product)
          create(:variant, product: product)
          product.reload
        end

        it "requires a unit" do
          product.variant_unit = nil
          expect(product).not_to be_valid
        end

        %w(weight volume).each do |unit|
          context "when unit is #{unit}" do
            it "is valid when unit scale is set and unit name is not" do
              product.variant_unit = unit
              product.variant_unit_scale = 1
              product.variant_unit_name = nil
              expect(product).to be_valid
            end

            it "is invalid when unit scale is not set" do
              product.variant_unit = unit
              product.variant_unit_scale = nil
              product.variant_unit_name = nil
              expect(product).not_to be_valid
            end
          end
        end

        context "saving a new product" do
          let!(:product){ Spree::Product.new }

          before do
            create(:stock_location)
            product.primary_taxon = create(:taxon)
            product.supplier = create(:supplier_enterprise)
            product.name = "Product1"
            product.variant_unit = "weight"
            product.variant_unit_scale = 1000
            product.unit_value = 1
            product.price = 4.27
            product.shipping_category = create(:shipping_category)
            product.save!
          end

          it "copies the properties on master variant to the first standard variant" do
            expect(product.variants(:reload).length).to eq 1
            standard_variant = product.variants(:reload).first
            expect(standard_variant.price).to eq product.master.price
          end

          it "only duplicates master with after_save when no standard variants exist" do
            expect(product).to receive :ensure_standard_variant
            product.name = "Something else"
            expect{ product.save! }.to_not change{ product.variants.count }
          end
        end

        context "when the unit is items" do
          it "is valid when unit name is set and unit scale is not" do
            product.variant_unit = 'items'
            product.variant_unit_name = 'loaf'
            product.variant_unit_scale = nil
            expect(product).to be_valid
          end

          it "is invalid when unit name is not set" do
            product.variant_unit = 'items'
            product.variant_unit_name = nil
            product.variant_unit_scale = nil
            expect(product).not_to be_valid
          end
        end
      end

      context "a basic product" do
        let(:product) { create(:simple_product) }

        it "requires variant unit fields" do
          product.variant_unit = nil
          product.variant_unit_name = nil
          product.variant_unit_scale = nil

          expect(product).to be_invalid
        end

        it "requires a unit scale when variant unit is weight" do
          product.variant_unit = 'weight'
          product.variant_unit_scale = nil
          product.variant_unit_name = nil

          expect(product).not_to be_valid
        end
      end
    end

    describe "callbacks" do
      let(:product) { create(:simple_product) }

      describe "touching affected enterprises when the product is deleted" do
        let(:product) { create(:simple_product) }
        let(:supplier) { product.supplier }
        let(:distributor) { create(:distributor_enterprise) }
        let!(:oc) { create(:simple_order_cycle, distributors: [distributor], variants: [product.variants.first]) }

        it "touches the supplier" do
          expect { product.destroy }.to change { supplier.reload.updated_at }
        end

        it "touches all distributors" do
          expect { product.destroy }.to change { distributor.reload.updated_at }
        end

        it "removes variants from order cycles" do
          expect { product.destroy }.to change { ExchangeVariant.count }
        end
      end

      it "adds the primary taxon to the product's taxon list" do
        taxon = create(:taxon)
        product = create(:product, primary_taxon: taxon)

        expect(product.taxons).to include(taxon)
      end

      it "removes the previous primary taxon from the taxon list" do
        original_taxon = create(:taxon)
        product = create(:product, primary_taxon: original_taxon)
        product.primary_taxon = create(:taxon)
        product.save!

        expect(product.taxons).not_to include(original_taxon)
      end
    end

    describe "scopes" do
      describe "in_supplier" do
        it "shows products in supplier" do
          s1 = create(:supplier_enterprise)
          s2 = create(:supplier_enterprise)
          p1 = create(:product, supplier: s1)
          p2 = create(:product, supplier: s2)
          expect(Product.in_supplier(s1)).to eq([p1])
        end
      end

      describe "in_distributor" do
        it "shows products in order cycle distribution" do
          s = create(:supplier_enterprise)
          d1 = create(:distributor_enterprise)
          d2 = create(:distributor_enterprise)
          p1 = create(:product)
          p2 = create(:product)
          create(:simple_order_cycle, suppliers: [s], distributors: [d1], variants: [p1.master])
          create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.master])
          expect(Product.in_distributor(d1)).to eq([p1])
        end

        it "shows products in order cycle distribution by variant" do
          s = create(:supplier_enterprise)
          d1 = create(:distributor_enterprise)
          d2 = create(:distributor_enterprise)
          p1 = create(:product)
          v1 = create(:variant, product: p1)
          p2 = create(:product)
          v2 = create(:variant, product: p2)
          create(:simple_order_cycle, suppliers: [s], distributors: [d1], variants: [v1])
          create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [v2])
          expect(Product.in_distributor(d1)).to eq([p1])
        end

        it "doesn't show products listed in the incoming exchange only" do
          s = create(:supplier_enterprise)
          c = create(:distributor_enterprise)
          d = create(:distributor_enterprise)
          p = create(:product)
          oc = create(:simple_order_cycle, coordinator: c, suppliers: [s], distributors: [d])
          ex = oc.exchanges.incoming.first
          ex.variants << p.master

          expect(Product.in_distributor(d)).to be_empty
        end
      end

      describe "in_supplier_or_distributor" do
        it "shows products in supplier" do
          s1 = create(:supplier_enterprise)
          s2 = create(:supplier_enterprise)
          p1 = create(:product, supplier: s1)
          p2 = create(:product, supplier: s2)
          expect(Product.in_supplier_or_distributor(s1)).to eq([p1])
        end

        it "shows products in order cycle distribution" do
          s = create(:supplier_enterprise)
          d1 = create(:distributor_enterprise)
          d2 = create(:distributor_enterprise)
          p1 = create(:product)
          p2 = create(:product)
          create(:simple_order_cycle, suppliers: [s], distributors: [d1], variants: [p1.master])
          create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.master])
          expect(Product.in_supplier_or_distributor(d1)).to eq([p1])
        end

        it "shows products in all three without duplicates" do
          s = create(:supplier_enterprise)
          d = create(:distributor_enterprise)
          p = create(:product, supplier: s)
          create(:simple_order_cycle, suppliers: [s], distributors: [d], variants: [p.master])
          [s, d].each { |e| expect(Product.in_supplier_or_distributor(e)).to eq([p]) }
        end
      end

      describe "in_order_cycle" do
        it "shows products in order cycle distribution" do
          s = create(:supplier_enterprise)
          d1 = create(:distributor_enterprise)
          d2 = create(:distributor_enterprise)
          p1 = create(:product)
          p2 = create(:product)
          oc1 = create(:simple_order_cycle, suppliers: [s], distributors: [d1], variants: [p1.master])
          oc2 = create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.master])
          expect(Product.in_order_cycle(oc1)).to eq([p1])
        end
      end

      describe "in_an_active_order_cycle" do
        it "shows products in order cycle distribution" do
          s = create(:supplier_enterprise)
          d2 = create(:distributor_enterprise)
          d3 = create(:distributor_enterprise)
          p1 = create(:product)
          p2 = create(:product)
          p3 = create(:product)
          oc2 = create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.master], orders_open_at: 8.days.ago, orders_close_at: 1.day.ago)
          oc2 = create(:simple_order_cycle, suppliers: [s], distributors: [d3], variants: [p3.master], orders_close_at: Date.tomorrow)
          expect(Product.in_an_active_order_cycle).to eq([p3])
        end
      end

      describe "access roles" do
        before(:each) do
          @e1 = create(:enterprise)
          @e2 = create(:enterprise)
          @p1 = create(:product, supplier: @e1)
          @p2 = create(:product, supplier: @e2)
        end

        it "shows only products for given user" do
          user = create(:user)
          user.spree_roles = []
          @e1.enterprise_roles.build(user: user).save

          product = Product.managed_by user
          expect(product.count).to eq(1)
          expect(product).to include @p1
        end

        it "shows all products for admin user" do
          user = create(:admin_user)

          product = Product.managed_by user
          expect(product.count).to eq(2)
          expect(product).to include @p1
          expect(product).to include @p2
        end
      end

      describe "visible_for" do
        let(:enterprise) { create(:distributor_enterprise) }
        let!(:new_variant) { create(:variant) }
        let!(:hidden_variant) { create(:variant) }
        let!(:visible_variant) { create(:variant) }
        let!(:hidden_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: hidden_variant, visible: false ) }
        let!(:visible_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: visible_variant, visible: true ) }

        let!(:products) { Spree::Product.visible_for(enterprise) }

        it "lists any products with variants that are listed as visible=true" do
          expect(products).to include visible_variant.product
          expect(products).to_not include new_variant.product, hidden_variant.product
        end
      end

      describe 'stockable_by' do
        let(:shop) { create(:distributor_enterprise) }
        let(:add_to_oc_producer) { create(:supplier_enterprise) }
        let(:other_producer) { create(:supplier_enterprise) }
        let!(:p1) { create(:simple_product, supplier: shop ) }
        let!(:p2) { create(:simple_product, supplier: add_to_oc_producer ) }
        let!(:p3) { create(:simple_product, supplier: other_producer ) }

        before do
          create(:enterprise_relationship, parent: add_to_oc_producer, child: shop, permissions_list: [:add_to_order_cycle])
          create(:enterprise_relationship, parent: other_producer, child: shop, permissions_list: [:manage_products])
        end

        it 'shows products produced by the enterprise and any producers granting P-OC' do
          stockable_products = Spree::Product.stockable_by(shop)
          expect(stockable_products).to include p1, p2
          expect(stockable_products).to_not include p3
        end
      end

      describe "imported_on" do
        let!(:v1) { create(:variant, import_date: 1.day.ago) }
        let!(:v2) { create(:variant, import_date: 2.days.ago) }
        let!(:v3) { create(:variant, import_date: 1.day.ago) }

        it "returns products imported on given day" do
          imported_products = Spree::Product.imported_on(1.day.ago.to_date)
          expect(imported_products).to include v1.product, v3.product
        end
      end
    end

    describe "properties" do
      it "returns product properties as a hash" do
        product = create(:simple_product)
        product.set_property 'Organic Certified', 'NASAA 12345'
        property = product.properties.last

        expect(product.properties_including_inherited).to eq([{ id: property.id, name: "Organic Certified", value: 'NASAA 12345' }])
      end

      it "returns producer properties as a hash" do
        supplier = create(:supplier_enterprise)
        product = create(:simple_product, supplier: supplier)

        supplier.set_producer_property 'Organic Certified', 'NASAA 54321'
        property = supplier.properties.last

        expect(product.properties_including_inherited).to eq([{ id: property.id, name: "Organic Certified", value: 'NASAA 54321' }])
      end

      it "overrides producer properties with product properties" do
        supplier = create(:supplier_enterprise)
        product = create(:simple_product, supplier: supplier)

        product.set_property 'Organic Certified', 'NASAA 12345'
        supplier.set_producer_property 'Organic Certified', 'NASAA 54321'
        property = product.properties.last

        expect(product.properties_including_inherited).to eq([{ id: property.id, name: "Organic Certified", value: 'NASAA 12345' }])
      end

      context "when product has an inherit_properties value set to true" do
        let(:supplier) { create(:supplier_enterprise) }
        let(:product) { create(:simple_product, supplier: supplier, inherits_properties: true) }

        it "inherits producer properties" do
          supplier.set_producer_property 'Organic Certified', 'NASAA 54321'
          property = supplier.properties.last

          expect(product.properties_including_inherited).to eq([{ id: property.id, name: "Organic Certified", value: 'NASAA 54321' }])
        end
      end

      context "when product has an inherit_properties value set to false" do
        let(:supplier) { create(:supplier_enterprise) }
        let(:product) { create(:simple_product, supplier: supplier, inherits_properties: false) }

        it "does not inherit producer properties" do
          supplier.set_producer_property 'Organic Certified', 'NASAA 54321'

          expect(product.properties_including_inherited).to eq([])
        end
      end

      it "sorts by position" do
        supplier = create(:supplier_enterprise)
        product = create(:simple_product, supplier: supplier)

        pa = Spree::Property.create! name: 'A', presentation: 'A'
        pb = Spree::Property.create! name: 'B', presentation: 'B'
        pc = Spree::Property.create! name: 'C', presentation: 'C'

        product.product_properties.create!({ property_id: pa.id, value: '1', position: 1 }, without_protection: true)
        product.product_properties.create!({ property_id: pc.id, value: '3', position: 3 }, without_protection: true)
        supplier.producer_properties.create!({ property_id: pb.id, value: '2', position: 2 }, without_protection: true)

        expect(product.properties_including_inherited).to eq(
          [{ id: pa.id, name: "A", value: '1' },
           { id: pb.id, name: "B", value: '2' },
           { id: pc.id, name: "C", value: '3' }]
        )
      end
    end

    describe "membership" do
      it "queries its membership of a particular order cycle distribution" do
        d1 = create(:distributor_enterprise)
        d2 = create(:distributor_enterprise)
        p1 = create(:product)
        p2 = create(:product)
        oc1 = create(:simple_order_cycle, distributors: [d1], variants: [p1.master])
        oc2 = create(:simple_order_cycle, distributors: [d2], variants: [p2.master])

        expect(p1).to be_in_distributor d1
        expect(p1).not_to be_in_distributor d2
      end

      it "queries its membership of a particular order cycle" do
        d1 = create(:distributor_enterprise)
        d2 = create(:distributor_enterprise)
        p1 = create(:product)
        p2 = create(:product)
        oc1 = create(:simple_order_cycle, distributors: [d1], variants: [p1.master])
        oc2 = create(:simple_order_cycle, distributors: [d2], variants: [p2.master])

        expect(p1).to be_in_order_cycle oc1
        expect(p1).not_to be_in_order_cycle oc2
      end
    end

    describe "variant units" do
      context "when the product already has a variant unit set (and all required option types exist)" do
        let!(:p) {
          create(:simple_product,
                 variant_unit: 'weight',
                 variant_unit_scale: 1,
                 variant_unit_name: nil)
        }

        let!(:ot_volume) { create(:option_type, name: 'unit_volume', presentation: 'Volume') }

        it "removes the old option type and assigns the new one" do
          p.update_attributes!(variant_unit: 'volume', variant_unit_scale: 0.001)
          expect(p.option_types).to eq([ot_volume])
        end

        it "does not remove and re-add the option type if it is not changed" do
          expect(p.option_types).to receive(:delete).never
          p.update_attributes!(name: 'foo')
        end

        it "removes the related option values from all its variants and replaces them" do
          ot = Spree::OptionType.find_by_name 'unit_weight'
          v = create(:variant, unit_value: 1, product: p)
          p.reload

          expect(v.option_values.map(&:name).include?("1L")).to eq(false)
          expect(v.option_values.map(&:name).include?("1g")).to eq(true)
          expect {
            p.update_attributes!(variant_unit: 'volume', variant_unit_scale: 0.001)
          }.to change(p.master.option_values(true), :count).by(0)
          v.reload
          expect(v.option_values.map(&:name).include?("1L")).to eq(true)
          expect(v.option_values.map(&:name).include?("1g")).to eq(false)
        end

        it "removes the related option values from its master variant and replaces them" do
          ot = Spree::OptionType.find_by_name 'unit_weight'
          p.master.update_attributes!(unit_value: 1)
          p.reload

          expect(p.master.option_values.map(&:name).include?("1L")).to eq(false)
          expect(p.master.option_values.map(&:name).include?("1g")).to eq(true)
          expect {
            p.update_attributes!(variant_unit: 'volume', variant_unit_scale: 0.001)
          }.to change(p.master.option_values(true), :count).by(0)
          p.reload
          expect(p.master.option_values.map(&:name).include?("1L")).to eq(true)
          expect(p.master.option_values.map(&:name).include?("1g")).to eq(false)
        end
      end

      it "finds all variant unit option types" do
        ot1 = create(:option_type, name: 'unit_weight', presentation: 'Weight')
        ot2 = create(:option_type, name: 'unit_volume', presentation: 'Volume')
        ot3 = create(:option_type, name: 'unit_items', presentation: 'Items')
        ot4 = create(:option_type, name: 'foo_unit_bar', presentation: 'Foo')

        expect(Spree::Product.all_variant_unit_option_types).to match_array [ot1, ot2, ot3]
      end
    end

    describe "option types" do
      describe "removing an option type" do
        it "removes the associated option values from all variants" do
          # Given a product with a variant unit option type and values
          p = create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1)
          v1 = create(:variant, product: p, unit_value: 100, option_values: [])
          v2 = create(:variant, product: p, unit_value: 200, option_values: [])

          # And a custom option type and values
          ot = create(:option_type, name: 'foo', presentation: 'foo')
          p.option_types << ot
          ov1 = create(:option_value, option_type: ot, name: 'One', presentation: 'One')
          ov2 = create(:option_value, option_type: ot, name: 'Two', presentation: 'Two')
          v1.option_values << ov1
          v2.option_values << ov2

          # When we remove the custom option type
          p.option_type_ids = p.option_type_ids.reject { |id| id == ot.id }

          # Then the associated option values should have been removed from the variants
          expect(v1.option_values(true)).not_to include ov1
          expect(v2.option_values(true)).not_to include ov2

          # And the option values themselves should still exist
          expect(Spree::OptionValue.where(id: [ov1.id, ov2.id]).count).to eq(2)
        end
      end
    end

    describe "stock filtering" do
      it "considers products that are on_demand as being in stock" do
        product = create(:simple_product, on_demand: true)
        product.master.update_attribute(:on_hand, 0)
        expect(product.has_stock?).to eq(true)
      end

      describe "finding products in stock for a particular distribution" do
        it "returns on-demand products" do
          p = create(:simple_product, on_demand: true)
          p.variants.first.update_attributes!(on_hand: 0, on_demand: true)
          d = create(:distributor_enterprise)
          oc = create(:simple_order_cycle, distributors: [d])
          oc.exchanges.outgoing.first.variants << p.variants.first

          expect(p).to have_stock_for_distribution(oc, d)
        end

        it "returns products with in-stock variants" do
          p = create(:simple_product)
          v = create(:variant, product: p)
          v.update_attribute(:on_hand, 1)
          d = create(:distributor_enterprise)
          oc = create(:simple_order_cycle, distributors: [d])
          oc.exchanges.outgoing.first.variants << v

          expect(p).to have_stock_for_distribution(oc, d)
        end

        it "returns products with on-demand variants" do
          p = create(:simple_product)
          v = create(:variant, product: p, on_demand: true)
          v.update_attribute(:on_hand, 0)
          d = create(:distributor_enterprise)
          oc = create(:simple_order_cycle, distributors: [d])
          oc.exchanges.outgoing.first.variants << v

          expect(p).to have_stock_for_distribution(oc, d)
        end

        it "does not return products that have stock not in the distribution" do
          p = create(:simple_product)
          p.master.update_attribute(:on_hand, 1)
          d = create(:distributor_enterprise)
          oc = create(:simple_order_cycle, distributors: [d])

          expect(p).not_to have_stock_for_distribution(oc, d)
        end
      end
    end

    describe "taxons" do
      let(:taxon1) { create(:taxon) }
      let(:taxon2) { create(:taxon) }
      let(:product) { create(:simple_product) }

      it "returns the first taxon as the primary taxon" do
        expect(product.taxons).to eq([product.primary_taxon])
      end
    end

    describe "deletion" do
      let(:p)  { create(:simple_product) }
      let(:v)  { create(:variant, product: p) }
      let(:oc) { create(:simple_order_cycle) }
      let(:s)  { create(:supplier_enterprise) }
      let(:e)  { create(:exchange, order_cycle: oc, incoming: true, sender: s, receiver: oc.coordinator) }

      it "removes the master variant from all order cycles" do
        e.variants << p.master
        p.destroy
        expect(e.variants(true)).to be_empty
      end

      it "removes all other variants from order cycles" do
        e.variants << v
        p.destroy
        expect(e.variants(true)).to be_empty
      end
    end
  end

  describe "product import" do
    describe "finding the most recent import date of the variants" do
      let!(:product) { create(:product) }

      let(:reference_time) { Time.zone.now.beginning_of_day }

      before do
        product.reload
      end

      context "when the variants do not have an import date" do
        let!(:variant_a) { create(:variant, product: product, import_date: nil) }
        let!(:variant_b) { create(:variant, product: product, import_date: nil) }

        it "returns nil" do
          expect(product.import_date).to be_nil
        end
      end

      context "when some variants have import date and some do not" do
        let!(:variant_a) { create(:variant, product: product, import_date: nil) }
        let!(:variant_b) { create(:variant, product: product, import_date: reference_time - 1.hour) }
        let!(:variant_c) { create(:variant, product: product, import_date: reference_time - 2.hours) }

        it "returns the most recent import date" do
          expect(product.import_date).to eq(variant_b.import_date)
        end
      end

      context "when all variants have import date" do
        let!(:variant_a) { create(:variant, product: product, import_date: reference_time - 2.hours) }
        let!(:variant_b) { create(:variant, product: product, import_date: reference_time - 1.hour) }
        let!(:variant_c) { create(:variant, product: product, import_date: reference_time - 3.hours) }

        it "returns the most recent import date" do
          expect(product.import_date).to eq(variant_b.import_date)
        end
      end
    end
  end
end
