require 'spec_helper'
require 'shared_examples/protect_product_actions'

module Spree
  describe Spree::Api::ProductsController, :type => :controller do
    render_views

    let!(:product) { create(:product) }
    let!(:inactive_product) { create(:product, available_on: Time.now.tomorrow, name: "inactive") }
    let(:base_attributes) { Api::ApiHelpers.product_attributes }
    let(:show_attributes) { base_attributes.dup.push(:has_variants) }
    let(:new_attributes) { base_attributes }

    let(:product_data) do
      { name: "The Other Product",
        price: 19.99,
        shipping_category_id: create(:shipping_category).id }
    end
    let(:attributes_for_variant) do
      h = attributes_for(:variant).except(:option_values, :product)
      h.merge({
        options: [
          { name: "size", value: "small" },
          { name: "color", value: "black" }
        ]
      })
    end

    before do
      stub_authentication!
    end

    context "as a normal user" do
      context "with caching enabled" do
        let!(:product_2) { create(:product) }

        before do
          ActionController::Base.perform_caching = true
        end

        it "returns unique products" do
          api_get :index
          product_ids = json_response["products"].map { |p| p["id"] }
          expect(product_ids.uniq.count).to eq(product_ids.count)
        end

        after do
          ActionController::Base.perform_caching = false
        end
      end

      it "retrieves a list of products" do
        api_get :index
        expect(json_response["products"].first).to have_attributes(show_attributes)
        expect(json_response["total_count"]).to eq(1)
        expect(json_response["current_page"]).to eq(1)
        expect(json_response["pages"]).to eq(1)
        expect(json_response["per_page"]).to eq(Kaminari.config.default_per_page)
      end

      it "retrieves a list of products by id" do
        api_get :index, :ids => [product.id]
        expect(json_response["products"].first).to have_attributes(show_attributes)
        expect(json_response["total_count"]).to eq(1)
        expect(json_response["current_page"]).to eq(1)
        expect(json_response["pages"]).to eq(1)
        expect(json_response["per_page"]).to eq(Kaminari.config.default_per_page)
      end

      context "specifying a rabl template for a custom action" do
        before do
          Spree::Api::ProductsController.class_eval do
            def custom_show
              @product = find_product(params[:id])
              respond_with(@product)
            end
          end
        end

        def set_custom_route
          @routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
            r.draw { get 'custom_show' => 'spree/api/products#custom_show' }
          end
        end

        it "uses the specified custom template through the request header" do
          set_custom_route

          request.headers['X-Spree-Template'] = 'show'
          api_get :custom_show, :id => product.id
          expect(response).to render_template('spree/api/products/show')
        end

        it "uses the specified custom template through the template URL parameter" do
          set_custom_route

          api_get :custom_show, :id => product.id, :template => 'show'
          expect(response).to render_template('spree/api/products/show')
        end

        it "falls back to the default template if the specified template does not exist" do
          request.headers['X-Spree-Template'] = 'invoice'

          api_get :show, :id => product.id
          expect(response).to render_template('spree/api/products/show')
        end
      end

      context "product has more than one price" do
        before { product.master.prices.create currency: "EUR", amount: 22 }

        it "returns distinct products only" do
          api_get :index
          expect(assigns(:products).map(&:id).uniq).to eq assigns(:products).map(&:id)
        end
      end

      it "retrieves a list of products by ids string" do
        second_product = create(:product)
        api_get :index, :ids => [product.id, second_product.id].join(",")
        expect(json_response["products"].first).to have_attributes(show_attributes)
        expect(json_response["products"][1]).to have_attributes(show_attributes)
        expect(json_response["total_count"]).to eq(2)
        expect(json_response["current_page"]).to eq(1)
        expect(json_response["pages"]).to eq(1)
        expect(json_response["per_page"]).to eq(Kaminari.config.default_per_page)
      end

      it "does not return inactive products when queried by ids" do
        api_get :index, :ids => [inactive_product.id]
        expect(json_response["count"]).to eq(0)
      end

      it "does not list unavailable products" do
        api_get :index
        expect(json_response["products"].first["name"]).not_to eq("inactive")
      end

      context "pagination" do
        it "can select the next page of products" do
          second_product = create(:product)
          api_get :index, :page => 2, :per_page => 1
          expect(json_response["products"].first).to have_attributes(show_attributes)
          expect(json_response["total_count"]).to eq(2)
          expect(json_response["current_page"]).to eq(2)
          expect(json_response["pages"]).to eq(2)
        end

        it 'can control the page size through a parameter' do
          create(:product)
          api_get :index, :per_page => 1
          expect(json_response['count']).to eq(1)
          expect(json_response['total_count']).to eq(2)
          expect(json_response['current_page']).to eq(1)
          expect(json_response['pages']).to eq(2)
        end
      end

      context "jsonp" do
        it "retrieves a list of products of jsonp" do
          api_get :index, {:callback => 'callback'}
          expect(response.body).to match(/^callback\(.*\)$/)
          expect(response.header['Content-Type']).to include('application/javascript')
        end

        # Regression test for #4332
        it "does not escape quotes" do
          api_get :index, {:callback => 'callback'}
          expect(response.body).to match(/^callback\({"count":1,"total_count":1/)
          expect(response.header['Content-Type']).to include('application/javascript')
        end
      end

      it "can search for products" do
        create(:product, :name => "The best product in the world")
        api_get :index, :q => { :name_cont => "best" }
        expect(json_response["products"].first).to have_attributes(show_attributes)
        expect(json_response["count"]).to eq(1)
      end

      it "gets a single product" do
        product.master.images.create!(:attachment => image("thinking-cat.jpg"))
        product.variants.create!
        product.variants.first.images.create!(:attachment => image("thinking-cat.jpg"))
        product.set_property("spree", "rocks")
        product.taxons << create(:taxon)

        api_get :show, :id => product.to_param

        expect(json_response).to have_attributes(show_attributes)
        expect(json_response['variants'].first).to have_attributes([:name,
                                                              :is_master,
                                                              :price,
                                                              :images,
                                                              :in_stock])

        expect(json_response['variants'].first['images'].first).to have_attributes([:attachment_file_name,
                                                                                :attachment_width,
                                                                                :attachment_height,
                                                                                :attachment_content_type,
                                                                                :mini_url,
                                                                                :small_url,
                                                                                :product_url,
                                                                                :large_url])

        expect(json_response["product_properties"].first).to have_attributes([:value,
                                                                         :product_id,
                                                                         :property_name])

        expect(json_response["classifications"].first).to have_attributes([:taxon_id, :position, :taxon])
        expect(json_response["classifications"].first['taxon']).to have_attributes([:id, :name, :pretty_name, :permalink, :taxonomy_id, :parent_id])
      end

      context "tracking is disabled" do
        before { Config.track_inventory_levels = false }

        it "still displays valid json with total_on_hand Float::INFINITY" do
          api_get :show, :id => product.to_param
          expect(response).to be_ok
          expect(json_response[:total_on_hand]).to eq nil
        end

        after { Config.track_inventory_levels = true }
      end

      context "finds a product by slug first then by id" do
        let!(:other_product) { create(:product, :slug => "these-are-not-the-droids-you-are-looking-for") }

        before do
          product.update_column(:slug, "#{other_product.id}-and-1-ways")
        end

        specify do
          api_get :show, :id => product.to_param
          expect(json_response["slug"]).to match(/and-1-ways/)
          product.destroy

          api_get :show, :id => other_product.id
          expect(json_response["slug"]).to match(/droids/)
        end
      end

      it "cannot see inactive products" do
        api_get :show, :id => inactive_product.to_param
        assert_not_found!
      end

      it "returns a 404 error when it cannot find a product" do
        api_get :show, :id => "non-existant"
        assert_not_found!
      end

      it "can learn how to create a new product" do
        api_get :new
        expect(json_response["attributes"]).to eq(new_attributes.map(&:to_s))
        required_attributes = json_response["required_attributes"]
        expect(required_attributes).to include("name")
        expect(required_attributes).to include("price")
        expect(required_attributes).to include("shipping_category_id")
      end

      it_behaves_like "modifying product actions are restricted"
    end

    context "as an admin" do
      let(:taxon_1) { create(:taxon) }
      let(:taxon_2) { create(:taxon) }

      sign_in_as_admin!

      it "can see all products" do
        api_get :index
        expect(json_response["products"].count).to eq(2)
        expect(json_response["count"]).to eq(2)
        expect(json_response["current_page"]).to eq(1)
        expect(json_response["pages"]).to eq(1)
      end

      # Regression test for #1626
      context "deleted products" do
        before do
          create(:product, :deleted_at => 1.day.ago)
        end

        it "does not include deleted products" do
          api_get :index
          expect(json_response["products"].count).to eq(2)
        end

        it "can include deleted products" do
          api_get :index, :show_deleted => 1
          expect(json_response["products"].count).to eq(3)
        end
      end

      describe "creating a product" do
        it "can create a new product" do
          api_post :create, :product => { :name => "The Other Product",
                                          :price => 19.99,
                                          :shipping_category_id => create(:shipping_category).id }
          expect(json_response).to have_attributes(base_attributes)
          expect(response.status).to eq(201)
        end

        it "creates with embedded variants" do
          product_data.merge!({
            variants: [attributes_for_variant, attributes_for_variant]
          })

          api_post :create, :product => product_data
          expect(response.status).to eq 201

          variants = json_response['variants']
          expect(variants.count).to eq(2)
          expect(variants.last['option_values'][0]['name']).to eq('small')
          expect(variants.last['option_values'][0]['option_type_name']).to eq('size')

          expect(json_response['option_types'].count).to eq(2) # size, color
        end

        it "can create a new product with embedded product_properties" do
          product_data.merge!({
            product_properties_attributes: [{
              property_name: "fabric",
              value: "cotton"
            }]
          })

          api_post :create, :product => product_data

          expect(json_response['product_properties'][0]['property_name']).to eq('fabric')
          expect(json_response['product_properties'][0]['value']).to eq('cotton')
        end

        it "can create a new product with option_types" do
          product_data.merge!({
            option_types: ['size', 'color']
          })

          api_post :create, :product => product_data
          expect(json_response['option_types'].count).to eq(2)
        end

        it "creates with shipping categories" do
          hash = { :name => "The Other Product",
                   :price => 19.99,
                   :shipping_category => "Free Ships" }

          api_post :create, :product => hash
          expect(response.status).to eq 201

          shipping_id = ShippingCategory.find_by_name("Free Ships").id
          expect(json_response['shipping_category_id']).to eq shipping_id
        end

        it "puts the created product in the given taxon" do
          product_data[:taxon_ids] = taxon_1.id.to_s
          api_post :create, :product => product_data
          expect(json_response["taxon_ids"]).to eq([taxon_1.id,])
        end

        # Regression test for #4123
        it "puts the created product in the given taxons" do
          product_data[:taxon_ids] = [taxon_1.id, taxon_2.id].join(',')
          api_post :create, :product => product_data
          expect(json_response["taxon_ids"]).to eq([taxon_1.id, taxon_2.id])
        end

        # Regression test for #2140
        context "with authentication_required set to false" do
          before do
            Spree::Api::Config.requires_authentication = false
          end

          after do
            Spree::Api::Config.requires_authentication = true
          end

          it "can still create a product" do
            api_post :create, :product => product_data, :token => "fake"
            expect(json_response).to have_attributes(show_attributes)
            expect(response.status).to eq(201)
          end
        end

        it "cannot create a new product with invalid attributes" do
          api_post :create, product: {}
          expect(response.status).to eq(422)
          expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
          errors = json_response["errors"]
          errors.delete("slug") # Don't care about this one.
          expect(errors.keys).to match_array(["name", "price", "shipping_category_id"])
        end
      end

      context 'updating a product' do
        it "can update a product" do
          api_put :update, :id => product.to_param, :product => { :name => "New and Improved Product!" }
          expect(response.status).to eq(200)
        end

        it "can create new option types on a product" do
          api_put :update, :id => product.to_param, :product => { :option_types => ['shape', 'color'] }
          expect(json_response['option_types'].count).to eq(2)
        end

        it "can create new variants on a product" do
          api_put :update, :id => product.to_param, :product => { :variants => [attributes_for_variant, attributes_for_variant.merge(sku: "ABC-#{Kernel.rand(9999)}")] }
          expect(response.status).to eq 200
          expect(json_response['variants'].count).to eq(2) # 2 variants

          variants = json_response['variants'].select { |v| !v['is_master'] }
          expect(variants.last['option_values'][0]['name']).to eq('small')
          expect(variants.last['option_values'][0]['option_type_name']).to eq('size')

          expect(json_response['option_types'].count).to eq(2) # size, color
        end

        it "can update an existing variant on a product" do
          variant_hash = {
            :sku => '123', :price => 19.99, :options => [{:name => "size", :value => "small"}]
          }
          variant_id = product.variants.create!({ product: product }.merge(variant_hash)).id

          api_put :update, :id => product.to_param, :product => {
            :variants => [
              variant_hash.merge(
                :id => variant_id.to_s,
                :sku => '456',
                :options => [{:name => "size", :value => "large" }]
              )
            ]
          }

          expect(json_response['variants'].count).to eq(1)
          variants = json_response['variants'].select { |v| !v['is_master'] }
          expect(variants.last['option_values'][0]['name']).to eq('large')
          expect(variants.last['sku']).to eq('456')
          expect(variants.count).to eq(1)
        end

        it "cannot update a product with an invalid attribute" do
          api_put :update, :id => product.to_param, :product => { :name => "" }
          expect(response.status).to eq(422)
          expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
          expect(json_response["errors"]["name"]).to eq(["can't be blank"])
        end

        # Regression test for #4123
        it "puts the created product in the given taxon" do
          api_put :update, :id => product.to_param, :product => {:taxon_ids => taxon_1.id.to_s}
          expect(json_response["taxon_ids"]).to eq([taxon_1.id,])
        end

        # Regression test for #4123
        it "puts the created product in the given taxons" do
          api_put :update, :id => product.to_param, :product => {:taxon_ids => [taxon_1.id, taxon_2.id].join(',')}
          expect(json_response["taxon_ids"]).to eq([taxon_1.id, taxon_2.id])
        end
      end

      it "can delete a product" do
        expect(product.deleted_at).to be_nil
        api_delete :destroy, :id => product.to_param
        expect(response.status).to eq(204)
        expect(product.reload.deleted_at).not_to be_nil
      end
    end
  end
end
