defmodule Microwaveprop.Workers.MrmsFetchWorkerTest do
  # async: false because the MrmsCache is an application-wide singleton with
  # shared ETS state. Concurrent tests would race on cache contents.
  use Microwaveprop.DataCase, async: false
  use Oban.Testing, repo: Microwaveprop.Repo

  alias Microwaveprop.Weather.MrmsCache
  alias Microwaveprop.Weather.MrmsClient
  alias Microwaveprop.Workers.MrmsFetchWorker

  @topic "mrms:cache"
  @pubsub Microwaveprop.PubSub

  setup do
    MrmsCache.clear()
    :ok
  end

  describe "perform/1" do
    test "returns :ok and leaves cache empty when the listing is empty" do
      Req.Test.stub(MrmsClient, fn conn ->
        # Empty directory index — no filenames matched by the parser.
        Plug.Conn.send_resp(conn, 200, "<html><body>no files here</body></html>")
      end)

      assert :ok = MrmsFetchWorker.perform(%Oban.Job{args: %{}})
      assert MrmsCache.fetch() == :miss
    end

    test "returns :ok and leaves cache empty when the listing request 500s" do
      Req.Test.stub(MrmsClient, fn conn ->
        Plug.Conn.send_resp(conn, 500, "upstream boom")
      end)

      assert :ok = MrmsFetchWorker.perform(%Oban.Job{args: %{}})
      assert MrmsCache.fetch() == :miss
    end

    test "short-circuits with :up_to_date when cache already has the latest valid_time" do
      latest_vt = ~U[2026-04-12 19:28:00Z]
      # Pre-populate the cache so fetch_latest/1 sees known_valid_time >= latest.
      MrmsCache.put(latest_vt, %{{32.0, -97.0} => 4.2})

      Req.Test.stub(MrmsClient, fn conn ->
        # Only the listing URL should be hit — never a .grib2.gz download.
        assert String.ends_with?(conn.request_path, "/PrecipRate/") or
                 String.ends_with?(conn.request_path, "/PrecipRate")

        html = """
        <a href="MRMS_PrecipRate_00.00_20260412-192800.grib2.gz">f</a>
        """

        Plug.Conn.send_resp(conn, 200, html)
      end)

      assert :ok = MrmsFetchWorker.perform(%Oban.Job{args: %{}})

      # Cache is untouched.
      assert {:ok, ^latest_vt, %{{32.0, -97.0} => 4.2}} = MrmsCache.fetch()
    end

    test "returns :ok and leaves cache empty when the download path 404s" do
      # Listing succeeds with one file; download 404s. The worker must log
      # and swallow the error rather than crash.
      #
      # The .grib2.gz URL triggers Req's URL-extension-based gzip decode step
      # regardless of response status, so even a 404 body must be valid gzip.
      gzipped_404 = :zlib.gzip("gone")

      Req.Test.stub(MrmsClient, fn conn ->
        if String.ends_with?(conn.request_path, ".grib2.gz") do
          Plug.Conn.send_resp(conn, 404, gzipped_404)
        else
          html = """
          <a href="MRMS_PrecipRate_00.00_20260412-193000.grib2.gz">f</a>
          """

          Plug.Conn.send_resp(conn, 200, html)
        end
      end)

      assert :ok = MrmsFetchWorker.perform(%Oban.Job{args: %{}})
      assert MrmsCache.fetch() == :miss
    end

    # The full happy path — successful listing + download + real GRIB2
    # decode into a populated cache grid — requires a real MRMS PrecipRate
    # payload that we don't have a hermetic fixture for. We defer that to
    # the wgrib2/GRIB2 coverage task.
    #
    # What we can still pin down here is the worker's delivery contract
    # when both listing and download return 200: if fetch_latest/1 returns
    # {:ok, vt, grid}, the worker MUST broadcast_put/2 to the mrms:cache
    # topic. We stub garbage gzipped bytes; wgrib2 (when installed) will
    # yield an empty grid rather than an error on "no matching messages",
    # and the worker broadcasts that through. When wgrib2 is NOT installed
    # the client returns :wgrib2_not_available, the worker logs a warning
    # and still returns :ok — no broadcast. Both arms are valid behavior
    # and both are exercised here.
    test "returns :ok on a 200 download (broadcasts iff wgrib2 yields :ok)" do
      Phoenix.PubSub.subscribe(@pubsub, @topic)

      # The .grib2.gz URL triggers Req's URL-extension-based gzip decoding,
      # so the response body must be real gzip bytes.
      gzipped_garbage = :zlib.gzip(:binary.copy(<<0>>, 1024))

      Req.Test.stub(MrmsClient, fn conn ->
        if String.ends_with?(conn.request_path, ".grib2.gz") do
          Plug.Conn.send_resp(conn, 200, gzipped_garbage)
        else
          html = """
          <a href="MRMS_PrecipRate_00.00_20260412-193200.grib2.gz">f</a>
          """

          Plug.Conn.send_resp(conn, 200, html)
        end
      end)

      assert :ok = MrmsFetchWorker.perform(%Oban.Job{args: %{}})

      # One of two valid outcomes:
      #   1. wgrib2 installed → fetch_latest returns {:ok, vt, %{}} →
      #      worker calls broadcast_put and the subscriber sees a refresh.
      #   2. wgrib2 missing → fetch_latest returns {:error, ...} → no
      #      broadcast, cache unchanged.
      receive do
        {:mrms_cache_refresh, vt, grid} ->
          assert %DateTime{} = vt
          assert is_map(grid)
          assert {:ok, ^vt, ^grid} = MrmsCache.fetch()
      after
        200 ->
          assert MrmsCache.fetch() == :miss
      end
    end
  end
end
