# frozen_string_literal: true

require "rails_helper"

# This file does not run on CI because it's suffixed `_integration_spec.rb`
#
# The goal is to use Rspec to run real HTTP request against Stripe API to ensure
# things still work when upgrading the API version.
# This also creates logs on Stripe so you can copy past them to update the fixtures.

describe "Stripe Payment Integration Test", :with_pdf_generation_stub, type: :request do
  let(:api_key) { ENV["STRIPE_API_KEY"] }

  let(:organization) { create(:organization, :premium, name: "Stripe IRL") }
  let(:addon) { create(:add_on, amount_cents: 310, amount_currency: currency, organization:, name: "Setup fee") }
  let(:external_id) { "#{Time.current.iso8601}--#{SecureRandom.hex(3)}" }
  let(:currency) { "EUR" }
  let(:provider_payment_methods) { ["card"] }

  let(:provider) { organization.stripe_payment_providers.sole }

  include_context "with webhook tracking"

  around { |test| lago_premium!(&test) }

  before do
    raise "You need to set the SPEC_STRIPE_SECRET_KEY environment variable" unless api_key

    # Uncomment to print Stripe logs
    # ::Stripe.log_level = Logger::INFO

    ::Stripe.api_version = ENV["STRIPE_API_VERSION"]
    ::Stripe.api_key = api_key
    WebMock.disable_net_connect!(allow: "api.stripe.com")

    # Setting up the stripe payment providers, which register the webhook endpoint
    ::PaymentProviders::StripeService.new.create_or_update(
      organization_id: organization.id,
      code: "stripe_spec",
      name: "Stripe Spec",
      secret_key: api_key,
      success_redirect_url: "https://app.lago.dev"
    )

    perform_all_enqueued_jobs
  end

  after do
    ids = []
    organization.stripe_payment_providers.each do |payment_provider|
      ids << payment_provider.webhook_id
      ::Stripe::WebhookEndpoint.delete(payment_provider.webhook_id, {})
    end

    ids.each do |id|
      ::Stripe::WebhookEndpoint.retrieve(id)
      raise StandardError, "#{id} was not deleted"
    rescue Stripe::InvalidRequestError
      nil
    end
  end

  it "can refresh the webhook endpoint" do
    expect(provider.webhook_id).to start_with "we_"
    expect(provider.webhook_secret).to start_with "whsec_"
    PaymentProviders::Stripe::RefreshWebhookService.call(provider.reload)
  end

  it "makes call to the actual API" do
    customer = create_customer

    expect(webhooks_sent.map { it["webhook_type"] }).to match_array(%w[
      customer.payment_provider_created customer.created customer.checkout_url_generated
    ])
    webhooks_sent.clear

    stripe_customer = customer.payment_provider_customers.sole
    expect(stripe_customer.provider_customer_id).to start_with "cus_" # generated by stripe API

    # WITH FAILING CARD
    failing_pm_id = attach_card(stripe_customer, :pm_card_chargeCustomerFail)
    create_one_off_invoice(customer, [addon], units: 22)
    expect(stripe_customer.reload.payment_method_id).to eq failing_pm_id
    invoice = customer.invoices.sole
    expect(invoice.payment_status).to eq("failed")
    expect(invoice.payments.sole.status).to eq("failed")
    expect(webhooks_sent.map { it["webhook_type"] }).to include("invoice.payment_failure")
    webhooks_sent.clear

    # RetrieveLatestPaymentMethodService
    latest_id = PaymentProviderCustomers::Stripe::RetrieveLatestPaymentMethodService.call!(provider_customer: stripe_customer).payment_method_id
    expect(latest_id).to eq failing_pm_id

    # CREATING SUBSCRIPTION WITH PRE-AUTH
    response = create_subscription({
      plan_code: "any_plan",
      external_customer_id: "cust_#{external_id}"
    }, {
      amount_cents: 21_00,
      amount_currency: "EUR"
    }, raise_on_error: false)
    expect(response["error_details"]["code"]).to eq "card_declined"
    expect(response["error_details"]["http_body"]["error"]["code"]).to eq "card_declined"

    # WITH VALID CARD
    customer.update!(country: "FR")
    valid_pm_id = attach_card(stripe_customer, :pm_card_visa)
    retry_invoice_payment(invoice.id)
    expect(stripe_customer.reload.payment_method_id).to eq valid_pm_id
    expect(invoice.reload.payment_status).to eq("succeeded")
    expect(invoice.payment_attempts).to eq(2)

    perform_all_enqueued_jobs

    # CREATE A REFUND
    create_credit_note({invoice_id: invoice.id, reason: :other, refund_amount_cents: 12_00,
      items: [
        {fee_id: invoice.fees.first.id, amount_cents: 12_00}
      ]})

    cn = invoice.reload.credit_notes.sole
    expect(cn.refund_amount_cents).to eq 12_00
    refund_id = cn.refunds.sole.provider_refund_id
    expect(refund_id).to start_with "re_"
    r = ::Stripe::Refund.retrieve(refund_id)
    expect(r.metadata.lago_credit_note_id).to eq cn.id
    expect(r.metadata.lago_invoice_id).to eq invoice.id
    expect(r.metadata.lago_customer_id).to eq customer.id

    # UPDATE THE CUSTOMER
    create_or_update_customer({
      external_id: "cust_#{external_id}",
      name: "Integration Test UPDATED #{Time.current.iso8601}"
    })
  end

  context "when invoice payment requires 3DS" do
    it "create a pending payment and send webhook with `next_action`" do
      customer = create_customer
      stripe_customer = customer.payment_provider_customers.sole
      webhooks_sent.clear

      three_ds_pm_id = attach_card(stripe_customer, :pm_card_authenticationRequired)
      create_one_off_invoice(customer, [addon], units: 22)

      invoice = customer.invoices.sole
      expect(stripe_customer.reload.payment_method_id).to eq three_ds_pm_id
      expect(invoice.reload.payment_status).to eq("pending")
      expect(invoice.payments.where(status: "requires_action").count).to eq 1
      expect(invoice.payment_attempts).to eq(1)
      expect(invoice).not_to be_ready_for_payment_processing

      webhook = webhooks_sent.find { it["webhook_type"] == "payment.requires_action" }
      expect(webhook.dig("payment", "next_action")).to match({
        "type" => "redirect_to_url",
        "redirect_to_url" => {
          "url" => start_with("https://hooks.stripe.com/3d_secure_2/hosted"),
          "return_url" => "https://app.lago.dev"
        }
      })
      webhooks_sent.clear

      # CREATING SUBSCRIPTION WITH PRE-AUTH
      # A 3DS challenge on authorization will not result in an error. The auth is canceled and the subscription is created.
      # The subscription invoice will be finalized with a pending payment because the card requests 3DS again.
      plan = create(:plan, organization:, code: "std_plan", pay_in_advance: true, amount_cents: 31_000_00, amount_currency: "EUR")
      json = create_subscription({
        plan_code: plan.code,
        external_id: "sub_#{external_id}",
        external_customer_id: customer.external_id
      }, {
        amount_cents: 1_99,
        amount_currency: "EUR"
      })

      expect(response).to have_http_status(:ok)
      expect(json[:authorization][:id]).to start_with "pi_"
      expect(json[:authorization][:status]).to eq "requires_action"

      invoice = customer.invoices.subscription.sole
      expect(invoice.total_amount_cents).to be > 1_000_00 # Ensure this is the subscription invoice
      expect(invoice.status).to eq "finalized"
      expect(invoice.payment_status).to eq "pending"

      payment_webhook = webhooks_sent.select { it["webhook_type"] == "payment.requires_action" }.sole
      expect(payment_webhook.dig("payment", "invoice_ids")).to contain_exactly(invoice.id)
    end
  end

  context "when customer is in India and has `require_action` on 3DS" do
    it do
      customer = create_customer(country: "IN")
      stripe_customer = customer.payment_provider_customers.sole
      webhooks_sent.clear

      three_ds_pm_id = attach_card(stripe_customer, :pm_card_authenticationRequired)
      create_one_off_invoice(customer, [addon], units: 22)
      invoice = customer.invoices.sole
      expect(stripe_customer.reload.payment_method_id).to eq three_ds_pm_id
      expect(invoice.reload.payment_status).to eq("pending")
      expect(invoice.payments.where(status: "requires_action").count).to eq 1
      expect(invoice.payment_attempts).to eq(1)
      expect(invoice).not_to be_ready_for_payment_processing
      webhook = webhooks_sent.find { it["webhook_type"] == "payment.requires_action" }
      expect(webhook.dig("payment", "next_action")).to match({
        "type" => "redirect_to_url",
        "redirect_to_url" => {
          "url" => start_with("https://hooks.stripe.com/3d_secure_2/hosted"),
          "return_url" => "https://app.lago.dev"
        }
      })
      webhooks_sent.clear
    end
  end

  context "with customer_balance" do
    let(:provider_payment_methods) { ["customer_balance"] }

    it "create a pending payment and send webhook with `next_action" do
      customer = create_customer
      stripe_customer = customer.payment_provider_customers.sole
      webhooks_sent.clear

      create_one_off_invoice(customer, [addon], units: 7)

      invoice = customer.invoices.sole
      expect(stripe_customer.reload.payment_method_id).to be_nil
      expect(invoice.reload.payment_status).to eq("pending")
      expect(invoice.payment_attempts).to eq(1)
      expect(invoice).not_to be_ready_for_payment_processing

      payment = invoice.payments.where(status: "requires_action").sole
      expect(payment.status).to eq "requires_action"
      expect(payment.payable_payment_status).to eq "processing"

      webhook = webhooks_sent.find { it["webhook_type"] == "payment.requires_action" }
      expect(webhook.dig("payment", "next_action", "type")).to eq "display_bank_transfer_instructions"
      expect(webhook.dig("payment", "next_action", "display_bank_transfer_instructions", "amount_remaining")).to eq 2170
      webhooks_sent.clear
    end
  end

  context "with SEPA Direct Debit" do
    let(:provider_payment_methods) { ["sepa_debit"] }

    it do
      customer = create_customer({
        name: "SEPA Direct Debit Test (#{Time.current.iso8601})"
      })
      stripe_customer = customer.payment_provider_customers.sole

      create_one_off_invoice(customer, [addon], units: 22)

      # Stripe should return a 400 because of `confirm: true` and no payment method
      invoice = customer.invoices.sole
      expect(invoice.ready_for_payment_processing).to eq true
      expect(invoice.payment_status).to eq("failed")
      expect(invoice.payments.count).to eq(1)
      payment = invoice.payments.sole
      expect(payment.provider_payment_id).to be_nil

      attach_iban(stripe_customer, "AT321904300235473204")
      retry_invoice_payment(invoice.id)
      expect(invoice.reload.payment_status).to eq("pending")
      expect(invoice.payment_attempts).to eq(2)
      processing_payment = invoice.payments.where.not(id: payment.id).sole
      expect(processing_payment.provider_payment_id).to start_with "pi_"
      expect(processing_payment.status).to eq "processing"
      expect(processing_payment.payable_payment_status).to eq "processing"
    end
  end

  context "with US Bank Account (ACH)" do
    let(:provider_payment_methods) { ["us_bank_account"] }
    let(:currency) { "USD" }

    it do
      customer = create_customer({
        name: "ACH Test (#{Time.current.iso8601})",
        country: "US",
        state: "Alaska"
      })
      stripe_customer = customer.payment_provider_customers.sole

      attach_ach(stripe_customer)

      res = create_one_off_invoice(customer, [addon], currency:, units: 14)
      invoice = Invoice.find res["invoice"]["lago_id"]

      expect(invoice.reload.payment_status).to eq("pending")
      expect(invoice.payment_attempts).to eq(1)
      expect(invoice.payments.sole.provider_payment_id).to start_with "pi_"
    end
  end

  def create_customer(params = {})
    create_or_update_customer({
      external_id: "cust_#{external_id}",
      name: "Integration Test [#{external_id}]",
      currency:,
      country: "FR",
      billing_configuration: {
        payment_provider: "stripe",
        payment_provider_code: provider.code,
        sync: "true",
        sync_with_provider: "true",
        provider_payment_methods:
      }
    }.merge(params))

    Customer.find(json[:customer][:lago_id])
  end

  def remove_all_payment_methods(stripe_customer)
    Stripe::PaymentMethod.list({
      customer: stripe_customer.provider_customer_id,
      type: "card"
    }).each { Stripe::PaymentMethod.detach(it.id, {}) }
  end

  # In Lago, we only attach payment method via checkout links
  def attach_card(stripe_customer, pm_id)
    remove_all_payment_methods(stripe_customer)
    Stripe::PaymentMethod.attach(
      pm_id.to_s,
      {customer: stripe_customer.provider_customer_id}
    ).id
  end

  def attach_iban(stripe_customer, iban)
    customer_id = stripe_customer.provider_customer_id

    pm_id = Stripe::PaymentMethod.create({
      type: "sepa_debit",
      sepa_debit: {iban:},
      billing_details: {
        name: "My Name",
        email: "my-email@inter.net"
      }
    }).id

    Stripe::SetupIntent.create({
      customer: customer_id,
      payment_method: pm_id,
      payment_method_types: ["sepa_debit"],
      confirm: true,
      usage: "off_session",
      mandate_data: {
        customer_acceptance: {
          type: "online",
          online: {
            ip_address: "127.0.0.1",
            user_agent: "Mozilla/5.0 (compatible; test)"
          }
        }
      }
    })

    pm_id
  end

  def attach_ach(stripe_customer, pm_id = "pm_usBankAccount_success")
    Stripe::PaymentMethod.attach(
      pm_id,
      {customer: stripe_customer.provider_customer_id}
    )

    pm_id
  end

  def attach_bacs(stripe_customer)
    pm = Stripe::PaymentMethod.create({
      type: "bacs_debit",
      bacs_debit: {
        sort_code: "108800",
        account_number: "00012345"
      },
      billing_details: {
        name: "John Doe",
        email: "john@example.com",
        address: {
          line1: "123 Test Street",
          city: "London",
          postal_code: "SW1A 1AA",
          country: "GB"
        }
      }
    })

    Stripe::PaymentMethod.attach(
      pm.id,
      {customer: stripe_customer.provider_customer_id}
    ).id
  end
end
