defmodule Mix.Tasks.PropagationMlTasksTest do
  @moduledoc """
  Coverage smoke tests for the two lib_ml/ mix tasks.

  Both tasks boot the full application via `Mix.Task.run("app.start")`
  and mutate the `:microwaveprop, Oban` env to disable queues. Those
  side effects are fine under `async: false` because the already-
  started test Oban stays on its testing: :inline wiring.
  """
  use Microwaveprop.DataCase, async: false

  alias Microwaveprop.Radio.Contact
  alias Microwaveprop.Weather.HrrrProfile
  alias Mix.Tasks.PropagationAnalyze
  alias Mix.Tasks.PropagationTrain

  setup do
    original_shell = Mix.shell()
    Mix.shell(Mix.Shell.Process)
    on_exit(fn -> Mix.shell(original_shell) end)
    :ok
  end

  describe "Mix.Tasks.PropagationAnalyze.run/1" do
    test "runs end-to-end on an empty DB and prints the main section headers" do
      output = ExUnit.CaptureIO.capture_io(fn -> PropagationAnalyze.run([]) end)

      # Walks the whole run/1 path: header + dataset summary + per-band
      # correlation loop + factor binning + interaction analysis + closer.
      assert output =~ "PROPAGATION ANALYSIS"
      assert output =~ "Dataset: 0 matched"
      assert output =~ "SPEARMAN RANK CORRELATION"
      assert output =~ "BINNED FACTOR ANALYSIS"
      assert output =~ "Analysis complete"
    end

    test "seeded contact + matching HRRR at both endpoints yields a non-empty dataset" do
      # Both hrrr_profiles rows must land on the exact 1/8° grid cell
      # that the SQL rounds pos1 / pos2 to — otherwise the INNER JOINs
      # leave the dataset empty and the test devolves into the empty-DB
      # path. Grid step is `ROUND((pos->>'lat')::numeric * 8) / 8`.
      valid_time = ~U[2020-06-15 18:00:00Z]

      {:ok, _c} =
        %Contact{}
        |> Contact.changeset(%{
          station1: "W5XD",
          station2: "K5TR",
          qso_timestamp: valid_time,
          mode: "CW",
          band: Decimal.new("10000"),
          grid1: "EM12",
          grid2: "EM00",
          pos1: %{"lat" => 33.0, "lon" => -97.0},
          pos2: %{"lat" => 30.5, "lon" => -97.5},
          distance_km: Decimal.new("295")
        })
        |> Repo.insert()

      {:ok, _h1} =
        %HrrrProfile{}
        |> HrrrProfile.changeset(%{
          valid_time: valid_time,
          lat: 33.0,
          lon: -97.0,
          surface_temp_c: 22.0,
          surface_dewpoint_c: 14.0,
          surface_pressure_mb: 1012.0,
          surface_refractivity: 320.0,
          min_refractivity_gradient: -100.0,
          hpbl_m: 1200.0,
          pwat_mm: 25.0,
          ducting_detected: false
        })
        |> Repo.insert()

      {:ok, _h2} =
        %HrrrProfile{}
        |> HrrrProfile.changeset(%{
          valid_time: valid_time,
          lat: 30.5,
          lon: -97.5,
          surface_temp_c: 21.0,
          surface_dewpoint_c: 13.0,
          surface_pressure_mb: 1011.0,
          surface_refractivity: 318.0,
          min_refractivity_gradient: -90.0,
          hpbl_m: 1100.0,
          pwat_mm: 24.0,
          ducting_detected: false
        })
        |> Repo.insert()

      output = ExUnit.CaptureIO.capture_io(fn -> PropagationAnalyze.run([]) end)

      # Non-empty dataset row count + the matching per-band summary
      # line exercising format_band + median + percentile helpers.
      assert output =~ "Dataset: 1 matched"
      assert output =~ "10 GHz:"
    end

    test "pre-cutoff contact is excluded by the 2016-06-30 WHERE clause" do
      # The SQL's `q.qso_timestamp >= '2016-06-30'` gate drops any
      # earlier contact outright. Using 2018-01 keeps HRRR partitioning
      # happy while still testing the filter logic (contact + HRRR rows
      # both exist and JOIN, so the absence of a match is in the
      # filter, not the data).
      valid_time = ~U[2016-06-29 12:00:00Z]

      {:ok, _c} =
        %Contact{}
        |> Contact.changeset(%{
          station1: "W5OLD",
          station2: "K5TR",
          qso_timestamp: valid_time,
          mode: "CW",
          band: Decimal.new("10000"),
          grid1: "EM12",
          grid2: "EM00",
          pos1: %{"lat" => 33.0, "lon" => -97.0},
          pos2: %{"lat" => 30.5, "lon" => -97.5},
          distance_km: Decimal.new("295")
        })
        |> Repo.insert()

      output = ExUnit.CaptureIO.capture_io(fn -> PropagationAnalyze.run([]) end)
      assert output =~ "Dataset: 0 matched"
    end
  end

  describe "Mix.Tasks.PropagationTrain.run/1" do
    test "empty hrrr_profiles raises a clear Nx error, exercising load + header" do
      # With zero rows Nx.tensor([]) raises `cannot build empty tensor`.
      # The run/1 path up through load_training_data walks the header
      # printer, sampling query, and encode_profile_rows wiring before
      # the raise. That covers the printable header helpers + band
      # iteration even though the task ultimately fails.
      assert_raise RuntimeError, ~r/empty tensor/, fn ->
        ExUnit.CaptureIO.capture_io(fn ->
          PropagationTrain.run(["--samples-per-month", "1", "--epochs", "1"])
        end)
      end
    end
  end
end
