defmodule TarakanWeb.Webhooks.StripeControllerTest do
  use TarakanWeb.ConnCase, async: true

  alias Tarakan.Accounts.Scope
  alias Tarakan.Market.Bounty
  alias Tarakan.Repo

  @secret "whsec_test_secret"

  defp fiat_bounty do
    repository = listed_github_repository_fixture()
    sponsor = active_account_fixture() |> fund_credits(1_000)

    {bounty, _checkout_url} = fiat_bounty_fixture(Scope.for_account(sponsor), repository)
    bounty
  end

  defp signed_post(conn, payload, opts \\ []) do
    timestamp = Keyword.get(opts, :timestamp, System.system_time(:second))
    secret = Keyword.get(opts, :secret, @secret)

    signature =
      :crypto.mac(:hmac, :sha256, secret, "#{timestamp}.#{payload}")
      |> Base.encode16(case: :lower)

    conn
    |> put_req_header("content-type", "application/json")
    |> put_req_header("stripe-signature", "t=#{timestamp},v1=#{signature}")
    |> post(~p"/webhooks/stripe", payload)
  end

  defp checkout_completed_payload(session_id) do
    Jason.encode!(%{
      "id" => "evt_1",
      "type" => "checkout.session.completed",
      "data" => %{"object" => %{"id" => session_id, "payment_intent" => "pi_1"}}
    })
  end

  test "a validly signed checkout.session.completed funds the bounty", %{conn: conn} do
    bounty = fiat_bounty()

    conn = signed_post(conn, checkout_completed_payload(bounty.stripe_checkout_session_id))

    assert json_response(conn, 200) == %{"received" => true}
    assert Repo.get!(Bounty, bounty.id).status == "open"
  end

  defp checkout_payload(session_id, type, extra) do
    Jason.encode!(%{
      "id" => "evt_1",
      "type" => type,
      "data" => %{"object" => Map.merge(%{"id" => session_id}, extra)}
    })
  end

  test "a completed but unpaid session does not fund the bounty", %{conn: conn} do
    bounty = fiat_bounty()

    # Delayed-notification methods complete the session before the money
    # arrives. Funding here would let work be claimed against nothing.
    conn =
      signed_post(
        conn,
        checkout_payload(
          bounty.stripe_checkout_session_id,
          "checkout.session.completed",
          %{"payment_status" => "unpaid"}
        )
      )

    assert json_response(conn, 200) == %{"received" => true}
    assert Repo.get!(Bounty, bounty.id).status == "pending_funding"
  end

  test "the later async payment confirmation funds it", %{conn: conn} do
    bounty = fiat_bounty()

    conn =
      signed_post(
        conn,
        checkout_payload(
          bounty.stripe_checkout_session_id,
          "checkout.session.completed",
          %{"payment_status" => "unpaid"}
        )
      )

    assert json_response(conn, 200) == %{"received" => true}
    assert Repo.get!(Bounty, bounty.id).status == "pending_funding"

    conn =
      conn
      |> recycle()
      |> signed_post(
        checkout_payload(
          bounty.stripe_checkout_session_id,
          "checkout.session.async_payment_succeeded",
          %{}
        )
      )

    assert json_response(conn, 200) == %{"received" => true}
    assert Repo.get!(Bounty, bounty.id).status == "open"
  end

  test "a failed async payment leaves the bounty unfunded", %{conn: conn} do
    bounty = fiat_bounty()

    conn =
      signed_post(
        conn,
        checkout_payload(
          bounty.stripe_checkout_session_id,
          "checkout.session.async_payment_failed",
          %{}
        )
      )

    assert json_response(conn, 200) == %{"received" => true}
    assert Repo.get!(Bounty, bounty.id).status == "pending_funding"
  end

  test "an invalid signature is rejected", %{conn: conn} do
    bounty = fiat_bounty()

    conn =
      signed_post(conn, checkout_completed_payload(bounty.stripe_checkout_session_id),
        secret: "whsec_wrong"
      )

    assert json_response(conn, 400) == %{"error" => "invalid_signature"}
    assert Repo.get!(Bounty, bounty.id).status == "pending_funding"
  end

  test "a stale timestamp is rejected", %{conn: conn} do
    bounty = fiat_bounty()

    conn =
      signed_post(conn, checkout_completed_payload(bounty.stripe_checkout_session_id),
        timestamp: System.system_time(:second) - 600
      )

    assert json_response(conn, 400) == %{"error" => "timestamp_outside_tolerance"}
    assert Repo.get!(Bounty, bounty.id).status == "pending_funding"
  end

  test "a missing signature header is rejected", %{conn: conn} do
    conn =
      conn
      |> put_req_header("content-type", "application/json")
      |> post(~p"/webhooks/stripe", checkout_completed_payload("cs_test_whatever"))

    assert json_response(conn, 400) == %{"error" => "missing_signature"}
  end

  test "a tampered body is rejected", %{conn: conn} do
    timestamp = System.system_time(:second)

    signature =
      :crypto.mac(
        :hmac,
        :sha256,
        @secret,
        "#{timestamp}.{\"type\":\"checkout.session.completed\"}"
      )
      |> Base.encode16(case: :lower)

    conn =
      conn
      |> put_req_header("content-type", "application/json")
      |> put_req_header("stripe-signature", "t=#{timestamp},v1=#{signature}")
      |> post(~p"/webhooks/stripe", checkout_completed_payload("cs_test_tampered"))

    assert json_response(conn, 400) == %{"error" => "invalid_signature"}
  end

  test "unknown events are acknowledged and ignored", %{conn: conn} do
    payload = Jason.encode!(%{"id" => "evt_2", "type" => "payment_intent.created", "data" => %{}})

    conn = signed_post(conn, payload)

    assert json_response(conn, 200) == %{"received" => true}
  end

  test "funding an unknown session still returns 200", %{conn: conn} do
    conn = signed_post(conn, checkout_completed_payload("cs_test_unknown"))

    assert json_response(conn, 200) == %{"received" => true}
  end

  describe "subscription events" do
    defp subscription_event_payload(type, object) do
      Jason.encode!(%{"id" => "evt_sub", "type" => type, "data" => %{"object" => object}})
    end

    defp checkout_subscription_payload(account) do
      subscription_event_payload("checkout.session.completed", %{
        "id" => "cs_test_sub",
        "mode" => "subscription",
        "client_reference_id" => Integer.to_string(account.id),
        "customer" => "cus_webhook_1",
        "subscription" => "sub_webhook_1",
        "metadata" => %{"plan" => "team"}
      })
    end

    test "a signed subscription checkout activates the account's subscription", %{conn: conn} do
      account = account_fixture()

      conn = signed_post(conn, checkout_subscription_payload(account))

      assert json_response(conn, 200) == %{"received" => true}

      subscription = Tarakan.Billing.subscription(account)
      assert subscription.status == "active"
      assert subscription.plan == "team"
      assert subscription.stripe_subscription_id == "sub_webhook_1"
    end

    test "customer.subscription.updated syncs the subscription", %{conn: conn} do
      account = account_fixture()

      conn = signed_post(conn, checkout_subscription_payload(account))
      assert json_response(conn, 200) == %{"received" => true}

      payload =
        subscription_event_payload("customer.subscription.updated", %{
          "id" => "sub_webhook_1",
          "status" => "active",
          "current_period_end" => 1_900_000_000,
          "items" => %{"data" => [%{"price" => %{"id" => "price_enterprise_test"}}]}
        })

      conn = signed_post(build_conn(), payload)

      assert json_response(conn, 200) == %{"received" => true}

      subscription = Tarakan.Billing.subscription(account)
      assert subscription.plan == "enterprise"
      assert DateTime.to_unix(subscription.current_period_end) == 1_900_000_000
    end

    test "invoice.payment_failed and customer.subscription.deleted move the status", %{
      conn: conn
    } do
      account = account_fixture()

      conn = signed_post(conn, checkout_subscription_payload(account))
      assert json_response(conn, 200) == %{"received" => true}

      failed =
        subscription_event_payload("invoice.payment_failed", %{
          "subscription" => "sub_webhook_1"
        })

      conn = signed_post(build_conn(), failed)
      assert json_response(conn, 200) == %{"received" => true}
      assert Tarakan.Billing.subscription(account).status == "past_due"

      deleted =
        subscription_event_payload("customer.subscription.deleted", %{
          "id" => "sub_webhook_1"
        })

      conn = signed_post(build_conn(), deleted)
      assert json_response(conn, 200) == %{"received" => true}
      assert Tarakan.Billing.subscription(account).status == "canceled"
    end

    test "subscription events require a valid signature", %{conn: conn} do
      account = account_fixture()

      conn =
        signed_post(conn, checkout_subscription_payload(account), secret: "whsec_wrong")

      assert json_response(conn, 400) == %{"error" => "invalid_signature"}
      assert Tarakan.Billing.subscription(account) == nil
    end
  end
end
