require 'rails_helper'
require 'support/gravity_helper'

describe Tax::CalculatorService, type: :services do
  let(:buyer_id) { 'buyer-id' }
  let(:seller_id) { 'seller-id' }
  let(:currency_code) { 'USD' }
  let(:line_item_total_cents) { 2000_00 }
  let(:line_item_unit_price) { 2000_00 }
  let(:line_item_quantity) { 1 }
  let(:shipping_total_cents) { 2222 }
  let(:refund_sales_tax_cents) { 100 }
  let(:transaction_id) { 'order_id_line_item_id' }
  let(:shipping) do
    {
      country: 'US',
      postal_code: '10013',
      region: 'NY',
      city: 'New York',
      address_line1: '123 Fake St'
    }
  end
  let(:shipping_address) { Address.new(shipping) }
  let!(:nexus_addresses) do
    [
      {
        country: 'US',
        state: 'NY',
        city: 'New York',
        address: '456 Fake St',
        postal_code: '10013'
      },
      {
        country: 'US',
        state: 'MA',
        city: 'Cambridge',
        address: '789 Fake St',
        postal_code: '02139'
      }
    ]
  end
  let!(:seller_addresses) { nexus_addresses.map { |ad| Address.new(ad) } }
  let(:artwork_location) { Address.new(gravity_v1_artwork[:location]) }
  let(:base_tax_params) do
    {
      currency_code: currency_code,
      fulfillment_type: Order::SHIP,
      artwork_location: artwork_location,
      total_amount_cents: line_item_total_cents,
      destination_address: shipping_address,
      seller_nexus_addresses: seller_addresses,
      shipping_total_cents: shipping_total_cents,
      unit_price_cents: line_item_unit_price,
      quantity: line_item_quantity
    }
  end
  let(:untaxable_address) { Address.new(country: 'IR') }

  let(:ship_order) do
    line_item = Fabricate(
      :line_item,
      order: Fabricate(
        :order,
        fulfillment_type: Order::SHIP,
        state: Order::SUBMITTED,
        buyer_id: buyer_id,
        mode: Order::BUY,
        seller_id: seller_id,
        currency_code: currency_code
      )
    )
    line_item.order.approve!
    line_item.order
  end
  let(:pickup_order) do
    line_item = Fabricate(
      :line_item,
      order: Fabricate(
        :order,
        fulfillment_type: Order::PICKUP,
        state: Order::SUBMITTED,
        buyer_id: buyer_id,
        mode: Order::BUY,
        seller_id: seller_id,
        currency_code: currency_code
      )
    )
    line_item.order.approve!
    line_item.order
  end
  let(:service_ship) do
    Tax::CalculatorService.new(
      order_id: ship_order.id,
      total_amount_cents: line_item_total_cents,
      currency_code: currency_code,
      quantity: line_item_quantity,
      unit_price_cents: line_item_unit_price,
      fulfillment_type: Order::SHIP,
      shipping_address: shipping_address,
      shipping_total_cents: shipping_total_cents,
      artwork_location: artwork_location,
      nexus_addresses: seller_addresses
    )
  end
  let(:service_pickup) do
    Tax::CalculatorService.new(
      order_id: pickup_order.id,
      currency_code: currency_code,
      total_amount_cents: line_item_total_cents,
      quantity: line_item_quantity,
      unit_price_cents: line_item_unit_price,
      fulfillment_type: Order::PICKUP,
      shipping_address: Address.new({}),
      shipping_total_cents: shipping_total_cents,
      artwork_location: artwork_location,
      nexus_addresses: seller_addresses
    )
  end

  before do
    silence_warnings do
      Tax::CalculatorService.const_set('REMITTING_STATES', ['fl'])
    end
    allow(Gravity).to receive(:get_artwork).and_return(gravity_v1_artwork)
  end

  after do
    silence_warnings do
      Tax::CalculatorService.const_set('REMITTING_STATES', [])
    end
  end

  describe '#initialize' do
    context 'with a destination address in a remitting state' do
      it 'sets seller_nexus_addresses to only be taxable seller addresses' do
        service = Tax::CalculatorService.new(
          order_id: 'order-id',
          currency_code: currency_code,
          total_amount_cents: line_item_total_cents,
          quantity: line_item_quantity,
          unit_price_cents: line_item_unit_price,
          fulfillment_type: Order::SHIP,
          shipping_address: Address.new(shipping),
          shipping_total_cents: shipping_total_cents,
          artwork_location: artwork_location,
          nexus_addresses: seller_addresses + [untaxable_address]
        )
        expect(service.instance_variable_get(:@seller_nexus_addresses)).to eq seller_addresses
      end
    end
  end

  describe '#sales_tax' do
    context 'with taxjar' do
      let(:taxjar_client) { instance_double(Taxjar::Client) }
      let(:tax_response_with_breakdown) { double(amount_to_collect: 3.00, breakdown: double(state_tax_collectable: 2.00)) }

      before do
        stub_const 'ENV', ENV.to_h.merge('TAX_ADAPTER' => 'taxjar')
        allow(Taxjar::Client).to receive(:new).with(api_key: Rails.application.config_for(:tax)['taxjar_api_key'], api_url: nil).and_return(taxjar_client)
      end

      it 'calls Taxjar::Client#estimate_tax and returns the total amount to collect' do
        expect(taxjar_client).to receive(:tax_for_order).and_return(tax_response_with_breakdown)
        expect(service_ship.sales_tax).to eq 300
      end
    end

    context 'with avalara' do
      # TODO: some of these properties need to be clarified with avalara/legal
      let(:avalara_client) { instance_double(AvaTax::Client) }
      let(:tax_response) { instance_double(Faraday::Response, success?: true, body: { 'totalTax' => 42.04 }) }

      before do
        stub_const 'ENV', ENV.to_h.merge('TAX_ADAPTER' => 'avalara')
        allow(AvaTax::Client).to receive(:new).and_return(avalara_client)
      end

      it 'calls AvaTax::Client#create_transaction and returns the total amount to collect' do
        expect(avalara_client).to receive(:create_transaction).and_return(tax_response)
        expect(service_ship.sales_tax).to eq 4204
      end

      it 'returns the avalara response totalTax property, converted to cents' do
        allow(avalara_client).to receive(:create_transaction)
          .and_return(tax_response)

        expect(service_ship.sales_tax).to eq 4204
      end

      context 'with an error from Avalara' do
        it 'raises a ProcessingError with a code of tax_calculator_failure when avalara returns a hash with "error"' do
          expect(avalara_client).to receive(:create_transaction)
            .and_return(instance_double(Faraday::Response, success?: false, status: 424, body: { 'error' => { 'message' => 'something bad' } }))
          expect { service_ship.sales_tax }.to raise_error do |error|
            expect(error).to be_a Errors::ProcessingError
            expect(error.type).to eq :processing
            expect(error.code).to eq :tax_calculator_failure
            expect(error.data[:message]).to eq '424 - something bad'
          end
        end
      end

      describe 'avalara transaction params' do
        it 'passes current date for tax_estimates' do
          Timecop.freeze do
            expect(avalara_client)
              .to receive(:create_transaction)
              .with(
                a_hash_including(
                  'date' => Time.now.utc
                ),
                { '$include' => 'TaxDetailsByTaxType' }
              ).and_return(tax_response)

            service_ship.sales_tax
          end
        end

        it 'passes type => SalesOrder' do
          expect(avalara_client)
            .to receive(:create_transaction)
            .with(
              a_hash_including(
                'type' => 'SalesOrder'
              ),
              { '$include' => 'TaxDetailsByTaxType' }
            ).and_return(tax_response)

          service_ship.sales_tax
        end

        it 'passes currency code' do
          expect(avalara_client)
            .to receive(:create_transaction)
            .with(
              a_hash_including(
                'currencyCode' => currency_code
              ), { '$include' => 'TaxDetailsByTaxType' }
            ).and_return(tax_response)

          service_ship.sales_tax
        end

        it 'passes line items for shipping and artwork' do
          expect(avalara_client)
            .to receive(:create_transaction)
            .with(
              a_hash_including(
                'lines' => a_collection_containing_exactly(
                  a_hash_including('itemCode' => 'artwork'),
                  a_hash_including('itemCode' => 'shipping_general')
                )
              ), { '$include' => 'TaxDetailsByTaxType' }
            ).and_return(tax_response)

          service_ship.sales_tax
        end

        it 'does not pass a shipping line item if there is no shipping' do
          expect(avalara_client)
            .to receive(:create_transaction)
            .with(
              a_hash_including(
                'lines' =>
                [
                  a_hash_including({ 'itemCode' => 'artwork' })
                ]
              ), { '$include' => 'TaxDetailsByTaxType' }
            ).and_return(tax_response)
          service_pickup.sales_tax
        end

        it 'builds an addresses model for the addresses' do
          addresses = double(
            build: { single_location: { line1: '401 Broadway' } }
          )
          expect(Avalara::AddressesModel).to receive(:new).with(
            artwork_location: artwork_location,
            shipping_address: shipping_address,
            fulfillment_type: Order::SHIP
          ).and_return(addresses)

          expect(avalara_client)
            .to receive(:create_transaction)
            .with(
              a_hash_including(
                'addresses' => {
                  'singleLocation' => {
                    'line1' => '401 Broadway'
                  }
                }
              ), { '$include' => 'TaxDetailsByTaxType' }
            ).and_return(tax_response)

          service_ship.sales_tax
        end

        it 'passes description: "Order {order.mode} {order.id}"' do
          expect(avalara_client)
            .to receive(:create_transaction)
            .with(
              a_hash_including(
                'description' => a_string_matching(/^Order \(buy\) [0-9a-f]/)
              ), { '$include' => 'TaxDetailsByTaxType' }
            ).and_return(tax_response)

          service_ship.sales_tax
        end

        it 'passes customerCode: order.buyer_id' do
          expect(avalara_client)
            .to receive(:create_transaction)
            .with(
              a_hash_including(
                'customerCode' => buyer_id
              ), { '$include' => 'TaxDetailsByTaxType' }
            ).and_return(tax_response)

          service_ship.sales_tax
        end

        it "passes ref1 -> seller id, number -> '1' and amount -> total amount in the artwork line" do
          expect(avalara_client)
            .to receive(:create_transaction)
            .with(
              a_hash_including(
                'lines' =>
                a_collection_including(
                  a_hash_including({
                                     'ref1' => "Partner #{seller_id}",
                                     'itemCode' => 'artwork',
                                     'amount' => UnitConverter.convert_cents_to_dollars(line_item_total_cents),
                                     'number' => '1'
                                   })
                )
              ), { '$include' => 'TaxDetailsByTaxType' }
            ).and_return(tax_response)

          service_ship.sales_tax
        end

        it "passes number -> 'shipping' in the shipping line" do
          expect(avalara_client)
            .to receive(:create_transaction)
            .with(
              a_hash_including(
                'lines' =>
                a_collection_including(
                  a_hash_including({
                                     'itemCode' => 'shipping_general',
                                     'number' => 'shipping'
                                   })
                )
              ), { '$include' => 'TaxDetailsByTaxType' }
            ).and_return(tax_response)

          service_ship.sales_tax
        end

        it 'passes order id as the referenceCode' do
          expect(avalara_client)
            .to receive(:create_transaction)
            .with(
              a_hash_including(
                'referenceCode' => ship_order.id
              ), { '$include' => 'TaxDetailsByTaxType' }
            ).and_return(tax_response)

          service_ship.sales_tax
        end
      end
    end
  end

  describe '#destination_address' do
    context 'with a fulfillment_type of SHIP' do
      context 'with a valid address' do
        it 'returns the shipping address' do
          expect(service_ship.send(:destination_address)).to eq shipping_address
        end
      end

      context 'with an invalid address' do
        context 'with missing region' do
          it 'raises a missing_region error' do
            shipping[:region] = nil
            service = Tax::CalculatorService.new(
              order_id: 'order-id',
              currency_code: currency_code,
              total_amount_cents: line_item_total_cents,
              quantity: line_item_quantity,
              unit_price_cents: line_item_unit_price,
              fulfillment_type: Order::SHIP,
              shipping_address: Address.new(shipping),
              shipping_total_cents: shipping_total_cents,
              artwork_location: artwork_location,
              nexus_addresses: seller_addresses
            )
            expect { service.send(:destination_address) }.to raise_error do |error|
              expect(error).to be_a Errors::ValidationError
              expect(error.type).to eq :validation
              expect(error.code).to eq :missing_region
            end
          end
        end

        context 'with missing postal code' do
          it 'raises a missing_postal_code error' do
            shipping[:postal_code] = nil
            service = Tax::CalculatorService.new(
              order_id: 'order-id',
              currency_code: currency_code,
              total_amount_cents: line_item_total_cents,
              quantity: line_item_quantity,
              unit_price_cents: line_item_unit_price,
              fulfillment_type: Order::SHIP,
              shipping_address: Address.new(shipping),
              shipping_total_cents: shipping_total_cents,
              artwork_location: artwork_location,
              nexus_addresses: seller_addresses
            )
            expect { service.send(:destination_address) }.to raise_error do |error|
              expect(error).to be_a Errors::ValidationError
              expect(error.type).to eq :validation
              expect(error.code).to eq :missing_postal_code
            end
          end
        end
      end
    end

    context 'with a fulfillment_type of PICKUP' do
      context 'with a valid address' do
        it 'returns the origin address' do
          expect(service_pickup.send(:destination_address)).to eq artwork_location
        end
      end

      context 'with an invalid address' do
        context 'with missing region' do
          it 'raises an invalid_artwork_address error' do
            invalid_artwork_location = Address.new(country: 'US', postal_code: '10013')
            service = Tax::CalculatorService.new(
              order_id: pickup_order.id,

              currency_code: currency_code,
              total_amount_cents: line_item_total_cents,
              quantity: line_item_quantity,
              unit_price_cents: line_item_unit_price,
              fulfillment_type: Order::PICKUP,
              shipping_address: Address.new(shipping),
              shipping_total_cents: shipping_total_cents,
              artwork_location: invalid_artwork_location,
              nexus_addresses: seller_addresses
            )
            expect { service.send(:destination_address) }.to raise_error do |error|
              expect(error).to be_a Errors::ValidationError
              expect(error.type).to eq :validation
              expect(error.code).to eq :invalid_artwork_address
            end
          end
        end

        context 'with missing postal code' do
          it 'raises an invalid_artwork_address error' do
            invalid_artwork_location = Address.new(country: 'US', region: 'NY')
            service = Tax::CalculatorService.new(
              order_id: pickup_order.id,
              currency_code: currency_code,
              total_amount_cents: line_item_total_cents,
              quantity: line_item_quantity,
              unit_price_cents: line_item_unit_price,
              fulfillment_type: Order::PICKUP,
              shipping_address: Address.new(shipping),
              shipping_total_cents: shipping_total_cents,
              artwork_location: invalid_artwork_location,
              nexus_addresses: seller_addresses
            )
            expect { service.send(:destination_address) }.to raise_error do |error|
              expect(error).to be_a Errors::ValidationError
              expect(error.type).to eq :validation
              expect(error.code).to eq :invalid_artwork_address
            end
          end
        end
      end
    end
  end

  describe '#artsy_should_remit_taxes?' do
    def build_service(destination_address_country: nil, artsy_collects_sales_tax: true)
      allow(Order).to receive(:find).and_return(instance_double(Order, artsy_collects_sales_tax?: artsy_collects_sales_tax))
      shipping[:country] = destination_address_country if destination_address_country
      Tax::CalculatorService.new(
        order_id: 'order-id',
        currency_code: currency_code,
        total_amount_cents: line_item_total_cents,
        quantity: line_item_quantity,
        unit_price_cents: line_item_unit_price,
        fulfillment_type: Order::SHIP,
        shipping_address: Address.new(shipping),
        shipping_total_cents: shipping_total_cents,
        artwork_location: artwork_location,
        nexus_addresses: seller_addresses
      )
    end

    context 'with taxjar' do
      before { stub_const 'ENV', ENV.to_h.merge('TAX_ADAPTER' => 'taxjar') }

      it 'returns false for a US destination' do
        service = build_service
        expect(service.send(:artsy_should_remit_taxes?)).to be false
      end

      it 'returns false for a non-US-destination' do
        service = build_service(destination_address_country: 'FR')
        expect(service.send(:artsy_should_remit_taxes?)).to be false
      end
    end

    context 'with avalara' do
      before { stub_const 'ENV', ENV.to_h.merge('TAX_ADAPTER' => 'avalara') }

      it 'returns true for a US destination' do
        service = build_service
        expect(service.send(:artsy_should_remit_taxes?)).to be true
      end

      it 'returns false if artsy does not collect sales tax on the order' do
        service = build_service(artsy_collects_sales_tax: false)
        expect(service.send(:artsy_should_remit_taxes?)).to be false
      end

      it 'returns false for a non-US destination' do
        service = build_service(destination_address_country: 'FR')
        expect(service.send(:artsy_should_remit_taxes?)).to be false
      end
    end
  end

  describe '#address_taxable?' do
    context 'with a US-based address' do
      it 'returns true' do
        expect(service_ship.send(:address_taxable?, Address.new(country: 'US', region: 'FL', postal_code: '12345'))).to eq true
      end
    end

    context 'with a non-US-based address' do
      it 'returns false' do
        expect(service_ship.send(:address_taxable?, untaxable_address)).to eq false
      end
    end
  end

  describe '#process_nexus_addresses' do
    context 'with no addresses' do
      it 'raises an error' do
        expect { service_ship.send(:process_nexus_addresses!, []) }.to raise_error do |error|
          expect(error).to be_a Errors::ValidationError
          expect(error.type).to eq :validation
          expect(error.code).to eq :no_taxable_addresses
        end
      end
    end

    context 'with no valid taxable addresses' do
      it 'raises an error' do
        expect { service_ship.send(:process_nexus_addresses!, [untaxable_address]) }.to raise_error do |error|
          expect(error).to be_a Errors::ValidationError
          expect(error.type).to eq :validation
          expect(error.code).to eq :no_taxable_addresses
        end
      end
    end

    context 'with taxable addresses' do
      it 'filters out non-taxable addresses' do
        addresses = seller_addresses + [untaxable_address]
        expect(service_ship.send(:process_nexus_addresses!, addresses)).to eq seller_addresses
      end

      it 'validates taxable addresses' do
        seller_addresses.each do |ad|
          expect(service_ship).to receive(:validate_nexus_address!).with(ad)
        end
        service_ship.send(:process_nexus_addresses!, seller_addresses)
      end
    end
  end

  describe '#validate_nexus_address!' do
    context 'with an address with a missing region' do
      it 'raises an error' do
        invalid_address = Address.new(country: 'US')
        expect { service_ship.send(:validate_nexus_address!, invalid_address) }.to raise_error do |error|
          expect(error).to be_a Errors::ValidationError
          expect(error.type).to eq :validation
          expect(error.code).to eq :invalid_seller_address
        end
      end
    end
  end
end
