defmodule Microwaveprop.Propagation.PipelineStatusTest do
  use Microwaveprop.DataCase, async: false

  alias Microwaveprop.Propagation.PipelineStatus
  alias Microwaveprop.Propagation.ScoresFile
  alias Microwaveprop.Repo

  @grid_worker "Microwaveprop.Workers.PropagationGridWorker"
  @asos_worker "Microwaveprop.Workers.AsosAdjustmentWorker"

  # Bypass Oban's normal insert pipeline (which executes inline in test)
  # by writing directly to the Oban.Job schema. The SQL sandbox keeps
  # Oban's executor from seeing these rows.
  defp insert_oban_job(overrides) do
    now = DateTime.utc_now()

    defaults = %{
      state: "available",
      queue: "propagation",
      worker: @grid_worker,
      args: %{},
      attempt: 0,
      max_attempts: 3,
      inserted_at: now,
      scheduled_at: now
    }

    attrs = Map.merge(defaults, Map.new(overrides))
    Repo.insert!(struct!(Oban.Job, attrs))
  end

  defp minutes_ago(n) do
    DateTime.add(DateTime.utc_now(), -n * 60, :second)
  end

  describe "current/0" do
    test "returns :unknown when no pipeline jobs have ever run" do
      status = PipelineStatus.current()

      assert status.state == :unknown
      assert status.last_update_at == nil
      assert is_binary(status.label)
    end

    test "returns :running with a single grid-worker detail when only PropagationGridWorker is executing" do
      insert_oban_job(%{
        state: "executing",
        worker: @grid_worker,
        attempted_at: minutes_ago(5)
      })

      status = PipelineStatus.current()

      assert status.state == :running
      assert status.label == "Updating propagation"
      assert [%{worker: :grid, label: grid_label}] = status.details
      assert grid_label =~ "HRRR"
    end

    test "returns :running with a single ASOS detail when only AsosAdjustmentWorker is executing" do
      insert_oban_job(%{
        state: "executing",
        worker: @asos_worker,
        attempted_at: minutes_ago(1)
      })

      status = PipelineStatus.current()

      assert status.state == :running
      assert status.label == "Updating propagation"
      assert [%{worker: :asos, label: asos_label}] = status.details
      assert asos_label =~ "ASOS"
    end

    test "returns :running with BOTH workers when grid + ASOS are executing at the same time" do
      # Queue concurrency was bumped to 2 so the chain-step grid worker
      # and the 10-minute ASOS nudge can run in parallel. The chip
      # should show one sub-line per running worker, in a deterministic
      # grid-before-asos order so the eye doesn't see them flicker.
      insert_oban_job(%{
        state: "executing",
        worker: @grid_worker,
        attempted_at: minutes_ago(5)
      })

      insert_oban_job(%{
        state: "executing",
        worker: @asos_worker,
        attempted_at: minutes_ago(1)
      })

      status = PipelineStatus.current()

      assert status.state == :running
      assert status.label == "Updating propagation"
      assert [grid, asos] = status.details
      assert grid.worker == :grid
      assert grid.label =~ "HRRR"
      assert asos.worker == :asos
      assert asos.label =~ "ASOS"
    end

    test "returns :idle with Up to date label when a recent ScoresFile exists" do
      ScoresFile.write!(
        10_000,
        minutes_ago(15),
        [%{lat: 25.0, lon: -125.0, score: 50}]
      )

      status = PipelineStatus.current()

      assert status.state == :idle
      assert status.last_update_at
      assert status.label =~ "Up to date"
    end

    test "drops the 'ago' suffix when the data is brand new" do
      ScoresFile.write!(
        10_000,
        DateTime.utc_now(),
        [%{lat: 25.0, lon: -125.0, score: 50}]
      )

      status = PipelineStatus.current()

      assert status.state == :idle
      assert status.label =~ "Up to date · just now"
      refute status.label =~ "just now ago"
    end

    test "returns :stale when the newest ScoresFile is more than 120 minutes old" do
      ScoresFile.write!(
        10_000,
        minutes_ago(180),
        [%{lat: 25.0, lon: -125.0, score: 50}]
      )

      status = PipelineStatus.current()

      assert status.state == :stale
      assert status.label =~ "stale"
    end

    test "prefers :running over :idle when a job is executing even with a recent ScoresFile" do
      ScoresFile.write!(
        10_000,
        minutes_ago(5),
        [%{lat: 25.0, lon: -125.0, score: 50}]
      )

      insert_oban_job(%{
        state: "executing",
        worker: @grid_worker,
        attempted_at: minutes_ago(1)
      })

      status = PipelineStatus.current()

      assert status.state == :running
    end

    test "ignores completed jobs from unrelated workers" do
      # WeatherFetchWorker enriches individual QSOs — it's not part of
      # the CONUS grid update pipeline, so its recent success should
      # not make the map page show 'Up to date'.
      completed = minutes_ago(5)

      insert_oban_job(%{
        state: "completed",
        worker: "Microwaveprop.Workers.WeatherFetchWorker",
        queue: "weather",
        attempted_at: completed,
        completed_at: completed
      })

      status = PipelineStatus.current()

      assert status.state == :unknown
    end
  end

  describe "running_detail/1" do
    test "valid_time within half an hour of now renders as 'through now'" do
      now = DateTime.utc_now()
      assert PipelineStatus.running_detail(%{valid_time: now}) == "through now"
      assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 20 * 60, :second)}) == "through now"
      assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, -20 * 60, :second)}) == "through now"
    end

    test "valid_time in the future renders as 'through +Nh'" do
      now = DateTime.utc_now()

      assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 3 * 3600, :second)}) ==
               "through +3h"

      assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 12 * 3600, :second)}) ==
               "through +12h"
    end

    test "valid_time in the past renders as 'through Nh ago'" do
      now = DateTime.utc_now()

      assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, -2 * 3600, :second)}) ==
               "through 2h ago"
    end

    test "returns nil when no valid_time is provided" do
      assert PipelineStatus.running_detail(nil) == nil
      assert PipelineStatus.running_detail(%{}) == nil
      assert PipelineStatus.running_detail(%{forecast_hour: 3}) == nil
    end
  end
end
