require 'sinatra/base'
require 'active_support/core_ext/hash/conversions'
require 'fake_braintree/customer'
require 'fake_braintree/subscription'
require 'fake_braintree/redirect'
require 'fake_braintree/credit_card'
require 'fake_braintree/address'
require 'fake_braintree/payment_method'
require 'fake_braintree/transaction'
require 'fake_braintree/client_token'
require 'fake_braintree/credit_card_serializer'
require 'fake_braintree/merchant_account'

module FakeBraintree
  class SinatraApp < Sinatra::Base
    set :show_exceptions, false
    set :dump_errors, true
    set :raise_errors, true
    set :public_folder, File.dirname(__FILE__) + '/braintree_assets'
    set :protection, except: :frame_options
    disable :logging

    include Helpers

    helpers do
      def hash_from_request_body_with_key(key)
        value = Hash.from_xml(request.body).delete(key)
        if value.is_a?(String) # This happens if there isn't actually nested data under `key`
          {}
        else
          value
        end
      end
    end

    # braintree.api.Client.prototype.tokenizeCard()
    get '/merchants/:merchant_id/client_api/v1/payment_methods/credit_cards' do
      request_hash = params

      callback = request_hash.delete('callback')
      nonce = FakeBraintree::PaymentMethod.tokenize_card(request_hash['creditCard'])

      headers = {
        'Content-Encoding' => 'gzip',
        'Content-Type' => 'application/javascript; charset=utf-8'
      }
      json = {
        creditCards: [nonce: nonce],
        status: 201
      }.to_json
      response = "#{callback}(#{json})"
      [200, headers, gzip(response)]
    end

    # braintree.api.Client.prototype.getCreditCards()
    get '/merchants/:merchant_id/client_api/v1/payment_methods' do
      request_hash = params

      callback = request_hash.delete('callback')
      customer_id = request_hash['authorizationFingerprint']
      begin
        customer = Braintree::Customer.find(customer_id)
        credit_cards = customer.credit_cards.collect do |card|
          FakeBraintree::CreditCardSerializer.new(card).to_h
        end
      rescue Braintree::NotFoundError, ArgumentError
        credit_cards = []
      end

      headers = {
        'Content-Encoding' => 'gzip',
        'Content-Type' => 'application/javascript; charset=utf-8'
      }
      json = {
        paymentMethods: credit_cards,
        status: 200
      }.to_json
      response = "#{callback}(#{json})"
      [200, headers, gzip(response)]
    end

    # Braintree::Customer.create
    post '/merchants/:merchant_id/customers' do
      customer_hash = hash_from_request_body_with_key('customer')
      options = {merchant_id: params[:merchant_id]}
      Customer.new(customer_hash, options).create
    end

    # Braintree::Customer.find
    get '/merchants/:merchant_id/customers/:id' do
      customer = FakeBraintree.registry.customers[params[:id]]
      if customer
        gzipped_response(200, customer.to_xml(root: 'customer'))
      else
        gzipped_response(404, {})
      end
    end

    # Braintree::Customer.update
    put '/merchants/:merchant_id/customers/:id' do
      customer_hash = hash_from_request_body_with_key('customer')
      options = {id: params[:id], merchant_id: params[:merchant_id]}
      Customer.new(customer_hash, options).update
    end

    # Braintree::Customer.delete
    delete '/merchants/:merchant_id/customers/:id' do
      customer_hash = {}
      options = {id: params[:id], merchant_id: params[:merchant_id]}
      Customer.new(customer_hash, options).delete
    end

    # Braintree::Address.create
    post "/merchants/:merchant_id/customers/:customer_id/addresses" do
      address_hash = hash_from_request_body_with_key('address')
      options = {customer_id: params[:customer_id], merchant_id: params[:merchant_id]}
      Address.new(address_hash, options).create
    end

    # Braintree::Subscription.create
    post '/merchants/:merchant_id/subscriptions' do
      subscription_hash = hash_from_request_body_with_key('subscription')
      options = {merchant_id: params[:merchant_id]}
      Subscription.new(subscription_hash, options).create
    end

    # Braintree::Subscription.find
    get '/merchants/:merchant_id/subscriptions/:id' do
      subscription = FakeBraintree.registry.subscriptions[params[:id]]
      if subscription
        gzipped_response(200, subscription.to_xml(root: 'subscription'))
      else
        gzipped_response(404, {})
      end
    end

    # Braintree::Subscription.update
    put '/merchants/:merchant_id/subscriptions/:id' do
      subscription_hash = hash_from_request_body_with_key('subscription')
      options = {id: params[:id], merchant_id: params[:merchant_id]}
      Subscription.new(subscription_hash, options).update
    end

    # Braintree::Subscription.cancel
    put '/merchants/:merchant_id/subscriptions/:id/cancel' do
      options = {id: params[:id], merchant_id: params[:merchant_id]}
      Subscription.new({}, options).cancel
    end

    # Braintree::PaymentMethod.find
    get '/merchants/:merchant_id/payment_methods/any/:token' do
      credit_card = FakeBraintree.registry.credit_cards[params[:token]]
      if credit_card
        gzipped_response(200, credit_card.to_xml(root: 'credit_card'))
      else
        gzipped_response(404, {})
      end
    end

    # Braintree::PaymentMethod.update
    put '/merchants/:merchant_id/payment_methods/any/:token' do
      credit_card = FakeBraintree.registry.credit_cards[params[:token]]
      updates     = hash_from_request_body_with_key('payment_method')
      options     = {token: params[:token], merchant_id: params[:merchant_id]}

      CreditCard.new(updates, options).update
    end

    # Braintree::CreditCard.find
    get '/merchants/:merchant_id/payment_methods/credit_card/:credit_card_token' do
      credit_card = FakeBraintree.registry.credit_cards[params[:credit_card_token]]
      if credit_card
        gzipped_response(200, credit_card.to_xml(root: 'credit_card'))
      else
        gzipped_response(404, {})
      end
    end

    # Braintree::CreditCard.update
    put '/merchants/:merchant_id/payment_methods/credit_card/:credit_card_token' do
      credit_card = FakeBraintree.registry.credit_cards[params[:credit_card_token]]
      updates     = hash_from_request_body_with_key('credit_card')
      options     = {token: params[:credit_card_token], merchant_id: params[:merchant_id]}

      CreditCard.new(updates, options).update
    end

    # Braintree::PaymentMethod.delete
    delete '/merchants/:merchant_id/payment_methods/any/:credit_card_token' do
      cc_hash     = {}
      options     = {token: params[:credit_card_token], merchant_id: params[:merchant_id]}

      CreditCard.new(cc_hash, options).delete
    end

    # Braintree::CreditCard.delete
    delete '/merchants/:merchant_id/payment_methods/credit_card/:credit_card_token' do
      cc_hash     = {}
      options     = {token: params[:credit_card_token], merchant_id: params[:merchant_id]}

      CreditCard.new(cc_hash, options).delete
    end

    # Braintree::PaymentMethod.create
    # Braintree::CreditCard.create
    post '/merchants/:merchant_id/payment_methods' do
      request_hash = Hash.from_xml(request.body)
      request.body.rewind

      credit_card_hash =
        if request_hash.key?('credit_card')
          hash_from_request_body_with_key('credit_card')
        else
          payment_method_hash = hash_from_request_body_with_key('payment_method')
          nonce = payment_method_hash.delete('payment_method_nonce')
          h = FakeBraintree.registry.payment_methods
          h[nonce] = (h[nonce] || {}).merge(payment_method_hash)
        end
      options = {merchant_id: params[:merchant_id]}

      if credit_card_hash['options']
        options.merge!(credit_card_hash.delete('options')).symbolize_keys!
      end

      CreditCard.new(credit_card_hash, options).create
    end

    # Braintree::Transaction.sale
    # Braintree::CreditCard.sale
    post '/merchants/:merchant_id/transactions' do
      if FakeBraintree.decline_all_cards?
        gzipped_response(422, FakeBraintree.create_failure.to_xml(root: "api_error_response"))
      else
        data = hash_from_request_body_with_key("transaction")
        transaction_id = md5("#{params[:merchant_id]}#{Time.now.to_f}")
        transaction = FakeBraintree::Transaction.new(data, transaction_id)
        response = transaction.create
        gzipped_response(200, response.to_xml(root: "transaction"))
      end
    end

    # Braintree::Transaction.find
    get '/merchants/:merchant_id/transactions/:transaction_id' do
      transaction = FakeBraintree.registry.transactions[params[:transaction_id]]
      if transaction
        gzipped_response(200, transaction.to_xml(root: 'transaction'))
      else
        gzipped_response(404, {})
      end
    end

    # Braintree::Transaction.refund
    post '/merchants/:merchant_id/transactions/:transaction_id/refund' do
      transaction          = hash_from_request_body_with_key('transaction')
      transaction_id       = md5("#{params[:merchant_id]}#{Time.now.to_f}")
      transaction_response = {'id' => transaction_id, 'amount' => transaction['amount'], 'type' => 'credit', 'created_at' => Time.now}
      FakeBraintree.registry.transactions[transaction_id] = transaction_response
      gzipped_response(200, transaction_response.to_xml(root: 'transaction'))
    end

    # Braintree:Transaction.submit_for_settlement
    put '/merchants/:merchant_id/transactions/:transaction_id/submit_for_settlement' do
      transaction = FakeBraintree.registry.transactions[params[:transaction_id]]
      transaction_response = {'id' => transaction['id'],
                              'type' => transaction['sale'],
                              'amount' => transaction['amount'],
                              'status' => Braintree::Transaction::Status::SubmittedForSettlement}
      FakeBraintree.registry.transactions[transaction['id']] = transaction_response
      gzipped_response(200, transaction_response.to_xml(root: 'transaction'))
    end

    # Braintree::Transaction.void
    put '/merchants/:merchant_id/transactions/:transaction_id/void' do
      transaction = FakeBraintree.registry.transactions[params[:transaction_id]]
      transaction_response = {'id' => transaction['id'],
                              'type' => transaction['sale'],
                              'amount' => transaction['amount'],
                              'status' => Braintree::Transaction::Status::Voided}
      FakeBraintree.registry.transactions[transaction['id']] = transaction_response
      gzipped_response(200, transaction_response.to_xml(root: 'transaction'))
    end

    # Braintree::TransparentRedirect.url
    post '/merchants/:merchant_id/transparent_redirect_requests' do
      if params[:tr_data]
        redirect = Redirect.new(params, params[:merchant_id])
        FakeBraintree.registry.redirects[redirect.id] = redirect
        redirect to(redirect.url), 303
      else
        [422, { 'Content-Type' => 'text/html' }, ['Invalid submission']]
      end
    end

    # Braintree::TransparentRedirect.confirm
    post '/merchants/:merchant_id/transparent_redirect_requests/:id/confirm' do
      redirect = FakeBraintree.registry.redirects[params[:id]]
      redirect.confirm
    end

    # Braintree::ClientToken.generate
    post '/merchants/:merchant_id/client_token' do
      client_token_hash = hash_from_request_body_with_key('client_token')
      token = FakeBraintree::ClientToken.generate(client_token_hash)
      response = { value: token }.to_xml(root: :client_token)
      gzipped_response(200, response)
    end

    get '/config' do
      headers = {
        'Content-Encoding' => 'gzip',
        'Content-Type' => 'application/javascript; charset=utf-8'
      }
      response = "#{params['callback']}(#{{ status: 200 }.to_json})"
      [200, headers, gzip(response)]
    end

    #Braintree::MerchantAccount.find
    get '/merchants/:merchant_id/merchant_accounts/:merchant_account_id' do
      merchant_account = FakeBraintree.registry.merchant_accounts[params[:merchant_account_id]]
      if merchant_account
        gzipped_response(200, merchant_account.to_xml(root: 'merchant_account'))
      else
        gzipped_response(404, {})
      end
    end

    # Braintree::MerchantAccount.update
    put '/merchants/:merchant_id/merchant_accounts/:merchant_account_id/update_via_api' do
      merchant_account_hash = hash_from_request_body_with_key('merchant_account')
      options = {id: params[:merchant_account_id], merchant_id: params[:merchant_id]}
      MerchantAccount.new(merchant_account_hash, options).update
    end

    # Braintree::MerchantAccount.create
    post '/merchants/:merchant_id/merchant_accounts/create_via_api' do
      merchant_account_hash = hash_from_request_body_with_key('merchant_account')
      options = {merchant_id: params[:merchant_id]}
      MerchantAccount.new(merchant_account_hash, options).create
    end
  end
end
