defmodule Microwaveprop.Workers.NarrFetchWorkerTest do
  use Microwaveprop.DataCase, async: false
  use Oban.Testing, repo: Microwaveprop.Repo

  alias Microwaveprop.Weather.NarrClient
  alias Microwaveprop.Weather.NarrProfile
  alias Microwaveprop.Workers.NarrFetchWorker

  @inv_fixture "test/fixtures/narr/narr-a_221_20100615_1200_000.inv"
  @grb_fixture "test/fixtures/narr/narr_dfw_2010-06-15_12z.grb"
  @file_size 56_221_430

  # Stub NarrClient HTTP so `fetch_profile_at/2` returns the spike fixture
  # composite without hitting NCEI.
  defp stub_narr_success do
    inv_body = File.read!(@inv_fixture)
    grb_body = File.read!(@grb_fixture)

    Req.Test.stub(NarrClient, fn conn ->
      cond do
        String.ends_with?(conn.request_path, ".inv") and conn.method == "GET" ->
          Plug.Conn.send_resp(conn, 200, inv_body)

        String.ends_with?(conn.request_path, ".grb") and conn.method == "HEAD" ->
          conn
          |> Plug.Conn.put_resp_header("content-length", Integer.to_string(@file_size))
          |> Plug.Conn.send_resp(200, "")

        String.ends_with?(conn.request_path, ".grb") and conn.method == "GET" ->
          Plug.Conn.send_resp(conn, 200, grb_body)

        true ->
          Plug.Conn.send_resp(conn, 500, "unexpected #{conn.method} #{conn.request_path}")
      end
    end)
  end

  describe "perform/1" do
    test "fetches a profile via NarrClient and inserts it as a NarrProfile" do
      stub_narr_success()

      valid_time = ~U[2010-06-15 12:00:00Z]

      args = %{
        "lat" => 32.9,
        "lon" => -97.0,
        "valid_time" => DateTime.to_iso8601(valid_time)
      }

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

      # Row snapped to 0.25° grid (32.9 → 33.0, -97.0 → -97.0) and stored at
      # the exact 3-hourly valid_time.
      assert [row] = Repo.all(NarrProfile)
      assert row.lat == 33.0
      assert row.lon == -97.0
      assert DateTime.compare(row.valid_time, valid_time) == :eq
      assert_in_delta row.surface_temp_c, 23.79, 1.0
      assert_in_delta row.surface_dewpoint_c, 21.34, 1.0
      assert_in_delta row.surface_pressure_mb, 993.84, 2.0
      assert_in_delta row.hpbl_m, 703.5, 10.0
      assert_in_delta row.pwat_mm, 45.1, 2.0
    end

    test "is a no-op when a NarrProfile row already exists for the snapped lat/lon/time" do
      valid_time = ~U[2010-06-15 12:00:00Z]

      # Stub that raises if called — this test asserts no HTTP happens.
      Req.Test.stub(NarrClient, fn _conn ->
        raise "NarrClient should not be contacted when cache hit"
      end)

      # Pre-populate with a row at the snapped coordinates.
      %NarrProfile{}
      |> NarrProfile.changeset(%{
        lat: 33.0,
        lon: -97.0,
        valid_time: valid_time,
        profile: []
      })
      |> Repo.insert!()

      args = %{
        "lat" => 32.9,
        "lon" => -97.0,
        "valid_time" => DateTime.to_iso8601(valid_time)
      }

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

      # Only the pre-seeded row exists — no new insert.
      assert Repo.aggregate(NarrProfile, :count, :id) == 1
    end

    test "returns {:error, _} when NarrClient.fetch_profile_at fails" do
      Req.Test.stub(NarrClient, fn conn ->
        Plug.Conn.send_resp(conn, 500, "ncei server error")
      end)

      args = %{
        "lat" => 32.9,
        "lon" => -97.0,
        "valid_time" => "2010-06-15T12:00:00Z"
      }

      assert {:error, _reason} = NarrFetchWorker.perform(%Oban.Job{args: args})
      assert Repo.aggregate(NarrProfile, :count, :id) == 0
    end

    test "raises ArgumentError when valid_time is not on a 3-hourly NARR slot" do
      # 13:00 is NOT a NARR analysis hour (only 00/03/06/09/12/15/18/21).
      args = %{
        "lat" => 32.9,
        "lon" => -97.0,
        "valid_time" => "2010-06-15T13:00:00Z"
      }

      assert_raise ArgumentError, fn ->
        NarrFetchWorker.perform(%Oban.Job{args: args})
      end
    end
  end

  describe "unique constraint" do
    test "duplicate enqueues collapse via Oban unique" do
      args = %{
        "lat" => 32.9,
        "lon" => -97.0,
        "valid_time" => "2010-06-15T12:00:00Z"
      }

      Oban.Testing.with_testing_mode(:manual, fn ->
        {:ok, job1} = Oban.insert(NarrFetchWorker.new(args))
        {:ok, job2} = Oban.insert(NarrFetchWorker.new(args))

        # Second insert should hit the unique constraint and return the
        # existing job (same id) instead of creating a new one.
        assert job1.id == job2.id
        assert Repo.aggregate(Oban.Job, :count, :id) == 1
      end)
    end
  end
end
