defmodule Microwaveprop.Rover.ComputeTest do
  use ExUnit.Case, async: true

  alias Microwaveprop.Rover.Compute

  defp build_grid(home_lat, home_lon) do
    # 3x3 grid of cells around home spaced 0.1 deg apart
    for dlat <- [-0.1, 0.0, 0.1], dlon <- [-0.1, 0.0, 0.1] do
      %{
        lat: Float.round(home_lat + dlat, 4),
        lon: Float.round(home_lon + dlon, 4),
        score: 80
      }
    end
  end

  test "returns top_candidates ordered by score desc with required fields" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 200}
    stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
    grid = build_grid(home.lat, home.lon)

    scores_at = fn _band, _t, _bbox -> grid end
    elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
    clearance_lookup = fn _cells, _stations -> %{} end
    prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
    road_lookup = fn _cells, _bbox -> {:error, :stubbed} end

    args = %{
      home: home,
      stations: stations,
      band_mhz: 10_000,
      valid_time: ~U[2026-04-25 12:00:00Z],
      mode: :ssb,
      max_distance_km: 65.0,
      min_elev_gain: 0
    }

    result =
      Compute.run(args,
        scores_at: scores_at,
        elev_lookup: elev_lookup,
        clearance_lookup: clearance_lookup,
        prominence_lookup: prominence_lookup,
        road_lookup: road_lookup
      )

    assert is_map(result)
    assert Map.has_key?(result, :cells)
    assert Map.has_key?(result, :top_candidates)

    assert is_list(result.cells)
    assert length(result.top_candidates) <= 5
    refute result.top_candidates == []

    # Sorted by score desc.
    scores = Enum.map(result.top_candidates, & &1.score)
    assert scores == Enum.sort(scores, :desc)

    # Required candidate fields.
    cand = hd(result.top_candidates)

    for key <- [
          :grid,
          :lat,
          :lon,
          :elev_m,
          :drive_min,
          :score,
          :tier_color,
          :distance_km,
          :bearing_compass,
          :name
        ] do
      assert Map.has_key?(cand, key), "expected candidate to have key #{inspect(key)}"
    end

    assert is_binary(cand.grid)
    assert is_binary(cand.tier_color)
    assert is_binary(cand.bearing_compass)
    assert is_binary(cand.name)
  end

  test "penalizes cells with tall nearby buildings (clutter)" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 200}
    stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
    grid = build_grid(home.lat, home.lon)

    tall_cell = {Float.round(home.lat + 0.1, 4), Float.round(home.lon, 4)}
    ref_cell = {Float.round(home.lat - 0.1, 4), Float.round(home.lon, 4)}

    scores_at = fn _band, _t, _bbox -> grid end
    elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
    clearance_lookup = fn _cells, _stations -> %{} end
    prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
    road_lookup = fn _cells, _bbox -> {:error, :stubbed} end

    buildings_clutter_lookup = fn cells ->
      Map.new(cells, fn c ->
        height = if {c.lat, c.lon} == tall_cell, do: 30.0, else: 0.0
        {{c.lat, c.lon}, height}
      end)
    end

    args = %{
      home: home,
      stations: stations,
      band_mhz: 10_000,
      valid_time: ~U[2026-04-25 12:00:00Z],
      mode: :ssb,
      max_distance_km: 65.0,
      min_elev_gain: 0
    }

    result =
      Compute.run(args,
        scores_at: scores_at,
        elev_lookup: elev_lookup,
        clearance_lookup: clearance_lookup,
        prominence_lookup: prominence_lookup,
        road_lookup: road_lookup,
        buildings_clutter_lookup: buildings_clutter_lookup
      )

    cells_by_pos = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end)
    tall_score = Map.fetch!(cells_by_pos, tall_cell)
    ref_score = Map.fetch!(cells_by_pos, ref_cell)

    # 30m nearby building → penalty = 30/5 = 6.0 dB
    assert ref_score - tall_score >= 5.5
  end

  test "calls progress callback with each pipeline step label" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 200}
    stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
    grid = build_grid(home.lat, home.lon)

    test_pid = self()
    progress = fn label, step, total -> send(test_pid, {:progress, label, step, total}) end

    Compute.run(
      %{
        home: home,
        stations: stations,
        band_mhz: 10_000,
        valid_time: ~U[2026-04-25 12:00:00Z],
        mode: :ssb,
        max_distance_km: 65.0,
        min_elev_gain: 0
      },
      scores_at: fn _, _, _ -> grid end,
      elev_lookup: fn points -> Map.new(points, fn p -> {p, 250} end) end,
      clearance_lookup: fn _, _ -> %{} end,
      prominence_lookup: fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end,
      road_lookup: fn _, _ -> {:error, :stubbed} end,
      progress: progress
    )

    assert_received {:progress, "Loading propagation grid", 1, total}
    assert is_integer(total) and total >= 7
    assert_received {:progress, "Looking up elevation", 2, ^total}
    assert_received {:progress, "Computing path clearance", _, ^total}
    assert_received {:progress, "Scoring cells", ^total, ^total}
  end

  test "filters cells failing min_elev_gain" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 500}
    stations = [%{callsign: "W5LUA", lat: 33.1, lon: -96.6, selected: true}]
    grid = build_grid(home.lat, home.lon)

    # All elevations below home — nothing should pass min_elev_gain=200.
    scores_at = fn _band, _t, _bbox -> grid end
    elev_lookup = fn points -> Map.new(points, fn p -> {p, 100} end) end
    clearance_lookup = fn _cells, _stations -> %{} end
    prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
    road_lookup = fn _cells, _bbox -> {:error, :stubbed} end

    result =
      Compute.run(
        %{
          home: home,
          stations: stations,
          band_mhz: 10_000,
          valid_time: ~U[2026-04-25 12:00:00Z],
          mode: :ssb,
          max_distance_km: 65.0,
          min_elev_gain: 200
        },
        scores_at: scores_at,
        elev_lookup: elev_lookup,
        clearance_lookup: clearance_lookup,
        prominence_lookup: prominence_lookup,
        road_lookup: road_lookup
      )

    assert result.cells == []
    assert result.top_candidates == []
  end

  test "road proximity penalty reduces score" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 200}
    stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
    grid = build_grid(home.lat, home.lon)

    scores_at = fn _, _, _ -> grid end
    elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
    clearance_lookup = fn _, _ -> %{} end
    prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end

    cell_with_penalty = {33.1, -96.0}

    road_lookup = fn cells, _bbox ->
      {:ok,
       Map.new(cells, fn c ->
         dist = if {c.lat, c.lon} == cell_with_penalty, do: 10.0, else: 0.0
         {{c.lat, c.lon}, dist}
       end)}
    end

    Application.put_env(:microwaveprop, :rover_road_proximity_enabled, true)

    try do
      result =
        Compute.run(
          %{
            home: home,
            stations: stations,
            band_mhz: 10_000,
            valid_time: ~U[2026-04-25 12:00:00Z],
            mode: :ssb,
            max_distance_km: 65.0,
            min_elev_gain: 0
          },
          scores_at: scores_at,
          elev_lookup: elev_lookup,
          clearance_lookup: clearance_lookup,
          prominence_lookup: prominence_lookup,
          road_lookup: road_lookup
        )

      cell_scores = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end)
      penalized_score = cell_scores[cell_with_penalty]
      other_scores = Map.drop(cell_scores, [cell_with_penalty]) |> Map.values()
      assert penalized_score != nil
      assert Enum.all?(other_scores, fn s -> s > penalized_score end)
    after
      Application.put_env(:microwaveprop, :rover_road_proximity_enabled, true)
    end
  end

  test "returns warnings when grid is empty" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 200}
    stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]

    scores_at = fn _, _, _ -> [] end
    elev_lookup = fn _ -> %{} end
    clearance_lookup = fn _, _ -> %{} end
    prominence_lookup = fn _ -> %{} end
    road_lookup = fn _, _ -> {:error, :stubbed} end

    result =
      Compute.run(
        %{
          home: home,
          stations: stations,
          band_mhz: 10_000,
          valid_time: ~U[2026-04-25 12:00:00Z],
          mode: :ssb,
          max_distance_km: 65.0,
          min_elev_gain: 0
        },
        scores_at: scores_at,
        elev_lookup: elev_lookup,
        clearance_lookup: clearance_lookup,
        prominence_lookup: prominence_lookup,
        road_lookup: road_lookup
      )

    assert result.cells == []
    assert result.top_candidates == []
    assert Enum.any?(result.warnings, &String.contains?(&1, "No HRRR"))
  end

  test "returns all-nil elevation warning" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 200}
    stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
    grid = build_grid(home.lat, home.lon)

    scores_at = fn _, _, _ -> grid end
    elev_lookup = fn points -> Map.new(points, fn p -> {p, nil} end) end
    clearance_lookup = fn _, _ -> %{} end
    prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
    road_lookup = fn _, _ -> {:error, :stubbed} end

    result =
      Compute.run(
        %{
          home: home,
          stations: stations,
          band_mhz: 10_000,
          valid_time: ~U[2026-04-25 12:00:00Z],
          mode: :ssb,
          max_distance_km: 65.0,
          min_elev_gain: 0
        },
        scores_at: scores_at,
        elev_lookup: elev_lookup,
        clearance_lookup: clearance_lookup,
        prominence_lookup: prominence_lookup,
        road_lookup: road_lookup
      )

    assert Enum.any?(result.warnings, &String.contains?(&1, "Elevation tiles"))
  end

  test "canopy clutter penalty reduces score" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 200}
    stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
    grid = build_grid(home.lat, home.lon)

    tall_cell = {Float.round(home.lat + 0.1, 4), Float.round(home.lon, 4)}
    ref_cell = {Float.round(home.lat - 0.1, 4), Float.round(home.lon, 4)}

    scores_at = fn _, _, _ -> grid end
    elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
    clearance_lookup = fn _, _ -> %{} end
    prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
    road_lookup = fn _, _ -> {:error, :stubbed} end

    canopy_clutter_lookup = fn cells ->
      Map.new(cells, fn c ->
        height = if {c.lat, c.lon} == tall_cell, do: 20.0, else: 0.0
        {{c.lat, c.lon}, height}
      end)
    end

    result =
      Compute.run(
        %{
          home: home,
          stations: stations,
          band_mhz: 10_000,
          valid_time: ~U[2026-04-25 12:00:00Z],
          mode: :ssb,
          max_distance_km: 65.0,
          min_elev_gain: 0
        },
        scores_at: scores_at,
        elev_lookup: elev_lookup,
        clearance_lookup: clearance_lookup,
        prominence_lookup: prominence_lookup,
        road_lookup: road_lookup,
        canopy_clutter_lookup: canopy_clutter_lookup
      )

    cells_by_pos = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end)
    tall_score = Map.fetch!(cells_by_pos, tall_cell)
    ref_score = Map.fetch!(cells_by_pos, ref_cell)
    assert tall_score < ref_score
  end

  test "positive prominence gives a score boost" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 200}
    stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
    grid = build_grid(home.lat, home.lon)

    high_cell = {Float.round(home.lat + 0.1, 4), Float.round(home.lon, 4)}
    low_cell = {Float.round(home.lat - 0.1, 4), Float.round(home.lon, 4)}

    scores_at = fn _, _, _ -> grid end
    elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
    clearance_lookup = fn _, _ -> %{} end
    road_lookup = fn _, _ -> {:error, :stubbed} end

    prominence_lookup = fn cells ->
      Map.new(cells, fn c ->
        value = if {c.lat, c.lon} == high_cell, do: 90, else: 0
        {{c.lat, c.lon}, value}
      end)
    end

    result =
      Compute.run(
        %{
          home: home,
          stations: stations,
          band_mhz: 10_000,
          valid_time: ~U[2026-04-25 12:00:00Z],
          mode: :ssb,
          max_distance_km: 65.0,
          min_elev_gain: 0
        },
        scores_at: scores_at,
        elev_lookup: elev_lookup,
        clearance_lookup: clearance_lookup,
        prominence_lookup: prominence_lookup,
        road_lookup: road_lookup
      )

    cells_by_pos = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end)
    high_score = Map.fetch!(cells_by_pos, high_cell)
    low_score = Map.fetch!(cells_by_pos, low_cell)
    assert high_score > low_score
  end

  test "terrain clearance gives score boost" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 200}
    stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
    grid = build_grid(home.lat, home.lon)

    scores_at = fn _, _, _ -> grid end
    elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
    prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
    road_lookup = fn _, _ -> {:error, :stubbed} end

    # Cell near home gets positive clearance to station
    near_cell = {Float.round(home.lat + 0.1, 4), Float.round(home.lon, 4)}
    far_cell = {Float.round(home.lat - 0.1, 4), Float.round(home.lon, 4)}

    clearance_lookup = fn cells, stations ->
      Map.new(cells, fn c ->
        s = hd(stations)
        key = {{c.lat, c.lon}, {s.lat, s.lon}}
        value = if {c.lat, c.lon} == near_cell, do: 150.0, else: 0.0
        {key, value}
      end)
    end

    result =
      Compute.run(
        %{
          home: home,
          stations: stations,
          band_mhz: 10_000,
          valid_time: ~U[2026-04-25 12:00:00Z],
          mode: :ssb,
          max_distance_km: 65.0,
          min_elev_gain: 0
        },
        scores_at: scores_at,
        elev_lookup: elev_lookup,
        clearance_lookup: clearance_lookup,
        prominence_lookup: prominence_lookup,
        road_lookup: road_lookup
      )

    cells_by_pos = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end)
    near_score = Map.fetch!(cells_by_pos, near_cell)
    far_score = Map.fetch!(cells_by_pos, far_cell)
    assert near_score > far_score
  end

  test "road_enabled? false skips road proximity check" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 200}
    stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
    grid = build_grid(home.lat, home.lon)

    scores_at = fn _, _, _ -> grid end
    elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
    clearance_lookup = fn _, _ -> %{} end
    prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
    road_lookup = fn _, _ -> {:error, :not_called} end

    Application.put_env(:microwaveprop, :rover_road_proximity_enabled, false)

    try do
      result =
        Compute.run(
          %{
            home: home,
            stations: stations,
            band_mhz: 10_000,
            valid_time: ~U[2026-04-25 12:00:00Z],
            mode: :ssb,
            max_distance_km: 65.0,
            min_elev_gain: 0
          },
          scores_at: scores_at,
          elev_lookup: elev_lookup,
          clearance_lookup: clearance_lookup,
          prominence_lookup: prominence_lookup,
          road_lookup: road_lookup
        )

      assert result.cells != []
      assert result.top_candidates != []
    after
      Application.put_env(:microwaveprop, :rover_road_proximity_enabled, true)
    end
  end

  test "good_locations snaps cells to exact locations" do
    home = %{lat: 33.0, lon: -96.0, elev_m: 200}
    stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
    grid = build_grid(home.lat, home.lon)

    scores_at = fn _, _, _ -> grid end
    elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
    clearance_lookup = fn _, _ -> %{} end
    prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
    road_lookup = fn _, _ -> {:error, :stubbed} end

    result =
      Compute.run(
        %{
          home: home,
          stations: stations,
          band_mhz: 10_000,
          valid_time: ~U[2026-04-25 12:00:00Z],
          mode: :ssb,
          max_distance_km: 65.0,
          min_elev_gain: 0,
          good_locations: [%{lat: 33.05, lon: -96.05}]
        },
        scores_at: scores_at,
        elev_lookup: elev_lookup,
        clearance_lookup: clearance_lookup,
        prominence_lookup: prominence_lookup,
        road_lookup: road_lookup
      )

    assert result.cells != []
  end
end
