defmodule Microwaveprop.Workers.IemreFetchWorkerTest do
  use Microwaveprop.DataCase, async: true
  use ExUnitProperties

  alias Microwaveprop.Weather
  alias Microwaveprop.Weather.IemClient
  alias Microwaveprop.Weather.IemreObservation
  alias Microwaveprop.Workers.IemreFetchWorker

  @sample_iemre_data [
    %{"utc_hour" => 0, "p01m_mm" => 0.0, "skyc_pct" => 25.0},
    %{"utc_hour" => 1, "p01m_mm" => 0.5, "skyc_pct" => 50.0}
  ]

  describe "perform/1" do
    test "skips if IEMRE observation already exists" do
      Weather.upsert_iemre_observation(%{
        lat: 32.875,
        lon: -97.0,
        date: ~D[2026-03-28],
        hourly: @sample_iemre_data
      })

      # Stub should NOT be called
      Req.Test.stub(IemClient, fn conn ->
        Plug.Conn.send_resp(conn, 500, "Should not be called")
      end)

      job = %Oban.Job{
        args: %{
          "lat" => 32.875,
          "lon" => -97.0,
          "date" => "2026-03-28"
        }
      }

      assert :ok = IemreFetchWorker.perform(job)
      assert Repo.aggregate(IemreObservation, :count) == 1
    end

    test "fetches and stores IEMRE data on success" do
      Req.Test.stub(IemClient, fn conn ->
        Req.Test.json(conn, %{"data" => @sample_iemre_data})
      end)

      job = %Oban.Job{
        args: %{
          "lat" => 32.875,
          "lon" => -97.0,
          "date" => "2026-03-28"
        }
      }

      assert :ok = IemreFetchWorker.perform(job)
      assert Repo.aggregate(IemreObservation, :count) == 1

      obs = Repo.one(IemreObservation)
      assert obs.lat == 32.875
      assert obs.lon == -97.0
      assert obs.date == ~D[2026-03-28]
      assert length(obs.hourly) == 2
    end

    test "stores stub on empty data so backfill doesn't retry" do
      Req.Test.stub(IemClient, fn conn ->
        Req.Test.json(conn, %{"data" => []})
      end)

      job = %Oban.Job{
        args: %{
          "lat" => 32.875,
          "lon" => -97.0,
          "date" => "2026-03-28"
        }
      }

      assert :ok = IemreFetchWorker.perform(job)
      assert Repo.aggregate(IemreObservation, :count) == 1

      obs = Repo.one!(IemreObservation)
      assert obs.hourly == []
      assert obs.lat == 32.875
      assert obs.lon == -97.0
    end

    test "retries on transient server error" do
      Req.Test.stub(IemClient, fn conn ->
        Plug.Conn.send_resp(conn, 503, "Service Unavailable")
      end)

      job = %Oban.Job{
        args: %{
          "lat" => 32.875,
          "lon" => -97.0,
          "date" => "2026-03-28"
        }
      }

      assert {:error, "IEM IEMRE HTTP 503"} = IemreFetchWorker.perform(job)
    end

    test "cancels on permanent failure (404) and stores stub so backfill reconciles" do
      Req.Test.stub(IemClient, fn conn ->
        Plug.Conn.send_resp(conn, 404, "not found")
      end)

      job = %Oban.Job{
        args: %{
          "lat" => 32.875,
          "lon" => -97.0,
          "date" => "2026-03-28"
        }
      }

      assert {:cancel, "IEM IEMRE HTTP 404"} = IemreFetchWorker.perform(job)
      assert Repo.aggregate(IemreObservation, :count) == 1
      obs = Repo.one!(IemreObservation)
      assert obs.hourly == []
      assert obs.lat == 32.875
      assert obs.lon == -97.0
    end

    test "cancels on permanent failure (422 out-of-grid) and stores stub" do
      Req.Test.stub(IemClient, fn conn ->
        Plug.Conn.send_resp(conn, 422, "out of grid")
      end)

      job = %Oban.Job{
        args: %{
          "lat" => 60.75,
          "lon" => -151.25,
          "date" => "2019-08-03"
        }
      }

      assert {:cancel, "IEM IEMRE HTTP 422"} = IemreFetchWorker.perform(job)
      assert Repo.aggregate(IemreObservation, :count) == 1
      obs = Repo.one!(IemreObservation)
      assert obs.hourly == []
      assert obs.lat == 60.75
      assert obs.lon == -151.25
      assert obs.date == ~D[2019-08-03]
    end
  end

  describe "backoff/1" do
    test "uses exponential backoff capped at 6 hours" do
      assert IemreFetchWorker.backoff(%Oban.Job{attempt: 1}) == 120
      assert IemreFetchWorker.backoff(%Oban.Job{attempt: 2}) == 240
      assert IemreFetchWorker.backoff(%Oban.Job{attempt: 10}) == 21_600
      assert IemreFetchWorker.backoff(%Oban.Job{attempt: 20}) == 21_600
    end

    property "is always in the [120, 21_600] interval" do
      check all(attempt <- integer(1..100)) do
        b = IemreFetchWorker.backoff(%Oban.Job{attempt: attempt})
        assert b >= 120
        assert b <= 21_600
      end
    end

    property "is monotonic non-decreasing in attempt" do
      check all(
              a <- integer(1..50),
              delta <- integer(0..50)
            ) do
        b = a + delta

        assert IemreFetchWorker.backoff(%Oban.Job{attempt: a}) <=
                 IemreFetchWorker.backoff(%Oban.Job{attempt: b})
      end
    end

    property "matches 120 * 2^(attempt-1) before the cap kicks in" do
      # cap = 21_600 = 120 * 180; 2^7 = 128 < 180 < 256 = 2^8, so
      # attempts 1..7 are always pre-cap.
      check all(attempt <- integer(1..7)) do
        expected = 120 * Integer.pow(2, attempt - 1)
        assert IemreFetchWorker.backoff(%Oban.Job{attempt: attempt}) == expected
      end
    end
  end
end
