defmodule Tarakan.BillingTest do
  use Tarakan.DataCase, async: true

  alias Tarakan.Accounts.Scope
  alias Tarakan.Billing
  alias Tarakan.Billing.{StripeStub, Subscription, Watchlist}
  alias Tarakan.Repo

  defp subscription_fixture(account, overrides \\ %{}) do
    attrs =
      Map.merge(
        %{
          account_id: account.id,
          plan: "team",
          status: "active",
          stripe_customer_id: "cus_#{System.unique_integer([:positive])}",
          stripe_subscription_id: "sub_#{System.unique_integer([:positive])}",
          current_period_end: DateTime.utc_now()
        },
        overrides
      )

    %Subscription{}
    |> Subscription.changeset(attrs)
    |> Repo.insert!()
  end

  defp checkout_completed_session(account, overrides \\ %{}) do
    Map.merge(
      %{
        "mode" => "subscription",
        "client_reference_id" => Integer.to_string(account.id),
        "customer" => "cus_test_1",
        "subscription" => "sub_test_1",
        "metadata" => %{"plan" => "team"}
      },
      overrides
    )
  end

  describe "ensure_checkout/2" do
    test "creates a subscription Checkout Session with the plan's price" do
      account = account_fixture()
      scope = Scope.for_account(account)

      test_pid = self()

      StripeStub.respond_with(fn :post, "/v1/checkout/sessions", body ->
        send(test_pid, {:checkout_body, body})
        {:ok, %{"id" => "cs_test_1", "url" => "https://checkout.stripe.com/c/pay/cs_test_1"}}
      end)

      assert {:ok, "https://checkout.stripe.com/c/pay/cs_test_1"} =
               Billing.ensure_checkout(scope, "enterprise")

      assert_received {:checkout_body, body}
      assert {"mode", "subscription"} in body
      assert {"client_reference_id", Integer.to_string(account.id)} in body
      assert {"line_items[0][price]", "price_enterprise_test"} in body
      assert {"metadata[plan]", "enterprise"} in body
    end

    test "uses the enterprise price for the enterprise plan" do
      account = account_fixture()
      test_pid = self()

      StripeStub.respond_with(fn :post, "/v1/checkout/sessions", body ->
        send(test_pid, {:checkout_body, body})
        {:ok, %{"id" => "cs_test_2", "url" => "https://checkout.stripe.com/c/pay/cs_test_2"}}
      end)

      assert {:ok, _url} = Billing.ensure_checkout(Scope.for_account(account), "enterprise")
      assert_received {:checkout_body, body}
      assert {"line_items[0][price]", "price_enterprise_test"} in body
    end

    test "rejects unknown plans" do
      assert {:error, :invalid_plan} =
               Billing.ensure_checkout(account_scope_fixture(), "gold")
    end

    test "rejects anonymous scopes" do
      assert {:error, :invalid_plan} = Billing.ensure_checkout(nil, "team")
    end
  end

  describe "webhook lifecycle" do
    test "checkout.session.completed creates an active subscription" do
      account = account_fixture()

      assert {:ok, %Subscription{} = subscription} =
               Billing.handle_checkout_completed(checkout_completed_session(account))

      assert subscription.account_id == account.id
      assert subscription.plan == "team"
      assert subscription.status == "active"
      assert subscription.stripe_customer_id == "cus_test_1"
      assert subscription.stripe_subscription_id == "sub_test_1"
    end

    test "checkout.session.completed is idempotent on replay" do
      account = account_fixture()
      session = checkout_completed_session(account)

      assert {:ok, first} = Billing.handle_checkout_completed(session)
      assert {:ok, replayed} = Billing.handle_checkout_completed(session)

      assert replayed.id == first.id
      assert Repo.aggregate(Subscription, :count) == 1
    end

    test "checkout.session.completed ignores unknown accounts and payment-mode sessions" do
      assert {:error, :account_not_found} =
               Billing.handle_checkout_completed(%{
                 "mode" => "subscription",
                 "client_reference_id" => "999999999",
                 "customer" => "cus_x",
                 "subscription" => "sub_x",
                 "metadata" => %{"plan" => "team"}
               })

      assert :ignored = Billing.handle_checkout_completed(%{"mode" => "payment"})
    end

    test "customer.subscription.updated syncs status, plan, and period end" do
      account = account_fixture()
      subscription = subscription_fixture(account)

      period_end = 1_900_000_000

      assert {:ok, updated} =
               Billing.handle_subscription_updated(%{
                 "id" => subscription.stripe_subscription_id,
                 "customer" => subscription.stripe_customer_id,
                 "status" => "active",
                 "current_period_end" => period_end,
                 "items" => %{
                   "data" => [%{"price" => %{"id" => "price_enterprise_test"}}]
                 }
               })

      assert updated.plan == "enterprise"
      assert updated.status == "active"
      assert DateTime.to_unix(updated.current_period_end) == period_end
    end

    test "customer.subscription.updated collapses stripe statuses" do
      account = account_fixture()
      subscription = subscription_fixture(account)

      assert {:ok, %{status: "past_due"}} =
               Billing.handle_subscription_updated(%{
                 "id" => subscription.stripe_subscription_id,
                 "status" => "unpaid"
               })

      assert {:ok, %{status: "incomplete"}} =
               Billing.handle_subscription_updated(%{
                 "id" => subscription.stripe_subscription_id,
                 "status" => "incomplete_expired"
               })
    end

    test "customer.subscription.updated ignores unknown subscriptions" do
      assert {:error, :not_found} =
               Billing.handle_subscription_updated(%{"id" => "sub_unknown", "status" => "active"})
    end

    test "customer.subscription.deleted cancels the subscription" do
      account = account_fixture()
      subscription = subscription_fixture(account)

      assert {:ok, canceled} =
               Billing.handle_subscription_deleted(%{
                 "id" => subscription.stripe_subscription_id
               })

      assert canceled.status == "canceled"
      assert Repo.get!(Subscription, subscription.id).status == "canceled"
    end

    test "invoice.payment_failed marks the subscription past_due" do
      account = account_fixture()
      subscription = subscription_fixture(account)

      assert {:ok, past_due} =
               Billing.handle_invoice_payment_failed(%{
                 "subscription" => subscription.stripe_subscription_id
               })

      assert past_due.status == "past_due"
    end

    test "full lifecycle: created → active → past_due → canceled" do
      account = account_fixture()
      session = checkout_completed_session(account)

      assert {:ok, %{status: "active"}} = Billing.handle_checkout_completed(session)

      assert {:ok, %{status: "past_due"}} =
               Billing.handle_invoice_payment_failed(%{"subscription" => "sub_test_1"})

      assert {:ok, %{status: "active"}} =
               Billing.handle_subscription_updated(%{
                 "id" => "sub_test_1",
                 "status" => "active",
                 "current_period_end" => 1_900_000_000
               })

      assert {:ok, %{status: "canceled"}} =
               Billing.handle_subscription_deleted(%{"id" => "sub_test_1"})
    end
  end

  describe "watchlists" do
    test "create, list, update, delete - owner-scoped" do
      account = account_fixture()
      other = account_fixture()
      scope = Scope.for_account(account)

      assert {:ok, %Watchlist{} = watchlist} =
               Billing.create_watchlist(scope, %{
                 "name" => "Deps",
                 "entries" => ["openai/codex", "acme/widget"]
               })

      assert watchlist.entries == ["openai/codex", "acme/widget"]
      assert watchlist.notify

      assert [listed] = Billing.list_watchlists(account)
      assert listed.id == watchlist.id
      assert Billing.list_watchlists(other) == []

      assert {:ok, updated} =
               Billing.update_watchlist(scope, watchlist, %{"notify" => false})

      refute updated.notify

      assert {:ok, _deleted} = Billing.delete_watchlist(scope, watchlist)
      assert Billing.list_watchlists(account) == []
    end

    test "other accounts cannot update or delete" do
      owner = account_fixture()
      stranger_scope = account_scope_fixture()

      {:ok, watchlist} =
        Billing.create_watchlist(Scope.for_account(owner), %{
          "name" => "Mine",
          "entries" => ["openai/codex"]
        })

      assert {:error, :unauthorized} =
               Billing.update_watchlist(stranger_scope, watchlist, %{"name" => "Theirs"})

      assert {:error, :unauthorized} = Billing.delete_watchlist(stranger_scope, watchlist)
    end

    test "validates entry shape, count, and name length" do
      scope = account_scope_fixture()

      assert {:error, changeset} =
               Billing.create_watchlist(scope, %{
                 "name" => "Bad entries",
                 "entries" => ["not-a-repo-ref"]
               })

      assert %{entries: [_]} = errors_on(changeset)

      assert {:error, changeset} =
               Billing.create_watchlist(scope, %{
                 "name" => "Too many",
                 "entries" => for(i <- 1..501, do: "owner-#{i}/name")
               })

      assert %{entries: [_]} = errors_on(changeset)

      assert {:error, changeset} =
               Billing.create_watchlist(scope, %{
                 "name" => String.duplicate("a", 101),
                 "entries" => []
               })

      assert %{name: [_]} = errors_on(changeset)
    end
  end
end
