defmodule Raxol.Payments.LedgerTest do
  use ExUnit.Case, async: true

  alias Raxol.Payments.{Ledger, SpendingPolicy}

  setup do
    {:ok, ledger} = Ledger.start_link(table_name: :"ledger_#{:erlang.unique_integer()}")
    %{ledger: ledger}
  end

  describe "record_spend/4 and get_history/3" do
    test "records and retrieves spend entries", %{ledger: ledger} do
      :ok = Ledger.record_spend(ledger, "agent_1", Decimal.new("0.05"), %{domain: "api.test.com"})

      # Give cast time to process
      :timer.sleep(10)

      entries = Ledger.get_history(ledger, "agent_1")
      assert length(entries) == 1
      assert Decimal.equal?(hd(entries).amount, Decimal.new("0.05"))
    end

    test "returns empty list for unknown agent", %{ledger: ledger} do
      assert Ledger.get_history(ledger, "unknown") == []
    end

    test "respects limit option", %{ledger: ledger} do
      for i <- 1..5 do
        :ok = Ledger.record_spend(ledger, "agent_1", Decimal.new("0.01"), %{i: i})
      end

      :timer.sleep(10)

      entries = Ledger.get_history(ledger, "agent_1", limit: 3)
      assert length(entries) == 3
    end
  end

  describe "check_budget/4" do
    test "allows spend within limits", %{ledger: ledger} do
      policy = %SpendingPolicy{
        per_request_max: Decimal.new("1.00"),
        session_max: Decimal.new("10.00"),
        lifetime_max: Decimal.new("100.00"),
        session_window_ms: 3_600_000
      }

      assert :ok = Ledger.check_budget(ledger, "agent_1", Decimal.new("0.50"), policy)
    end

    test "denies per-request over limit", %{ledger: ledger} do
      policy = %SpendingPolicy{
        per_request_max: Decimal.new("0.10"),
        session_max: Decimal.new("10.00"),
        lifetime_max: Decimal.new("100.00"),
        session_window_ms: 3_600_000
      }

      assert {:over_limit, :per_request} =
               Ledger.check_budget(ledger, "agent_1", Decimal.new("0.50"), policy)
    end

    test "denies session over limit", %{ledger: ledger} do
      policy = %SpendingPolicy{
        per_request_max: Decimal.new("1.00"),
        session_max: Decimal.new("0.10"),
        lifetime_max: Decimal.new("100.00"),
        session_window_ms: 3_600_000
      }

      :ok = Ledger.record_spend(ledger, "agent_1", Decimal.new("0.08"), %{})
      :timer.sleep(10)

      assert {:over_limit, :session} =
               Ledger.check_budget(ledger, "agent_1", Decimal.new("0.05"), policy)
    end

    test "denies lifetime over limit", %{ledger: ledger} do
      policy = %SpendingPolicy{
        per_request_max: Decimal.new("1.00"),
        session_max: Decimal.new("100.00"),
        lifetime_max: Decimal.new("0.10"),
        session_window_ms: 3_600_000
      }

      :ok = Ledger.record_spend(ledger, "agent_1", Decimal.new("0.08"), %{})
      :timer.sleep(10)

      assert {:over_limit, :lifetime} =
               Ledger.check_budget(ledger, "agent_1", Decimal.new("0.05"), policy)
    end
  end

  describe "try_spend/5" do
    test "atomically checks and records a spend within limits", %{ledger: ledger} do
      policy = %SpendingPolicy{
        per_request_max: Decimal.new("1.00"),
        session_max: Decimal.new("5.00"),
        session_window_ms: 60_000,
        lifetime_max: Decimal.new("100.00")
      }

      assert :ok =
               Ledger.try_spend(ledger, "agent_1", Decimal.new("0.50"), policy, %{
                 domain: "example.com"
               })

      entries = Ledger.get_history(ledger, "agent_1")
      assert length(entries) == 1
      assert Decimal.equal?(hd(entries).amount, Decimal.new("0.50"))
    end

    test "rejects spend over per_request limit", %{ledger: ledger} do
      policy = %SpendingPolicy{
        per_request_max: Decimal.new("0.10"),
        session_max: Decimal.new("5.00"),
        session_window_ms: 60_000,
        lifetime_max: Decimal.new("100.00")
      }

      assert {:over_limit, :per_request} =
               Ledger.try_spend(ledger, "agent_1", Decimal.new("0.50"), policy)

      assert Ledger.get_history(ledger, "agent_1") == []
    end

    test "rejects spend over session limit after prior spends", %{ledger: ledger} do
      policy = %SpendingPolicy{
        per_request_max: Decimal.new("1.00"),
        session_max: Decimal.new("0.30"),
        session_window_ms: 60_000,
        lifetime_max: Decimal.new("100.00")
      }

      assert :ok = Ledger.try_spend(ledger, "agent_1", Decimal.new("0.20"), policy)
      assert :ok = Ledger.try_spend(ledger, "agent_1", Decimal.new("0.08"), policy)

      assert {:over_limit, :session} =
               Ledger.try_spend(ledger, "agent_1", Decimal.new("0.05"), policy)

      entries = Ledger.get_history(ledger, "agent_1")
      assert length(entries) == 2

      total = Enum.reduce(entries, Decimal.new(0), &Decimal.add(&1.amount, &2))
      assert Decimal.equal?(total, Decimal.new("0.28"))
    end

    test "concurrent safety -- total recorded never exceeds session_max", %{ledger: ledger} do
      policy = %SpendingPolicy{
        per_request_max: Decimal.new("0.20"),
        session_max: Decimal.new("1.00"),
        session_window_ms: 60_000,
        lifetime_max: Decimal.new("100.00")
      }

      # Each task gets unique metadata so ETS :bag does not dedup
      # entries that land on the same millisecond timestamp.
      tasks =
        for i <- 1..20 do
          Task.async(fn ->
            Ledger.try_spend(ledger, "agent_c", Decimal.new("0.10"), policy, %{seq: i})
          end)
        end

      results = Task.await_many(tasks, 5_000)

      ok_count = Enum.count(results, &(&1 == :ok))
      over_count = Enum.count(results, &match?({:over_limit, _}, &1))
      assert ok_count + over_count == 20

      # At most 10 spends of $0.10 fit within $1.00 session_max
      assert ok_count <= 10
      assert ok_count >= 1

      entries = Ledger.get_history(ledger, "agent_c")
      total = Enum.reduce(entries, Decimal.new(0), &Decimal.add(&1.amount, &2))
      assert Decimal.compare(total, Decimal.new("1.00")) in [:lt, :eq]
    end
  end

  describe "get_totals/3" do
    test "returns session and lifetime totals", %{ledger: ledger} do
      policy = %SpendingPolicy{
        per_request_max: Decimal.new("10.00"),
        session_max: Decimal.new("100.00"),
        lifetime_max: Decimal.new("1000.00"),
        session_window_ms: 3_600_000
      }

      :ok = Ledger.record_spend(ledger, "agent_1", Decimal.new("0.05"), %{})
      :ok = Ledger.record_spend(ledger, "agent_1", Decimal.new("0.10"), %{})
      :timer.sleep(10)

      totals = Ledger.get_totals(ledger, "agent_1", policy)
      assert Decimal.equal?(totals.session, Decimal.new("0.15"))
      assert Decimal.equal?(totals.lifetime, Decimal.new("0.15"))
    end

    test "returns zero for unknown agent", %{ledger: ledger} do
      policy = SpendingPolicy.dev()
      totals = Ledger.get_totals(ledger, "unknown", policy)
      assert Decimal.equal?(totals.session, Decimal.new("0"))
      assert Decimal.equal?(totals.lifetime, Decimal.new("0"))
    end
  end
end
